--- old/make/gensrc/Gensrc-jdk.compiler.gmk 2018-04-12 12:51:05.256493730 -0700 +++ new/make/gensrc/Gensrc-jdk.compiler.gmk 2018-04-12 12:51:05.008482905 -0700 @@ -1,5 +1,5 @@ # -# Copyright (c) 2014, 2017, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -32,6 +32,7 @@ $(JAVAC_VERSION))) $(eval $(call SetupParseProperties,PARSE_PROPERTIES, \ - com/sun/tools/javac/resources/compiler.properties)) + com/sun/tools/javac/resources/compiler.properties \ + com/sun/tools/javac/resources/launcher.properties)) all: $(COMPILE_PROPERTIES) $(PARSE_PROPERTIES) --- old/make/langtools/build.properties 2018-04-12 12:51:05.872520616 -0700 +++ new/make/langtools/build.properties 2018-04-12 12:51:05.628509966 -0700 @@ -1,5 +1,5 @@ # -# Copyright (c) 2007, 2017, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -40,7 +40,8 @@ jdk.jshell langtools.resource.includes = \ - com/sun/tools/javac/resources/compiler.properties + com/sun/tools/javac/resources/compiler.properties \ + com/sun/tools/javac/resources/launcher.properties # Version info -- override as needed jdk.version = 9 --- old/src/java.base/share/classes/sun/launcher/LauncherHelper.java 2018-04-12 12:51:06.500548026 -0700 +++ new/src/java.base/share/classes/sun/launcher/LauncherHelper.java 2018-04-12 12:51:06.244536853 -0700 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -502,12 +502,13 @@ } // From src/share/bin/java.c: - // enum LaunchMode { LM_UNKNOWN = 0, LM_CLASS, LM_JAR, LM_MODULE } + // enum LaunchMode { LM_UNKNOWN = 0, LM_CLASS, LM_JAR, LM_MODULE, LM_SOURCE } private static final int LM_UNKNOWN = 0; private static final int LM_CLASS = 1; private static final int LM_JAR = 2; private static final int LM_MODULE = 3; + private static final int LM_SOURCE = 4; static void abort(Throwable t, String msgKey, Object... args) { if (msgKey != null) { @@ -538,13 +539,21 @@ * * @return the application's main class */ + @SuppressWarnings("fallthrough") public static Class checkAndLoadMain(boolean printToStderr, int mode, String what) { initOutput(printToStderr); - Class mainClass = (mode == LM_MODULE) ? loadModuleMainClass(what) - : loadMainClass(mode, what); + Class mainClass = null; + switch (mode) { + case LM_MODULE: case LM_SOURCE: + mainClass = loadModuleMainClass(what); + break; + default: + mainClass = loadMainClass(mode, what); + break; + } // record the real main class for UI purposes // neither method above can return null, they will abort() --- old/src/java.base/share/classes/sun/launcher/resources/launcher.properties 2018-04-12 12:51:07.272581722 -0700 +++ new/src/java.base/share/classes/sun/launcher/resources/launcher.properties 2018-04-12 12:51:07.020570723 -0700 @@ -1,5 +1,5 @@ # -# Copyright (c) 2007, 2017, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2007, 2018, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # This code is free software; you can redistribute it and/or modify it @@ -29,9 +29,11 @@ \ (to execute a jar file)\n\ \ or {0} [options] -m [/] [args...]\n\ \ {0} [options] --module [/] [args...]\n\ -\ (to execute the main class in a module)\n\n\ -\ Arguments following the main class, -jar , -m or --module\n\ -\ / are passed as the arguments to main class.\n\n\ +\ (to execute the main class in a module)\n\ +\ or {0} [options] java source-file [args]\n\n\ +\ Arguments following the main class, source-file, -jar ,\n\ +\ -m or --module / are passed as the arguments to\n\ +\ main class.\n\n\ \ where options include:\n\n java.launcher.opt.vmselect =\ {0}\t to select the "{1}" VM\n @@ -174,7 +176,9 @@ \ --patch-module =({0})*\n\ \ override or augment a module with classes and resources\n\ \ in JAR files or directories.\n\ -\ --disable-@files disable further argument file expansion\n\n\ +\ --disable-@files disable further argument file expansion\n\ +\ --source \n\ +\ set the version of the source in source-file mode.\n\n\ These extra options are subject to change without notice.\n # Translators please note do not translate the options themselves --- old/src/java.base/share/native/launcher/main.c 2018-04-12 12:51:08.192621877 -0700 +++ new/src/java.base/share/native/launcher/main.c 2018-04-12 12:51:07.932610529 -0700 @@ -183,7 +183,7 @@ } // Iterate the rest of command line for (i = 1; i < argc; i++) { - JLI_List argsInFile = JLI_PreprocessArg(argv[i]); + JLI_List argsInFile = JLI_PreprocessArg(argv[i], JNI_TRUE); if (NULL == argsInFile) { JLI_List_add(args, JLI_StringDup(argv[i])); } else { --- old/src/java.base/share/native/libjli/args.c 2018-04-12 12:51:09.084660810 -0700 +++ new/src/java.base/share/native/libjli/args.c 2018-04-12 12:51:08.836649985 -0700 @@ -79,6 +79,11 @@ static jboolean stopExpansion = JNI_FALSE; static jboolean relaunch = JNI_FALSE; +/* + * Prototypes for internal functions. + */ +static jboolean expand(JLI_List args, const char *str, const char *var_name); + JNIEXPORT void JNICALL JLI_InitArgProcessing(jboolean hasJavaArgs, jboolean disableArgFile) { // No expansion for relaunch @@ -376,9 +381,22 @@ return rv; } +/* + * expand a string into a list of words separated by whitespace. + */ +static JLI_List expandArg(const char *arg) { + JLI_List rv; + + /* arbitrarily pick 8, seems to be a reasonable number of arguments */ + rv = JLI_List_new(8); + + expand(rv, arg, NULL); + + return rv; +} + JNIEXPORT JLI_List JNICALL -JLI_PreprocessArg(const char *arg) -{ +JLI_PreprocessArg(const char *arg, jboolean expandSourceOpt) { JLI_List rv; if (firstAppArgIndex > 0) { @@ -392,6 +410,12 @@ return NULL; } + if (expandSourceOpt + && JLI_StrCCmp(arg, "--source") == 0 + && JLI_StrChr(arg, ' ') != NULL) { + return expandArg(arg); + } + if (arg[0] != '@') { checkArg(arg); return NULL; @@ -435,9 +459,6 @@ JNIEXPORT jboolean JNICALL JLI_AddArgsFromEnvVar(JLI_List args, const char *var_name) { char *env = getenv(var_name); - char *p, *arg; - char quote; - JLI_List argsInFile; if (firstAppArgIndex == 0) { // Not 'java', return @@ -453,44 +474,64 @@ } JLI_ReportMessage(ARG_INFO_ENVVAR, var_name, env); + return expand(args, env, var_name); +} + +/* + * Expand a string into a list of args. + * If the string is the result of looking up an environment variable, + * var_name should be set to the name of that environment variable, + * for use if needed in error messages. + */ + +static jboolean expand(JLI_List args, const char *str, const char *var_name) { + jboolean inEnvVar = (var_name != NULL); + + char *p, *arg; + char quote; + JLI_List argsInFile; // This is retained until the process terminates as it is saved as the args - p = JLI_MemAlloc(JLI_StrLen(env) + 1); - while (*env != '\0') { - while (*env != '\0' && isspace(*env)) { - env++; + p = JLI_MemAlloc(JLI_StrLen(str) + 1); + while (*str != '\0') { + while (*str != '\0' && isspace(*str)) { + str++; } // Trailing space - if (*env == '\0') { + if (*str == '\0') { break; } arg = p; - while (*env != '\0' && !isspace(*env)) { - if (*env == '"' || *env == '\'') { - quote = *env++; - while (*env != quote && *env != '\0') { - *p++ = *env++; + while (*str != '\0' && !isspace(*str)) { + if (inEnvVar && (*str == '"' || *str == '\'')) { + quote = *str++; + while (*str != quote && *str != '\0') { + *p++ = *str++; } - if (*env == '\0') { + if (*str == '\0') { JLI_ReportMessage(ARG_ERROR8, var_name); exit(1); } - env++; + str++; } else { - *p++ = *env++; + *p++ = *str++; } } *p++ = '\0'; - argsInFile = JLI_PreprocessArg(arg); + argsInFile = JLI_PreprocessArg(arg, JNI_FALSE); if (NULL == argsInFile) { if (isTerminalOpt(arg)) { - JLI_ReportMessage(ARG_ERROR9, arg, var_name); + if (inEnvVar) { + JLI_ReportMessage(ARG_ERROR9, arg, var_name); + } else { + JLI_ReportMessage(ARG_ERROR15, arg); + } exit(1); } JLI_List_add(args, arg); @@ -501,7 +542,11 @@ for (idx = 0; idx < cnt; idx++) { arg = argsInFile->elements[idx]; if (isTerminalOpt(arg)) { - JLI_ReportMessage(ARG_ERROR10, arg, argFile, var_name); + if (inEnvVar) { + JLI_ReportMessage(ARG_ERROR10, arg, argFile, var_name); + } else { + JLI_ReportMessage(ARG_ERROR16, arg, argFile); + } exit(1); } JLI_List_add(args, arg); @@ -517,11 +562,15 @@ * caught now. */ if (firstAppArgIndex != NOT_FOUND) { - JLI_ReportMessage(ARG_ERROR11, var_name); + if (inEnvVar) { + JLI_ReportMessage(ARG_ERROR11, var_name); + } else { + JLI_ReportMessage(ARG_ERROR17); + } exit(1); } - assert (*env == '\0' || isspace(*env)); + assert (*str == '\0' || isspace(*str)); } return JNI_TRUE; @@ -642,7 +691,7 @@ if (argc > 1) { for (i = 0; i < argc; i++) { - JLI_List tokens = JLI_PreprocessArg(argv[i]); + JLI_List tokens = JLI_PreprocessArg(argv[i], JNI_FALSE); if (NULL != tokens) { for (j = 0; j < tokens->size; j++) { printf("Token[%lu]: <%s>\n", (unsigned long) j, tokens->elements[j]); --- old/src/java.base/share/native/libjli/emessages.h 2018-04-12 12:51:09.864694854 -0700 +++ new/src/java.base/share/native/libjli/emessages.h 2018-04-12 12:51:09.612683855 -0700 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -51,6 +51,11 @@ #define ARG_ERROR10 "Error: Option %s in %s is not allowed in environment variable %s" #define ARG_ERROR11 "Error: Cannot specify main class in environment variable %s" #define ARG_ERROR12 "Error: %s requires module name" +#define ARG_ERROR13 "Error: %s requires source version" +#define ARG_ERROR14 "Error: Option %s is not allowed with --source" +#define ARG_ERROR15 "Error: Option %s is not allowed in this context" +#define ARG_ERROR16 "Error: Option %s in %s is not allowed in this context" +#define ARG_ERROR17 "Error: Cannot specify main class in this context" #define JVM_ERROR1 "Error: Could not create the Java Virtual Machine.\n" GEN_ERROR #define JVM_ERROR2 "Error: Could not detach main thread.\n" JNI_ERROR --- old/src/java.base/share/native/libjli/java.c 2018-04-12 12:51:10.628728200 -0700 +++ new/src/java.base/share/native/libjli/java.c 2018-04-12 12:51:10.376717202 -0700 @@ -172,6 +172,9 @@ static void FreeKnownVMs(); static jboolean IsWildCardEnabled(); + +#define SOURCE_LAUNCHER_MAIN_ENTRY "jdk.compiler/com.sun.tools.javac.launcher.Main" + /* * This reports error. VM will not be created and no usage is printed. */ @@ -214,7 +217,7 @@ * Entry point. */ JNIEXPORT int JNICALL -JLI_Launch(int argc, char ** argv, /* main argc, argc */ +JLI_Launch(int argc, char ** argv, /* main argc, argv */ int jargc, const char** jargv, /* java args */ int appclassc, const char** appclassv, /* app classpath */ const char* fullversion, /* full version defined */ @@ -317,8 +320,7 @@ /* Parse command line options; if the return value of * ParseArguments is false, the program should exit. */ - if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath)) - { + if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath)) { return(ret); } @@ -585,7 +587,8 @@ return IsClassPathOption(name) || IsLauncherMainOption(name) || JLI_StrCmp(name, "--describe-module") == 0 || - JLI_StrCmp(name, "-d") == 0; + JLI_StrCmp(name, "-d") == 0 || + JLI_StrCmp(name, "--source") == 0; } /* @@ -627,6 +630,29 @@ } /* + * Check if it is OK to set the mode. + * If the mode was previously set, and should not be changed, + * a fatal error is reported. + */ +static int +checkMode(int mode, int newMode, const char *arg) { + if (mode == LM_SOURCE) { + JLI_ReportErrorMessage(ARG_ERROR14, arg); + exit(1); + } + return newMode; +} + +/* + * Test if an arg identifies a source file. + */ +jboolean +IsSourceFile(const char *arg) { + struct stat st; + return (JLI_HasSuffix(arg, ".java") && stat(arg, &st) == 0); +} + +/* * Checks the command line options to find which JVM type was * specified. If no command line option was given for the JVM type, * the default type is used. The environment variable @@ -1230,7 +1256,8 @@ value = equals+1; if (JLI_StrCCmp(arg, "--describe-module=") == 0 || JLI_StrCCmp(arg, "--module=") == 0 || - JLI_StrCCmp(arg, "--class-path=") == 0) { + JLI_StrCCmp(arg, "--class-path=") == 0|| + JLI_StrCCmp(arg, "--source=") == 0) { kind = LAUNCHER_OPTION_WITH_ARGUMENT; } else { kind = VM_LONG_OPTION; @@ -1274,17 +1301,29 @@ */ if (JLI_StrCmp(arg, "-jar") == 0) { ARG_CHECK(argc, ARG_ERROR2, arg); - mode = LM_JAR; + mode = checkMode(mode, LM_JAR, arg); } else if (JLI_StrCmp(arg, "--module") == 0 || JLI_StrCCmp(arg, "--module=") == 0 || JLI_StrCmp(arg, "-m") == 0) { REPORT_ERROR (has_arg, ARG_ERROR5, arg); SetMainModule(value); - mode = LM_MODULE; + mode = checkMode(mode, LM_MODULE, arg); if (has_arg) { *pwhat = value; break; } + } else if (JLI_StrCmp(arg, "--source") == 0 || + JLI_StrCCmp(arg, "--source=") == 0) { + REPORT_ERROR (has_arg, ARG_ERROR13, arg); + mode = LM_SOURCE; + if (has_arg) { + const char *prop = "-Djdk.internal.javac.source="; + size_t size = JLI_StrLen(prop) + JLI_StrLen(value) + 1; + char *propValue = (char *)JLI_MemAlloc(size + 1); + JLI_StrCpy(propValue, prop); + JLI_StrCat(propValue, value); + AddOption(propValue, NULL); + } } else if (JLI_StrCmp(arg, "--class-path") == 0 || JLI_StrCCmp(arg, "--class-path=") == 0 || JLI_StrCmp(arg, "-classpath") == 0 || @@ -1435,12 +1474,22 @@ if (!_have_classpath) { SetClassPath("."); } - mode = LM_CLASS; + mode = IsSourceFile(arg) ? LM_SOURCE : LM_CLASS; + } else if (mode == LM_CLASS && IsSourceFile(arg)) { + /* override LM_CLASS mode if given a source file */ + mode = LM_SOURCE; } - if (argc >= 0) { - *pargc = argc; - *pargv = argv; + if (mode == LM_SOURCE) { + AddOption("--add-modules=ALL-DEFAULT", NULL); + *pwhat = SOURCE_LAUNCHER_MAIN_ENTRY; + *pargc = argc + 1; + *pargv = argv - 1; + } else { + if (argc >= 0) { + *pargc = argc; + *pargv = argv; + } } *pmode = mode; --- old/src/java.base/share/native/libjli/java.h 2018-04-12 12:51:11.576769577 -0700 +++ new/src/java.base/share/native/libjli/java.h 2018-04-12 12:51:11.324758578 -0700 @@ -230,11 +230,12 @@ LM_UNKNOWN = 0, LM_CLASS, LM_JAR, - LM_MODULE + LM_MODULE, + LM_SOURCE }; static const char *launchModeNames[] - = { "Unknown", "Main class", "JAR file", "Module" }; + = { "Unknown", "Main class", "JAR file", "Module", "Source" }; typedef struct { int argc; --- old/src/java.base/share/native/libjli/jli_util.c 2018-04-12 12:51:12.472808685 -0700 +++ new/src/java.base/share/native/libjli/jli_util.c 2018-04-12 12:51:12.224797860 -0700 @@ -84,6 +84,16 @@ free(ptr); } +jboolean +JLI_HasSuffix(const char *s1, const char *s2) +{ + char *p = JLI_StrRChr(s1, '.'); + if (p == NULL || *p == '\0') { + return JNI_FALSE; + } + return (JLI_StrCaseCmp(p, s2) == 0); +} + /* * debug helpers we use */ --- old/src/java.base/share/native/libjli/jli_util.h 2018-04-12 12:51:13.228841682 -0700 +++ new/src/java.base/share/native/libjli/jli_util.h 2018-04-12 12:51:13.024832778 -0700 @@ -51,7 +51,8 @@ JNIEXPORT void JNICALL JLI_MemFree(void *ptr); -int JLI_StrCCmp(const char *s1, const char* s2); +int JLI_StrCCmp(const char *s1, const char *s2); +jboolean JLI_HasSuffix(const char *s1, const char *s2); typedef struct { char *arg; @@ -158,7 +159,7 @@ JLI_InitArgProcessing(jboolean hasJavaArgs, jboolean disableArgFile); JNIEXPORT JLI_List JNICALL -JLI_PreprocessArg(const char *arg); +JLI_PreprocessArg(const char *arg, jboolean expandSourceOpt); JNIEXPORT jboolean JNICALL JLI_AddArgsFromEnvVar(JLI_List args, const char *var_name); --- old/src/java.base/windows/native/libjli/cmdtoargs.c 2018-04-12 12:51:14.120880615 -0700 +++ new/src/java.base/windows/native/libjli/cmdtoargs.c 2018-04-12 12:51:13.872869790 -0700 @@ -246,7 +246,7 @@ // iterate through rest of command line while (src != NULL) { src = next_arg(src, arg, &wildcard); - argsInFile = JLI_PreprocessArg(arg); + argsInFile = JLI_PreprocessArg(arg, JNI_TRUE); if (argsInFile != NULL) { // resize to accommodate another Arg cnt = argsInFile->size; --- old/test/langtools/tools/javac/diags/CheckResourceKeys.java 2018-04-12 12:51:14.912915183 -0700 +++ new/test/langtools/tools/javac/diags/CheckResourceKeys.java 2018-04-12 12:51:14.636903136 -0700 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -118,7 +118,8 @@ void findDeadKeys(Set codeStrings, Set resourceKeys) { String[] prefixes = { "compiler.err.", "compiler.warn.", "compiler.note.", "compiler.misc.", - "javac." + "javac.", + "launcher.err." }; for (String rk: resourceKeys) { // some keys are used directly, without a prefix. @@ -396,7 +397,7 @@ Set getResourceKeys() { Module jdk_compiler = ModuleLayer.boot().findModule("jdk.compiler").get(); Set results = new TreeSet(); - for (String name : new String[]{"javac", "compiler"}) { + for (String name : new String[]{"javac", "compiler", "launcher"}) { ResourceBundle b = ResourceBundle.getBundle("com.sun.tools.javac.resources." + name, jdk_compiler); results.addAll(b.keySet()); --- old/test/langtools/tools/lib/toolbox/ToolBox.java 2018-04-12 12:51:15.684948878 -0700 +++ new/test/langtools/tools/lib/toolbox/ToolBox.java 2018-04-12 12:51:15.408936831 -0700 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -142,7 +142,7 @@ // report first difference for (int i = 0; i < Math.min(l1.size(), l2.size()); i++) { String s1 = l1.get(i); - String s2 = l1.get(i); + String s2 = l2.get(i); if (!Objects.equals(s1, s2)) { throw new Error("comparison failed, index " + i + ", (" + s1 + ":" + s2 + ")"); --- /dev/null 2017-01-21 22:54:52.877512947 -0800 +++ new/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java 2018-04-12 12:51:16.180970527 -0700 @@ -0,0 +1,513 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.tools.javac.launcher; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.TypeElement; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; + +import com.sun.source.util.JavacTask; +import com.sun.source.util.TaskEvent; +import com.sun.source.util.TaskListener; +import com.sun.tools.javac.api.JavacTool; +import com.sun.tools.javac.code.Source; +import com.sun.tools.javac.resources.LauncherProperties.Errors; +import com.sun.tools.javac.util.JCDiagnostic.Error; + +import jdk.internal.misc.VM; + +import static javax.tools.JavaFileObject.Kind.SOURCE; + +/** + * Compiles a source file, and executes the main method it contains. + * + *

This is NOT part of any supported API. + * If you write code that depends on this, you do so at your own + * risk. This code and its internal interfaces are subject to change + * or deletion without notice.

+ */ +public class Main { + /** + * An exception used to report errors. + */ + public class Fault extends Exception { + private static final long serialVersionUID = 1L; + Fault(Error error) { + super(Main.this.getMessage(error)); + } + } + + /** + * Compiles a source file, and executes the main method it contains. + * + *

This is normally invoked from the Java launcher, either when + * the {@code --source} option is used, or when the first argument + * that is not part of a runtime option ends in {@code .java}. + * + *

The first entry in the {@code args} array is the source file + * to be compiled and run; all subsequent entries are passed as + * arguments to the main method of the first class found in the file. + * + *

If any problem occurs before executing the main class, it will + * be reported to the standard error stream, and the the JVM will be + * terminated by calling {@code System.exit} with a non-zero return code. + * + * @param args the arguments + * @throws Throwable if the main method throws an exception + */ + public static void main(String... args) throws Throwable { + try { + new Main(System.err).run(VM.getRuntimeArguments(), args); + } catch (Fault f) { + System.err.println(f.getMessage()); + System.exit(1); + } catch (InvocationTargetException e) { + // leave VM to handle the stacktrace, in the standard manner + throw e.getTargetException(); + } + } + + /** Stream for reporting errors, such as compilation errors. */ + private PrintWriter out; + + /** + * Creates an instance of this class, providing a stream to which to report + * any errors. + * + * @param out the stream + */ + public Main(PrintStream out) { + this(new PrintWriter(new OutputStreamWriter(out), true)); + } + + /** + * Creates an instance of this class, providing a stream to which to report + * any errors. + * + * @param out the stream + */ + public Main(PrintWriter out) { + this.out = out; + } + + /** + * Compiles a source file, and executes the main method it contains. + * + *

The first entry in the {@code args} array is the source file + * to be compiled and run; all subsequent entries are passed as + * arguments to the main method of the first class found in the file. + * + *

Options for {@code javac} are obtained by filtering the runtime arguments. + * + *

If the main method throws an exception, it will be propagated in an + * {@code InvocationTargetException}. In that case, the stack trace of the + * target exception will be truncated such that the main method will be the + * last entry on the stack. In other words, the stack frames leading up to the + * invocation of the main method will be removed. + * + * @param runtimeArgs the runtime arguments + * @param args the arguments + * @throws Fault if a problem is detected before the main method can be executed + * @throws InvocationTargetException if the main method throws an exception + */ + public void run(String[] runtimeArgs, String[] args) throws Fault, InvocationTargetException { + Path file = getFile(args); + + Map inMemoryClasses = new HashMap<>(); + String mainClassName = compile(file, getJavacOpts(runtimeArgs), inMemoryClasses); + + String[] appArgs = Arrays.copyOfRange(args, 1, args.length); + execute(mainClassName, appArgs, inMemoryClasses); + } + + /** + * Returns the path for the filename found in the first of an array of arguments. + * + * @param args the array + * @return the path + * @throws Fault if there is a problem determining the path, or if the file does not exist + */ + private Path getFile(String[] args) throws Fault { + if (args.length == 0) { + // should not happen when invoked from launcher + throw new Fault(Errors.NoArgs); + } + Path file; + try { + file = Paths.get(args[0]); + } catch (InvalidPathException e) { + throw new Fault(Errors.InvalidFilename(args[0])); + } + if (!Files.exists(file)) { + // should not happen when invoked from launcher + throw new Fault(Errors.FileNotFound(file)); + } + return file; + } + + /** + * Reads a source file, ignoring the first line if it begins {@code #!}. + * + *

If the first two bytes are {@code #!}, indicating a "magic number" of an executable + * text file, the rest of the first line up to but not including the newline is ignored. + * All characters after the first two are read in the + * {@link Charset#defaultCharset default platform encoding}. + * + * @param file the file + * @return a file object containing the content of the file + * @throws Fault if an error occurs while reading the file + */ + private JavaFileObject readFile(Path file) throws Fault { + // use a BufferedInputStream to guarantee that we can use mark and reset. + try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file))) { + in.mark(2); + int magic = (in.read() << 8) + in.read(); + boolean shebang = magic == (('#' << 8) + '!'); + if (!shebang) { + in.reset(); + } + try (BufferedReader r = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) { + StringBuilder sb = new StringBuilder(); + if (shebang) { + r.readLine(); + sb.append("\n"); // preserve line numbers + } + char[] buf = new char[1024]; + int n; + while ((n = r.read(buf, 0, buf.length)) != -1) { + sb.append(buf, 0, n); + } + return new SimpleJavaFileObject(file.toUri(), SOURCE) { + @Override + public String getName() { + return file.toString(); + } + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return sb; + } + @Override + public boolean isNameCompatible(String simpleName, JavaFileObject.Kind kind) { + return (kind == JavaFileObject.Kind.SOURCE) + && !simpleName.equals("package-info"); + } + @Override + public String toString() { + return "JavacSourceLauncher[" + file + "]"; + } + }; + } + } catch (IOException e) { + throw new Fault(Errors.CantReadFile(file, e)); + } + } + + /** + * Returns the subset of the runtime arguments that are relevant to {@code javac}. + * Generally, the relevant options are those for setting paths and for configuring the + * module system. + * + * @param runtimeArgs the runtime arguments + * @return the subset of the runtime arguments + **/ + private List getJavacOpts(String... runtimeArgs) throws Fault { + List javacOpts = new ArrayList<>(); + + String sourceOpt = System.getProperty("jdk.internal.javac.source"); + if (sourceOpt != null) { + Source source = Source.lookup(sourceOpt); + if (source == null) { + throw new Fault(Errors.InvalidValueForSource(sourceOpt)); + } + javacOpts.addAll(List.of("--release", sourceOpt)); + } + + for (int i = 0; i < runtimeArgs.length; i++) { + String arg = runtimeArgs[i]; + String opt = arg, value = null; + if (arg.startsWith("--")) { + int eq = arg.indexOf('='); + if (eq > 0) { + opt = arg.substring(0, eq); + value = arg.substring(eq + 1); + } + } + switch (opt) { + // The following options all expect a value, either in the following + // position, or after '=', for options beginning "--". + case "--class-path": case "-classpath": case "-cp": + case "--module-path": case "-p": + case "--add-exports": + case "--add-modules": + case "--limit-modules": + case "--patch-module": + case "--upgrade-module-path": + if (value == null) { + if (i== runtimeArgs.length - 1) { + // should not happen when invoked from launcher + throw new Fault(Errors.NoValueForOption(opt)); + } + value = runtimeArgs[++i]; + } + if (opt.equals("--add-modules") && value.equals("ALL-DEFAULT")) { + break; + } + javacOpts.add(opt); + javacOpts.add(value); + break; + default: + // ignore all other runtime args + } + } + return javacOpts; + } + + /** + * Compiles a source file, placing the class files in a map in memory. + * Any messages generated during compilation will be written to the stream + * provided when this object was created. + * + * @param file the source file + * @param javacOptscompilation options for {@code javac} + * @param inMemoryClasses a map in which to store the generated classes + * @return the name of the first class found in the source file + * @throws Fault if any compilation errors occur, or if no class was found + */ + private String compile(Path file, List javacOpts, Map inMemoryClasses) throws Fault { + JavaFileObject fo = readFile(file); + + JavacTool javaCompiler = JavacTool.create(); + StandardJavaFileManager stdFileMgr = javaCompiler.getStandardFileManager(null, null, null); + JavaFileManager fm = new MemoryFileManager(inMemoryClasses, stdFileMgr); + JavacTask t = javaCompiler.getTask(out, fm, null, javacOpts, null, List.of(fo)); + MainClassListener l = new MainClassListener(t); + Boolean ok = t.call(); + if (!ok) { + throw new Fault(Errors.CompilationFailed); + } + if (l.mainClass == null) { + throw new Fault(Errors.NoClass); + } + String mainClassName = l.mainClass.getQualifiedName().toString(); + return mainClassName; + } + + /** + * Invokes the {@code main} method of a specified class, using a class loader that + * will load recently compiled classes from memory. + * + * @param mainClassName the class to be executed + * @param appArgs the arguments for the {@code main} method + * @param inMemoryClasses the map of recently compiled classes + * @throws Fault if there is a problem finding or invoking the {@code main} method + * @throws InvocationTargetException if the {@code main} method throws an exception + */ + private void execute(String mainClassName, String[] appArgs, Map inMemoryClasses) + throws Fault, InvocationTargetException { + ClassLoader cl = new MemoryClassLoader(inMemoryClasses, ClassLoader.getSystemClassLoader()); + try { + Class appClass = Class.forName(mainClassName, true, cl); + if (appClass.getClassLoader() != cl) { + throw new Fault(Errors.UnexpectedClass(mainClassName)); + } + Method main = appClass.getDeclaredMethod("main", String[].class); + int PUBLIC_STATIC = Modifier.PUBLIC | Modifier.STATIC; + if ((main.getModifiers() & PUBLIC_STATIC) != PUBLIC_STATIC) { + throw new Fault(Errors.MainNotPublicStatic); + } + if (!main.getReturnType().equals(void.class)) { + throw new Fault(Errors.MainNotVoid); + } + main.setAccessible(true); + main.invoke(0, (Object) appArgs); + } catch (ClassNotFoundException e) { + throw new Fault(Errors.CantFindClass(mainClassName)); + } catch (NoSuchMethodException e) { + throw new Fault(Errors.CantFindMainMethod(mainClassName)); + } catch (IllegalAccessException e) { + throw new Fault(Errors.CantAccessMainMethod(mainClassName)); + } catch (InvocationTargetException e) { + // remove stack frames for source launcher + int invocationFrames = e.getStackTrace().length; + Throwable target = e.getTargetException(); + StackTraceElement[] targetTrace = target.getStackTrace(); + target.setStackTrace(Arrays.copyOfRange(targetTrace, 0, targetTrace.length - invocationFrames)); + throw e; + } + } + + private static final String bundleName = "com.sun.tools.javac.resources.launcher"; + private ResourceBundle resourceBundle = null; + private String errorPrefix; + + /** + * Returns a localized string from a resource bundle. + * + * @param key the key for the resource + * @param args values to be inserted into the resource + * @return the localized string + */ + private String getMessage(Error error) { + String key = error.key(); + Object[] args = error.getArgs(); + try { + if (resourceBundle == null) { + resourceBundle = ResourceBundle.getBundle(bundleName); + errorPrefix = resourceBundle.getString("launcher.error"); + } + String resource = resourceBundle.getString(key); + String message = MessageFormat.format(resource, args); + return errorPrefix + message; + } catch (MissingResourceException e) { + return "Cannot access resource; " + key + Arrays.toString(args); + } + } + + /** + * A listener to detect the first class found in a compilation. + */ + static class MainClassListener implements TaskListener { + private final JavacTask task; + TypeElement mainClass; + + MainClassListener(JavacTask t) { + task = t; + t.addTaskListener(this); + } + + @Override + public void started(TaskEvent ev) { + if (ev.getKind() == TaskEvent.Kind.ANALYZE && mainClass == null) { + TypeElement te = ev.getTypeElement(); + if (te.getNestingKind() == NestingKind.TOP_LEVEL) { + mainClass = te; + } + } + } + } + + /** + * An in-memory file manager. + * + *

Class files (of kind {@JavaFileObject.Kind.CLASS CLASS} written to + * {@link StandardLocation#CLASS_OUTPUT} will be written to an in-memory cache. + * All other file manager operations will be delegated to a specified file manager. + */ + static class MemoryFileManager extends ForwardingJavaFileManager { + private final Map map; + + MemoryFileManager(Map map, JavaFileManager delegate) { + super(delegate); + this.map = map; + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, + JavaFileObject.Kind kind, FileObject sibling) throws IOException { + if (location == StandardLocation.CLASS_OUTPUT && kind == JavaFileObject.Kind.CLASS) { + return createInMemoryClassFile(className); + } else { + return super.getJavaFileForOutput(location, className, kind, sibling); + } + } + + private JavaFileObject createInMemoryClassFile(String className) { + URI uri = URI.create("memory:///" + className.replace('.', '/') + ".class"); + return new SimpleJavaFileObject(uri, JavaFileObject.Kind.CLASS) { + @Override + public OutputStream openOutputStream() { + return new FilterOutputStream(new ByteArrayOutputStream()) { + @Override + public void close() throws IOException { + out.close(); + byte[] bytes = ((ByteArrayOutputStream) out).toByteArray(); + map.put(className, bytes); + } + }; + } + }; + } + } + + /** + * An in-memory classloader, that uses an in-memory cache written by {@link MemoryFileManager}. + * + *

The classloader uses the standard parent-delegation model, just providing + * {@code findClass} to find classes in the in-memory cache. + */ + static class MemoryClassLoader extends ClassLoader { + private final Map map; + + MemoryClassLoader(Map map, ClassLoader parent) { + super(parent); + this.map = map; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + byte[] bytes = map.get(name); + if (bytes == null) { + throw new ClassNotFoundException(name); + } + return defineClass(name, bytes, 0, bytes.length); + } + } +} --- /dev/null 2017-01-21 22:54:52.877512947 -0800 +++ new/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties 2018-04-12 12:51:16.816998286 -0700 @@ -0,0 +1,133 @@ +# +# Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. Oracle designates this +# particular file as subject to the "Classpath" exception as provided +# by Oracle in the LICENSE file that accompanied this code. +# +# This code is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# + +# Messages in this file which use "placeholders" for values (e.g. {0}, {1}) +# are preceded by a stylized comment describing the type of the corresponding +# values. +# The simple types currently in use are: +# +# annotation annotation compound +# boolean true or false +# diagnostic a sub-message; see compiler.misc.* +# fragment similar to 'message segment', but with more specific type +# modifier a Java modifier; e.g. public, private, protected +# file a file URL +# file object a file URL - similar to 'file' but typically used for source/class files, hence more specific +# flag a Flags.Flag instance +# name a name, typically a Java identifier +# number an integer +# option name the name of a command line option +# source version a source version number, such as 1.5, 1.6, 1.7 +# string a general string +# symbol the name of a declared type +# symbol kind the kind of a symbol (i.e. method, variable) +# kind name an informative description of the kind of a declaration; see compiler.misc.kindname.* +# token the name of a non-terminal in source code; see compiler.misc.token.* +# type a Java type; e.g. int, X, X +# object a Java object (unspecified) +# unused the value is not used in this message +# +# The following compound types are also used: +# +# collection of X a comma-separated collection of items; e.g. collection of type +# list of X a comma-separated list of items; e.g. list of type +# set of X a comma-separated set of items; e.g. set of modifier +# +# These may be composed: +# +# list of type or message segment +# +# The following type aliases are supported: +# +# message segment --> diagnostic or fragment +# file name --> file, path or file object +# +# Custom comments are supported in parenthesis i.e. +# +# number (classfile major version) +# +# These comments are used internally in order to generate an enum-like class declaration containing +# a method/field for each of the diagnostic keys listed here. Those methods/fields can then be used +# by javac code to build diagnostics in a type-safe fashion. +# +# In addition, these comments are verified by the jtreg test test/tools/javac/diags/MessageInfo, +# using info derived from the collected set of examples in test/tools/javac/diags/examples. +# MessageInfo can also be run as a standalone utility providing more facilities +# for manipulating this file. For more details, see MessageInfo.java. + +## All errors are preceded by this string. +launcher.error=\ + error:\u0020 + +launcher.err.no.args=\ + no filename + +# 0: string +launcher.err.invalid.filename=\ + invalid filename: {0} + +# 0: path +launcher.err.file.not.found=\ + file not found: {0} + +launcher.err.compilation.failed=\ + compilation failed + +launcher.err.no.class=\ + no class declared in file + +launcher.err.main.not.public.static=\ + ''main'' method is not declared ''public static'' + +launcher.err.main.not.void=\ + ''main'' method is not declared with a return type of ''void'' + +# 0: string +launcher.err.cant.find.class=\ + can''t find class: {0} + +# 0: string +launcher.err.unexpected.class=\ + class found on application class path: {0} + +# 0: string +launcher.err.cant.find.main.method=\ + can''t find main(String[]) method in class: {0} + +# 0: string +launcher.err.cant.access.main.method=\ + can''t access main method in class: {0} + +# 0: path, 1: object +launcher.err.cant.read.file=\ + error reading file {0}: {1} + +# 0: string +launcher.err.no.value.for.option=\ + no value given for option: {0} + +# 0: string +launcher.err.invalid.value.for.source=\ + invalid value for --source option: {0} --- /dev/null 2017-01-21 22:54:52.877512947 -0800 +++ new/test/jdk/tools/launcher/SourceMode.java 2018-04-12 12:51:17.429024997 -0700 @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2017, 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8192920 + * @summary Test source mode + * @modules jdk.compiler + * @run main SourceMode + */ + + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class SourceMode extends TestHelper { + + public static void main(String... args) throws Exception { + new SourceMode().run(args); + } + + // java Simple.java 1 2 3 + @Test + void testSimpleJava() throws IOException { + Path file = getSimpleFile("Simple.java", false); + TestResult tr = doExec(javaCmd, file.toString(), "1", "2", "3"); + if (!tr.isOK()) + error(tr, "Bad exit code: " + tr.exitValue); + if (!tr.contains("[1, 2, 3]")) + error(tr, "Expected output not found"); + System.out.println(tr.testOutput); + } + + // java --source 10 simple 1 2 3 + @Test + void testSimple() throws IOException { + Path file = getSimpleFile("simple", false); + TestResult tr = doExec(javaCmd, "--source", "10", file.toString(), "1", "2", "3"); + if (!tr.isOK()) + error(tr, "Bad exit code: " + tr.exitValue); + if (!tr.contains("[1, 2, 3]")) + error(tr, "Expected output not found"); + System.out.println(tr.testOutput); + } + + // execSimple 1 2 3 + @Test + void testExecSimple() throws IOException { + if (isWindows) // Will not work without cygwin, pass silently + return; + Path file = setExecutable(getSimpleFile("execSimple", true)); + TestResult tr = doExec(file.toAbsolutePath().toString(), "1", "2", "3"); + if (!tr.isOK()) + error(tr, "Bad exit code: " + tr.exitValue); + if (!tr.contains("[1, 2, 3]")) + error(tr, "Expected output not found"); + System.out.println(tr.testOutput); + } + + // java @simpleJava.at (contains Simple.java 1 2 3) + @Test + void testSimpleJavaAtFile() throws IOException { + Path file = getSimpleFile("Simple.java", false); + Path atFile = Paths.get("simpleJava.at"); + createFile(atFile.toFile(), List.of(file + " 1 2 3")); + TestResult tr = doExec(javaCmd, "@" + atFile); + if (!tr.isOK()) + error(tr, "Bad exit code: " + tr.exitValue); + if (!tr.contains("[1, 2, 3]")) + error(tr, "Expected output not found"); + System.out.println(tr.testOutput); + } + + // java @simple.at (contains --source 10 simple 1 2 3) + @Test + void testSimpleAtFile() throws IOException { + Path file = getSimpleFile("simple", false); + Path atFile = Paths.get("simple.at"); + createFile(atFile.toFile(), List.of("--source 10 " + file + " 1 2 3")); + TestResult tr = doExec(javaCmd, "@" + atFile); + if (!tr.isOK()) + error(tr, "Bad exit code: " + tr.exitValue); + if (!tr.contains("[1, 2, 3]")) + error(tr, "Expected output not found"); + System.out.println(tr.testOutput); + } + + // java -cp classes Main.java 1 2 3 + @Test + void testClasspath() throws IOException { + Path base = Files.createDirectories(Paths.get("testClasspath")); + Path otherJava = base.resolve("Other.java"); + createFile(otherJava.toFile(), List.of( + "public class Other {", + " public static String join(String[] args) {", + " return String.join(\"-\", args);", + " }", + "}" + )); + Path classes = Files.createDirectories(base.resolve("classes")); + Path mainJava = base.resolve("Main.java"); + createFile(mainJava.toFile(), List.of( + "class Main {", + " public static void main(String[] args) {", + " System.out.println(Other.join(args));", + " }}" + )); + compile("-d", classes.toString(), otherJava.toString()); + TestResult tr = doExec(javaCmd, "-cp", classes.toString(), + mainJava.toString(), "1", "2", "3"); + if (!tr.isOK()) + error(tr, "Bad exit code: " + tr.exitValue); + if (!tr.contains("1-2-3")) + error(tr, "Expected output not found"); + System.out.println(tr.testOutput); + } + + // java --add-exports=... Export.java --help + @Test + void testAddExports() throws IOException { + Path exportJava = Paths.get("Export.java"); + createFile(exportJava.toFile(), List.of( + "public class Export {", + " public static void main(String[] args) {", + " new com.sun.tools.javac.main.Main(\"demo\").compile(args);", + " }", + "}" + )); + // verify access fails without --add-exports + TestResult tr1 = doExec(javaCmd, exportJava.toString(), "--help"); + if (tr1.isOK()) + error(tr1, "Compilation succeeded unexpectedly"); + // verify access succeeds with --add-exports + TestResult tr2 = doExec(javaCmd, + "--add-exports", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + exportJava.toString(), "--help"); + if (!tr2.isOK()) + error(tr2, "Bad exit code: " + tr2.exitValue); + if (!(tr2.contains("demo") && tr2.contains("Usage"))) + error(tr2, "Expected output not found"); + } + + // java -cp ... HelloWorld.java (for a class "java" in package "HelloWorld") + @Test + void testClassNamedJava() throws IOException { + Path base = Files.createDirectories(Paths.get("testClassNamedJava")); + Path src = Files.createDirectories(base.resolve("src")); + Path srcfile = src.resolve("java.java"); + createFile(srcfile.toFile(), List.of( + "package HelloWorld;", + "class java {", + " public static void main(String... args) {", + " System.out.println(HelloWorld.java.class.getName());", + " }", + "}" + )); + Path classes = base.resolve("classes"); + compile("-d", classes.toString(), srcfile.toString()); + TestResult tr = + doExec(javaCmd, "-cp", classes.toString(), "HelloWorld.java"); + if (!tr.isOK()) + error(tr, "Command failed"); + if (!tr.contains("HelloWorld.java")) + error(tr, "Expected output not found"); + } + + // java --source + @Test + void testSourceNoArg() throws IOException { + TestResult tr = doExec(javaCmd, "--source"); + if (tr.isOK()) + error(tr, "Command succeeded unexpectedly"); + System.err.println(tr); + if (!tr.contains("--source requires source version")) + error(tr, "Expected output not found"); + } + + // java --source 10 -jar simple.jar + @Test + void testSourceJarConflict() throws IOException { + Path base = Files.createDirectories(Paths.get("testSourceJarConflict")); + Path file = getSimpleFile("Simple.java", false); + Path classes = Files.createDirectories(base.resolve("classes")); + compile("-d", classes.toString(), file.toString()); + Path simpleJar = base.resolve("simple.jar"); + createJar("cf", simpleJar.toString(), "-C", classes.toString(), "."); + TestResult tr = + doExec(javaCmd, "--source", "10", "-jar", simpleJar.toString()); + if (tr.isOK()) + error(tr, "Command succeeded unexpectedly"); + if (!tr.contains("Option -jar is not allowed with --source")) + error(tr, "Expected output not found"); + } + + // java --source 10 -m jdk.compiler + @Test + void testSourceModuleConflict() throws IOException { + TestResult tr = doExec(javaCmd, "--source", "10", "-m", "jdk.compiler"); + if (tr.isOK()) + error(tr, "Command succeeded unexpectedly"); + if (!tr.contains("Option -m is not allowed with --source")) + error(tr, "Expected output not found"); + } + + // #!.../java --source 10 -version + @Test + void testTerminalOptionInShebang() throws IOException { + if (isWindows) // Will not work without cygwin, pass silently + return; + Path base = Files.createDirectories( + Paths.get("testTerminalOptionInShebang")); + Path bad = base.resolve("bad"); + createFile(bad.toFile(), List.of( + "#!" + javaCmd + " --source 10 -version")); + setExecutable(bad); + TestResult tr = doExec(bad.toString()); + if (!tr.contains("Option -version is not allowed in this context")) + error(tr, "Expected output not found"); + } + + // #!.../java --source 10 @bad.at (contains -version) + @Test + void testTerminalOptionInShebangAtFile() throws IOException { + if (isWindows) // Will not work without cygwin, pass silently + return; + // Use a short directory name, to avoid line length limitations + Path base = Files.createDirectories(Paths.get("testBadAtFile")); + Path bad_at = base.resolve("bad.at"); + createFile(bad_at.toFile(), List.of("-version")); + Path bad = base.resolve("bad"); + createFile(bad.toFile(), List.of( + "#!" + javaCmd + " --source 10 @" + bad_at)); + setExecutable(bad); + TestResult tr = doExec(bad.toString()); + System.err.println("JJG JJG " + tr); + if (!tr.contains("Option -version in @testBadAtFile/bad.at is " + + "not allowed in this context")) + error(tr, "Expected output not found"); + } + + // #!.../java --source 10 HelloWorld + @Test + void testMainClassInShebang() throws IOException { + if (isWindows) // Will not work without cygwin, pass silently + return; + Path base = Files.createDirectories(Paths.get("testMainClassInShebang")); + Path bad = base.resolve("bad"); + createFile(bad.toFile(), List.of( + "#!" + javaCmd + " --source 10 HelloWorld")); + setExecutable(bad); + TestResult tr = doExec(bad.toString()); + if (!tr.contains("Cannot specify main class in this context")) + error(tr, "Expected output not found"); + } + + //-------------------------------------------------------------------------- + + private Map getLauncherDebugEnv() { + return Map.of("_JAVA_LAUNCHER_DEBUG", "1"); + } + + private Path getSimpleFile(String name, boolean shebang) throws IOException { + Path file = Paths.get(name); + if (!Files.exists(file)) { + createFile(file.toFile(), List.of( + (shebang ? "#!" + javaCmd + " --source 10" : ""), + "public class Simple {", + " public static void main(String[] args) {", + " System.out.println(java.util.Arrays.toString(args));", + " }}")); + } + return file; + } + + private Path setExecutable(Path file) throws IOException { + Set perms = Files.getPosixFilePermissions(file); + perms.add(PosixFilePermission.OWNER_EXECUTE); + Files.setPosixFilePermissions(file, perms); + return file; + } + + private void error(TestResult tr, String message) { + System.err.println(tr); + throw new RuntimeException(message); + } +} --- /dev/null 2017-01-21 22:54:52.877512947 -0800 +++ new/test/langtools/tools/javac/launcher/SourceLauncherTest.java 2018-04-12 12:51:18.077053281 -0700 @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8192920 + * @summary Test source launcher + * @library /tools/lib + * @modules jdk.compiler/com.sun.tools.javac.api + * jdk.compiler/com.sun.tools.javac.launcher + * jdk.compiler/com.sun.tools.javac.main + * @build toolbox.JavaTask toolbox.JavacTask toolbox.TestRunner toolbox.ToolBox + * @run main SourceLauncherTest + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import com.sun.tools.javac.launcher.Main; + +import toolbox.JavaTask; +import toolbox.JavacTask; +import toolbox.Task; +import toolbox.TestRunner; +import toolbox.TestRunner; +import toolbox.ToolBox; + +public class SourceLauncherTest extends TestRunner { + public static void main(String... args) throws Exception { + SourceLauncherTest t = new SourceLauncherTest(); + t.runTests(m -> new Object[] { Paths.get(m.getName()) }); + } + + SourceLauncherTest() { + super(System.err); + tb = new ToolBox(); + } + + private final ToolBox tb; + + /* + * Positive tests. + */ + + @Test + public void testHelloWorld(Path base) throws IOException { + tb.writeJavaFiles(base, + "import java.util.Arrays;\n" + + "class HelloWorld {\n" + + " public static void main(String... args) {\n" + + " System.out.println(\"Hello World! \" + Arrays.toString(args));\n" + + " }\n" + + "}"); + testSuccess(base.resolve("HelloWorld.java"), "Hello World! [1, 2, 3]\n"); + } + + @Test + public void testHelloWorldInPackage(Path base) throws IOException { + tb.writeJavaFiles(base, + "package hello;\n" + + "import java.util.Arrays;\n" + + "class World {\n" + + " public static void main(String... args) {\n" + + " System.out.println(\"Hello World! \" + Arrays.toString(args));\n" + + " }\n" + + "}"); + testSuccess(base.resolve("hello").resolve("World.java"), "Hello World! [1, 2, 3]\n"); + } + + @Test + public void testHelloWorldWithAux(Path base) throws IOException { + tb.writeJavaFiles(base, + "import java.util.Arrays;\n" + + "class HelloWorld {\n" + + " public static void main(String... args) {\n" + + " Aux.write(args);\n" + + " }\n" + + "}\n" + + "class Aux {\n" + + " static void write(String... args) {\n" + + " System.out.println(\"Hello World! \" + Arrays.toString(args));\n" + + " }\n" + + "}"); + testSuccess(base.resolve("HelloWorld.java"), "Hello World! [1, 2, 3]\n"); + } + + void testSuccess(Path file, String expect) throws IOException { + Result r = run(file, Collections.emptyList(), List.of("1", "2", "3")); + checkEqual("stdout", r.stdOut, expect); + checkEmpty("stderr", r.stdErr); + checkNull("exception", r.exception); + } + + /* + * Negative tests: cannot find or execute main method. + */ + + @Test + public void testNoClass(Path base) throws IOException { + Files.createDirectories(base); + Path file = base.resolve("NoClass.java"); + Files.write(file, List.of("package p;")); + testError(file, "", "error: no class declared in file"); + } + + @Test + public void testWrongClass(Path base) throws IOException { + Path src = base.resolve("src"); + Path file = src.resolve("WrongClass.java"); + tb.writeJavaFiles(src, "class WrongClass { }"); + Path classes = Files.createDirectories(base.resolve("classes")); + new JavacTask(tb) + .outdir(classes) + .files(file) + .run(); + String log = new JavaTask(tb) + .classpath(classes.toString()) + .className(file.toString()) + .run(Task.Expect.FAIL) + .getOutput(Task.OutputKind.STDERR); + checkEqual("stderr", log.trim(), + "error: class found on application class path: WrongClass"); + } + + @Test + public void testSyntaxErr(Path base) throws IOException { + tb.writeJavaFiles(base, "class SyntaxErr {"); + Path file = base.resolve("SyntaxErr.java"); + testError(file, + file + ":1: error: reached end of file while parsing\n" + + "class SyntaxErr {\n" + + " ^\n" + + "1 error\n", + "error: compilation failed"); + } + + @Test + public void testNoMain(Path base) throws IOException { + tb.writeJavaFiles(base, "class NoMain { }"); + testError(base.resolve("NoMain.java"), "", + "error: can't find main(String[]) method in class: NoMain"); + } + + @Test + public void testMainBadParams(Path base) throws IOException { + tb.writeJavaFiles(base, + "class BadParams { public static void main() { } }"); + testError(base.resolve("BadParams.java"), "", + "error: can't find main(String[]) method in class: BadParams"); + } + + @Test + public void testMainNotPublic(Path base) throws IOException { + tb.writeJavaFiles(base, + "class NotPublic { static void main(String... args) { } }"); + testError(base.resolve("NotPublic.java"), "", + "error: 'main' method is not declared 'public static'"); + } + + @Test + public void testMainNotStatic(Path base) throws IOException { + tb.writeJavaFiles(base, + "class NotStatic { public void main(String... args) { } }"); + testError(base.resolve("NotStatic.java"), "", + "error: 'main' method is not declared 'public static'"); + } + + @Test + public void testMainNotVoid(Path base) throws IOException { + tb.writeJavaFiles(base, + "class NotVoid { public static int main(String... args) { return 0; } }"); + testError(base.resolve("NotVoid.java"), "", + "error: 'main' method is not declared with a return type of 'void'"); + } + + @Test + public void testClassInModule(Path base) throws IOException { + tb.writeJavaFiles(base, "package java.net; class InModule { }"); + Path file = base.resolve("java").resolve("net").resolve("InModule.java"); + testError(file, + file + ":1: error: package exists in another module: java.base\n" + + "package java.net; class InModule { }\n" + + "^\n" + + "1 error\n", + "error: compilation failed"); + } + + @Test + public void testBadSourceOpt(Path base) throws IOException { + Files.createDirectories(base); + Path file = base.resolve("DummyClass.java"); + Files.write(file, List.of("class DummyClass { }")); + Properties sysProps = System.getProperties(); + Properties p = new Properties(sysProps); + p.setProperty("jdk.internal.javac.source", ""); + System.setProperties(p); + try { + testError(file, "", "error: invalid value for --source option: "); + } finally { + System.setProperties(sysProps); + } + } + + void testError(Path file, String expectStdErr, String expectFault) throws IOException { + Result r = run(file, Collections.emptyList(), List.of("1", "2", "3")); + checkEmpty("stdout", r.stdOut); + checkEqual("stderr", r.stdErr, expectStdErr); + checkFault("exception", r.exception, expectFault); + } + + /* + * Tests in which main throws an exception. + */ + @Test + public void testTargetException1(Path base) throws IOException { + tb.writeJavaFiles(base, + "import java.util.Arrays;\n" + + "class Thrower {\n" + + " public static void main(String... args) {\n" + + " throwWhenZero(Integer.parseInt(args[0]));\n" + + " }\n" + + " static void throwWhenZero(int arg) {\n" + + " if (arg == 0) throw new Error(\"zero!\");\n" + + " throwWhenZero(arg - 1);\n" + + " }\n" + + "}"); + Path file = base.resolve("Thrower.java"); + Result r = run(file, Collections.emptyList(), List.of("3")); + checkEmpty("stdout", r.stdOut); + checkEmpty("stderr", r.stdErr); + checkTrace("exception", r.exception, + "java.lang.Error: zero!", + "at Thrower.throwWhenZero(Thrower.java:7)", + "at Thrower.throwWhenZero(Thrower.java:8)", + "at Thrower.throwWhenZero(Thrower.java:8)", + "at Thrower.throwWhenZero(Thrower.java:8)", + "at Thrower.main(Thrower.java:4)"); + } + + Result run(Path file, List runtimeArgs, List appArgs) { + List args = new ArrayList<>(); + args.add(file.toString()); + args.addAll(appArgs); + + PrintStream prev = System.out; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintStream out = new PrintStream(baos, true)) { + System.setOut(out); + StringWriter sw = new StringWriter(); + try (PrintWriter err = new PrintWriter(sw, true)) { + Main m = new Main(err); + m.run(toArray(runtimeArgs), toArray(args)); + return new Result(baos.toString(), sw.toString(), null); + } catch (Throwable t) { + return new Result(baos.toString(), sw.toString(), t); + } + } finally { + System.setOut(prev); + } + } + + void checkEqual(String name, String found, String expect) { + expect = expect.replace("\n", tb.lineSeparator); + out.println(name + ": " + found); + out.println(name + ": " + found); + if (!expect.equals(found)) { + error("Unexpected output; expected: " + expect); + } + } + + void checkEmpty(String name, String found) { + out.println(name + ": " + found); + if (!found.isEmpty()) { + error("Unexpected output; expected empty string"); + } + } + + void checkNull(String name, Throwable found) { + out.println(name + ": " + found); + if (found != null) { + error("Unexpected exception; expected null"); + } + } + + void checkFault(String name, Throwable found, String expect) { + expect = expect.replace("\n", tb.lineSeparator); + out.println(name + ": " + found); + if (!(found instanceof Main.Fault)) { + error("Unexpected exception; expected Main.Fault"); + } + if (!(found.getMessage().equals(expect))) { + error("Unexpected detail message; expected: " + expect); + } + } + + void checkTrace(String name, Throwable found, String... expect) { + if (!(found instanceof InvocationTargetException)) { + error("Unexpected exception; expected InvocationTargetException"); + out.println("Found:"); + found.printStackTrace(out); + } + StringWriter sw = new StringWriter(); + try (PrintWriter pw = new PrintWriter(sw)) { + ((InvocationTargetException) found).getTargetException().printStackTrace(pw); + } + String trace = sw.toString(); + out.println(name + ":\n" + trace); + String[] traceLines = trace.trim().split("[\r\n]+\\s+"); + try { + tb.checkEqual(List.of(traceLines), List.of(expect)); + } catch (Error e) { + error(e.getMessage()); + } + } + + String[] toArray(List list) { + return list.toArray(new String[list.size()]); + } + + class Result { + private final String stdOut; + private final String stdErr; + private final Throwable exception; + + Result(String stdOut, String stdErr, Throwable exception) { + this.stdOut = stdOut; + this.stdErr = stdErr; + this.exception = exception; + } + } +}