| /* | |
| * Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package toolbox; | |
| import java.io.IOException; | |
| import java.io.PrintWriter; | |
| import java.nio.file.Path; | |
| import java.nio.file.Paths; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.function.Consumer; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.Stream; | |
| import javax.annotation.processing.Processor; | |
| import javax.tools.JavaCompiler; | |
| import javax.tools.JavaFileManager; | |
| import javax.tools.JavaFileObject; | |
| import javax.tools.StandardJavaFileManager; | |
| import javax.tools.StandardLocation; | |
| import com.sun.tools.javac.api.JavacTaskImpl; | |
| import com.sun.tools.javac.api.JavacTool; | |
| /** | |
| * A task to configure and run the Java compiler, javac. | |
| */ | |
| public class JavacTask extends AbstractTask<JavacTask> { | |
| private boolean includeStandardOptions; | |
| private List<Path> classpath; | |
| private List<Path> sourcepath; | |
| private Path outdir; | |
| private Path headerdir; | |
| private List<String> options; | |
| private List<String> classes; | |
| private List<String> files; | |
| private List<JavaFileObject> fileObjects; | |
| private JavaFileManager fileManager; | |
| private Consumer<com.sun.source.util.JavacTask> callback; | |
| private List<Processor> procs; | |
| private JavaCompiler compiler; | |
| private StandardJavaFileManager internalFileManager; | |
| /** | |
| * Creates a task to execute {@code javac} using API mode. | |
| * @param toolBox the {@code ToolBox} to use | |
| */ | |
| public JavacTask(ToolBox toolBox) { | |
| super(toolBox, Task.Mode.API); | |
| } | |
| /** | |
| * Creates a task to execute {@code javac} in a specified mode. | |
| * @param toolBox the {@code ToolBox} to use | |
| * @param mode the mode to be used | |
| */ | |
| public JavacTask(ToolBox toolBox, Task.Mode mode) { | |
| super(toolBox, mode); | |
| } | |
| /** | |
| * Sets the classpath. | |
| * @param classpath the classpath | |
| * @return this task object | |
| */ | |
| public JavacTask classpath(String classpath) { | |
| this.classpath = Stream.of(classpath.split(ToolBox.pathSeparator)) | |
| .filter(s -> !s.isEmpty()) | |
| .map(s -> Paths.get(s)) | |
| .collect(Collectors.toList()); | |
| return this; | |
| } | |
| /** | |
| * Sets the classpath. | |
| * @param classpath the classpath | |
| * @return this task object | |
| */ | |
| public JavacTask classpath(Path... classpath) { | |
| this.classpath = Arrays.asList(classpath); | |
| return this; | |
| } | |
| /** | |
| * Sets the classpath. | |
| * @param classpath the classpath | |
| * @return this task object | |
| */ | |
| public JavacTask classpath(List<Path> classpath) { | |
| this.classpath = classpath; | |
| return this; | |
| } | |
| /** | |
| * Sets the sourcepath. | |
| * @param sourcepath the sourcepath | |
| * @return this task object | |
| */ | |
| public JavacTask sourcepath(String sourcepath) { | |
| this.sourcepath = Stream.of(sourcepath.split(ToolBox.pathSeparator)) | |
| .filter(s -> !s.isEmpty()) | |
| .map(s -> Paths.get(s)) | |
| .collect(Collectors.toList()); | |
| return this; | |
| } | |
| /** | |
| * Sets the sourcepath. | |
| * @param sourcepath the sourcepath | |
| * @return this task object | |
| */ | |
| public JavacTask sourcepath(Path... sourcepath) { | |
| this.sourcepath = Arrays.asList(sourcepath); | |
| return this; | |
| } | |
| /** | |
| * Sets the sourcepath. | |
| * @param sourcepath the sourcepath | |
| * @return this task object | |
| */ | |
| public JavacTask sourcepath(List<Path> sourcepath) { | |
| this.sourcepath = sourcepath; | |
| return this; | |
| } | |
| /** | |
| * Sets the output directory. | |
| * @param outdir the output directory | |
| * @return this task object | |
| */ | |
| public JavacTask outdir(String outdir) { | |
| this.outdir = Paths.get(outdir); | |
| return this; | |
| } | |
| /** | |
| * Sets the output directory. | |
| * @param outdir the output directory | |
| * @return this task object | |
| */ | |
| public JavacTask outdir(Path outdir) { | |
| this.outdir = outdir; | |
| return this; | |
| } | |
| /** | |
| * Sets the native header output directory. | |
| * @param headerdir the native header output directory | |
| * @return this task object | |
| */ | |
| public JavacTask headerdir(String headerdir) { | |
| this.headerdir = Paths.get(headerdir); | |
| return this; | |
| } | |
| /** | |
| * Sets the native header output directory. | |
| * @param headerdir the native header output directory | |
| * @return this task object | |
| */ | |
| public JavacTask headerdir(Path headerdir) { | |
| this.headerdir = headerdir; | |
| return this; | |
| } | |
| /** | |
| * Sets the options. | |
| * @param options the options | |
| * @return this task object | |
| */ | |
| public JavacTask options(String... options) { | |
| this.options = Arrays.asList(options); | |
| return this; | |
| } | |
| /** | |
| * Sets the options. | |
| * @param spaceSeparatedOption the space separated options | |
| * @return this task object | |
| */ | |
| public JavacTask spaceSeparatedOptions(String spaceSeparatedOption) { | |
| this.options = Arrays.asList(spaceSeparatedOption.split("\\s+")); | |
| return this; | |
| } | |
| /** | |
| * Sets the options. | |
| * @param options the options | |
| * @return this task object | |
| */ | |
| public JavacTask options(List<String> options) { | |
| this.options = options; | |
| return this; | |
| } | |
| /** | |
| * Sets the classes to be analyzed. | |
| * @param classes the classes | |
| * @return this task object | |
| */ | |
| public JavacTask classes(String... classes) { | |
| this.classes = Arrays.asList(classes); | |
| return this; | |
| } | |
| /** | |
| * Sets the files to be compiled or analyzed. | |
| * @param files the files | |
| * @return this task object | |
| */ | |
| public JavacTask files(String... files) { | |
| this.files = Arrays.asList(files); | |
| return this; | |
| } | |
| /** | |
| * Sets the files to be compiled or analyzed. | |
| * @param files the files | |
| * @return this task object | |
| */ | |
| public JavacTask files(Path... files) { | |
| this.files = Stream.of(files) | |
| .map(Path::toString) | |
| .collect(Collectors.toList()); | |
| return this; | |
| } | |
| /** | |
| * Sets the files to be compiled or analyzed. | |
| * @param files the files | |
| * @return this task object | |
| */ | |
| public JavacTask files(List<Path> files) { | |
| this.files = files.stream() | |
| .map(Path::toString) | |
| .collect(Collectors.toList()); | |
| return this; | |
| } | |
| /** | |
| * Sets the sources to be compiled or analyzed. | |
| * Each source string is converted into an in-memory object that | |
| * can be passed directly to the compiler. | |
| * @param sources the sources | |
| * @return this task object | |
| */ | |
| public JavacTask sources(String... sources) { | |
| fileObjects = Stream.of(sources) | |
| .map(s -> new ToolBox.JavaSource(s)) | |
| .collect(Collectors.toList()); | |
| return this; | |
| } | |
| /** | |
| * Sets the annotation processors to be used. | |
| */ | |
| public JavacTask processors(Processor... procs) { | |
| this.procs = List.of(procs); | |
| return this; | |
| } | |
| /** | |
| * Sets the file manager to be used by this task. | |
| * @param fileManager the file manager | |
| * @return this task object | |
| */ | |
| public JavacTask fileManager(JavaFileManager fileManager) { | |
| this.fileManager = fileManager; | |
| return this; | |
| } | |
| /** | |
| * Set a callback to be used by this task. | |
| * @param callback the callback | |
| * @return this task object | |
| */ | |
| public JavacTask callback(Consumer<com.sun.source.util.JavacTask> callback) { | |
| this.callback = callback; | |
| return this; | |
| } | |
| /** | |
| * {@inheritDoc} | |
| * @return the name "javac" | |
| */ | |
| @Override | |
| public String name() { | |
| return "javac"; | |
| } | |
| @Override | |
| public Result run(Expect expect) { | |
| int expectedExitCode = expect == Expect.SUCCESS ? 0 : 1; | |
| return run(expect, (exitCode, testName) -> { | |
| if (exitCode == 4) { | |
| throw new TaskError("Task " + testName + " failed due to a javac crash " | |
| + "(exit code 4)"); | |
| } | |
| }); | |
| } | |
| @Override | |
| public Result run(Expect expect, int exitCode) { | |
| if (exitCode == 4) { | |
| throw new IllegalArgumentException("Disallowed exit code: 4"); | |
| } | |
| return super.run(expect, exitCode); | |
| } | |
| /** | |
| * Calls the compiler with the arguments as currently configured. | |
| * @return a Result object indicating the outcome of the compilation | |
| * and the content of any output written to stdout, stderr, or the | |
| * main stream by the compiler. | |
| * @throws TaskError if the outcome of the task is not as expected. | |
| */ | |
| @Override | |
| public Task.Result run() { | |
| if (mode == Task.Mode.EXEC) | |
| return runExec(); | |
| AbstractTask.WriterOutput direct = new AbstractTask.WriterOutput(); | |
| // The following are to catch output to System.out and System.err, | |
| // in case these are used instead of the primary (main) stream | |
| AbstractTask.StreamOutput sysOut = new AbstractTask.StreamOutput(System.out, System::setOut); | |
| AbstractTask.StreamOutput sysErr = new AbstractTask.StreamOutput(System.err, System::setErr); | |
| int rc; | |
| Map<Task.OutputKind, String> outputMap = new HashMap<>(); | |
| try { | |
| switch (mode == null ? Task.Mode.API : mode) { | |
| case API: | |
| rc = runAPI(direct.pw); | |
| break; | |
| case CMDLINE: | |
| if (fileManager != null) { | |
| throw new IllegalStateException("file manager set in CMDLINE mode"); | |
| } | |
| if (callback != null) { | |
| throw new IllegalStateException("callback set in CMDLINE mode"); | |
| } | |
| rc = runCommand(direct.pw); | |
| break; | |
| default: | |
| throw new IllegalStateException("unknown mode " + mode); | |
| } | |
| } catch (IOException e) { | |
| toolBox.out.println("Exception occurred: " + e); | |
| rc = 99; | |
| } finally { | |
| outputMap.put(Task.OutputKind.STDOUT, sysOut.close()); | |
| outputMap.put(Task.OutputKind.STDERR, sysErr.close()); | |
| outputMap.put(Task.OutputKind.DIRECT, direct.close()); | |
| } | |
| return checkExit(new Task.Result(toolBox, this, rc, outputMap)); | |
| } | |
| private int runAPI(PrintWriter pw) throws IOException { | |
| try { | |
| // if (compiler == null) { | |
| // TODO: allow this to be set externally | |
| // compiler = ToolProvider.getSystemJavaCompiler(); | |
| compiler = JavacTool.create(); | |
| // } | |
| if (fileManager == null) | |
| fileManager = internalFileManager = compiler.getStandardFileManager(null, null, null); | |
| if (outdir != null) | |
| setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Collections.singletonList(outdir)); | |
| if (headerdir != null) | |
| setLocationFromPaths(StandardLocation.NATIVE_HEADER_OUTPUT, Collections.singletonList(headerdir)); | |
| if (classpath != null) | |
| setLocationFromPaths(StandardLocation.CLASS_PATH, classpath); | |
| if (sourcepath != null) | |
| setLocationFromPaths(StandardLocation.SOURCE_PATH, sourcepath); | |
| List<String> allOpts = new ArrayList<>(); | |
| if (options != null) | |
| allOpts.addAll(options); | |
| Iterable<? extends JavaFileObject> allFiles = joinFiles(files, fileObjects); | |
| JavaCompiler.CompilationTask task = compiler.getTask(pw, | |
| fileManager, | |
| null, // diagnostic listener; should optionally collect diags | |
| allOpts, | |
| classes, | |
| allFiles); | |
| if (procs != null) { | |
| task.setProcessors(procs); | |
| } | |
| JavacTaskImpl taskImpl = (JavacTaskImpl) task; | |
| if (callback != null) { | |
| callback.accept(taskImpl); | |
| } | |
| return taskImpl.doCall().exitCode; | |
| } finally { | |
| if (internalFileManager != null) | |
| internalFileManager.close(); | |
| } | |
| } | |
| private void setLocationFromPaths(StandardLocation location, List<Path> files) throws IOException { | |
| if (!(fileManager instanceof StandardJavaFileManager)) | |
| throw new IllegalStateException("not a StandardJavaFileManager"); | |
| ((StandardJavaFileManager) fileManager).setLocationFromPaths(location, files); | |
| } | |
| private int runCommand(PrintWriter pw) { | |
| List<String> args = getAllArgs(); | |
| String[] argsArray = args.toArray(new String[args.size()]); | |
| return com.sun.tools.javac.Main.compile(argsArray, pw); | |
| } | |
| private Task.Result runExec() { | |
| List<String> args = new ArrayList<>(); | |
| Path javac = toolBox.getJDKTool("javac"); | |
| args.add(javac.toString()); | |
| if (includeStandardOptions) { | |
| args.addAll(toolBox.split(System.getProperty("test.tool.vm.opts"), " +")); | |
| args.addAll(toolBox.split(System.getProperty("test.compiler.opts"), " +")); | |
| } | |
| args.addAll(getAllArgs()); | |
| String[] argsArray = args.toArray(new String[args.size()]); | |
| ProcessBuilder pb = getProcessBuilder(); | |
| pb.command(argsArray); | |
| try { | |
| return runProcess(toolBox, this, pb.start()); | |
| } catch (IOException | InterruptedException e) { | |
| throw new Error(e); | |
| } | |
| } | |
| private List<String> getAllArgs() { | |
| List<String> args = new ArrayList<>(); | |
| if (options != null) | |
| args.addAll(options); | |
| if (outdir != null) { | |
| args.add("-d"); | |
| args.add(outdir.toString()); | |
| } | |
| if (headerdir != null) { | |
| args.add("-h"); | |
| args.add(headerdir.toString()); | |
| } | |
| if (classpath != null) { | |
| args.add("-classpath"); | |
| args.add(toSearchPath(classpath)); | |
| } | |
| if (sourcepath != null) { | |
| args.add("-sourcepath"); | |
| args.add(toSearchPath(sourcepath)); | |
| } | |
| if (classes != null) | |
| args.addAll(classes); | |
| if (files != null) | |
| args.addAll(files); | |
| return args; | |
| } | |
| private String toSearchPath(List<Path> files) { | |
| return files.stream() | |
| .map(Path::toString) | |
| .collect(Collectors.joining(ToolBox.pathSeparator)); | |
| } | |
| private Iterable<? extends JavaFileObject> joinFiles( | |
| List<String> files, List<JavaFileObject> fileObjects) { | |
| if (files == null) | |
| return fileObjects; | |
| if (internalFileManager == null) | |
| internalFileManager = compiler.getStandardFileManager(null, null, null); | |
| Iterable<? extends JavaFileObject> filesAsFileObjects = | |
| internalFileManager.getJavaFileObjectsFromStrings(files); | |
| if (fileObjects == null) | |
| return filesAsFileObjects; | |
| List<JavaFileObject> combinedList = new ArrayList<>(); | |
| for (JavaFileObject o : filesAsFileObjects) | |
| combinedList.add(o); | |
| combinedList.addAll(fileObjects); | |
| return combinedList; | |
| } | |
| } |