| // Copyright 2017 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.devtools.build.buildjar; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static java.util.Locale.ENGLISH; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import com.google.common.io.MoreFiles; |
| import com.google.common.io.RecursiveDeleteOption; |
| import com.google.devtools.build.buildjar.jarhelper.JarCreator; |
| import com.google.devtools.build.buildjar.javac.JavacOptions; |
| import com.google.devtools.build.buildjar.proto.JavaCompilation.Manifest; |
| import com.google.devtools.build.lib.view.proto.Deps; |
| import com.google.devtools.build.lib.worker.WorkerProtocol.WorkRequest; |
| import com.google.devtools.build.lib.worker.WorkerProtocol.WorkResponse; |
| import java.io.Closeable; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.net.URI; |
| import java.nio.file.FileSystem; |
| import java.nio.file.FileSystems; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import javax.annotation.processing.Processor; |
| import javax.tools.Diagnostic; |
| import javax.tools.DiagnosticCollector; |
| import javax.tools.JavaCompiler; |
| import javax.tools.JavaCompiler.CompilationTask; |
| import javax.tools.JavaFileObject; |
| import javax.tools.SimpleJavaFileObject; |
| import javax.tools.StandardJavaFileManager; |
| import javax.tools.StandardLocation; |
| import javax.tools.ToolProvider; |
| |
| /** |
| * A JavaBuilder that supports non-standard JDKs and unmodified javac's. |
| * |
| * <p>Does not support: |
| * |
| * <ul> |
| * <li>Error Prone |
| * <li>strict Java deps |
| * <li>Android desugaring |
| * <li>coverage instrumentation |
| * <li>genclass handling for IDEs |
| * </ul> |
| */ |
| public class VanillaJavaBuilder implements Closeable { |
| |
| /** Cache of opened zip filesystems. */ |
| private final Map<Path, FileSystem> filesystems = new HashMap<>(); |
| |
| private FileSystem getJarFileSystem(Path sourceJar) throws IOException { |
| FileSystem fs = filesystems.get(sourceJar); |
| if (fs == null) { |
| filesystems.put(sourceJar, fs = FileSystems.newFileSystem(sourceJar, (ClassLoader) null)); |
| } |
| return fs; |
| } |
| |
| public static void main(String[] args) throws IOException { |
| if (args.length == 1 && args[0].equals("--persistent_worker")) { |
| System.exit(runPersistentWorker()); |
| } else { |
| try (VanillaJavaBuilder builder = new VanillaJavaBuilder()) { |
| VanillaJavaBuilderResult result = builder.run(ImmutableList.copyOf(args)); |
| System.err.print(result.output()); |
| System.exit(result.ok() ? 0 : 1); |
| } |
| } |
| } |
| |
| private static int runPersistentWorker() { |
| while (true) { |
| try { |
| WorkRequest request = WorkRequest.parseDelimitedFrom(System.in); |
| if (request == null) { |
| break; |
| } |
| VanillaJavaBuilderResult result; |
| try (VanillaJavaBuilder builder = new VanillaJavaBuilder()) { |
| result = builder.run(request.getArgumentsList()); |
| } |
| /* As soon as we write the response, bazel will start cleaning |
| * up the working tree. The VanillaJavaBuilder must be fully |
| * closed at this point. |
| */ |
| WorkResponse response = |
| WorkResponse.newBuilder() |
| .setOutput(result.output()) |
| .setExitCode(result.ok() ? 0 : 1) |
| .setRequestId(request.getRequestId()) |
| .build(); |
| response.writeDelimitedTo(System.out); |
| System.out.flush(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return 1; |
| } |
| } |
| return 0; |
| } |
| |
| /** Return result of a {@link VanillaJavaBuilder} build. */ |
| public static class VanillaJavaBuilderResult { |
| private final boolean ok; |
| private final String output; |
| |
| public VanillaJavaBuilderResult(boolean ok, String output) { |
| this.ok = ok; |
| this.output = output; |
| } |
| |
| /** True if the compilation was succesfull. */ |
| public boolean ok() { |
| return ok; |
| } |
| |
| /** Log output from the compilation. */ |
| public String output() { |
| return output; |
| } |
| } |
| |
| public VanillaJavaBuilderResult run(List<String> args) throws IOException { |
| OptionsParser optionsParser; |
| try { |
| optionsParser = new OptionsParser(args); |
| } catch (InvalidCommandLineException e) { |
| return new VanillaJavaBuilderResult(false, e.getMessage()); |
| } |
| DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>(); |
| StringWriter output = new StringWriter(); |
| JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); |
| Path tempDir = Files.createTempDirectory("_tmp"); |
| Path nativeHeaderDir = tempDir.resolve("native_headers"); |
| Files.createDirectories(nativeHeaderDir); |
| Path sourceGenDir = tempDir.resolve("sources"); |
| Files.createDirectories(sourceGenDir); |
| Path classDir = tempDir.resolve("classes"); |
| Files.createDirectories(classDir); |
| boolean ok; |
| try (StandardJavaFileManager fileManager = |
| javaCompiler.getStandardFileManager(diagnosticCollector, ENGLISH, UTF_8)) { |
| setLocations(optionsParser, fileManager, nativeHeaderDir, sourceGenDir, classDir); |
| ImmutableList<JavaFileObject> sources = getSources(optionsParser, fileManager); |
| if (sources.isEmpty()) { |
| ok = true; |
| } else { |
| CompilationTask task = |
| javaCompiler.getTask( |
| new PrintWriter(output, true), |
| fileManager, |
| diagnosticCollector, |
| JavacOptions.removeBazelSpecificFlags(optionsParser.getJavacOpts()), |
| ImmutableList.<String>of() /*classes*/, |
| sources); |
| setProcessors(optionsParser, fileManager, task); |
| ok = task.call(); |
| } |
| } |
| if (ok) { |
| writeOutput(classDir, optionsParser); |
| writeNativeHeaderOutput(optionsParser, nativeHeaderDir); |
| } |
| writeGeneratedSourceOutput(sourceGenDir, optionsParser); |
| // the jdeps output doesn't include any information about dependencies, but Bazel still expects |
| // the file to be created |
| if (optionsParser.getOutputDepsProtoFile() != null) { |
| try (OutputStream os = |
| Files.newOutputStream(Paths.get(optionsParser.getOutputDepsProtoFile()))) { |
| Deps.Dependencies.newBuilder() |
| .setRuleLabel(optionsParser.getTargetLabel()) |
| .setSuccess(ok) |
| .build() |
| .writeTo(os); |
| } |
| } |
| // TODO(cushon): support manifest protos & genjar |
| if (optionsParser.getManifestProtoPath() != null) { |
| try (OutputStream os = |
| Files.newOutputStream(Paths.get(optionsParser.getManifestProtoPath()))) { |
| Manifest.getDefaultInstance().writeTo(os); |
| } |
| } |
| |
| for (Diagnostic<? extends JavaFileObject> diagnostic : diagnosticCollector.getDiagnostics()) { |
| String code = diagnostic.getCode(); |
| if (code.startsWith("compiler.note.deprecated") |
| || code.startsWith("compiler.note.unchecked") |
| || code.equals("compiler.warn.sun.proprietary")) { |
| continue; |
| } |
| StringBuilder message = new StringBuilder(); |
| if (diagnostic.getSource() != null) { |
| message.append(diagnostic.getSource().getName()); |
| if (diagnostic.getLineNumber() != -1) { |
| message.append(':').append(diagnostic.getLineNumber()); |
| } |
| message.append(": "); |
| } |
| message.append(diagnostic.getKind().toString().toLowerCase(ENGLISH)); |
| message.append(": ").append(diagnostic.getMessage(ENGLISH)).append(System.lineSeparator()); |
| output.write(message.toString()); |
| } |
| return new VanillaJavaBuilderResult(ok, output.toString()); |
| } |
| |
| /** Returns the sources to compile, including any source jar entries. */ |
| private ImmutableList<JavaFileObject> getSources( |
| OptionsParser optionsParser, StandardJavaFileManager fileManager) throws IOException { |
| final ImmutableList.Builder<JavaFileObject> sources = ImmutableList.builder(); |
| sources.addAll(fileManager.getJavaFileObjectsFromStrings(optionsParser.getSourceFiles())); |
| for (String sourceJar : optionsParser.getSourceJars()) { |
| for (final Path root : getJarFileSystem(Paths.get(sourceJar)).getRootDirectories()) { |
| Files.walkFileTree( |
| root, |
| new SimpleFileVisitor<Path>() { |
| @Override |
| public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) |
| throws IOException { |
| if (path.getFileName().toString().endsWith(".java")) { |
| sources.add(new SourceJarFileObject(root, path)); |
| } |
| return FileVisitResult.CONTINUE; |
| } |
| }); |
| } |
| } |
| return sources.build(); |
| } |
| |
| /** Sets the compilation search paths and output directories. */ |
| private static void setLocations( |
| OptionsParser optionsParser, |
| StandardJavaFileManager fileManager, |
| Path nativeHeaderDir, |
| Path sourceGenDir, |
| Path classDir) |
| throws IOException { |
| fileManager.setLocation(StandardLocation.CLASS_PATH, toFiles(optionsParser.getClassPath())); |
| Iterable<File> bootClassPath = toFiles(optionsParser.getBootClassPath()); |
| // The bootclasspath may legitimately be empty if --release is being used. |
| if (!Iterables.isEmpty(bootClassPath)) { |
| fileManager.setLocation(StandardLocation.PLATFORM_CLASS_PATH, bootClassPath); |
| } |
| fileManager.setLocation( |
| StandardLocation.ANNOTATION_PROCESSOR_PATH, toFiles(optionsParser.getProcessorPath())); |
| setOutputLocation(fileManager, StandardLocation.SOURCE_OUTPUT, sourceGenDir); |
| if (optionsParser.getNativeHeaderOutput() != null) { |
| setOutputLocation(fileManager, StandardLocation.NATIVE_HEADER_OUTPUT, nativeHeaderDir); |
| } |
| setOutputLocation(fileManager, StandardLocation.CLASS_OUTPUT, classDir); |
| } |
| |
| private static void setOutputLocation( |
| StandardJavaFileManager fileManager, StandardLocation location, Path path) |
| throws IOException { |
| createOutputDirectory(path); |
| fileManager.setLocation(location, ImmutableList.of(path.toFile())); |
| } |
| |
| /** Sets the compilation's annotation processors. */ |
| private static void setProcessors( |
| OptionsParser optionsParser, StandardJavaFileManager fileManager, CompilationTask task) { |
| ClassLoader processorLoader = |
| fileManager.getClassLoader(StandardLocation.ANNOTATION_PROCESSOR_PATH); |
| ImmutableList.Builder<Processor> processors = ImmutableList.builder(); |
| for (String processor : optionsParser.getProcessorNames()) { |
| try { |
| processors.add( |
| (Processor) processorLoader.loadClass(processor).getConstructor().newInstance()); |
| } catch (ReflectiveOperationException e) { |
| throw new LinkageError(e.getMessage(), e); |
| } |
| } |
| task.setProcessors(processors.build()); |
| } |
| |
| /** Writes a jar containing any sources generated by annotation processors. */ |
| private static void writeGeneratedSourceOutput(Path sourceGenDir, OptionsParser optionsParser) |
| throws IOException { |
| if (optionsParser.getGeneratedSourcesOutputJar() == null) { |
| return; |
| } |
| JarCreator jar = new JarCreator(optionsParser.getGeneratedSourcesOutputJar()); |
| jar.setNormalize(true); |
| jar.setCompression(optionsParser.compressJar()); |
| jar.addDirectory(sourceGenDir); |
| jar.execute(); |
| } |
| |
| private static void writeNativeHeaderOutput(OptionsParser optionsParser, Path nativeHeaderDir) |
| throws IOException { |
| if (optionsParser.getNativeHeaderOutput() == null) { |
| return; |
| } |
| JarCreator jar = new JarCreator(optionsParser.getNativeHeaderOutput()); |
| try { |
| jar.setNormalize(true); |
| jar.setCompression(optionsParser.compressJar()); |
| jar.addDirectory(nativeHeaderDir); |
| } finally { |
| jar.execute(); |
| } |
| } |
| |
| /** Writes the class output jar, including any resource entries. */ |
| private static void writeOutput(Path classDir, OptionsParser optionsParser) throws IOException { |
| JarCreator jar = new JarCreator(optionsParser.getOutputJar()); |
| jar.setNormalize(true); |
| jar.setCompression(optionsParser.compressJar()); |
| jar.addDirectory(classDir); |
| jar.execute(); |
| } |
| |
| private static ImmutableList<File> toFiles(List<String> classPath) { |
| if (classPath == null) { |
| return ImmutableList.of(); |
| } |
| ImmutableList.Builder<File> files = ImmutableList.builder(); |
| for (String path : classPath) { |
| files.add(new File(path)); |
| } |
| return files.build(); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| for (FileSystem fs : filesystems.values()) { |
| fs.close(); |
| } |
| } |
| |
| /** |
| * Wraps a {@link Path} as a {@link JavaFileObject}; used to avoid extracting source jar entries |
| * to disk when using file managers that don't support nio. |
| */ |
| private static class SourceJarFileObject extends SimpleJavaFileObject { |
| private final Path path; |
| |
| public SourceJarFileObject(Path root, Path path) { |
| super(URI.create("file:/" + root + "!" + root.resolve(path)), Kind.SOURCE); |
| this.path = path; |
| } |
| |
| @Override |
| public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { |
| return Files.readString(path); |
| } |
| } |
| |
| private static void createOutputDirectory(Path dir) throws IOException { |
| if (Files.exists(dir)) { |
| try { |
| MoreFiles.deleteRecursively(dir, RecursiveDeleteOption.ALLOW_INSECURE); |
| } catch (IOException e) { |
| throw new IOException("Cannot clean output directory '" + dir + "'", e); |
| } |
| } |
| Files.createDirectories(dir); |
| } |
| } |