8201274: Launch Single-File Source-Code Programs
Reviewed-by: mcimadamore, jlahoda, ksrini, mchung, ihse, alanb
--- a/make/gensrc/Gensrc-jdk.compiler.gmk Thu Jun 07 23:30:05 2018 +0200
+++ b/make/gensrc/Gensrc-jdk.compiler.gmk Thu Jun 07 16:06:49 2018 -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)
--- a/make/langtools/build.properties Thu Jun 07 23:30:05 2018 +0200
+++ b/make/langtools/build.properties Thu Jun 07 16:06:49 2018 -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
@@ -38,7 +38,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
--- a/src/java.base/share/classes/sun/launcher/LauncherHelper.java Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/classes/sun/launcher/LauncherHelper.java Thu Jun 07 16:06:49 2018 -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()
--- a/src/java.base/share/classes/sun/launcher/resources/launcher.properties Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/classes/sun/launcher/resources/launcher.properties Thu Jun 07 16:06:49 2018 -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
@@ -25,13 +25,17 @@
# Translators please note do not translate the options themselves
java.launcher.opt.header = Usage: {0} [options] <mainclass> [args...]\n\
-\ (to execute a class)\n or {0} [options] -jar <jarfile> [args...]\n\
+\ (to execute a class)\n\
+\ or {0} [options] -jar <jarfile> [args...]\n\
\ (to execute a jar file)\n\
\ or {0} [options] -m <module>[/<mainclass>] [args...]\n\
\ {0} [options] --module <module>[/<mainclass>] [args...]\n\
-\ (to execute the main class in a module)\n\n\
-\ Arguments following the main class, -jar <jarfile>, -m or --module\n\
-\ <module>/<mainclass> are passed as the arguments to main class.\n\n\
+\ (to execute the main class in a module)\n\
+\ or {0} [options] <sourcefile> [args]\n\
+\ (to execute a single source-file program)\n\n\
+\ Arguments following the main class, source file, -jar <jarfile>,\n\
+\ -m or --module <module>/<mainclass> 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
@@ -114,7 +118,7 @@
\ -disable-@files\n\
\ prevent further argument file expansion\n\
\ --enable-preview\n\
-\ allow classes to depend on preview features of this release
+\ allow classes to depend on preview features of this release\n\
\To specify an argument for a long option, you can use --<name>=<value> or\n\
\--<name> <value>.\n
@@ -176,7 +180,9 @@
\ --patch-module <module>=<file>({0}<file>)*\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 <version>\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
--- a/src/java.base/share/native/launcher/main.c Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/native/launcher/main.c Thu Jun 07 16:06:49 2018 -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 {
--- a/src/java.base/share/native/libjli/args.c Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/native/libjli/args.c Thu Jun 07 16:06:49 2018 -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]);
--- a/src/java.base/share/native/libjli/emessages.h Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/native/libjli/emessages.h Thu Jun 07 16:06:49 2018 -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
--- a/src/java.base/share/native/libjli/java.c Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/native/libjli/java.c Thu Jun 07 16:06:49 2018 -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,28 @@
*/
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);
+ JLI_Snprintf(propValue, size, "%s%s", prop, 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 +1473,25 @@
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;
+ // adjust (argc, argv) so that the name of the source file
+ // is included in the args passed to the source launcher
+ // main entry class
+ *pargc = argc + 1;
+ *pargv = argv - 1;
+ } else {
+ if (argc >= 0) {
+ *pargc = argc;
+ *pargv = argv;
+ }
}
*pmode = mode;
--- a/src/java.base/share/native/libjli/java.h Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/native/libjli/java.h Thu Jun 07 16:06:49 2018 -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;
--- a/src/java.base/share/native/libjli/jli_util.c Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/native/libjli/jli_util.c Thu Jun 07 16:06:49 2018 -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
*/
--- a/src/java.base/share/native/libjli/jli_util.h Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/share/native/libjli/jli_util.h Thu Jun 07 16:06:49 2018 -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);
--- a/src/java.base/windows/native/libjli/cmdtoargs.c Thu Jun 07 23:30:05 2018 +0200
+++ b/src/java.base/windows/native/libjli/cmdtoargs.c Thu Jun 07 16:06:49 2018 -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;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java Thu Jun 07 16:06:49 2018 -0700
@@ -0,0 +1,550 @@
+/*
+ * 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.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.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import javax.lang.model.SourceVersion;
+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.
+ *
+ * <p><b>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.</b></p>
+ */
+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.
+ *
+ * <p>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}.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>Options for {@code javac} are obtained by filtering the runtime arguments.
+ *
+ * <p>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);
+
+ Context context = new Context();
+ String mainClassName = compile(file, getJavacOpts(runtimeArgs), context);
+
+ String[] appArgs = Arrays.copyOfRange(args, 1, args.length);
+ execute(mainClassName, appArgs, context);
+ }
+
+ /**
+ * 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 is not a Java source file and
+ * it begins with {@code #!}.
+ *
+ * <p>If it is not a Java source file, and 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))) {
+ boolean ignoreFirstLine;
+ if (file.getFileName().toString().endsWith(".java")) {
+ ignoreFirstLine = false;
+ } else {
+ in.mark(2);
+ ignoreFirstLine = (in.read() == '#') && (in.read() == '!');
+ if (!ignoreFirstLine) {
+ in.reset();
+ }
+ }
+ try (BufferedReader r = new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) {
+ StringBuilder sb = new StringBuilder();
+ if (ignoreFirstLine) {
+ 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) {
+ // reject package-info and module-info; accept other names
+ return (kind == JavaFileObject.Kind.SOURCE)
+ && SourceVersion.isIdentifier(simpleName);
+ }
+ @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<String> getJavacOpts(String... runtimeArgs) throws Fault {
+ List<String> 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")) {
+ // this option is only supported at run time;
+ // it is not required or supported at compile time
+ break;
+ }
+ javacOpts.add(opt);
+ javacOpts.add(value);
+ break;
+ case "--enable-preview":
+ javacOpts.add(opt);
+ if (sourceOpt == null) {
+ throw new Fault(Errors.EnablePreviewRequiresSource);
+ }
+ break;
+ default:
+ // ignore all other runtime args
+ }
+ }
+
+ // add implicit options
+ javacOpts.add("-proc:none");
+
+ 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 javacOpts compilation options for {@code javac}
+ * @param context the context for the compilation
+ * @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<String> javacOpts, Context context) throws Fault {
+ JavaFileObject fo = readFile(file);
+
+ JavacTool javaCompiler = JavacTool.create();
+ StandardJavaFileManager stdFileMgr = javaCompiler.getStandardFileManager(null, null, null);
+ try {
+ stdFileMgr.setLocation(StandardLocation.SOURCE_PATH, Collections.emptyList());
+ } catch (IOException e) {
+ throw new java.lang.Error("unexpected exception from file manager", e);
+ }
+ JavaFileManager fm = context.getFileManager(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 context the context for the class to be executed
+ * @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, Context context)
+ throws Fault, InvocationTargetException {
+ ClassLoader cl = context.getClassLoader(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 error the error for which to get the localized text
+ * @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 {
+ TypeElement mainClass;
+
+ MainClassListener(JavacTask 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 object to encapulate the set of in-memory classes, such that
+ * they can be written by a file manager and subsequently used by
+ * a class loader.
+ */
+ private static class Context {
+ private Map<String, byte[]> inMemoryClasses = new HashMap<>();
+
+ JavaFileManager getFileManager(StandardJavaFileManager delegate) {
+ return new MemoryFileManager(inMemoryClasses, delegate);
+ }
+
+ ClassLoader getClassLoader(ClassLoader parent) {
+ return new MemoryClassLoader(inMemoryClasses, parent);
+ }
+ }
+
+ /**
+ * An in-memory file manager.
+ *
+ * <p>Class files (of kind {@link 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.
+ */
+ private static class MemoryFileManager extends ForwardingJavaFileManager<JavaFileManager> {
+ private final Map<String, byte[]> map;
+
+ MemoryFileManager(Map<String, byte[]> 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 ByteArrayOutputStream() {
+ @Override
+ public void close() throws IOException {
+ super.close();
+ map.put(className, toByteArray());
+ }
+ };
+ }
+ };
+ }
+ }
+
+ /**
+ * An in-memory classloader, that uses an in-memory cache written by {@link MemoryFileManager}.
+ *
+ * <p>The classloader uses the standard parent-delegation model, just providing
+ * {@code findClass} to find classes in the in-memory cache.
+ */
+ private static class MemoryClassLoader extends ClassLoader {
+ private final Map<String, byte[]> map;
+
+ MemoryClassLoader(Map<String, byte[]> 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 Thu Jan 01 00:00:00 1970 +0000
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties Thu Jun 07 16:06:49 2018 -0700
@@ -0,0 +1,136 @@
+#
+# 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<T>
+# 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}
+
+launcher.err.enable.preview.requires.source=\
+ --enable-preview must be used with --source
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/tools/launcher/SourceMode.java Thu Jun 07 16:06:49 2018 -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<String,String> 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<PosixFilePermission> 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);
+ }
+}
--- a/test/langtools/tools/javac/diags/CheckResourceKeys.java Thu Jun 07 23:30:05 2018 +0200
+++ b/test/langtools/tools/javac/diags/CheckResourceKeys.java Thu Jun 07 16:06:49 2018 -0700
@@ -118,7 +118,8 @@
void findDeadKeys(Set<String> codeStrings, Set<String> 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.
@@ -395,7 +396,7 @@
Set<String> getResourceKeys() {
Module jdk_compiler = ModuleLayer.boot().findModule("jdk.compiler").get();
Set<String> results = new TreeSet<String>();
- 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());
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/langtools/tools/javac/launcher/SourceLauncherTest.java Thu Jun 07 16:06:49 2018 -0700
@@ -0,0 +1,542 @@
+/*
+ * 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();
+ System.err.println("version: " + thisVersion);
+ }
+
+ private final ToolBox tb;
+ private static final String thisVersion = System.getProperty("java.specification.version");
+
+ /*
+ * 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");
+ }
+
+ @Test
+ public void testHelloWorldWithShebang(Path base) throws IOException {
+ tb.writeJavaFiles(base,
+ "#!/usr/bin/java --source 11\n" +
+ "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" +
+ "}");
+ Files.copy(base.resolve("HelloWorld.java"), base.resolve("HelloWorld"));
+ testSuccess(base.resolve("HelloWorld"), "Hello World! [1, 2, 3]\n");
+ }
+
+ @Test
+ public void testNoAnnoProcessing(Path base) throws IOException {
+ Path annoSrc = base.resolve("annoSrc");
+ tb.writeJavaFiles(annoSrc,
+ "import java.util.*;\n" +
+ "import javax.annotation.processing.*;\n" +
+ "import javax.lang.model.element.*;\n" +
+ "@SupportedAnnotationTypes(\"*\")\n" +
+ "public class AnnoProc extends AbstractProcessor {\n" +
+ " public boolean process(Set<? extends TypeElement> annos, RoundEnvironment rEnv) {\n" +
+ " throw new Error(\"Annotation processor should not be invoked\");\n" +
+ " }\n" +
+ "}\n");
+ Path annoClasses = Files.createDirectories(base.resolve("classes"));
+ new JavacTask(tb)
+ .outdir(annoClasses)
+ .files(annoSrc.resolve("AnnoProc.java").toString())
+ .run();
+ Path serviceFile = annoClasses.resolve("META-INF").resolve("services")
+ .resolve("javax.annotation.processing.Processor");
+ tb.writeFile(serviceFile, "AnnoProc");
+
+ Path mainSrc = base.resolve("mainSrc");
+ tb.writeJavaFiles(mainSrc,
+ "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" +
+ "}");
+
+ List<String> javacArgs = List.of("-classpath", annoClasses.toString());
+ List<String> classArgs = List.of("1", "2", "3");
+ String expect = "Hello World! [1, 2, 3]\n";
+ Result r = run(mainSrc.resolve("HelloWorld.java"), javacArgs, classArgs);
+ checkEqual("stdout", r.stdOut, expect);
+ checkEmpty("stderr", r.stdErr);
+ checkNull("exception", r.exception);
+ }
+
+ @Test
+ public void testEnablePreview(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" +
+ "}");
+
+ String log = new JavaTask(tb)
+ .vmOptions("--enable-preview", "--source", thisVersion)
+ .className(base.resolve("HelloWorld.java").toString())
+ .classArgs("1", "2", "3")
+ .run(Task.Expect.SUCCESS)
+ .getOutput(Task.OutputKind.STDOUT);
+ checkEqual("stdout", log.trim(), "Hello World! [1, 2, 3]");
+ }
+
+ 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: such as cannot find or execute main method.
+ */
+
+ @Test
+ public void testHelloWorldWithShebangJava(Path base) throws IOException {
+ tb.writeJavaFiles(base,
+ "#!/usr/bin/java --source 11\n" +
+ "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" +
+ "}");
+ Path file = base.resolve("HelloWorld.java");
+ testError(file,
+ file + ":1: error: illegal character: '#'\n" +
+ "#!/usr/bin/java --source 11\n" +
+ "^\n" +
+ file + ":1: error: class, interface, or enum expected\n" +
+ "#!/usr/bin/java --source 11\n" +
+ " ^\n" +
+ "2 errors\n",
+ "error: compilation failed");
+ }
+
+ @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 testNoSourceOnClassPath(Path base) throws IOException {
+ Path auxSrc = base.resolve("auxSrc");
+ tb.writeJavaFiles(auxSrc,
+ "public class Aux {\n" +
+ " static final String MESSAGE = \"Hello World\";\n" +
+ "}\n");
+
+ Path mainSrc = base.resolve("mainSrc");
+ tb.writeJavaFiles(mainSrc,
+ "import java.util.Arrays;\n" +
+ "class HelloWorld {\n" +
+ " public static void main(String... args) {\n" +
+ " System.out.println(Aux.MESSAGE + Arrays.toString(args));\n" +
+ " }\n" +
+ "}");
+
+ List<String> javacArgs = List.of("-classpath", auxSrc.toString());
+ List<String> classArgs = List.of("1", "2", "3");
+ String expectStdErr =
+ "testNoSourceOnClassPath/mainSrc/HelloWorld.java:4: error: cannot find symbol\n" +
+ " System.out.println(Aux.MESSAGE + Arrays.toString(args));\n" +
+ " ^\n" +
+ " symbol: variable Aux\n" +
+ " location: class HelloWorld\n" +
+ "1 error\n";
+ Result r = run(mainSrc.resolve("HelloWorld.java"), javacArgs, classArgs);
+ checkEmpty("stdout", r.stdOut);
+ checkEqual("stderr", r.stdErr, expectStdErr);
+ checkFault("exception", r.exception, "error: compilation failed");
+
+ }
+
+ // For any source file that is invoked through the OS shebang mechanism, invalid shebang
+ // lines will be caught and handled by the OS, before the launcher is even invoked.
+ // However, if such a file is passed directly to the launcher, perhaps using the --source
+ // option, a well-formed shebang line will be removed but a badly-formed one will be not be
+ // removed and will cause compilation errors.
+ @Test
+ public void testBadShebang(Path base) throws IOException {
+ tb.writeJavaFiles(base,
+ "#/usr/bin/java --source 11\n" +
+ "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" +
+ "}");
+ Path file = base.resolve("HelloWorld.java");
+ testError(file,
+ file + ":1: error: illegal character: '#'\n" +
+ "#/usr/bin/java --source 11\n" +
+ "^\n" +
+ file + ":1: error: class, interface, or enum expected\n" +
+ "#/usr/bin/java --source 11\n" +
+ " ^\n" +
+ "2 errors\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", "<BAD>");
+ System.setProperties(p);
+ try {
+ testError(file, "", "error: invalid value for --source option: <BAD>");
+ } finally {
+ System.setProperties(sysProps);
+ }
+ }
+
+ @Test
+ public void testEnablePreviewNoSource(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" +
+ "}");
+
+ String log = new JavaTask(tb)
+ .vmOptions("--enable-preview")
+ .className(base.resolve("HelloWorld.java").toString())
+ .run(Task.Expect.FAIL)
+ .getOutput(Task.OutputKind.STDERR);
+ checkEqual("stderr", log.trim(),
+ "error: --enable-preview must be used with --source");
+ }
+
+ @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");
+ }
+
+ 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<String> runtimeArgs, List<String> appArgs) {
+ List<String> 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 == null) {
+ error("No exception thrown; expected Main.Fault");
+ } else {
+ 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<String> 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;
+ }
+ }
+}