// 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.ImmutableList.Builder; import com.google.common.collect.Iterables; 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.JavaFileObject.Kind; 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>header compilation * <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, 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; } try (VanillaJavaBuilder builder = new VanillaJavaBuilder()) { VanillaJavaBuilderResult result = builder.run(request.getArgumentsList()); WorkResponse response = WorkResponse.newBuilder() .setOutput(result.output()) .setExitCode(result.ok() ? 0 : 1) .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(); StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(diagnosticCollector, ENGLISH, UTF_8); setLocations(optionsParser, fileManager); ImmutableList<JavaFileObject> sources = getSources(optionsParser, fileManager); boolean ok; 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(optionsParser); } writeGeneratedSourceOutput(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()) { 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) throws IOException { fileManager.setLocation(StandardLocation.CLASS_PATH, toFiles(optionsParser.getClassPath())); fileManager.setLocation( StandardLocation.PLATFORM_CLASS_PATH, Iterables.concat( toFiles(optionsParser.getBootClassPath()), toFiles(optionsParser.getExtClassPath()))); fileManager.setLocation( StandardLocation.ANNOTATION_PROCESSOR_PATH, toFiles(optionsParser.getProcessorPath())); if (optionsParser.getSourceGenDir() != null) { Path sourceGenDir = Paths.get(optionsParser.getSourceGenDir()); createOutputDirectory(sourceGenDir); fileManager.setLocation( StandardLocation.SOURCE_OUTPUT, ImmutableList.of(sourceGenDir.toFile())); } Path classDir = Paths.get(optionsParser.getClassDir()); createOutputDirectory(classDir); fileManager.setLocation(StandardLocation.CLASS_OUTPUT, ImmutableList.of(classDir.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); 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(OptionsParser optionsParser) throws IOException { if (optionsParser.getGeneratedSourcesOutputJar() == null) { return; } JarCreator jar = new JarCreator(optionsParser.getGeneratedSourcesOutputJar()); jar.setNormalize(true); jar.setCompression(optionsParser.compressJar()); jar.addDirectory(optionsParser.getSourceGenDir()); jar.execute(); } /** Writes the class output jar, including any resource entries. */ private static void writeOutput(OptionsParser optionsParser) throws IOException { JarCreator jar = new JarCreator(optionsParser.getOutputJar()); jar.setNormalize(true); jar.setCompression(optionsParser.compressJar()); jar.addDirectory(optionsParser.getClassDir()); 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 new String(Files.readAllBytes(path), UTF_8); } } private static void createOutputDirectory(Path dir) throws IOException { if (Files.exists(dir)) { try { // TODO(b/27069912): handle symlinks Files.walkFileTree( dir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } catch (IOException e) { throw new IOException("Cannot clean output directory '" + dir + "'", e); } } Files.createDirectories(dir); } }