diff --git a/make/CompileInterimLangtools.gmk b/make/CompileInterimLangtools.gmk index bbc2d103696..855b88ca0a2 100644 --- a/make/CompileInterimLangtools.gmk +++ b/make/CompileInterimLangtools.gmk @@ -99,6 +99,9 @@ define SetupInterimModule EXCLUDE_FILES := $(TOPDIR)/src/$1/share/classes/module-info.java \ $(TOPDIR)/src/$1/share/classes/javax/tools/ToolProvider.java \ $(TOPDIR)/src/$1/share/classes/com/sun/tools/javac/launcher/Main.java \ + $(TOPDIR)/src/$1/share/classes/com/sun/tools/javac/launcher/MemoryContext.java \ + $(TOPDIR)/src/$1/share/classes/com/sun/tools/javac/launcher/MemoryModuleFinder.java \ + $(TOPDIR)/src/$1/share/classes/com/sun/tools/javac/launcher/SourceLauncher.java \ Standard.java, \ EXTRA_FILES := $(BUILDTOOLS_OUTPUTDIR)/gensrc/$1.interim/module-info.java \ $($1.interim_EXTRA_FILES), \ diff --git a/src/java.base/share/classes/module-info.java b/src/java.base/share/classes/module-info.java index 0e05c3542f0..17a66b856a1 100644 --- a/src/java.base/share/classes/module-info.java +++ b/src/java.base/share/classes/module-info.java @@ -228,6 +228,7 @@ module java.base { java.instrument, java.management.rmi, jdk.jartool, + jdk.compiler, jdk.jfr, jdk.jlink, jdk.jpackage; diff --git a/src/java.base/share/classes/sun/launcher/resources/launcher.properties b/src/java.base/share/classes/sun/launcher/resources/launcher.properties index 6bea2e1f51f..b61bf9540cd 100644 --- a/src/java.base/share/classes/sun/launcher/resources/launcher.properties +++ b/src/java.base/share/classes/sun/launcher/resources/launcher.properties @@ -32,7 +32,7 @@ java.launcher.opt.header = Usage: {0} [options] [args...]\n\ \ {0} [options] --module [/] [args...]\n\ \ (to execute the main class in a module)\n\ \ or {0} [options] [args]\n\ -\ (to execute a single source-file program)\n\n\ +\ (to execute a source-file program)\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\ diff --git a/src/java.base/share/native/libjli/java.c b/src/java.base/share/native/libjli/java.c index fbc9ecb1c4e..59cdeb770d3 100644 --- a/src/java.base/share/native/libjli/java.c +++ b/src/java.base/share/native/libjli/java.c @@ -178,7 +178,7 @@ static void FreeKnownVMs(); static jboolean IsWildCardEnabled(); -#define SOURCE_LAUNCHER_MAIN_ENTRY "jdk.compiler/com.sun.tools.javac.launcher.Main" +#define SOURCE_LAUNCHER_MAIN_ENTRY "jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher" /* * This reports error. VM will not be created and no usage is printed. diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java index 3c696a3e65d..fa911b5c04a 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Preview.java @@ -47,9 +47,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import static com.sun.tools.javac.code.Flags.RECORD; -import static com.sun.tools.javac.code.Flags.SEALED; -import static com.sun.tools.javac.code.Flags.NON_SEALED; import static com.sun.tools.javac.main.Option.PREVIEW; import com.sun.tools.javac.util.JCDiagnostic; @@ -84,7 +81,7 @@ public class Preview { private final Log log; private final Source source; - private static final Context.Key previewKey = new Context.Key<>(); + protected static final Context.Key previewKey = new Context.Key<>(); public static Preview instance(Context context) { Preview instance = context.get(previewKey); @@ -94,7 +91,8 @@ public class Preview { return instance; } - Preview(Context context) { + @SuppressWarnings("this-escape") + protected Preview(Context context) { context.put(previewKey, this); Options options = Options.instance(context); names = Names.instance(context); diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Fault.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Fault.java new file mode 100644 index 00000000000..a5916dbafd9 --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Fault.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, 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 com.sun.tools.javac.util.JCDiagnostic.Error; + +import java.io.Serial; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +/** + * A runtime exception used to report errors. + * + *

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 final class Fault extends RuntimeException { + @Serial + private static final long serialVersionUID = 2L; + + private static final String BUNDLE_NAME = "com.sun.tools.javac.resources.launcher"; + + private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); + + private static final String ERROR_PREFIX = RESOURCE_BUNDLE.getString("launcher.error"); + + /** + * Returns a localized string from a resource bundle. + * + * @param error the error for which to get the localized text + * @return the localized string + */ + private static String getMessage(Error error) { + String key = error.key(); + Object[] args = error.getArgs(); + try { + String resource = RESOURCE_BUNDLE.getString(key); + String message = MessageFormat.format(resource, args); + return ERROR_PREFIX + message; + } catch (MissingResourceException e) { + return "Cannot access resource; " + key + Arrays.toString(args); + } + } + + Fault(Error error) { + super(getMessage(error)); + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java deleted file mode 100644 index cd35ccaa3b7..00000000000 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Main.java +++ /dev/null @@ -1,827 +0,0 @@ -/* - * Copyright (c) 2018, 2023, 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.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -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.Constructor; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -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.security.CodeSigner; -import java.security.CodeSource; -import java.security.ProtectionDomain; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.MissingResourceException; -import java.util.NoSuchElementException; -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.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.MethodFinder; -import jdk.internal.misc.PreviewFeatures; -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 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) - .checkSecurityManager() - .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.getCause(); - } - } - - /** 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; - } - - /** - * Checks if a security manager is present and throws an exception if so. - * @return this object - * @throws Fault if a security manager is present - */ - @SuppressWarnings("removal") - private Main checkSecurityManager() throws Fault { - if (System.getSecurityManager() != null) { - throw new Fault(Errors.SecurityManager); - } - return this; - } - - /** - * 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); - - Context context = new Context(file.toAbsolutePath()); - String mainClassName = compile(file, getJavacOpts(runtimeArgs), context); - - String[] mainArgs = Arrays.copyOfRange(args, 1, args.length); - execute(mainClassName, mainArgs, context); - } - - /** - * Returns the path for the filename found in the first of an array of arguments. - * - * @param args the array - * @return the path, as given in the array of args - * @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 #!}. - * - *

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 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")) { - // 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: - if (opt.startsWith("-agentlib:jdwp=") || opt.startsWith("-Xrunjdwp:")) { - javacOpts.add("-g"); - } - // ignore all other runtime args - } - } - - // add implicit options - javacOpts.add("-proc:none"); - javacOpts.add("-Xdiags:verbose"); - javacOpts.add("-Xlint:deprecation"); - javacOpts.add("-Xlint:unchecked"); - javacOpts.add("-Xlint:-options"); - javacOpts.add("-XDsourceLauncher"); - 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 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); - } - TypeElement mainClass = l.mainClass; - String mainClassName = 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 mainArgs 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[] mainArgs, Context context) - throws Fault, InvocationTargetException { - System.setProperty("jdk.launcher.sourcefile", context.file.toString()); - ClassLoader cl = context.getClassLoader(ClassLoader.getSystemClassLoader()); - - Class appClass; - try { - appClass = Class.forName(mainClassName, true, cl); - } catch (ClassNotFoundException e) { - throw new Fault(Errors.CantFindClass(mainClassName)); - } - - Method mainMethod = MethodFinder.findMainMethod(appClass); - - if (mainMethod == null) { - throw new Fault(Errors.CantFindMainMethod(mainClassName)); - } - - boolean isStatic = Modifier.isStatic(mainMethod.getModifiers()); - Object instance = null; - - if (!isStatic) { - Constructor constructor; - try { - constructor = appClass.getDeclaredConstructor(); - } catch (NoSuchMethodException e) { - throw new Fault(Errors.CantFindConstructor(mainClassName)); - } - - try { - constructor.setAccessible(true); - instance = constructor.newInstance(); - } catch (InstantiationException | IllegalAccessException e) { - throw new Fault(Errors.CantAccessConstructor(mainClassName)); - } - } - - try { - // Similar to sun.launcher.LauncherHelper#executeMainClass - // but duplicated here to prevent additional launcher frames - mainMethod.setAccessible(true); - Object receiver = isStatic ? appClass : instance; - - if (mainMethod.getParameterCount() == 0) { - mainMethod.invoke(receiver); - } else { - mainMethod.invoke(receiver, (Object)mainArgs); - } - } 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.getCause(); - 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 encapsulate 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 final Path file; - private final Map inMemoryClasses = new HashMap<>(); - - Context(Path file) { - this.file = file; - } - - JavaFileManager getFileManager(StandardJavaFileManager delegate) { - return new MemoryFileManager(inMemoryClasses, delegate); - } - - ClassLoader getClassLoader(ClassLoader parent) { - return new MemoryClassLoader(inMemoryClasses, parent, file); - } - } - - /** - * An in-memory file manager. - * - *

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 { - 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 ByteArrayOutputStream() { - @Override - public void close() throws IOException { - super.close(); - map.put(className, toByteArray()); - } - }; - } - }; - } - } - - /** - * An in-memory classloader, that uses an in-memory cache of classes written by - * {@link MemoryFileManager}. - * - *

The classloader inverts the standard parent-delegation model, giving preference - * to classes defined in the source file before classes known to the parent (such - * as any like-named classes that might be found on the application class path.) - */ - private static class MemoryClassLoader extends ClassLoader { - /** - * The map of all classes found in the source file, indexed by - * {@link ClassLoader#name binary name}. - */ - private final Map sourceFileClasses; - - /** - * A minimal protection domain, specifying a code source of the source file itself, - * used for classes found in the source file and defined by this loader. - */ - private final ProtectionDomain domain; - - MemoryClassLoader(Map sourceFileClasses, ClassLoader parent, Path file) { - super(parent); - this.sourceFileClasses = sourceFileClasses; - CodeSource codeSource; - try { - codeSource = new CodeSource(file.toUri().toURL(), (CodeSigner[]) null); - } catch (MalformedURLException e) { - codeSource = null; - } - domain = new ProtectionDomain(codeSource, null, this, null); - } - - /** - * Override loadClass to check for classes defined in the source file - * before checking for classes in the parent class loader, - * including those on the classpath. - * - * {@code loadClass(String name)} calls this method, and so will have the same behavior. - * - * @param name the name of the class to load - * @param resolve whether or not to resolve the class - * @return the class - * @throws ClassNotFoundException if the class is not found - */ - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - synchronized (getClassLoadingLock(name)) { - Class c = findLoadedClass(name); - if (c == null) { - if (sourceFileClasses.containsKey(name)) { - c = findClass(name); - } else { - c = getParent().loadClass(name); - } - if (resolve) { - resolveClass(c); - } - } - return c; - } - } - - - /** - * Override getResource to check for resources (i.e. class files) defined in the - * source file before checking resources in the parent class loader, - * including those on the class path. - * - * {@code getResourceAsStream(String name)} calls this method, - * and so will have the same behavior. - * - * @param name the name of the resource - * @return a URL for the resource, or null if not found - */ - @Override - public URL getResource(String name) { - if (sourceFileClasses.containsKey(toBinaryName(name))) { - return findResource(name); - } else { - return getParent().getResource(name); - } - } - - /** - * Override getResources to check for resources (i.e. class files) defined in the - * source file before checking resources in the parent class loader, - * including those on the class path. - * - * @param name the name of the resource - * @return an enumeration of the resources in this loader and in the application class loader - */ - @Override - public Enumeration getResources(String name) throws IOException { - URL u = findResource(name); - Enumeration e = getParent().getResources(name); - if (u == null) { - return e; - } else { - List list = new ArrayList<>(); - list.add(u); - while (e.hasMoreElements()) { - list.add(e.nextElement()); - } - return Collections.enumeration(list); - } - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - byte[] bytes = sourceFileClasses.get(name); - if (bytes == null) { - throw new ClassNotFoundException(name); - } - return defineClass(name, bytes, 0, bytes.length, domain); - } - - @Override - public URL findResource(String name) { - String binaryName = toBinaryName(name); - if (binaryName == null || sourceFileClasses.get(binaryName) == null) { - return null; - } - - URLStreamHandler handler = this.handler; - if (handler == null) { - this.handler = handler = new MemoryURLStreamHandler(); - } - - try { - @SuppressWarnings("deprecation") - var result = new URL(PROTOCOL, null, -1, name, handler); - return result; - } catch (MalformedURLException e) { - return null; - } - } - - @Override - public Enumeration findResources(String name) { - return new Enumeration() { - private URL next = findResource(name); - - @Override - public boolean hasMoreElements() { - return (next != null); - } - - @Override - public URL nextElement() { - if (next == null) { - throw new NoSuchElementException(); - } - URL u = next; - next = null; - return u; - } - }; - } - - /** - * Converts a "resource name" (as used in the getResource* methods) - * to a binary name if the name identifies a class, or null otherwise. - * @param name the resource name - * @return the binary name - */ - private String toBinaryName(String name) { - if (!name.endsWith(".class")) { - return null; - } - return name.substring(0, name.length() - DOT_CLASS_LENGTH).replace('/', '.'); - } - - private static final int DOT_CLASS_LENGTH = ".class".length(); - private final String PROTOCOL = "sourcelauncher-" + getClass().getSimpleName() + hashCode(); - private URLStreamHandler handler; - - /** - * A URLStreamHandler for use with URLs returned by MemoryClassLoader.getResource. - */ - private class MemoryURLStreamHandler extends URLStreamHandler { - @Override - public URLConnection openConnection(URL u) { - if (!u.getProtocol().equalsIgnoreCase(PROTOCOL)) { - throw new IllegalArgumentException(u.toString()); - } - return new MemoryURLConnection(u, sourceFileClasses.get(toBinaryName(u.getPath()))); - } - - } - - /** - * A URLConnection for use with URLs returned by MemoryClassLoader.getResource. - */ - private static class MemoryURLConnection extends URLConnection { - private byte[] bytes; - private InputStream in; - - MemoryURLConnection(URL u, byte[] bytes) { - super(u); - this.bytes = bytes; - } - - @Override - public void connect() throws IOException { - if (!connected) { - if (bytes == null) { - throw new FileNotFoundException(getURL().getPath()); - } - in = new ByteArrayInputStream(bytes); - connected = true; - } - } - - @Override - public InputStream getInputStream() throws IOException { - connect(); - return in; - } - - @Override - public long getContentLengthLong() { - return bytes.length; - } - - @Override - public String getContentType() { - return "application/octet-stream"; - } - } - } -} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryClassLoader.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryClassLoader.java new file mode 100644 index 00000000000..c6fa5d11778 --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryClassLoader.java @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2023, 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.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.module.ModuleDescriptor; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.nio.file.Files; +import java.security.CodeSigner; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.Function; + +/** + * An in-memory classloader, that uses an in-memory cache of classes written by + * {@link MemoryFileManager}. + * + *

The classloader inverts the standard parent-delegation model, giving preference + * to classes defined in the source file before classes known to the parent (such + * as any like-named classes that might be found on the application class path.) + * + *

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.

+ */ +final class MemoryClassLoader extends ClassLoader { + /** + * The parent class loader instance. + */ + private final ClassLoader parentClassLoader; + + /** + * The map of all classes found in the source file, indexed by + * {@link Class#getName()} binary name. + */ + private final Map sourceFileClasses; + + /** + * A minimal protection domain, specifying a code source of the source file itself, + * used for classes found in the source file and defined by this loader. + */ + private final ProtectionDomain domain; + + private final ModuleDescriptor moduleDescriptor; + private final ProgramDescriptor programDescriptor; + private final Function compileSourceFile; + + MemoryClassLoader(Map sourceFileClasses, + ClassLoader parentClassLoader, + ModuleDescriptor moduleDescriptor, + ProgramDescriptor programDescriptor, + Function compileSourceFile) { + super(parentClassLoader); + this.parentClassLoader = parentClassLoader; + this.sourceFileClasses = sourceFileClasses; + CodeSource codeSource; + try { + codeSource = new CodeSource(programDescriptor.fileObject().getFile().toUri().toURL(), (CodeSigner[])null); + } catch (MalformedURLException e) { + codeSource = null; + } + domain = new ProtectionDomain(codeSource, null, this, null); + this.moduleDescriptor = moduleDescriptor; + this.programDescriptor = programDescriptor; + this.compileSourceFile = compileSourceFile; + } + + /** + * Override loadClass to check for classes defined in the source file + * before checking for classes in the parent class loader, + * including those on the classpath. + *

+ * {@code loadClass(String name)} calls this method, and so will have the same behavior. + * + * @param name the name of the class to load + * @param resolve whether to resolve the class + * @return the class + * @throws ClassNotFoundException if the class is not found + */ + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + c = findOrCompileClass(name); + if (c == null) { + c = parentClassLoader.loadClass(name); + } + if (resolve) { + resolveClass(c); + } + } + return c; + } + } + + + /** + * Override getResource to check for resources (i.e. class files) defined in the + * source file before checking resources in the parent class loader, + * including those on the class path. + *

+ * {@code getResourceAsStream(String name)} calls this method, + * and so will have the same behavior. + * + * @param name the name of the resource + * @return a URL for the resource, or null if not found + */ + @Override + public URL getResource(String name) { + if (sourceFileClasses.containsKey(toBinaryName(name))) { + return findResource(name); + } + var programPath = programDescriptor.sourceRootPath().resolve(name); + if (Files.exists(programPath)) { + try { + return programPath.toUri().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + return parentClassLoader.getResource(name); + } + + /** + * Override getResources to check for resources (i.e. class files) defined in the + * source file before checking resources in the parent class loader, + * including those on the class path. + * + * @param name the name of the resource + * @return an enumeration of the resources in this loader and in the application class loader + */ + @Override + public Enumeration getResources(String name) throws IOException { + URL u = findResource(name); + Enumeration e = parentClassLoader.getResources(name); + if (u == null) { + return e; + } else { + List list = new ArrayList<>(); + list.add(u); + while (e.hasMoreElements()) { + list.add(e.nextElement()); + } + return Collections.enumeration(list); + } + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + var foundOrCompiledClass = findOrCompileClass(name); + if (foundOrCompiledClass == null) { + throw new ClassNotFoundException(name); + } + return foundOrCompiledClass; + } + + private Class findOrCompileClass(String name) { + byte[] bytes = sourceFileClasses.get(name); + if (bytes == null) { + bytes = compileSourceFile.apply(name); + if (bytes == null) { + return null; + } + } + return defineClass(name, bytes, 0, bytes.length, domain); + } + + @Override + protected URL findResource(String moduleName, String name) throws IOException { + if (moduleName == null) { + return getResource(name); + } + if (moduleDescriptor != null && moduleDescriptor.name().equals(moduleName)) { + return getResource(name); + } + return super.findResource(moduleName, name); + } + + @Override + public URL findResource(String name) { + String binaryName = toBinaryName(name); + if (binaryName == null || sourceFileClasses.get(binaryName) == null) { + return null; + } + + URLStreamHandler handler = this.handler; + if (handler == null) { + this.handler = handler = new MemoryURLStreamHandler(); + } + + try { + var uri = new URI(PROTOCOL, name, null); + return URL.of(uri, handler); + } catch (URISyntaxException | MalformedURLException e) { + return null; + } + } + + @Override + public Enumeration findResources(String name) { + return new Enumeration() { + private URL next = findResource(name); + + @Override + public boolean hasMoreElements() { + return (next != null); + } + + @Override + public URL nextElement() { + if (next == null) { + throw new NoSuchElementException(); + } + URL u = next; + next = null; + return u; + } + }; + } + + /** + * Converts a "resource name" (as used in the getResource* methods) + * to a binary name if the name identifies a class, or null otherwise. + * + * @param name the resource name + * @return the binary name + */ + private String toBinaryName(String name) { + if (!name.endsWith(".class")) { + return null; + } + return name.substring(0, name.length() - DOT_CLASS_LENGTH).replace('/', '.'); + } + + private static final int DOT_CLASS_LENGTH = ".class".length(); + private final String PROTOCOL = "sourcelauncher-" + getClass().getSimpleName() + hashCode(); + private URLStreamHandler handler; + + /** + * A URLStreamHandler for use with URLs returned by MemoryClassLoader.getResource. + */ + private class MemoryURLStreamHandler extends URLStreamHandler { + @Override + public URLConnection openConnection(URL u) { + if (!u.getProtocol().equalsIgnoreCase(PROTOCOL)) { + throw new IllegalArgumentException(u.toString()); + } + return new MemoryURLConnection(u, sourceFileClasses.get(toBinaryName(u.getPath()))); + } + + } + + /** + * A URLConnection for use with URLs returned by MemoryClassLoader.getResource. + */ + private static class MemoryURLConnection extends URLConnection { + private final byte[] bytes; + private InputStream in; + + MemoryURLConnection(URL u, byte[] bytes) { + super(u); + this.bytes = bytes; + } + + @Override + public void connect() throws IOException { + if (!connected) { + if (bytes == null) { + throw new FileNotFoundException(getURL().getPath()); + } + in = new ByteArrayInputStream(bytes); + connected = true; + } + } + + @Override + public InputStream getInputStream() throws IOException { + connect(); + return in; + } + + @Override + public long getContentLengthLong() { + return bytes.length; + } + + @Override + public String getContentType() { + return "application/octet-stream"; + } + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryContext.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryContext.java new file mode 100644 index 00000000000..ff36db080b2 --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryContext.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2023, 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 com.sun.source.util.TaskEvent; +import com.sun.source.util.TaskListener; +import com.sun.tools.javac.api.JavacTool; +import com.sun.tools.javac.code.Preview; +import com.sun.tools.javac.file.JavacFileManager; +import com.sun.tools.javac.resources.LauncherProperties.Errors; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Context.Factory; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.TypeElement; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReference; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * An object to encapsulate the set of in-memory classes, such that + * they can be written by a file manager and subsequently used by + * a class loader. + * + *

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.

+ */ +final class MemoryContext { + private final PrintWriter out; + private final ProgramDescriptor descriptor; + + private final RelevantJavacOptions options; + + private final JavacTool compiler; + private final JavacFileManager standardFileManager; + private final JavaFileManager memoryFileManager; + + private final Map inMemoryClasses = new HashMap<>(); + + MemoryContext(PrintWriter out, ProgramDescriptor descriptor, RelevantJavacOptions options) throws Fault { + this.out = out; + this.descriptor = descriptor; + this.options = options; + + this.compiler = JavacTool.create(); + this.standardFileManager = compiler.getStandardFileManager(null, null, null); + try { + List searchPath = descriptor.fileObject().isFirstLineIgnored() ? List.of() : List.of(descriptor.sourceRootPath().toFile()); + standardFileManager.setLocation(StandardLocation.SOURCE_PATH, searchPath); + } catch (IOException e) { + throw new Error("unexpected exception from file manager", e); + } + this.memoryFileManager = new MemoryFileManager(inMemoryClasses, standardFileManager); + } + + ProgramDescriptor getProgramDescriptor() { + return descriptor; + } + + String getSourceFileAsString() { + return descriptor.fileObject().getFile().toAbsolutePath().toString(); + } + + Set getNamesOfCompiledClasses() { + return Set.copyOf(inMemoryClasses.keySet()); + } + + /** + * 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. + * + * @return the list of top-level types defined in the source file + * @throws Fault if any compilation errors occur, or if no class was found + */ + List compileProgram() throws Fault { + var units = new ArrayList(); + units.add(descriptor.fileObject()); + if (descriptor.isModular()) { + var root = descriptor.sourceRootPath(); + units.add(standardFileManager.getJavaFileObject(root.resolve("module-info.java"))); + } + var opts = options.forProgramCompilation(); + var context = new Context(); + MemoryPreview.registerInstance(context); + var task = compiler.getTask(out, memoryFileManager, null, opts, null, units, context); + var fileUri = descriptor.fileObject().toUri(); + var names = new ArrayList(); + task.addTaskListener(new TaskListener() { + @Override + public void started(TaskEvent event) { + if (event.getKind() != TaskEvent.Kind.ANALYZE) return; + TypeElement element = event.getTypeElement(); + if (element.getNestingKind() != NestingKind.TOP_LEVEL) return; + JavaFileObject source = event.getSourceFile(); + if (source == null) return; + if (!source.toUri().equals(fileUri)) return; + ElementKind kind = element.getKind(); + if (kind != ElementKind.CLASS + && kind != ElementKind.ENUM + && kind != ElementKind.INTERFACE + && kind != ElementKind.RECORD) + return; + var name = element.getQualifiedName().toString(); + names.add(name); + } + }); + var ok = task.call(); + if (!ok) { + throw new Fault(Errors.CompilationFailed); + } + if (names.isEmpty()) { + throw new Fault(Errors.NoClass); + } + return List.copyOf(names); + } + + /** + * Determines a source file from the given class name and compiles it. + * Any messages generated during compilation will be written to the stream + * provided when this object was created. + *

+ * This method is passed a reference to an instance of {@link MemoryClassLoader}, + * that uses it to compile a source file on demand. + * + * @param name the name of the class to be compiled. + * @return the byte code of the compiled class or {@code null} + * if no source file was found for the given name + */ + byte[] compileJavaFileByName(String name) { + // Determine source file from class name. + var firstDollarSign = name.indexOf('$'); // [package . ] name [ $ enclosed [$ deeper] ] + var packageAndClassName = firstDollarSign == -1 ? name : name.substring(0, firstDollarSign); + var path = packageAndClassName.replace('.', '/') + ".java"; + var file = descriptor.sourceRootPath().resolve(path); + + // Trivial case: no matching source file exists + if (Files.notExists(file)) return null; + + // Compile source file (unit) with similar options as the program. + var opts = options.forSubsequentCompilations(); + var unit = standardFileManager.getJavaFileObject(file); + var task = compiler.getTask(out, memoryFileManager, null, opts, null, List.of(unit)); + + var ok = task.call(); + if (!ok) { + var fault = new Fault(Errors.CompilationFailed); + // Don't throw fault - fail fast! + out.println(fault.getMessage()); + System.exit(2); + } + + // The memory file manager stored bytes in the context map, indexed by the class names. + return inMemoryClasses.get(name); + } + + /** + * Create a new class load for the main entry-point class. + * + * @param parent the class loader to be used as the parent loader + * @param mainClassName the fully-qualified name of the application class to load + * @return class loader object able to find and load the desired class + * @throws ClassNotFoundException if the class cannot be located + * @throws Fault if a modular application class is in the unnamed package + */ + ClassLoader newClassLoaderFor(ClassLoader parent, String mainClassName) throws ClassNotFoundException, Fault { + var moduleInfoBytes = inMemoryClasses.get("module-info"); + if (moduleInfoBytes == null) { + // Trivial case: no compiled module descriptor available, no extra module layer required + return new MemoryClassLoader(inMemoryClasses, parent, null, descriptor, this::compileJavaFileByName); + } + + // Ensure main class resides in a named package. + var lastDotInMainClassName = mainClassName.lastIndexOf('.'); + if (lastDotInMainClassName == -1) { + throw new Fault(Errors.UnnamedPkgNotAllowedNamedModules); + } + + var bootLayer = ModuleLayer.boot(); + var parentLayer = bootLayer; + var parentLoader = parent; + + // Optionally create module layer for all modules on the module path. + var modulePathFinder = createModuleFinderFromModulePath(); + var modulePathModules = modulePathFinder.findAll().stream().map(ModuleReference::descriptor).map(ModuleDescriptor::name).toList(); + if (!modulePathModules.isEmpty()) { + var modulePathConfiguration = bootLayer.configuration().resolveAndBind(modulePathFinder, ModuleFinder.of(), Set.copyOf(modulePathModules)); + var modulePathLayer = ModuleLayer.defineModulesWithOneLoader(modulePathConfiguration, List.of(bootLayer), parent).layer(); + parentLayer = modulePathLayer; + parentLoader = modulePathLayer.findLoader(modulePathModules.getFirst()); + } + + // Create in-memory module layer for the modular application. + var applicationModule = ModuleDescriptor.read(ByteBuffer.wrap(moduleInfoBytes), descriptor::computePackageNames); + var memoryFinder = new MemoryModuleFinder(inMemoryClasses, applicationModule, descriptor); + var memoryConfig = parentLayer.configuration().resolveAndBind(memoryFinder, ModuleFinder.of(), Set.of(applicationModule.name())); + var memoryClassLoader = new MemoryClassLoader(inMemoryClasses, parentLoader, applicationModule, descriptor, this::compileJavaFileByName); + var memoryController = ModuleLayer.defineModules(memoryConfig, List.of(parentLayer), __ -> memoryClassLoader); + var memoryLayer = memoryController.layer(); + + // Make application class accessible from the calling (unnamed) module, that loaded this class. + var module = memoryLayer.findModule(applicationModule.name()).orElseThrow(); + var mainClassNamePackageName = mainClassName.substring(0, lastDotInMainClassName); + memoryController.addOpens(module, mainClassNamePackageName, getClass().getModule()); + + return memoryLayer.findLoader(applicationModule.name()); + } + + private static ModuleFinder createModuleFinderFromModulePath() { + var elements = System.getProperty("jdk.module.path"); + if (elements == null) { + return ModuleFinder.of(); + } + var paths = Arrays.stream(elements.split(File.pathSeparator)).map(Path::of); + return ModuleFinder.of(paths.toArray(Path[]::new)); + } + + static class MemoryPreview extends Preview { + static void registerInstance(Context context) { + context.put(previewKey, (Factory)MemoryPreview::new); + } + + MemoryPreview(Context context) { + super(context); + } + + @Override + public void reportDeferredDiagnostics() { + // suppress diagnostics like "Note: Recompile with -Xlint:preview for details." + } + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryFileManager.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryFileManager.java new file mode 100644 index 00000000000..15a2be87cdb --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryFileManager.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023, 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 javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardLocation; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.util.Map; + +/** + * An in-memory file manager. + * + *

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. + * + *

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.

+ */ +final 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 ByteArrayOutputStream() { + @Override + public void close() throws IOException { + super.close(); + map.put(className, toByteArray()); + } + }; + } + }; + } + + @Override + public boolean contains(Location location, FileObject fo) throws IOException { + return fo instanceof ProgramFileObject || super.contains(location, fo); + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryModuleFinder.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryModuleFinder.java new file mode 100644 index 00000000000..d82580c993f --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryModuleFinder.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2023, 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.ByteArrayInputStream; +import java.io.IOError; +import java.io.IOException; +import java.io.InputStream; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReader; +import java.lang.module.ModuleReference; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import jdk.internal.module.Resources; + +/** + * An in-memory module finder, that uses an in-memory cache of classes written by + * {@link MemoryFileManager}. + * + *

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.

+ */ +record MemoryModuleFinder(Map classes, + ModuleDescriptor descriptor, + ProgramDescriptor programDescriptor) implements ModuleFinder { + @Override + public Optional find(String name) { + if (name.equals(descriptor.name())) { + return Optional.of(new MemoryModuleReference()); + } + return Optional.empty(); + } + + @Override + public Set findAll() { + return Set.of(new MemoryModuleReference()); + } + + class MemoryModuleReference extends ModuleReference { + protected MemoryModuleReference() { + super(descriptor, URI.create("memory:///" + descriptor.toNameAndVersion())); + } + + @Override + public ModuleReader open() { + return new MemoryModuleReader(); + } + } + + // Implementation based on jdk.internal.module.ModuleReferences#ExplodedModuleReader + class MemoryModuleReader implements ModuleReader { + private volatile boolean closed; + + private void ensureOpen() throws IOException { + if (closed) { + throw new IOException("ModuleReader is closed"); + } + } + + public Optional find(String name) throws IOException { + ensureOpen(); + // Try to find an in-memory compiled class first + if (classes.get(name) != null) { + return Optional.of(URI.create("memory:///" + name.replace('.', '/') + ".class")); + } + // Try to find file resource from root path next + Path path = Resources.toFilePath(programDescriptor.sourceRootPath(), name); + if (path != null) { + try { + return Optional.of(path.toUri()); + } catch (IOError error) { + throw (IOException) error.getCause(); + } + } else { + return Optional.empty(); + } + } + + public Optional open(String name) throws IOException { + ensureOpen(); + // Try to find an in-memory compiled class first + byte[] bytes = classes.get(name); + if (bytes != null) { + return Optional.of(new ByteArrayInputStream(bytes)); + } + // Try to find file resource from root path next + Path path = Resources.toFilePath(programDescriptor.sourceRootPath(), name); + return path != null ? Optional.of(Files.newInputStream(path)) : Optional.empty(); + } + + public Optional read(String name) throws IOException { + ensureOpen(); + // Try to find an in-memory compiled class first + byte[] bytes = classes.get(name); + if (bytes != null) { + return Optional.of(ByteBuffer.wrap(bytes)); + } + // Try to find file resource from root path next + Path path = Resources.toFilePath(programDescriptor.sourceRootPath(), name); + return path != null ? Optional.of(ByteBuffer.wrap(Files.readAllBytes(path))) : Optional.empty(); + } + + public Stream list() throws IOException { + ensureOpen(); + var root = programDescriptor.sourceRootPath(); + var list = new ArrayList(); + classes.keySet().stream().map(name -> name.replace('.', '/') + ".class").forEach(list::add); + try (var stream = Files.walk(root, Integer.MAX_VALUE, new FileVisitOption[0])) { + stream + .map(file -> Resources.toResourceName(root, file)) + .filter(name -> !name.isEmpty()) + .forEach(list::add); + } + Collections.sort(list); + return list.stream(); + } + + public void close() { + this.closed = true; + } + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/ProgramDescriptor.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/ProgramDescriptor.java new file mode 100644 index 00000000000..9c9831ecedf --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/ProgramDescriptor.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2023, 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 com.sun.tools.javac.api.JavacTool; +import com.sun.tools.javac.resources.LauncherProperties.Errors; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +/** + * Describes a launch-able Java compilation unit. + * + *

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 record ProgramDescriptor(ProgramFileObject fileObject, Optional packageName, Path sourceRootPath) { + static ProgramDescriptor of(ProgramFileObject fileObject) throws Fault { + var file = fileObject.getFile(); + try { + var compiler = JavacTool.create(); + var standardFileManager = compiler.getStandardFileManager(null, null, null); + var units = List.of(fileObject); + var task = compiler.getTask(null, standardFileManager, diagnostic -> {}, null, null, units); + for (var tree : task.parse()) { + var packageTree = tree.getPackage(); + if (packageTree != null) { + var packageName = packageTree.getPackageName().toString(); + var root = computeSourceRootPath(file, packageName); + return new ProgramDescriptor(fileObject, Optional.of(packageName), root); + } + } + } catch (IOException ignore) { + // fall through to let actual compilation determine the error message + } + var root = computeSourceRootPath(file, ""); + return new ProgramDescriptor(fileObject, Optional.empty(), root); + } + + public static Path computeSourceRootPath(Path program, String packageName) { + var absolute = program.normalize().toAbsolutePath(); + var absoluteRoot = absolute.getRoot(); + assert absoluteRoot != null; + // unnamed package "": program's directory is the root path + if (packageName.isEmpty()) { + var parent = absolute.getParent(); + if (parent == null) return absoluteRoot; + return parent; + } + // named package "a.b.c": ensure end of path to program is "a/b/c" + var packagePath = Path.of(packageName.replace('.', '/')); + var ending = packagePath.resolve(program.getFileName()); + if (absolute.endsWith(ending)) { + var max = absolute.getNameCount() - ending.getNameCount(); + if (max == 0) return absoluteRoot; + return absoluteRoot.resolve(absolute.subpath(0, max)); + } + throw new Fault(Errors.MismatchEndOfPathAndPackageName(packageName, program)); + } + + public boolean isModular() { + return Files.exists(sourceRootPath.resolve("module-info.java")); + } + + public Set computePackageNames() { + try (var stream = Files.find(sourceRootPath, 99, (path, attr) -> attr.isDirectory())) { + var names = new TreeSet(); + stream.filter(ProgramDescriptor::containsAtLeastOneRegularFile) + .map(sourceRootPath::relativize) + .map(Path::toString) + .filter(string -> !string.isEmpty()) + .map(string -> string.replace(File.separatorChar, '.')) + .forEach(names::add); + return names; + } catch (IOException exception) { + throw new UncheckedIOException(exception); + } + } + + private static boolean containsAtLeastOneRegularFile(Path directory) { + try (var stream = Files.newDirectoryStream(directory, Files::isRegularFile)) { + return stream.iterator().hasNext(); + } catch (IOException exception) { + throw new UncheckedIOException(exception); + } + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/ProgramFileObject.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/ProgramFileObject.java new file mode 100644 index 00000000000..f697ebf199e --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/ProgramFileObject.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023, 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 com.sun.tools.javac.resources.LauncherProperties.Errors; + +import javax.lang.model.SourceVersion; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; + +import static javax.tools.JavaFileObject.Kind.SOURCE; + +/** + * The program to launch as Java file object. + * + *

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.

+ */ +final class ProgramFileObject extends SimpleJavaFileObject { + + /** + * Reads a source file, ignoring the first line if it is not a Java source file and + * it begins with {@code #!}. + * + *

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 + */ + static ProgramFileObject of(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(System.lineSeparator()); // 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 ProgramFileObject(file, sb, ignoreFirstLine); + } + } catch (IOException e) { + throw new Fault(Errors.CantReadFile(file, e)); + } + } + + private final Path file; + private final CharSequence chars; + private final boolean ignoreFirstLine; + + ProgramFileObject(Path file, CharSequence chars, boolean ignoreFirstLine) { + super(file.toUri(), SOURCE); + this.file = file; + this.chars = chars; + this.ignoreFirstLine = ignoreFirstLine; + } + + public Path getFile() { + return file; + } + + public boolean isFirstLineIgnored() { + return ignoreFirstLine; + } + + @Override + public String getName() { + return file.toString(); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return chars; + } + + @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 + "]"; + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/RelevantJavacOptions.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/RelevantJavacOptions.java new file mode 100644 index 00000000000..88d1c875bc5 --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/RelevantJavacOptions.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023, 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 com.sun.tools.javac.code.Source; +import com.sun.tools.javac.main.Option; +import com.sun.tools.javac.resources.LauncherProperties.Errors; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents runtime arguments that are relevant to {@code javac}. + * + *

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.

+ */ +record RelevantJavacOptions(List forProgramCompilation, + List forSubsequentCompilations) { + + /** + * 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 program the program descriptor + * @param runtimeArgs the runtime arguments + * @return the subset of the runtime arguments + */ + static RelevantJavacOptions of(ProgramDescriptor program, String... runtimeArgs) throws Fault { + var programOptions = new ArrayList(); + var subsequentOptions = 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)); + } + programOptions.addAll(List.of("--release", sourceOpt)); + subsequentOptions.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", "-classpath", "-cp", + "--module-path", "-p", + "--add-exports", "--add-modules", + "--limit-modules", + "--patch-module", + "--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")) { + var modules = computeListOfAddModules(program, value); + if (modules.isEmpty()) { + break; + } + value = String.join(",", modules); + } + programOptions.add(opt); + programOptions.add(value); + var javacOption = Option.lookup(opt); + if (javacOption != null && javacOption.isInBasicOptionGroup()) { + subsequentOptions.add(opt); + subsequentOptions.add(value); + } + } + case "--enable-preview" -> { + programOptions.add(opt); + subsequentOptions.add(opt); + if (sourceOpt == null) { + throw new Fault(Errors.EnablePreviewRequiresSource); + } + } + default -> { + if (opt.startsWith("-agentlib:jdwp=") || opt.startsWith("-Xrunjdwp:")) { + programOptions.add("-g"); + subsequentOptions.add("-g"); + } + } + // ignore all other runtime args + } + } + + // add implicit options to both lists + var implicitOptions = """ + -proc:none + -implicit:none + -Xprefer:source + -Xdiags:verbose + -Xlint:deprecation + -Xlint:unchecked + -Xlint:-options + -XDsourceLauncher + """; + implicitOptions.lines() + .filter(line -> !line.isBlank()) + .forEach(option -> { + programOptions.add(option); + subsequentOptions.add(option); + }); + + return new RelevantJavacOptions(List.copyOf(programOptions), List.copyOf(subsequentOptions)); + } + + private static List computeListOfAddModules(ProgramDescriptor program, String value) { + var modules = new ArrayList<>(List.of(value.split(","))); + // these options are only supported at run time; + // they are not required or supported at compile time + modules.remove("ALL-DEFAULT"); + modules.remove("ALL-SYSTEM"); + + // ALL-MODULE-PATH can only be used when compiling the + // unnamed module or when compiling in the context of + // an automatic module + if (program.isModular()) { + modules.remove("ALL-MODULE-PATH"); + } + return modules; + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Result.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Result.java new file mode 100644 index 00000000000..a95c0fa5c7d --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/Result.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023, 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.util.Set; + +/** + * Contains information about the launched program. + * + *

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.

+ * + * @param programClass the class instance of the launched program. + * @param classNames the names of classes compiled into memory. + */ +public record Result(Class programClass, Set classNames) {} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/SourceLauncher.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/SourceLauncher.java new file mode 100644 index 00000000000..75dc779cb33 --- /dev/null +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/SourceLauncher.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2023, 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 com.sun.tools.javac.resources.LauncherProperties.Errors; + +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +import jdk.internal.misc.MethodFinder; +import jdk.internal.misc.VM; + +/** + * 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 final class SourceLauncher { + /** + * 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 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 SourceLauncher(System.err) + .checkSecurityManager() + .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.getCause(); + } + } + + /** Stream for reporting errors, such as compilation errors. */ + private final PrintWriter out; + + /** + * Creates an instance of this class, providing a stream to which to report + * any errors. + * + * @param out the stream + */ + public SourceLauncher(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 SourceLauncher(PrintWriter out) { + this.out = out; + } + + /** + * Checks if a security manager is present and throws an exception if so. + * @return this object + * @throws Fault if a security manager is present + */ + @SuppressWarnings("removal") + private SourceLauncher checkSecurityManager() throws Fault { + if (System.getSecurityManager() != null) { + throw new Fault(Errors.SecurityManager); + } + return this; + } + + /** + * 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 Result run(String[] runtimeArgs, String[] args) throws Fault, InvocationTargetException { + Path file = getFile(args); + + ProgramDescriptor program = ProgramDescriptor.of(ProgramFileObject.of(file)); + RelevantJavacOptions options = RelevantJavacOptions.of(program, runtimeArgs); + MemoryContext context = new MemoryContext(out, program, options); + List names = context.compileProgram(); + + String[] mainArgs = Arrays.copyOfRange(args, 1, args.length); + var appClass = execute(names, mainArgs, context); + + return new Result(appClass, context.getNamesOfCompiledClasses()); + } + + /** + * Returns the path for the filename found in the first of an array of arguments. + * + * @param args the array + * @return the path, as given in the array of args + * @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; + } + + /** + * Invokes the {@code main} method of a program class, using a class loader that + * will load recently compiled classes from memory. + * + * @param topLevelClassNames the names of classes in the program compilation unit + * @param mainArgs 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 Class execute(List topLevelClassNames, String[] mainArgs, MemoryContext context) + throws Fault, InvocationTargetException { + System.setProperty("jdk.launcher.sourcefile", context.getSourceFileAsString()); + ClassLoader parentLoader = ClassLoader.getSystemClassLoader(); + + // 1. Find a main method in the first class and if there is one - invoke it + Class firstClass; + String firstClassName = topLevelClassNames.getFirst(); + try { + ClassLoader loader = context.newClassLoaderFor(parentLoader, firstClassName); + firstClass = Class.forName(firstClassName, false, loader); + } catch (ClassNotFoundException e) { + throw new Fault(Errors.CantFindClass(firstClassName)); + } + + Method mainMethod = MethodFinder.findMainMethod(firstClass); + if (mainMethod == null) { + // 2. If the first class doesn't have a main method, look for a class with a matching name + var compilationUnitName = context.getProgramDescriptor().fileObject().getFile().getFileName().toString(); + assert compilationUnitName.endsWith(".java"); + var expectedName = compilationUnitName.substring(0, compilationUnitName.length() - 5); + var actualName = topLevelClassNames.stream() + .filter(name -> name.equals(expectedName)) + .findFirst() + .orElseThrow(() -> new Fault(Errors.CantFindClass(expectedName))); + + Class actualClass; + try { + actualClass = Class.forName(actualName, false, firstClass.getClassLoader()); + } catch (ClassNotFoundException ignore) { + throw new Fault(Errors.CantFindClass(actualName)); + } + mainMethod = MethodFinder.findMainMethod(actualClass); + if (mainMethod == null) { + throw new Fault(Errors.CantFindMainMethod(actualName)); + } + } + + // selected main method instance points back to its declaring class + Class mainClass = mainMethod.getDeclaringClass(); + String mainClassName = mainClass.getName(); + + var isStatic = Modifier.isStatic(mainMethod.getModifiers()); + + Object instance = null; + + if (!isStatic) { + Constructor constructor; + try { + constructor = mainClass.getDeclaredConstructor(); + } catch (NoSuchMethodException e) { + throw new Fault(Errors.CantFindConstructor(mainClassName)); + } + + try { + constructor.setAccessible(true); + instance = constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new Fault(Errors.CantAccessConstructor(mainClassName)); + } + } + + try { + // Similar to sun.launcher.LauncherHelper#executeMainClass + // but duplicated here to prevent additional launcher frames + mainMethod.setAccessible(true); + Object receiver = isStatic ? mainClass : instance; + + if (mainMethod.getParameterCount() == 0) { + mainMethod.invoke(receiver); + } else { + mainMethod.invoke(receiver, (Object)mainArgs); + } + } 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.getCause(); + StackTraceElement[] targetTrace = target.getStackTrace(); + target.setStackTrace(Arrays.copyOfRange(targetTrace, 0, targetTrace.length - invocationFrames)); + throw e; + } + + return mainClass; + } +} diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java index 74f513a4af5..e49ce45240a 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/main/Option.java @@ -83,6 +83,10 @@ import static com.sun.tools.javac.main.Option.OptionKind.*; * {@code handleOption} then calls {@link #process process} providing a suitable * {@link OptionHelper} to provide access the compiler state. * + *

A subset of options is relevant to the source launcher implementation + * located in {@link com.sun.tools.javac.launcher} package. When an option is + * added, changed, or removed, also update the {@code RelevantJavacOptions} class + * in the launcher package accordingly. * *

Maintenance note: when adding new annotation processing related * options, the list of options regarded as requesting explicit @@ -1081,6 +1085,10 @@ public enum Option { return kind; } + public boolean isInBasicOptionGroup() { + return group == BASIC; + } + public ArgKind getArgKind() { return argKind; } diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties index c1d4ac02c90..134d2ccd839 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/resources/launcher.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2023, 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 @@ -141,3 +141,10 @@ launcher.err.invalid.value.for.source=\ launcher.err.enable.preview.requires.source=\ --enable-preview must be used with --source + +launcher.err.unnamed.pkg.not.allowed.named.modules=\ + unnamed package is not allowed in named modules + +# 0: string, 1: path +launcher.err.mismatch.end.of.path.and.package.name=\ + end of path to source file does not match its package name {0}: {1} diff --git a/test/langtools/tools/javac/launcher/BasicSourceLauncherTests.java b/test/langtools/tools/javac/launcher/BasicSourceLauncherTests.java new file mode 100644 index 00000000000..1a686a8a38f --- /dev/null +++ b/test/langtools/tools/javac/launcher/BasicSourceLauncherTests.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023, 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. + */ + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/* + * @test + * @enablePreview + * @bug 8304400 + * @summary Test basic features of javac's source-code launcher + * @modules jdk.compiler/com.sun.tools.javac.launcher + * @run junit BasicSourceLauncherTests + */ +class BasicSourceLauncherTests { + @Test + void launchHelloClassInHelloJavaUnit(@TempDir Path base) throws Exception { + var hello = Files.writeString(base.resolve("Hello.java"), + """ + public class Hello { + public static void main(String... args) { + System.out.println("Hi"); + } + } + """); + + var run = Run.of(hello); + var result = run.result(); + assertAll("# " + run, + () -> assertLinesMatch( + """ + Hi + """.lines(), run.stdOut().lines()), + () -> assertTrue(run.stdErr().isEmpty()), + () -> assertNull(run.exception()), + () -> assertEquals(Set.of("Hello"), result.classNames()), + () -> assertNotNull(result.programClass().getResource("Hello.java")), + () -> assertNotNull(result.programClass().getResource("Hello.class"))); + } + + @Test + void launchHelloClassInHalloJavaUnit(@TempDir Path base) throws Exception { + var hallo = Files.writeString(base.resolve("Hallo.java"), + """ + public class Hello { + public static void main(String... args) { + System.out.println("Hi!"); + } + } + """); + + var run = Run.of(hallo); + var result = run.result(); + assertAll("# " + run, + () -> assertLinesMatch( + """ + Hi! + """.lines(), run.stdOut().lines()), + () -> assertTrue(run.stdErr().isEmpty()), + () -> assertNull(run.exception()), + () -> assertEquals(Set.of("Hello"), result.classNames()), + () -> assertNotNull(result.programClass().getResource("Hallo.java")), + () -> assertNotNull(result.programClass().getResource("Hello.class"))); + } + + @Test + void launchMinifiedJavaProgram(@TempDir Path base) throws Exception { + var hi = Files.writeString(base.resolve("Hi.java"), + """ + void main() { + System.out.println("Hi!"); + } + """); + + // Replace with plain Run.of(hi) once implict classes are out of preview + System.setProperty("jdk.internal.javac.source", String.valueOf(Runtime.version().feature())); + var run = Run.of(hi, List.of("--enable-preview"), List.of()); + System.clearProperty("jdk.internal.javac.source"); + + assertAll("# " + run, + () -> assertLinesMatch( + """ + Hi! + """.lines(), run.stdOut().lines()), + () -> assertTrue(run.stdErr().isEmpty()), + () -> assertNull(run.exception())); + } +} diff --git a/test/langtools/tools/javac/launcher/GetResourceTest.java b/test/langtools/tools/javac/launcher/GetResourceTest.java index 02aa198396b..095a43f39f1 100644 --- a/test/langtools/tools/javac/launcher/GetResourceTest.java +++ b/test/langtools/tools/javac/launcher/GetResourceTest.java @@ -40,7 +40,7 @@ import toolbox.Task; import toolbox.ToolBox; /* - * The body of this test is in ${test.src}/src/CLTest.java, + * The body of this test is in ${test.src}/src/p/q/CLTest.java, * which is executed in single-file source-launcher mode, * in order to test the classloader used to launch such programs. */ @@ -52,7 +52,7 @@ public class GetResourceTest { void run() throws Exception { ToolBox tb = new ToolBox(); - Path file = Paths.get(tb.testSrc).resolve("src").resolve("CLTest.java"); + Path file = Paths.get(tb.testSrc).resolve("src/p/q").resolve("CLTest.java"); new JavaTask(tb) .vmOptions("--enable-preview", "--source", String.valueOf(Runtime.version().feature())) .className(file.toString()) // implies source file mode diff --git a/test/langtools/tools/javac/launcher/ModuleSourceLauncherTests.java b/test/langtools/tools/javac/launcher/ModuleSourceLauncherTests.java new file mode 100644 index 00000000000..d5103525f57 --- /dev/null +++ b/test/langtools/tools/javac/launcher/ModuleSourceLauncherTests.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2023, 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. + */ + +/* + * @test + * @bug 8304400 + * @summary Test source launcher running Java programs contained in one module + * @modules jdk.compiler/com.sun.tools.javac.launcher + * @run junit ModuleSourceLauncherTests + */ + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.spi.ToolProvider; + +class ModuleSourceLauncherTests { + @Test + void testHelloModularWorld(@TempDir Path base) throws Exception { + var packageFolder = Files.createDirectories(base.resolve("com/greetings")); + var mainFile = Files.writeString(packageFolder.resolve("Main.java"), + """ + package com.greetings; + public class Main { + public static void main(String... args) { + System.out.println("Greetings!"); + System.out.println(" module -> " + Main.class.getModule().getName()); + System.out.println(" package -> " + Main.class.getPackageName()); + System.out.println(" class -> " + Main.class.getSimpleName()); + } + } + """); + Files.writeString(base.resolve("module-info.java"), + """ + module com.greetings {} + """); + + var run = Run.of(mainFile); + assertAll("Run -> " + run, + () -> assertLinesMatch( + """ + Greetings! + module -> com.greetings + package -> com.greetings + class -> Main + """.lines(), + run.stdOut().lines()), + () -> assertTrue(run.stdErr().isEmpty()), + () -> assertNull(run.exception()) + ); + + var module = run.result().programClass().getModule(); + assertEquals("com.greetings", module.getName()); + var reference = module.getLayer().configuration().findModule(module.getName()).orElseThrow().reference(); + try (var reader = reference.open()) { + assertLinesMatch( + """ + com/ + com/greetings/ + com/greetings/Main.class + com/greetings/Main.java + module-info.class + module-info.java + """.lines(), + reader.list()); + } + } + + @Test + void testTwoAndHalfPackages(@TempDir Path base) throws Exception { + var fooFolder = Files.createDirectories(base.resolve("foo")); + var program = Files.writeString(fooFolder.resolve("Main.java"), + """ + package foo; + public class Main { + public static void main(String... args) throws Exception { + var module = Main.class.getModule(); + System.out.println("To the " + bar.Bar.class + " from " + module); + try (var stream = module.getResourceAsStream("baz/baz.txt")) { + System.out.println(new String(stream.readAllBytes())); + } + } + } + """); + var barFolder = Files.createDirectories(base.resolve("bar")); + Files.writeString(barFolder.resolve("Bar.java"), "package bar; public record Bar() {}"); + var bazFolder = Files.createDirectories(base.resolve("baz")); + Files.writeString(bazFolder.resolve("baz.txt"), "baz"); + + Files.writeString(base.resolve("module-info.java"), + """ + module m { + exports foo; + exports bar; + opens baz; + } + """); + + var run = Run.of(program); + var result = run.result(); + assertAll("Run -> " + run, + () -> assertLinesMatch( + """ + To the class bar.Bar from module m + baz + """.lines(), + run.stdOut().lines()), + () -> assertTrue(run.stdErr().isEmpty()), + () -> assertNull(run.exception()), + () -> assertEquals(Set.of("foo", "bar", "baz"), result.programClass().getModule().getPackages()) + ); + + var module = run.result().programClass().getModule(); + assertEquals("m", module.getName()); + var reference = module.getLayer().configuration().findModule(module.getName()).orElseThrow().reference(); + try (var reader = reference.open()) { + assertLinesMatch( + """ + bar/ + bar/Bar.class + bar/Bar.java + baz/ + baz/baz.txt + foo/ + foo/Main.class + foo/Main.java + module-info.class + module-info.java + """.lines(), + reader.list()); + } + } + + @Test + void testUserModuleOnModulePath(@TempDir Path base) throws Exception { + Files.createDirectories(base.resolve("foo", "foo")); + Files.writeString(base.resolve("foo", "module-info.java"), + """ + module foo { + exports foo; + } + """); + Files.writeString(base.resolve("foo", "foo", "Foo.java"), + """ + package foo; + public record Foo() {} + """); + var javac = ToolProvider.findFirst("javac").orElseThrow(); + javac.run(System.out, System.err, "--module-source-path", base.toString(), "--module", "foo", "-d", base.toString()); + + Files.createDirectories(base.resolve("bar", "bar")); + Files.writeString(base.resolve("bar", "module-info.java"), + """ + module bar { + requires foo; + } + """); + Files.writeString(base.resolve("bar", "bar","Prog1.java"), + """ + package bar; + class Prog1 { + public static void main(String... args) { + System.out.println(new foo.Foo()); + } + } + """); + + var command = List.of( + Path.of(System.getProperty("java.home"), "bin", "java").toString(), + "-p", ".", + "bar/bar/Prog1.java"); + var redirectedOut = base.resolve("out.redirected"); + var redirectedErr = base.resolve("err.redirected"); + var process = new ProcessBuilder(command) + .directory(base.toFile()) + .redirectOutput(redirectedOut.toFile()) + .redirectError(redirectedErr.toFile()) + .start(); + var code = process.waitFor(); + var out = Files.readAllLines(redirectedOut); + var err = Files.readAllLines(redirectedErr); + + assertAll( + () -> assertEquals(0, code), + () -> assertLinesMatch( + """ + Foo[] + """.lines(), out.stream()), + () -> assertTrue(err.isEmpty()) + ); + } +} diff --git a/test/langtools/tools/javac/launcher/MultiFileSourceLauncherTests.java b/test/langtools/tools/javac/launcher/MultiFileSourceLauncherTests.java new file mode 100644 index 00000000000..12a5bf09dfe --- /dev/null +++ b/test/langtools/tools/javac/launcher/MultiFileSourceLauncherTests.java @@ -0,0 +1,319 @@ +/* + * Copyright (c) 2023, 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. + */ + +/* + * @test + * @bug 8304400 + * @summary Test source launcher running Java programs spanning multiple files + * @modules jdk.compiler/com.sun.tools.javac.launcher + * @run junit MultiFileSourceLauncherTests + */ + +import static org.junit.jupiter.api.Assertions.*; + +import com.sun.tools.javac.launcher.Fault; +import java.nio.file.*; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.io.*; + +class MultiFileSourceLauncherTests { + @Test + void testHelloWorldInTwoCompilationUnits(@TempDir Path base) throws Exception { + var hello = Files.writeString(base.resolve("Hello.java"), + """ + public class Hello { + public static void main(String... args) { + System.out.println("Hello " + new World("Terra")); + System.out.println(Hello.class.getResource("Hello.java")); + System.out.println(Hello.class.getResource("World.java")); + } + } + """); + Files.writeString(base.resolve("World.java"), + """ + record World(String name) {} + """); + + var run = Run.of(hello); + assertLinesMatch( + """ + Hello World[name=Terra] + \\Qfile:\\E.+\\QHello.java\\E + \\Qfile:\\E.+\\QWorld.java\\E + """.lines(), + run.stdOut().lines()); + assertTrue(run.stdErr().isEmpty(), run.toString()); + assertNull(run.exception(), run.toString()); + } + + @Test + void testLoadingOfEnclosedTypes(@TempDir Path base) throws Exception { + var hello = Files.writeString(base.resolve("Hello.java"), + """ + public class Hello { + public static void main(String... args) throws Exception { + System.out.println(Class.forName("World$Core")); + System.out.println(Class.forName("p.q.Unit$First$Second")); + } + } + """); + Files.writeString(base.resolve("World.java"), + """ + record World(String name) { + record Core() {} + } + """); + var pq = Files.createDirectories(base.resolve("p/q")); + Files.writeString(pq.resolve("Unit.java"), + """ + package p.q; + record Unit() { + record First() { + record Second() {} + } + } + """); + + var run = Run.of(hello); + assertAll("Run -> " + run, + () -> assertLinesMatch( + """ + class World$Core + class p.q.Unit$First$Second + """.lines(), + run.stdOut().lines()), + () -> assertTrue(run.stdErr().isEmpty()), + () -> assertNull(run.exception()) + ); + } + + @Test + void testMultiplePackages(@TempDir Path base) throws Exception { + var packageA = Files.createDirectories(base.resolve("a")); + var hello = Files.writeString(packageA.resolve("Hello.java"), + """ + package a; + import b.World; + public class Hello { + public static void main(String... args) { + System.out.println("Hello " + new World("in package b")); + } + } + """); + var packageB = Files.createDirectories(base.resolve("b")); + Files.writeString(packageB.resolve("World.java"), + """ + package b; + public record World(String name) {} + """); + + var run = Run.of(hello); + assertLinesMatch( + """ + Hello World[name=in package b] + """.lines(), + run.stdOut().lines()); + assertTrue(run.stdErr().isEmpty(), run.toString()); + assertNull(run.exception(), run.toString()); + } + + @Test + void testMissingSecondUnit(@TempDir Path base) throws Exception { + var program = Files.writeString(base.resolve("Program.java"), + """ + public class Program { + public static void main(String... args) { + System.out.println("Hello " + new MissingSecondUnit()); + } + } + """); + + var run = Run.of(program); + assertTrue(run.stdOut().isEmpty(), run.toString()); + assertLinesMatch( + """ + %s:3: error: cannot find symbol + System.out.println("Hello " + new MissingSecondUnit()); + ^ + symbol: class MissingSecondUnit + location: class Program + 1 error + """.formatted(program.toString()) + .lines(), + run.stdErr().lines(), + run.toString()); + assertTrue(run.exception() instanceof Fault); + } + + @Test + void testSecondUnitWithSyntaxError(@TempDir Path base) throws Exception { + var program = Files.writeString(base.resolve("Program.java"), + """ + public class Program { + public static void main(String... args) { + System.out.println("Hello " + new BrokenSecondUnit()); + } + } + """); + var broken = Files.writeString(base.resolve("BrokenSecondUnit.java"), + """ + record BrokenSecondUnit {} + """); + + var run = Run.of(program); + assertTrue(run.stdOut().isEmpty(), run.toString()); + assertLinesMatch( + """ + %s:1: error: '(' expected + >> MORE LINES >> + """.formatted(broken.toString()) + .lines(), + run.stdErr().lines(), + run.toString()); + assertTrue(run.exception() instanceof Fault); + } + + @Test + void onlyJavaFilesReferencedByTheProgramAreCompiled(@TempDir Path base) throws Exception { + var prog = Files.writeString(base.resolve("Prog.java"), + """ + class Prog { + public static void main(String... args) { + Helper.run(); + } + } + """); + Files.writeString(base.resolve("Helper.java"), + """ + class Helper { + static void run() { + System.out.println("Hello!"); + } + } + """); + + var old = Files.writeString(base.resolve("OldProg.java"), + """ + class OldProg { + public static void main(String... args) { + Helper.run() + } + } + """); + + var run = Run.of(prog); + assertAll("Run := " + run, + () -> assertLinesMatch( + """ + Hello! + """.lines(), run.stdOut().lines()), + () -> assertTrue(run.stdErr().isEmpty()), + () -> assertNull(run.exception())); + + var fail = Run.of(old); + assertAll("Run := " + fail, + () -> assertTrue(fail.stdOut().isEmpty()), + () -> assertLinesMatch( + """ + %s:3: error: ';' expected + Helper.run() + ^ + 1 error + """.formatted(old).lines(), fail.stdErr().lines()), + () -> assertNotNull(fail.exception())); + } + + @Test + void classesDeclaredInSameFileArePreferredToClassesInOtherFiles(@TempDir Path base) throws Exception { + var prog = Files.writeString(base.resolve("Prog.java"), + """ + class Helper { + static void run() { + System.out.println("Same file."); + } + } + public class Prog { + public static void main(String... args) { + Helper.run(); + } + } + """); + Files.writeString(base.resolve("Helper.java"), + """ + class Helper { + static void run() { + System.out.println("Other file."); + } + } + """); + + var run = Run.of(prog); + assertAll("Run := " + run, + () -> assertLinesMatch( + """ + Same file. + """.lines(), run.stdOut().lines()), + () -> assertTrue(run.stdErr().isEmpty()), + () -> assertNull(run.exception())); + } + + @Test + void duplicateDeclarationOfClassFails(@TempDir Path base) throws Exception { + var prog = Files.writeString(base.resolve("Prog.java"), + """ + class Prog { + public static void main(String... args) { + Helper.run(); + Aux.cleanup(); + } + } + class Aux { + static void cleanup() {} + } + """); + var helper = Files.writeString(base.resolve("Helper.java"), + """ + class Helper { + static void run() {} + } + class Aux { + static void cleanup() {} + } + """); + + + var fail = Run.of(prog); + assertAll("Run := " + fail, + () -> assertTrue(fail.stdOut().isEmpty()), + () -> assertLinesMatch( + """ + %s:4: error: duplicate class: Aux + class Aux { + ^ + 1 error + """.formatted(helper).lines(), fail.stdErr().lines()), + () -> assertNotNull(fail.exception())); + } +} diff --git a/test/langtools/tools/javac/launcher/ProgramDescriptorTests.java b/test/langtools/tools/javac/launcher/ProgramDescriptorTests.java new file mode 100644 index 00000000000..8e0f1cdba90 --- /dev/null +++ b/test/langtools/tools/javac/launcher/ProgramDescriptorTests.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023, 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. + */ + +/* + * @test + * @bug 8304400 + * @summary Test source root directory computation + * @modules jdk.compiler/com.sun.tools.javac.launcher + * @run junit ProgramDescriptorTests + */ + +import com.sun.tools.javac.launcher.ProgramDescriptor; +import java.nio.file.Path; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.*; + +class ProgramDescriptorTests { + @ParameterizedTest + @CsvSource(textBlock = + """ + '/', '/Program.java', '' + '/', '/a/Program.java', 'a', + '/', '/a/b/Program.java', 'a.b', + '/', '/a/b/c/Program.java', 'a.b.c' + + '/a', '/a/b/c/Program.java', 'b.c' + '/a/b', '/a/b/c/Program.java', 'c' + '/a/b/c', '/a/b/c/Program.java', '' + """) + @DisabledOnOs(OS.WINDOWS) + void checkComputeSourceRootPath(Path expected, Path program, String packageName) { + check(expected, program, packageName); + } + + @ParameterizedTest + @CsvSource(textBlock = + """ + 'C:\\', 'C:\\Program.java', '' + 'C:\\', 'C:\\a\\Program.java', 'a', + 'C:\\', 'C:\\a\\b\\Program.java', 'a.b', + 'C:\\', 'C:\\a\\b\\c\\Program.java', 'a.b.c' + + 'C:\\a', 'C:\\a\\b\\c\\Program.java', 'b.c' + 'C:\\a\\b', 'C:\\a\\b\\c\\Program.java', 'c' + 'C:\\a\\b\\c', 'C:\\a\\b\\c\\Program.java', '' + """) + @EnabledOnOs(OS.WINDOWS) + void checkComputeSourceRootPathOnWindows(Path expected, Path program, String packageName) { + check(expected, program, packageName); + } + + private void check(Path expectedRoot, Path programPath, String packageName) { + assertTrue(expectedRoot.isAbsolute(), "Expected path not absolute: " + expectedRoot); + assertTrue(programPath.isAbsolute(), "Program path not absolute: " + programPath); + + var actual = ProgramDescriptor.computeSourceRootPath(programPath, packageName); + assertEquals(expectedRoot, actual); + } +} diff --git a/test/langtools/tools/javac/launcher/Run.java b/test/langtools/tools/javac/launcher/Run.java new file mode 100644 index 00000000000..4d40d9d6a4c --- /dev/null +++ b/test/langtools/tools/javac/launcher/Run.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023, 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. + */ + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import com.sun.tools.javac.launcher.SourceLauncher; +import com.sun.tools.javac.launcher.Result; + +record Run(String stdOut, String stdErr, Throwable exception, Result result) { + static Run of(Path file) { + return Run.of(file, List.of(), List.of("1", "2", "3")); + } + + static Run of(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)) { + var launcher = new SourceLauncher(err); + var result = launcher.run(runtimeArgs.toArray(String[]::new), args.toArray(String[]::new)); + return new Run(baos.toString(), sw.toString(), null, result); + } catch (Throwable throwable) { + return new Run(baos.toString(), sw.toString(), throwable, null); + } + } finally { + System.setOut(prev); + } + } +} diff --git a/test/langtools/tools/javac/launcher/SourceLauncherTest.java b/test/langtools/tools/javac/launcher/SourceLauncherTest.java index d7b8b734630..a054415cfe1 100644 --- a/test/langtools/tools/javac/launcher/SourceLauncherTest.java +++ b/test/langtools/tools/javac/launcher/SourceLauncherTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2023, 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 @@ -58,13 +58,13 @@ import java.util.Properties; import java.util.regex.Pattern; import java.util.stream.Collectors; -import com.sun.tools.javac.launcher.Main; +import com.sun.tools.javac.launcher.SourceLauncher; +import com.sun.tools.javac.launcher.Fault; import toolbox.JavaTask; import toolbox.JavacTask; import toolbox.Task; import toolbox.TestRunner; -import toolbox.TestRunner.Test; import toolbox.ToolBox; import static jdk.internal.module.ClassFileConstants.WARN_INCUBATING; @@ -293,12 +293,20 @@ public class SourceLauncherTest extends TestRunner { @Test public void testNoClass(Path base) throws IOException { - Files.createDirectories(base); - Path file = base.resolve("NoClass.java"); + var path = Files.createDirectories(base.resolve("p")); + Path file = path.resolve("NoClass.java"); Files.write(file, List.of("package p;")); testError(file, "", "error: no class declared in source file"); } + @Test + public void testMismatchOfPathAndPackage(Path base) throws IOException { + Files.createDirectories(base); + Path file = base.resolve("MismatchOfPathAndPackage.java"); + Files.write(file, List.of("package p;")); + testError(file, "", "error: end of path to source file does not match its package name p: " + file); + } + @Test public void testLoadClass(Path base) throws IOException { Path src1 = base.resolve("src1"); @@ -619,7 +627,7 @@ public class SourceLauncherTest extends TestRunner { public void testNoOptionsWarnings(Path base) throws IOException { tb.writeJavaFiles(base, "public class Main { public static void main(String... args) {}}"); String log = new JavaTask(tb) - .vmOptions("--source", "8") + .vmOptions("--source", "21") .className(base.resolve("Main.java").toString()) .run(Task.Expect.SUCCESS) .getOutput(Task.OutputKind.STDERR); @@ -736,7 +744,7 @@ public class SourceLauncherTest extends TestRunner { System.setOut(out); StringWriter sw = new StringWriter(); try (PrintWriter err = new PrintWriter(sw, true)) { - Main m = new Main(err); + SourceLauncher m = new SourceLauncher(err); m.run(toArray(runtimeArgs), toArray(args)); return new Result(baos.toString(), sw.toString(), null); } catch (Throwable t) { @@ -793,10 +801,10 @@ public class SourceLauncherTest extends TestRunner { expect = expect.replace("\n", tb.lineSeparator); out.println(name + ": " + found); if (found == null) { - error("No exception thrown; expected Main.Fault"); + error("No exception thrown; expected Fault"); } else { - if (!(found instanceof Main.Fault)) { - error("Unexpected exception; expected Main.Fault"); + if (!(found instanceof Fault)) { + error("Unexpected exception; expected Fault"); } if (!(found.getMessage().equals(expect))) { error("Unexpected detail message; expected: " + expect); @@ -828,15 +836,5 @@ public class SourceLauncherTest extends TestRunner { 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; - } - } + record Result(String stdOut, String stdErr, Throwable exception) {} } diff --git a/test/langtools/tools/javac/launcher/src/CLTest.java b/test/langtools/tools/javac/launcher/src/p/q/CLTest.java similarity index 95% rename from test/langtools/tools/javac/launcher/src/CLTest.java rename to test/langtools/tools/javac/launcher/src/p/q/CLTest.java index cd3358ee6b1..e4517e78fc4 100644 --- a/test/langtools/tools/javac/launcher/src/CLTest.java +++ b/test/langtools/tools/javac/launcher/src/p/q/CLTest.java @@ -1,10 +1,12 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2023, 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. + * 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 diff --git a/test/langtools/tools/jdeps/listdeps/ListModuleDeps.java b/test/langtools/tools/jdeps/listdeps/ListModuleDeps.java index a9e9f78b28d..63814220c03 100644 --- a/test/langtools/tools/jdeps/listdeps/ListModuleDeps.java +++ b/test/langtools/tools/jdeps/listdeps/ListModuleDeps.java @@ -95,6 +95,7 @@ public class ListModuleDeps { "java.base/jdk.internal.javac", "java.base/jdk.internal.jmod", "java.base/jdk.internal.misc", + "java.base/jdk.internal.module", "java.base/sun.reflect.annotation", "java.compiler", "jdk.internal.opt/jdk.internal.opt",