aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Liam Miller-Cushon <cushon@google.com>2016-02-09 20:27:13 +0000
committerGravatar Dmitry Lomov <dslomov@google.com>2016-02-10 10:24:29 +0000
commit7896c3aaa911d429d4200951469132727b73b7e8 (patch)
treed350f2ebc08c05072f31ab92db9ae5bf6e616dd9 /src
parentef9cba3dea5bc0a37f23de11dda79261f1e137e5 (diff)
Turbine, a tool for improving Java build performance
Turbine compiles ijars from source, ignoring method bodies and relaxing error checks for performance. We can then do normal compilation against those ijars and move javac off the build's critical path. -- MOS_MIGRATED_REVID=114247125
Diffstat (limited to 'src')
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/BUILD33
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/BUILD101
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbine.java371
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompileRequest.java117
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompileResult.java64
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompiler.java93
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineJavaCompiler.java96
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/TreePruner.java231
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/ZipOutputFileManager.java161
-rw-r--r--src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/ZipUtil.java53
-rw-r--r--src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/BUILD18
-rw-r--r--src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/javac/BUILD27
-rw-r--r--src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/javac/JavacTurbineTest.java832
13 files changed, 2197 insertions, 0 deletions
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/BUILD b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/BUILD
new file mode 100644
index 0000000000..173ea98a94
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/BUILD
@@ -0,0 +1,33 @@
+package_group(
+ name = "packages",
+ packages = ["//src/java_tools/buildjar/..."],
+)
+
+package(default_visibility = [":packages"])
+
+java_binary(
+ name = "turbine",
+ main_class = "com.google.devtools.build.java.turbine.javac.JavacTurbine",
+ runtime_deps = [
+ "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac:javac_turbine",
+ ],
+)
+
+java_library(
+ name = "turbine_options",
+ srcs = ["TurbineOptions.java"],
+ deps = [
+ "//third_party:guava",
+ "//third_party:jsr305",
+ ],
+)
+
+java_library(
+ name = "turbine_options_parser",
+ srcs = ["TurbineOptionsParser.java"],
+ deps = [
+ ":turbine_options",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ ],
+)
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/BUILD b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/BUILD
new file mode 100644
index 0000000000..4188035404
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/BUILD
@@ -0,0 +1,101 @@
+package_group(
+ name = "packages",
+ packages = ["//src/java_tools/buildjar/..."],
+)
+
+package(default_visibility = [":packages"])
+
+java_library(
+ name = "javac_turbine",
+ srcs = ["JavacTurbine.java"],
+ deps = [
+ ":javac_turbine_compile_request",
+ ":javac_turbine_compile_result",
+ ":javac_turbine_compiler",
+ ":zip_output_file_manager",
+ ":zip_util",
+ "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins:dependency",
+ "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine:turbine_options",
+ "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine:turbine_options_parser",
+ "//third_party:asm",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
+
+java_library(
+ name = "javac_turbine_compile_request",
+ srcs = ["JavacTurbineCompileRequest.java"],
+ deps = [
+ "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins:dependency",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
+
+java_library(
+ name = "javac_turbine_compile_result",
+ srcs = ["JavacTurbineCompileResult.java"],
+ deps = [
+ ":zip_output_file_manager",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
+
+java_library(
+ name = "javac_turbine_compiler",
+ srcs = ["JavacTurbineCompiler.java"],
+ deps = [
+ ":javac_turbine_compile_request",
+ ":javac_turbine_compile_result",
+ ":javac_turbine_java_compiler",
+ ":zip_output_file_manager",
+ "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins:dependency",
+ "//third_party:guava",
+ "//third_party:jsr305",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
+
+java_library(
+ name = "javac_turbine_java_compiler",
+ srcs = ["JavacTurbineJavaCompiler.java"],
+ deps = [
+ ":tree_pruner",
+ "//src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/plugins:dependency",
+ "//third_party:jsr305",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
+
+java_library(
+ name = "zip_output_file_manager",
+ srcs = ["ZipOutputFileManager.java"],
+ deps = [
+ "//third_party:jsr305",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
+
+java_library(
+ name = "zip_util",
+ srcs = ["ZipUtil.java"],
+ deps = [
+ "//third_party:jsr305",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
+
+java_library(
+ name = "tree_pruner",
+ srcs = ["TreePruner.java"],
+ deps = [
+ "//third_party:guava",
+ "//third_party:jsr305",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbine.java b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbine.java
new file mode 100644
index 0000000000..e30b672c5b
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbine.java
@@ -0,0 +1,371 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.buildjar.javac.plugins.dependency.DependencyModule;
+import com.google.devtools.build.buildjar.javac.plugins.dependency.DependencyModule.StrictJavaDeps;
+import com.google.devtools.build.buildjar.javac.plugins.dependency.StrictJavaDepsPlugin;
+import com.google.devtools.build.java.turbine.TurbineOptions;
+import com.google.devtools.build.java.turbine.TurbineOptionsParser;
+import com.google.devtools.build.java.turbine.javac.ZipOutputFileManager.OutputFileObject;
+
+import com.sun.tools.javac.util.Context;
+
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+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.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+
+import javax.tools.StandardLocation;
+
+/**
+ * An header compiler implementation based on javac.
+ *
+ * <p>This is a reference implementation used to develop the blaze integration, and to validate
+ * the real header compilation implementation.
+ */
+public class JavacTurbine implements AutoCloseable {
+
+ public static void main(String[] args) throws IOException {
+ System.exit(compile(TurbineOptionsParser.parse(Arrays.asList(args))).exitCode());
+ }
+
+ public static Result compile(TurbineOptions turbineOptions) throws IOException {
+ try (JavacTurbine turbine = new JavacTurbine(turbineOptions)) {
+ return turbine.compile();
+ }
+ }
+
+ /** A header compilation result. */
+ enum Result {
+ /** The compilation succeeded with the reduced classpath optimization. */
+ OK_WITH_REDUCED_CLASSPATH(true),
+
+ /** The compilation succeeded, but had to fall back to a transitive classpath. */
+ OK_WITH_FULL_CLASSPATH(true),
+
+ /** The compilation did not succeed. */
+ ERROR(false);
+
+ private final boolean ok;
+
+ private Result(boolean ok) {
+ this.ok = ok;
+ }
+
+ boolean ok() {
+ return ok;
+ }
+
+ int exitCode() {
+ return ok ? 0 : 1;
+ }
+ }
+
+ private static final int ZIPFILE_BUFFER_SIZE = 1024 * 16;
+
+ private static final Joiner CLASSPATH_JOINER = Joiner.on(':');
+
+ private final PrintWriter out;
+ private final TurbineOptions turbineOptions;
+ @VisibleForTesting Context context;
+
+ public JavacTurbine(PrintWriter out, TurbineOptions turbineOptions) {
+ this.out = out;
+ this.turbineOptions = turbineOptions;
+ }
+
+ public JavacTurbine(TurbineOptions turbineOptions) {
+ this(new PrintWriter(System.err, true), turbineOptions);
+ }
+
+ Result compile() throws IOException {
+ Path tmpdir = Paths.get(turbineOptions.tempDir());
+ Files.createDirectories(tmpdir);
+
+ ImmutableList.Builder<String> argbuilder = ImmutableList.builder();
+
+ addLanguageLevel(argbuilder, turbineOptions.javacOpts());
+
+ // Disable compilation of implicit source files.
+ // This is insurance: the sourcepath is empty, so we don't expect implicit sources.
+ argbuilder.add("-implicit:none");
+
+ ImmutableList<Path> processorpath;
+ if (!turbineOptions.processors().isEmpty()) {
+ argbuilder.add("-processor");
+ argbuilder.add(Joiner.on(',').join(turbineOptions.processors()));
+ processorpath = asPaths(turbineOptions.processorPath());
+ } else {
+ processorpath = ImmutableList.of();
+ }
+
+ List<String> sources = new ArrayList<>();
+ sources.addAll(turbineOptions.sources());
+ sources.addAll(extractSourceJars(turbineOptions, tmpdir));
+
+ argbuilder.addAll(sources);
+
+ JavacTurbineCompileRequest.Builder requestBuilder =
+ JavacTurbineCompileRequest.builder()
+ .setJavacOptions(argbuilder.build())
+ .setBootClassPath(asPaths(turbineOptions.bootClassPath()))
+ .setProcessorClassPath(processorpath);
+
+ StrictJavaDeps strictDepsMode = StrictJavaDeps.valueOf(turbineOptions.strictDepsMode());
+
+ DependencyModule dependencyModule = buildDependencyModule(turbineOptions, strictDepsMode);
+
+ Result result = Result.ERROR;
+ JavacTurbineCompileResult compileResult;
+ List<String> actualClasspath;
+
+ if (strictDepsMode != StrictJavaDeps.OFF) {
+
+ List<String> originalClasspath = turbineOptions.classPath();
+ List<String> compressedClasspath =
+ dependencyModule.computeStrictClasspath(turbineOptions.classPath());
+
+ requestBuilder.setStrictDepsPlugin(new StrictJavaDepsPlugin(dependencyModule));
+
+ {
+ // compile with reduced classpath
+ actualClasspath = compressedClasspath;
+ requestBuilder.setClassPath(asPaths(actualClasspath));
+ compileResult = JavacTurbineCompiler.compile(requestBuilder.build());
+ if (compileResult.success()) {
+ result = Result.OK_WITH_REDUCED_CLASSPATH;
+ context = compileResult.context();
+ }
+ }
+
+ if (!compileResult.success() && hasRecognizedError(compileResult.output())) {
+ // fall back to transitive classpath
+ deleteRecursively(tmpdir);
+ extractSourceJars(turbineOptions, tmpdir);
+
+ actualClasspath = originalClasspath;
+ requestBuilder.setClassPath(asPaths(actualClasspath));
+ compileResult = JavacTurbineCompiler.compile(requestBuilder.build());
+ if (compileResult.success()) {
+ result = Result.OK_WITH_FULL_CLASSPATH;
+ context = compileResult.context();
+ }
+ }
+
+ } else {
+ actualClasspath = turbineOptions.classPath();
+ requestBuilder.setClassPath(asPaths(actualClasspath));
+ compileResult = JavacTurbineCompiler.compile(requestBuilder.build());
+ if (compileResult.success()) {
+ result = Result.OK_WITH_FULL_CLASSPATH;
+ context = compileResult.context();
+ }
+ }
+
+ if (!result.ok()) {
+ out.println(compileResult.output());
+ return result;
+ }
+
+ emitClassJar(Paths.get(turbineOptions.outputFile()), compileResult);
+ dependencyModule.emitUsedClasspath(CLASSPATH_JOINER.join(actualClasspath));
+ dependencyModule.emitDependencyInformation(
+ CLASSPATH_JOINER.join(actualClasspath), compileResult.success());
+
+ return result;
+ }
+
+ private static DependencyModule buildDependencyModule(
+ TurbineOptions turbineOptions, StrictJavaDeps strictDepsMode) {
+ DependencyModule.Builder dependencyModuleBuilder =
+ new DependencyModule.Builder()
+ .setReduceClasspath()
+ .setTargetLabel(turbineOptions.targetLabel())
+ .addDepsArtifacts(turbineOptions.depsArtifacts())
+ .setStrictJavaDeps(strictDepsMode.toString())
+ .addDirectMappings(turbineOptions.directJarsToTargets())
+ .addIndirectMappings(turbineOptions.indirectJarsToTargets());
+
+ if (turbineOptions.outputDeps().isPresent()) {
+ dependencyModuleBuilder.setOutputDepsProtoFile(turbineOptions.outputDeps().get());
+ }
+
+ return dependencyModuleBuilder.build();
+ }
+
+ /** Write the class output from a successful compilation to the output jar. */
+ private static void emitClassJar(Path outputJar, JavacTurbineCompileResult result)
+ throws IOException {
+ try (OutputStream fos = Files.newOutputStream(outputJar);
+ ZipOutputStream zipOut =
+ new ZipOutputStream(new BufferedOutputStream(fos, ZIPFILE_BUFFER_SIZE))) {
+ boolean hasEntries = false;
+ for (Map.Entry<String, OutputFileObject> entry : result.files().entrySet()) {
+ if (entry.getValue().location != StandardLocation.CLASS_OUTPUT) {
+ continue;
+ }
+ String name = entry.getKey();
+ byte[] bytes = entry.getValue().asBytes();
+ if (name.endsWith(".class")) {
+ bytes = removeCode(bytes);
+ }
+ ZipUtil.storeEntry(name, bytes, zipOut);
+ hasEntries = true;
+ }
+ if (!hasEntries) {
+ // ZipOutputStream refuses to create a completely empty zip file.
+ ZipUtil.storeEntry("dummy", new byte[0], zipOut);
+ }
+ }
+ }
+
+ /**
+ * Strip any remaining code attributes.
+ *
+ * <p>Most code will already have been removed after parsing, but the bytecode will still
+ * contain e.g. lowered class and instance initializers.
+ */
+ // TODO(cushon): add additional stripping to produce ijar-compatible output,
+ // e.g. removing private members.
+ private static byte[] removeCode(byte[] bytes) {
+ ClassWriter cw = new ClassWriter(0);
+ new ClassReader(bytes)
+ .accept(cw, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
+ return cw.toByteArray();
+ }
+
+ /** Convert string elements of a classpath to {@link Path}s. */
+ private static ImmutableList<Path> asPaths(Iterable<String> classpath) {
+ ImmutableList.Builder<Path> result = ImmutableList.builder();
+ for (String element : classpath) {
+ result.add(Paths.get(element));
+ }
+ return result.build();
+ }
+
+ /** Extract the language level from the javacopts. */
+ @VisibleForTesting
+ static void addLanguageLevel(
+ ImmutableList.Builder<String> javacArgs, Iterable<String> defaultJavacopts) {
+ Iterator<String> it = defaultJavacopts.iterator();
+ while (it.hasNext()) {
+ String curr = it.next();
+ switch (curr) {
+ case "-source":
+ case "-target":
+ if (it.hasNext()) {
+ javacArgs.add(curr);
+ javacArgs.add(it.next());
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /** Extra sources in srcjars to disk. */
+ private static List<String> extractSourceJars(TurbineOptions turbineOptions, Path tmpdir)
+ throws IOException {
+ if (turbineOptions.sourceJars().isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ ArrayList<String> extractedSources = new ArrayList<>();
+ for (String sourceJar : turbineOptions.sourceJars()) {
+ try (ZipFile zf = new ZipFile(sourceJar)) {
+ Enumeration<? extends ZipEntry> entries = zf.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry ze = entries.nextElement();
+ if (!ze.getName().endsWith(".java")) {
+ continue;
+ }
+ Path dest = tmpdir.resolve(ze.getName());
+ Files.createDirectories(dest.getParent());
+ Files.copy(zf.getInputStream(ze), dest);
+ extractedSources.add(dest.toAbsolutePath().toString());
+ }
+ }
+ }
+ return extractedSources;
+ }
+
+ private static final Pattern MISSING_PACKAGE =
+ Pattern.compile("error: package ([\\p{javaJavaIdentifierPart}\\.]+) does not exist");
+
+ /**
+ * The compilation failed with an error that may indicate that the reduced class path was too
+ * aggressive.
+ *
+ * <p>WARNING: keep in sync with ReducedClasspathJavaLibraryBuilder.
+ */
+ // TODO(cushon): use a diagnostic listener and match known codes instead
+ private static boolean hasRecognizedError(String javacOutput) {
+ return javacOutput.contains("error: cannot access")
+ || javacOutput.contains("error: cannot find symbol")
+ || javacOutput.contains("com.sun.tools.javac.code.Symbol$CompletionFailure")
+ || MISSING_PACKAGE.matcher(javacOutput).find();
+ }
+
+ @Override
+ public void close() throws IOException {
+ deleteRecursively(Paths.get(turbineOptions.tempDir()));
+ }
+
+ private static void deleteRecursively(final Path dir) throws IOException {
+ Files.walkFileTree(
+ dir,
+ new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
+ throws IOException {
+ Files.delete(path);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path path, IOException exc) throws IOException {
+ if (!path.equals(dir)) {
+ Files.delete(path);
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+}
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompileRequest.java b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompileRequest.java
new file mode 100644
index 0000000000..68f99fe9d6
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompileRequest.java
@@ -0,0 +1,117 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.buildjar.javac.plugins.dependency.StrictJavaDepsPlugin;
+
+import java.nio.file.Path;
+
+import javax.annotation.Nullable;
+
+/** The input to a {@link JavacTurbineCompiler} compilation. */
+class JavacTurbineCompileRequest {
+
+ private final ImmutableList<Path> classPath;
+ private final ImmutableList<Path> bootClassPath;
+ private final ImmutableList<Path> processorClassPath;
+ private final ImmutableList<String> javacOptions;
+ @Nullable private final StrictJavaDepsPlugin strictJavaDepsPlugin;
+
+ JavacTurbineCompileRequest(
+ ImmutableList<Path> classPath,
+ ImmutableList<Path> bootClassPath,
+ ImmutableList<Path> processorClassPath,
+ ImmutableList<String> javacOptions,
+ @Nullable StrictJavaDepsPlugin strictJavaDepsPlugin) {
+ this.classPath = classPath;
+ this.bootClassPath = bootClassPath;
+ this.processorClassPath = processorClassPath;
+ this.javacOptions = javacOptions;
+ this.strictJavaDepsPlugin = strictJavaDepsPlugin;
+ }
+
+ /** The class path; correspond's to javac -classpath. */
+ ImmutableList<Path> classPath() {
+ return classPath;
+ }
+
+ /** The boot class path; corresponds to javac -bootclasspath. */
+ ImmutableList<Path> bootClassPath() {
+ return bootClassPath;
+ }
+
+ /** The class path to search for processors; corresponds to javac -processorpath. */
+ ImmutableList<Path> processorClassPath() {
+ return processorClassPath;
+ }
+
+ /** Miscellaneous javac options. */
+ ImmutableList<String> javacOptions() {
+ return javacOptions;
+ }
+
+ /**
+ * The build's {@link StrictJavaDepsPlugin}, or {@code null} if Strict Java Deps is not enabled.
+ */
+ @Nullable
+ StrictJavaDepsPlugin strictJavaDepsPlugin() {
+ return strictJavaDepsPlugin;
+ }
+
+ static JavacTurbineCompileRequest.Builder builder() {
+ return new Builder();
+ }
+
+ static class Builder {
+ private ImmutableList<Path> classPath;
+ private ImmutableList<Path> bootClassPath;
+ private ImmutableList<Path> processorClassPath;
+ private ImmutableList<String> javacOptions;
+ @Nullable private StrictJavaDepsPlugin strictDepsPlugin;
+
+ private Builder() {}
+
+ JavacTurbineCompileRequest build() {
+ return new JavacTurbineCompileRequest(
+ classPath, bootClassPath, processorClassPath, javacOptions, strictDepsPlugin);
+ }
+
+ Builder setClassPath(ImmutableList<Path> classPath) {
+ this.classPath = classPath;
+ return this;
+ }
+
+ Builder setBootClassPath(ImmutableList<Path> bootClassPath) {
+ this.bootClassPath = bootClassPath;
+ return this;
+ }
+
+ Builder setProcessorClassPath(ImmutableList<Path> processorClassPath) {
+ this.processorClassPath = processorClassPath;
+ return this;
+ }
+
+ Builder setJavacOptions(ImmutableList<String> javacOptions) {
+ this.javacOptions = javacOptions;
+ return this;
+ }
+
+ Builder setStrictDepsPlugin(@Nullable StrictJavaDepsPlugin strictDepsPlugin) {
+ this.strictDepsPlugin = strictDepsPlugin;
+ return this;
+ }
+ }
+}
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompileResult.java b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompileResult.java
new file mode 100644
index 0000000000..0d034316c7
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompileResult.java
@@ -0,0 +1,64 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.java.turbine.javac.ZipOutputFileManager.OutputFileObject;
+
+import com.sun.tools.javac.util.Context;
+
+import java.io.StringWriter;
+
+/** The output from a {@link JavacTurbineCompiler} compilation. */
+class JavacTurbineCompileResult {
+
+ private final ImmutableMap<String, OutputFileObject> files;
+ private final boolean success;
+ private final StringWriter sb;
+ private final Context context;
+
+ JavacTurbineCompileResult(
+ ImmutableMap<String, OutputFileObject> files,
+ boolean success,
+ StringWriter sb,
+ Context context) {
+ this.files = files;
+ this.success = success;
+ this.sb = sb;
+ this.context = context;
+ }
+
+ /** True iff the compilation succeeded. */
+ boolean success() {
+ return success;
+ }
+
+ /** The stderr from the compilation. */
+ String output() {
+ return sb.toString();
+ }
+
+ /** The files produced by the compilation's {@link ZipOutputFileManager}. */
+ ImmutableMap<String, OutputFileObject> files() {
+ return files;
+ }
+
+ /** The compilation context, may by inspected by integration tests. */
+ @VisibleForTesting
+ Context context() {
+ return context;
+ }
+}
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompiler.java b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompiler.java
new file mode 100644
index 0000000000..af697a833d
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineCompiler.java
@@ -0,0 +1,93 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.buildjar.javac.plugins.dependency.StrictJavaDepsPlugin;
+import com.google.devtools.build.java.turbine.javac.ZipOutputFileManager.OutputFileObject;
+
+import com.sun.tools.javac.file.CacheFSInfo;
+import com.sun.tools.javac.main.Arguments;
+import com.sun.tools.javac.main.CommandLine;
+import com.sun.tools.javac.main.JavaCompiler;
+import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Log;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.tools.JavaFileManager;
+import javax.tools.StandardLocation;
+
+/** Performs a javac-based turbine compilation given a {@link JavacTurbineCompileRequest}. */
+public class JavacTurbineCompiler {
+
+ static JavacTurbineCompileResult compile(JavacTurbineCompileRequest request) throws IOException {
+
+ Map<String, OutputFileObject> files = new LinkedHashMap<>();
+ boolean success;
+ StringWriter sw = new StringWriter();
+ Context context = new Context();
+
+ try (PrintWriter pw = new PrintWriter(sw)) {
+ ZipOutputFileManager.preRegister(context, files);
+ setupContext(context, request.strictJavaDepsPlugin());
+ CacheFSInfo.preRegister(context);
+
+ context.put(Log.outKey, pw);
+
+ ZipOutputFileManager fm = (ZipOutputFileManager) context.get(JavaFileManager.class);
+ fm.setLocationFromPaths(StandardLocation.SOURCE_PATH, Collections.<Path>emptyList());
+ fm.setLocationFromPaths(StandardLocation.CLASS_PATH, request.classPath());
+ fm.setLocationFromPaths(StandardLocation.PLATFORM_CLASS_PATH, request.bootClassPath());
+ fm.setLocationFromPaths(
+ StandardLocation.ANNOTATION_PROCESSOR_PATH, request.processorClassPath());
+
+ String[] javacArgArray = request.javacOptions().toArray(new String[0]);
+ javacArgArray = CommandLine.parse(javacArgArray);
+
+ Arguments args = Arguments.instance(context);
+ args.init("turbine", javacArgArray);
+
+ fm.setContext(context);
+ fm.handleOptions(args.getDeferredFileManagerOptions());
+
+ JavaCompiler comp = JavaCompiler.instance(context);
+ if (request.strictJavaDepsPlugin() != null) {
+ request.strictJavaDepsPlugin().init(context, Log.instance(context), comp);
+ }
+
+ try {
+ comp.compile(args.getFileObjects(), args.getClassNames(), null);
+ success = comp.errorCount() == 0;
+ } catch (Throwable t) {
+ t.printStackTrace(pw);
+ success = false;
+ }
+ }
+
+ return new JavacTurbineCompileResult(ImmutableMap.copyOf(files), success, sw, context);
+ }
+
+ static void setupContext(Context context, @Nullable StrictJavaDepsPlugin sjd) {
+ JavacTurbineJavaCompiler.preRegister(context, sjd);
+ }
+}
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineJavaCompiler.java b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineJavaCompiler.java
new file mode 100644
index 0000000000..874b1811a4
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/JavacTurbineJavaCompiler.java
@@ -0,0 +1,96 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import com.google.devtools.build.buildjar.javac.plugins.dependency.StrictJavaDepsPlugin;
+
+import com.sun.tools.javac.comp.AttrContext;
+import com.sun.tools.javac.comp.CompileStates.CompileState;
+import com.sun.tools.javac.comp.Env;
+import com.sun.tools.javac.main.JavaCompiler;
+import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
+import com.sun.tools.javac.util.Context;
+
+import java.util.Queue;
+
+import javax.annotation.Nullable;
+import javax.tools.JavaFileObject;
+
+/**
+ * A {@link JavaCompiler} that drops method bodies and top-level blocks after parsing, and runs
+ * Strict Java Deps.
+ *
+ * <p>The information dropped from the AST improves compilation performance and has no effect
+ * on the output of header compilation.
+ */
+class JavacTurbineJavaCompiler extends JavaCompiler implements AutoCloseable {
+
+ @Nullable private final StrictJavaDepsPlugin strictJavaDeps;
+
+ public JavacTurbineJavaCompiler(Context context, @Nullable StrictJavaDepsPlugin strictJavaDeps) {
+ super(context);
+ this.strictJavaDeps = strictJavaDeps;
+ }
+
+ @Override
+ protected JCCompilationUnit parse(JavaFileObject javaFileObject, CharSequence charSequence) {
+ JCCompilationUnit result = super.parse(javaFileObject, charSequence);
+ TreePruner.prune(result);
+ return result;
+ }
+
+ @Override
+ public Env<AttrContext> attribute(Env<AttrContext> env) {
+ if (compileStates.isDone(env, CompileState.ATTR)) {
+ return env;
+ }
+ Env<AttrContext> result = super.attribute(env);
+ if (strictJavaDeps != null) {
+ strictJavaDeps.postAttribute(result);
+ }
+ return result;
+ }
+
+ @Override
+ protected void flow(Env<AttrContext> env, Queue<Env<AttrContext>> results) {
+ // skip FLOW (as if -relax was enabled, except -relax is broken for JDK >= 8)
+ if (!compileStates.isDone(env, CompileState.FLOW)) {
+ compileStates.put(env, CompileState.FLOW);
+ }
+ results.add(env);
+ }
+
+ @Override
+ public void close() {
+ if (strictJavaDeps != null) {
+ strictJavaDeps.finish();
+ }
+ }
+
+ /**
+ * Override the default {@link JavaCompiler} implementation with {@link JavacTurbineJavaCompiler}
+ * for the given compilation context.
+ */
+ public static void preRegister(Context context, @Nullable final StrictJavaDepsPlugin sjd) {
+ context.put(
+ compilerKey,
+ new Context.Factory<JavaCompiler>() {
+ @Override
+ public JavaCompiler make(Context c) {
+ return new JavacTurbineJavaCompiler(c, sjd);
+ }
+ });
+ }
+}
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/TreePruner.java b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/TreePruner.java
new file mode 100644
index 0000000000..da748442d5
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/TreePruner.java
@@ -0,0 +1,231 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.sun.source.tree.BinaryTree;
+import com.sun.source.tree.ConditionalExpressionTree;
+import com.sun.source.tree.IdentifierTree;
+import com.sun.source.tree.LiteralTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.ParenthesizedTree;
+import com.sun.source.tree.TypeCastTree;
+import com.sun.source.tree.UnaryTree;
+import com.sun.source.util.SimpleTreeVisitor;
+import com.sun.tools.javac.code.Flags;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCBlock;
+import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
+import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
+import com.sun.tools.javac.tree.TreeScanner;
+import com.sun.tools.javac.util.List;
+
+/**
+ * Prunes AST nodes that are not required for header compilation.
+ *
+ * <p>Used by Turbine after parsing and before all subsequent phases to avoid
+ * doing unnecessary work.
+ */
+public class TreePruner {
+
+ /**
+ * Prunes AST nodes that are not required for header compilation.
+ *
+ * <p>Specifically:
+ *
+ * <ul>
+ * <li>method bodies
+ * <li>class and instance initializer blocks
+ * <li>initializers of definitely non-constant fields
+ * </ul>
+ */
+ static void prune(JCTree tree) {
+ tree.accept(PRUNING_VISITOR);
+ }
+
+ /** A {@link TreeScanner} that deletes method bodies and blocks from the AST. */
+ private static final TreeScanner PRUNING_VISITOR =
+ new TreeScanner() {
+
+ @Override
+ public void visitMethodDef(JCMethodDecl tree) {
+ if (tree.body == null) {
+ return;
+ }
+ tree.body.stats = com.sun.tools.javac.util.List.nil();
+ }
+
+ @Override
+ public void visitBlock(JCBlock tree) {
+ tree.stats = List.nil();
+ }
+
+ @Override
+ public void visitVarDef(JCVariableDecl tree) {
+ if ((tree.mods.flags & Flags.ENUM) == Flags.ENUM) {
+ // javac desugars enum constants into fields during parsing
+ return;
+ }
+ // drop field initializers unless the field looks like a JLS §4.12.4 constant variable
+ if (isConstantVariable(tree)) {
+ return;
+ }
+ tree.init = null;
+ }
+ };
+
+ private static boolean isConstantVariable(JCVariableDecl tree) {
+ if ((tree.mods.flags & Flags.FINAL) != Flags.FINAL) {
+ return false;
+ }
+ if (!constantType(tree.getType())) {
+ return false;
+ }
+ if (tree.getInitializer() != null) {
+ Boolean result = tree.getInitializer().accept(CONSTANT_VISITOR, null);
+ if (result == null || !result) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns true iff the given tree could be the type name of a constant type.
+ *
+ * <p>This is a conservative over-approximation: an identifier named {@code String}
+ * isn't necessarily a type name, but this is used at parse-time before types have
+ * been attributed.
+ */
+ private static boolean constantType(JCTree tree) {
+ switch (tree.getKind()) {
+ case PRIMITIVE_TYPE:
+ return true;
+ case IDENTIFIER:
+ return tree.toString().contentEquals("String");
+ case MEMBER_SELECT:
+ return tree.toString().contentEquals("java.lang.String");
+ default:
+ return false;
+ }
+ }
+
+ /** A visitor that identifies JLS §15.28 constant expressions. */
+ private static final SimpleTreeVisitor<Boolean, Void> CONSTANT_VISITOR =
+ new SimpleTreeVisitor<Boolean, Void>(false) {
+
+ @Override
+ public Boolean visitConditionalExpression(ConditionalExpressionTree node, Void p) {
+ return reduce(
+ node.getCondition().accept(this, null),
+ node.getTrueExpression().accept(this, null),
+ node.getFalseExpression().accept(this, null));
+ }
+
+ @Override
+ public Boolean visitParenthesized(ParenthesizedTree node, Void p) {
+ return node.getExpression().accept(this, null);
+ }
+
+ @Override
+ public Boolean visitUnary(UnaryTree node, Void p) {
+ switch (node.getKind()) {
+ case UNARY_PLUS:
+ case UNARY_MINUS:
+ case BITWISE_COMPLEMENT:
+ case LOGICAL_COMPLEMENT:
+ break;
+ default:
+ // non-constant unary expression
+ return false;
+ }
+ return node.getExpression().accept(this, null);
+ }
+
+ @Override
+ public Boolean visitBinary(BinaryTree node, Void p) {
+ switch (node.getKind()) {
+ case MULTIPLY:
+ case DIVIDE:
+ case REMAINDER:
+ case PLUS:
+ case MINUS:
+ case LEFT_SHIFT:
+ case RIGHT_SHIFT:
+ case UNSIGNED_RIGHT_SHIFT:
+ case LESS_THAN:
+ case LESS_THAN_EQUAL:
+ case GREATER_THAN:
+ case GREATER_THAN_EQUAL:
+ case AND:
+ case XOR:
+ case OR:
+ case CONDITIONAL_AND:
+ case CONDITIONAL_OR:
+ case EQUAL_TO:
+ case NOT_EQUAL_TO:
+ break;
+ default:
+ // non-constant binary expression
+ return false;
+ }
+ return reduce(
+ node.getLeftOperand().accept(this, null), node.getRightOperand().accept(this, null));
+ }
+
+ @Override
+ public Boolean visitTypeCast(TypeCastTree node, Void p) {
+ return reduce(
+ constantType((JCTree) node.getType()), node.getExpression().accept(this, null));
+ }
+
+ @Override
+ public Boolean visitMemberSelect(MemberSelectTree node, Void p) {
+ return node.getExpression().accept(this, null);
+ }
+
+ @Override
+ public Boolean visitIdentifier(IdentifierTree node, Void p) {
+ // Assume all variables are constant variables. This is a conservative assumption, but
+ // it's the best we can do with only syntactic information.
+ return true;
+ }
+
+ @Override
+ public Boolean visitLiteral(LiteralTree node, Void unused) {
+ switch (node.getKind()) {
+ case STRING_LITERAL:
+ case INT_LITERAL:
+ case LONG_LITERAL:
+ case FLOAT_LITERAL:
+ case DOUBLE_LITERAL:
+ case BOOLEAN_LITERAL:
+ case CHAR_LITERAL:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public boolean reduce(Boolean... bx) {
+ boolean r = true;
+ for (Boolean b : bx) {
+ r &= firstNonNull(b, false);
+ }
+ return r;
+ }
+ };
+}
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/ZipOutputFileManager.java b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/ZipOutputFileManager.java
new file mode 100644
index 0000000000..f2410afdba
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/ZipOutputFileManager.java
@@ -0,0 +1,161 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import com.sun.tools.javac.api.ClientCodeWrapper.Trusted;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.util.Context;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import javax.tools.FileObject;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+
+/** A {@link JavacFileManager} that collects output into a zipfile. */
+@Trusted
+public class ZipOutputFileManager extends JavacFileManager {
+
+ private final Map<String, OutputFileObject> files;
+
+ public ZipOutputFileManager(Context context, Map<String, OutputFileObject> files) {
+ super(context, true, StandardCharsets.UTF_8);
+ this.files = files;
+ }
+
+ /**
+ * Returns true if the file manager owns this location; otherwise it delegates to the underlying
+ * implementation.
+ */
+ private boolean ownedLocation(Location location) {
+ return location.isOutputLocation();
+ }
+
+ @Override
+ public boolean hasLocation(Location location) {
+ return ownedLocation(location) || super.hasLocation(location);
+ }
+
+ private OutputFileObject getOutput(String name, JavaFileObject.Kind kind, Location location) {
+ if (files.containsKey(name)) {
+ return files.get(name);
+ }
+ OutputFileObject result = new OutputFileObject(name, kind, location);
+ files.put(name, result);
+ return result;
+ }
+
+ @Override
+ public JavaFileObject getJavaFileForOutput(
+ Location location, String className, JavaFileObject.Kind kind, FileObject sibling)
+ throws IOException {
+ if (!ownedLocation(location)) {
+ return super.getJavaFileForOutput(location, className, kind, sibling);
+ }
+ // The classname parameter will be something like
+ // "com.google.common.base.Flag$String"; nested classes are delimited with
+ // dollar signs, so the following transformation works as intended.
+ return getOutput(className.replace('.', '/') + kind.extension, kind, location);
+ }
+
+ @Override
+ public FileObject getFileForOutput(
+ Location location, String packageName, String relativeName, FileObject sibling)
+ throws IOException {
+ if (!ownedLocation(location)) {
+ return super.getFileForOutput(location, packageName, relativeName, sibling);
+ }
+ String path = "";
+ if (packageName != null && !packageName.isEmpty()) {
+ path = packageName.replace('.', '/') + '/';
+ }
+ path += relativeName;
+ return getOutput(path, JavaFileObject.Kind.OTHER, location);
+ }
+
+ @Override
+ public boolean isSameFile(FileObject a, FileObject b) {
+ boolean at = a instanceof OutputFileObject;
+ boolean bt = b instanceof OutputFileObject;
+ if (at || bt) {
+ if (at ^ bt) {
+ return false;
+ }
+ return ((OutputFileObject) a).toUri().equals(((OutputFileObject) b).toUri());
+ }
+ return super.isSameFile(a, b);
+ }
+
+ /** A {@link JavaFileObject} that accumulates output in memory. */
+ public static class OutputFileObject extends SimpleJavaFileObject {
+
+ public final Location location;
+
+ private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+ public OutputFileObject(String name, Kind kind, Location location) {
+ super(URI.create("outputbuffer://" + name), kind);
+ this.location = location;
+ }
+
+ @Override
+ public OutputStream openOutputStream() {
+ return buffer;
+ }
+
+ @Override
+ public InputStream openInputStream() throws IOException {
+ return new ByteArrayInputStream(asBytes());
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ CodingErrorAction errorAction =
+ ignoreEncodingErrors ? CodingErrorAction.IGNORE : CodingErrorAction.REPORT;
+ CharsetDecoder decoder =
+ StandardCharsets.UTF_8
+ .newDecoder()
+ .onUnmappableCharacter(errorAction)
+ .onMalformedInput(errorAction);
+ return decoder.decode(ByteBuffer.wrap(asBytes()));
+ }
+
+ public byte[] asBytes() {
+ return buffer.toByteArray();
+ }
+ }
+
+ public static void preRegister(Context context, final Map<String, OutputFileObject> files) {
+ context.put(
+ JavaFileManager.class,
+ new Context.Factory<JavaFileManager>() {
+ @Override
+ public JavaFileManager make(Context c) {
+ return new ZipOutputFileManager(c, files);
+ }
+ });
+ }
+}
diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/ZipUtil.java b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/ZipUtil.java
new file mode 100644
index 0000000000..a4e7d7e033
--- /dev/null
+++ b/src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac/ZipUtil.java
@@ -0,0 +1,53 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import java.io.IOException;
+import java.util.GregorianCalendar;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Static utility methods for working with ZipOutputStream.
+ */
+public abstract class ZipUtil {
+
+ /** The earliest date representable in a zip file, the DOS epoch. */
+ private static final long DOS_EPOCH =
+ new GregorianCalendar(1980, 0, 1, 0, 0, 0).getTimeInMillis();
+
+ /**
+ * This is a helper method for adding an uncompressed entry to a
+ * ZipOutputStream. The entry timestamp is also set to a fixed value.
+ *
+ * @param name filename to use within the zip file
+ * @param content file contents
+ * @param zip the ZipOutputStream to which this entry will be appended
+ */
+ public static void storeEntry(String name, byte[] content, ZipOutputStream zip)
+ throws IOException {
+ ZipEntry entry = new ZipEntry(name);
+ entry.setMethod(ZipEntry.STORED);
+ entry.setTime(DOS_EPOCH);
+ entry.setSize(content.length);
+ CRC32 crc32 = new CRC32();
+ crc32.update(content);
+ entry.setCrc(crc32.getValue());
+ zip.putNextEntry(entry);
+ zip.write(content);
+ zip.closeEntry();
+ }
+}
diff --git a/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/BUILD b/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/BUILD
new file mode 100644
index 0000000000..e703bf3036
--- /dev/null
+++ b/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/BUILD
@@ -0,0 +1,18 @@
+package_group(
+ name = "packages",
+ packages = ["//src/java_tools/buildjar/..."],
+)
+
+package(default_visibility = [":packages"])
+
+java_test(
+ name = "TurbineOptionsTest",
+ srcs = ["TurbineOptionsTest.java"],
+ deps = [
+ "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine:turbine_options",
+ "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine:turbine_options_parser",
+ "//third_party:guava",
+ "//third_party:junit4",
+ "//third_party:truth",
+ ],
+)
diff --git a/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/javac/BUILD b/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/javac/BUILD
new file mode 100644
index 0000000000..8914405c89
--- /dev/null
+++ b/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/javac/BUILD
@@ -0,0 +1,27 @@
+java_test(
+ name = "JavacTurbineTest",
+ srcs = ["JavacTurbineTest.java"],
+ deps = [
+ "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine:turbine_options",
+ "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac:javac_turbine",
+ "//src/main/protobuf:deps_proto",
+ "//third_party:asm",
+ "//third_party:asm-util",
+ "//third_party:guava",
+ "//third_party:junit4",
+ "//third_party:truth",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
+
+java_test(
+ name = "TreePrunerTest",
+ srcs = ["TreePrunerTest.java"],
+ deps = [
+ "//src/java_tools/buildjar/java/com/google/devtools/build/java/turbine/javac:tree_pruner",
+ "//third_party:guava",
+ "//third_party:junit4",
+ "//third_party:truth",
+ "//third_party/java/jdk/langtools:javac",
+ ],
+)
diff --git a/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/javac/JavacTurbineTest.java b/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/javac/JavacTurbineTest.java
new file mode 100644
index 0000000000..f80dec42d0
--- /dev/null
+++ b/src/java_tools/buildjar/javatests/com/google/devtools/build/java/turbine/javac/JavacTurbineTest.java
@@ -0,0 +1,832 @@
+// Copyright 2016 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.java.turbine.javac;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.common.io.ByteStreams;
+import com.google.devtools.build.java.turbine.TurbineOptions;
+import com.google.devtools.build.java.turbine.javac.JavacTurbine.Result;
+import com.google.devtools.build.lib.view.proto.Deps;
+import com.google.devtools.build.lib.view.proto.Deps.Dependency;
+
+import com.sun.source.util.JavacTask;
+import com.sun.tools.javac.api.ClientCodeWrapper.Trusted;
+import com.sun.tools.javac.api.JavacTool;
+import com.sun.tools.javac.file.JavacFileManager;
+import com.sun.tools.javac.util.Context;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.util.Textifier;
+import org.objectweb.asm.util.TraceClassVisitor;
+
+import java.io.BufferedInputStream;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.tools.FileObject;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardLocation;
+
+/** Unit tests for {@link JavacTurbine}. */
+@RunWith(JUnit4.class)
+public class JavacTurbineTest {
+
+ @Rule public TemporaryFolder temp = new TemporaryFolder();
+
+ Path sourcedir;
+ List<Path> sources;
+ Path tempdir;
+ Path output;
+ Path outputDeps;
+
+ TurbineOptions.Builder optionsBuilder = TurbineOptions.builder();
+
+ @Before
+ public void setUp() throws IOException {
+ sourcedir = temp.newFolder().toPath();
+ tempdir = temp.newFolder("_temp").toPath();
+ output = temp.newFile("out.jar").toPath();
+ outputDeps = temp.newFile("out.jdeps").toPath();
+
+ sources = new ArrayList<>();
+
+ optionsBuilder
+ .setOutput(output.toString())
+ .setTempDir(tempdir.toString())
+ .addBootClassPathEntries(
+ ImmutableList.copyOf(Splitter.on(':').split(System.getProperty("sun.boot.class.path"))))
+ .setOutputDeps(outputDeps.toString())
+ .setStrictJavaDeps("ERROR")
+ .addAllJavacOpts(Arrays.asList("-source", "7", "-target", "7"))
+ .setTargetLabel("//test")
+ .setRuleKind("java_library");
+ }
+
+ private void addSourceLines(String path, String... lines) throws IOException {
+ Path source = sourcedir.resolve(path);
+ sources.add(source);
+ Files.write(source, Arrays.asList(lines), StandardCharsets.UTF_8);
+ }
+
+ void compile() throws IOException {
+ optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
+ try (JavacTurbine turbine = new JavacTurbine(optionsBuilder.build())) {
+ assertThat(turbine.compile()).isEqualTo(Result.OK_WITH_REDUCED_CLASSPATH);
+ }
+ }
+
+ private Map<String, byte[]> collectOutputs() throws IOException {
+ return collectFiles(output);
+ }
+
+ @Test
+ public void hello() throws Exception {
+ addSourceLines(
+ "Hello.java",
+ "class Hello {",
+ " public static void main(String[] args) {",
+ " System.err.println(\"Hello World\");",
+ " }",
+ "}");
+
+ compile();
+
+ Map<String, byte[]> outputs = collectOutputs();
+
+ assertThat(outputs.keySet()).containsExactly("Hello.class");
+
+ String text = textify(outputs.get("Hello.class"));
+ String[] expected = {
+ "// class version 51.0 (51)",
+ "// access flags 0x20",
+ "class Hello {",
+ "",
+ "",
+ " // access flags 0x0",
+ " <init>()V",
+ "",
+ " // access flags 0x9",
+ " public static main([Ljava/lang/String;)V",
+ "}",
+ ""
+ };
+ assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
+ }
+
+ // verify that FLOW is disabled, as if we had passed -relax
+ // if it isn't we'd get an error about the missing return in f().
+ @Test
+ public void relax() throws Exception {
+ addSourceLines("Hello.java", "class Hello {", " int f() {}", "}");
+
+ compile();
+
+ Map<String, byte[]> outputs = collectOutputs();
+
+ assertThat(outputs.keySet()).containsExactly("Hello.class");
+
+ String text = textify(outputs.get("Hello.class"));
+ String[] expected = {
+ "// class version 51.0 (51)",
+ "// access flags 0x20",
+ "class Hello {",
+ "",
+ "",
+ " // access flags 0x0",
+ " <init>()V",
+ "",
+ " // access flags 0x0",
+ " f()I",
+ "}",
+ ""
+ };
+ assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
+ }
+
+ public @interface MyAnnotation {}
+
+ /**
+ * A sample annotation processor for testing.
+ *
+ * <p>Writes two output files (one source, one data) the very first round it's called. Used to
+ * verify that annotation processor output is collected into the output jar.
+ */
+ @SupportedAnnotationTypes("MyAnnotation")
+ public static class MyProcessor extends AbstractProcessor {
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ boolean first = true;
+
+ @Override
+ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ if (!first) {
+ // Write the output files exactly once to ensure we don't try to write the same file
+ // twice or do work on the final round.
+ return false;
+ }
+ if (roundEnv.getRootElements().isEmpty()) {
+ return false;
+ }
+ first = false;
+ Element element = roundEnv.getRootElements().iterator().next();
+ try {
+ JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("Generated", element);
+ try (OutputStream os = sourceFile.openOutputStream()) {
+ os.write("class Generated {}".getBytes(StandardCharsets.UTF_8));
+ }
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ try {
+ FileObject file =
+ processingEnv
+ .getFiler()
+ .createResource(StandardLocation.CLASS_OUTPUT, "com.foo", "hello.txt", element);
+ try (OutputStream os = file.openOutputStream()) {
+ os.write("hello".getBytes(StandardCharsets.UTF_8));
+ }
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ return false;
+ }
+ }
+
+ @Test
+ public void processing() throws Exception {
+ addSourceLines("MyAnnotation.java", "public @interface MyAnnotation {}");
+ addSourceLines(
+ "Hello.java",
+ "@MyAnnotation",
+ "class Hello {",
+ " public static void main(String[] args) {",
+ " System.err.println(\"Hello World\");",
+ " }",
+ "}");
+
+ optionsBuilder.setProcessors(ImmutableList.of(MyProcessor.class.getName()));
+ optionsBuilder.addProcessorPathEntries(
+ ImmutableList.copyOf(Splitter.on(':').split(System.getProperty("java.class.path"))));
+ optionsBuilder.addClassPathEntries(
+ ImmutableList.copyOf(Splitter.on(':').split(System.getProperty("java.class.path"))));
+
+ compile();
+
+ Map<String, byte[]> outputs = collectOutputs();
+ assertThat(outputs.keySet())
+ .containsExactly(
+ "Generated.class", "MyAnnotation.class", "Hello.class", "com/foo/hello.txt");
+
+ {
+ String text = textify(outputs.get("Generated.class"));
+ String[] expected = {
+ "// class version 51.0 (51)",
+ "// access flags 0x20",
+ "class Generated {",
+ "",
+ "",
+ " // access flags 0x0",
+ " <init>()V",
+ "}",
+ ""
+ };
+ assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
+ }
+
+ // sanity-check that annotation processing doesn't interfere with stripping
+ {
+ String text = textify(outputs.get("Hello.class"));
+ String[] expected = {
+ "// class version 51.0 (51)",
+ "// access flags 0x20",
+ "class Hello {",
+ "",
+ "",
+ " @LMyAnnotation;() // invisible",
+ "",
+ " // access flags 0x0",
+ " <init>()V",
+ "",
+ " // access flags 0x9",
+ " public static main([Ljava/lang/String;)V",
+ "}",
+ ""
+ };
+ assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
+ }
+ }
+
+ static Map<String, byte[]> collectFiles(Path jar) throws IOException {
+ Map<String, byte[]> files = new LinkedHashMap<>();
+ try (JarFile jf = new JarFile(jar.toFile())) {
+ Enumeration<JarEntry> entries = jf.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ files.put(entry.getName(), ByteStreams.toByteArray(jf.getInputStream(entry)));
+ }
+ }
+ return files;
+ }
+
+ static String textify(byte[] bytes) {
+ StringWriter sw = new StringWriter();
+ ClassReader cr = new ClassReader(bytes);
+ cr.accept(new TraceClassVisitor(null, new Textifier(), new PrintWriter(sw, true)), 0);
+ return sw.toString();
+ }
+
+ private static final Function<Object, String> TO_STRING =
+ new Function<Object, String>() {
+ @Override
+ public String apply(Object input) {
+ return String.valueOf(input);
+ }
+ };
+
+ @Test
+ public void extractLanguageLevel() {
+ ImmutableList.Builder<String> output = ImmutableList.builder();
+ JavacTurbine.addLanguageLevel(
+ output,
+ Arrays.asList(
+ "-g", "-source", "6", "-target", "6", "-Xlint:all", "-source", "7", "-target", "7"));
+ assertThat(output.build())
+ .containsExactly("-source", "6", "-target", "6", "-source", "7", "-target", "7")
+ .inOrder();
+ }
+
+ @Test
+ public void jdeps() throws Exception {
+
+ Path libC = temp.newFile("libc.jar").toPath();
+ compileLib(
+ libC,
+ Collections.<Path>emptyList(),
+ Arrays.asList(new StringJavaFileObject("C.java", "interface C { String getString(); }")));
+
+ Path libA = temp.newFile("liba.jar").toPath();
+ compileLib(
+ libA,
+ Collections.singleton(libC),
+ Arrays.asList(new StringJavaFileObject("A.java", "interface A { C getC(); }")));
+
+ Path depsA =
+ writedeps(
+ "liba.jdeps",
+ Deps.Dependencies.newBuilder()
+ .setSuccess(true)
+ .setRuleLabel("//lib:a")
+ .addDependency(
+ Deps.Dependency.newBuilder()
+ .setPath(libC.toString())
+ .setKind(Deps.Dependency.Kind.EXPLICIT))
+ .build());
+
+ Path libB = temp.newFile("libb.jar").toPath();
+ compileLib(
+ libB,
+ Collections.<Path>emptyList(),
+ Arrays.asList(new StringJavaFileObject("B.java", "interface B {}")));
+
+ optionsBuilder.addClassPathEntries(
+ ImmutableList.of(libA.toString(), libB.toString(), libC.toString()));
+ optionsBuilder.addAllDepsArtifacts(ImmutableList.of(depsA.toString()));
+ optionsBuilder.addDirectJarToTarget(libA.toString(), "//lib:a");
+ optionsBuilder.addDirectJarToTarget(libB.toString(), "//lib:b");
+ optionsBuilder.addIndirectJarToTarget(libC.toString(), "//lib:c");
+ optionsBuilder.setTargetLabel("//my:target");
+
+ addSourceLines(
+ "Hello.java",
+ "class Hello {",
+ " public static A a = null;",
+ " public static String s = a.getC().getString();",
+ " public static void main(String[] args) {",
+ " B b = null;",
+ " }",
+ "}");
+
+ compile();
+
+ Deps.Dependencies depsProto = getDeps();
+
+ assertThat(depsProto.getSuccess()).isTrue();
+ assertThat(depsProto.getRuleLabel()).isEqualTo("//my:target");
+ assertThat(getEntries(depsProto))
+ .containsExactlyEntriesIn(
+ ImmutableMap.of(
+ libA.toString(), Deps.Dependency.Kind.EXPLICIT,
+ libB.toString(), Deps.Dependency.Kind.IMPLICIT,
+ libC.toString(), Deps.Dependency.Kind.IMPLICIT));
+ }
+
+ private Map<String, Deps.Dependency.Kind> getEntries(Deps.Dependencies deps) {
+ Map<String, Deps.Dependency.Kind> result = new LinkedHashMap<>();
+ for (Dependency dep : deps.getDependencyList()) {
+ result.put(dep.getPath(), dep.getKind());
+ }
+ return result;
+ }
+
+ private Deps.Dependencies getDeps() throws IOError {
+ Deps.Dependencies depsProto;
+ try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(outputDeps))) {
+ Deps.Dependencies.Builder builder = Deps.Dependencies.newBuilder();
+ builder.mergeFrom(in);
+ depsProto = builder.build();
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ return depsProto;
+ }
+
+ private void compileLib(
+ Path jar, Iterable<Path> classpath, Iterable<? extends JavaFileObject> units)
+ throws IOException {
+ final Path outdir = temp.newFolder().toPath();
+ JavacFileManager fm = new JavacFileManager(new Context(), false, StandardCharsets.UTF_8);
+ fm.setLocationFromPaths(StandardLocation.CLASS_OUTPUT, Collections.singleton(outdir));
+ fm.setLocationFromPaths(StandardLocation.CLASS_PATH, classpath);
+ List<String> options = Arrays.asList("-d", outdir.toString());
+ JavacTool tool = JavacTool.create();
+
+ JavacTask task =
+ tool.getTask(new PrintWriter(System.err, true), fm, null, options, null, units);
+ assertThat(task.call()).isTrue();
+
+ try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(jar))) {
+ Files.walkFileTree(
+ outdir,
+ new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path path, BasicFileAttributes attrs)
+ throws IOException {
+ JarEntry je = new JarEntry(outdir.relativize(path).toString());
+ jos.putNextEntry(je);
+ Files.copy(path, jos);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+ }
+
+ @Trusted
+ static class StringJavaFileObject extends SimpleJavaFileObject {
+ private final String content;
+
+ StringJavaFileObject(String name, String... lines) {
+ super(URI.create(name), JavaFileObject.Kind.SOURCE);
+ this.content = Joiner.on('\n').join(lines);
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ return content;
+ }
+ }
+
+ @Test
+ public void reducedClasspath() throws Exception {
+
+ Path libD = temp.newFile("libd.jar").toPath();
+ compileLib(
+ libD,
+ Collections.<Path>emptyList(),
+ Arrays.asList(new StringJavaFileObject("D.java", "public class D {}")));
+
+ Path libC = temp.newFile("libc.jar").toPath();
+ compileLib(
+ libC,
+ Collections.singleton(libD),
+ Arrays.asList(new StringJavaFileObject("C.java", "class C { static D d; }")));
+
+ Path libB = temp.newFile("libb.jar").toPath();
+ compileLib(
+ libB,
+ Arrays.asList(libC, libD),
+ Arrays.asList(new StringJavaFileObject("B.java", "class B { static C c; }")));
+
+ Path libA = temp.newFile("liba.jar").toPath();
+ compileLib(
+ libA,
+ Arrays.asList(libB, libC, libD),
+ Arrays.asList(new StringJavaFileObject("A.java", "class A { static B b; }")));
+ Path depsA =
+ writedeps(
+ "liba.jdeps",
+ Deps.Dependencies.newBuilder()
+ .setSuccess(true)
+ .setRuleLabel("//lib:a")
+ .addDependency(
+ Deps.Dependency.newBuilder()
+ .setPath(libB.toString())
+ .setKind(Deps.Dependency.Kind.EXPLICIT))
+ .build());
+
+ optionsBuilder.addClassPathEntries(
+ ImmutableList.of(libA.toString(), libB.toString(), libC.toString(), libD.toString()));
+ optionsBuilder.addAllDepsArtifacts(ImmutableList.of(depsA.toString()));
+ optionsBuilder.addDirectJarToTarget(libA.toString(), "//lib:a");
+ optionsBuilder.addIndirectJarToTarget(libB.toString(), "//lib:b");
+ optionsBuilder.addIndirectJarToTarget(libC.toString(), "//lib:c");
+ optionsBuilder.addIndirectJarToTarget(libD.toString(), "//lib:d");
+ optionsBuilder.setTargetLabel("//my:target");
+
+ addSourceLines(
+ "Hello.java",
+ "class Hello {",
+ " public static A a = new A();",
+ " public static void main(String[] args) {",
+ " A a = null;",
+ " B b = null;",
+ " C c = null;",
+ " D d = null;",
+ " }",
+ "}");
+
+ optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
+
+ try (JavacTurbine turbine = new JavacTurbine(optionsBuilder.build())) {
+ assertThat(turbine.compile()).isEqualTo(Result.OK_WITH_REDUCED_CLASSPATH);
+ Context context = turbine.context;
+
+ JavacFileManager fm = (JavacFileManager) context.get(JavaFileManager.class);
+ assertThat(fm.getLocationAsPaths(StandardLocation.CLASS_PATH)).containsExactly(libA, libB);
+
+ Deps.Dependencies depsProto = getDeps();
+
+ assertThat(depsProto.getSuccess()).isTrue();
+ assertThat(depsProto.getRuleLabel()).isEqualTo("//my:target");
+ assertThat(getEntries(depsProto))
+ .containsExactlyEntriesIn(
+ ImmutableMap.of(
+ libA.toString(),
+ Deps.Dependency.Kind.EXPLICIT,
+ libB.toString(),
+ Deps.Dependency.Kind.IMPLICIT));
+ }
+ }
+
+ Path writedeps(String name, Deps.Dependencies deps) throws IOException {
+ Path path = temp.newFile(name).toPath();
+ try (OutputStream os = Files.newOutputStream(path)) {
+ deps.writeTo(os);
+ }
+ return path;
+ }
+
+ @Test
+ public void reducedClasspathFallback() throws Exception {
+
+ Path libD = temp.newFile("libd.jar").toPath();
+ compileLib(
+ libD,
+ Collections.<Path>emptyList(),
+ Arrays.asList(
+ new StringJavaFileObject("D.java", "public class D { static final int CONST = 42; }")));
+
+ Path libC = temp.newFile("libc.jar").toPath();
+ compileLib(
+ libC,
+ Collections.singleton(libD),
+ Arrays.asList(new StringJavaFileObject("C.java", "class C extends D {}")));
+
+ Path libB = temp.newFile("libb.jar").toPath();
+ compileLib(
+ libB,
+ Arrays.asList(libC, libD),
+ Arrays.asList(new StringJavaFileObject("B.java", "class B extends C {}")));
+
+ Path libA = temp.newFile("liba.jar").toPath();
+ compileLib(
+ libA,
+ Arrays.asList(libB, libC, libD),
+ Arrays.asList(new StringJavaFileObject("A.java", "class A extends B {}")));
+ Path depsA =
+ writedeps(
+ "liba.jdeps",
+ Deps.Dependencies.newBuilder()
+ .setSuccess(true)
+ .setRuleLabel("//lib:a")
+ .addDependency(
+ Deps.Dependency.newBuilder()
+ .setPath(libB.toString())
+ .setKind(Deps.Dependency.Kind.EXPLICIT))
+ .build());
+
+ optionsBuilder.addClassPathEntries(
+ ImmutableList.of(libA.toString(), libB.toString(), libC.toString(), libD.toString()));
+ optionsBuilder.addAllDepsArtifacts(ImmutableList.of(depsA.toString()));
+ optionsBuilder.addDirectJarToTarget(libA.toString(), "//lib:a");
+ optionsBuilder.addIndirectJarToTarget(libB.toString(), "//lib:b");
+ optionsBuilder.addIndirectJarToTarget(libC.toString(), "//lib:c");
+ optionsBuilder.addIndirectJarToTarget(libD.toString(), "//lib:d");
+ optionsBuilder.setTargetLabel("//my:target");
+
+ addSourceLines(
+ "Hello.java",
+ "class Hello {",
+ " public static final int CONST = A.CONST;",
+ " public static void main(String[] args) {}",
+ "}");
+
+ optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
+
+ try (JavacTurbine turbine = new JavacTurbine(optionsBuilder.build())) {
+ assertThat(turbine.compile()).isEqualTo(Result.OK_WITH_FULL_CLASSPATH);
+ Context context = turbine.context;
+
+ JavacFileManager fm = (JavacFileManager) context.get(JavaFileManager.class);
+ assertThat(fm.getLocationAsPaths(StandardLocation.CLASS_PATH))
+ .containsExactly(libA, libB, libC, libD);
+
+ Deps.Dependencies depsProto = getDeps();
+
+ assertThat(depsProto.getSuccess()).isTrue();
+ assertThat(depsProto.getRuleLabel()).isEqualTo("//my:target");
+ assertThat(getEntries(depsProto))
+ .containsExactlyEntriesIn(
+ ImmutableMap.of(
+ libA.toString(), Deps.Dependency.Kind.EXPLICIT,
+ libB.toString(), Deps.Dependency.Kind.IMPLICIT,
+ libC.toString(), Deps.Dependency.Kind.IMPLICIT,
+ libD.toString(), Deps.Dependency.Kind.IMPLICIT));
+ }
+ }
+
+ @Test
+ public void constants() throws Exception {
+ addSourceLines(
+ "Const.java",
+ "class Const {",
+ " public static final int A = 42;",
+ " public static final int B = 42 + 42;",
+ " public static final int C = new Integer(42);",
+ " public static final int D = 42 + new Integer(42);",
+ " public static final Integer E = 42;",
+ " public static final String F = \"42\";",
+ " public static final java.lang.String G = \"42\";",
+ "}");
+
+ compile();
+
+ Map<String, byte[]> outputs = collectOutputs();
+
+ assertThat(outputs.keySet()).containsExactly("Const.class");
+
+ String text = textify(outputs.get("Const.class"));
+ String[] expected = {
+ "// class version 51.0 (51)",
+ "// access flags 0x20",
+ "class Const {",
+ "",
+ "",
+ " // access flags 0x19",
+ " public final static I A = 42",
+ "",
+ " // access flags 0x19",
+ " public final static I B = 84",
+ "",
+ " // access flags 0x19",
+ " public final static I C",
+ "",
+ " // access flags 0x19",
+ " public final static I D",
+ "",
+ " // access flags 0x19",
+ " public final static Ljava/lang/Integer; E",
+ "",
+ " // access flags 0x19",
+ " public final static Ljava/lang/String; F = \"42\"",
+ "",
+ " // access flags 0x19",
+ " public final static Ljava/lang/String; G = \"42\"",
+ "",
+ " // access flags 0x0",
+ " <init>()V",
+ "}",
+ "",
+ };
+ assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
+ }
+
+ @Test
+ public void constantsEnum() throws Exception {
+ addSourceLines(
+ "TheEnum.java",
+ // TODO(cushon): fix google-java-format's handling of lists of string literals
+ "public enum TheEnum {",
+ " ONE, TWO, THREE;",
+ "}");
+
+ compile();
+ Map<String, byte[]> outputs = collectOutputs();
+ // just don't crash; enum constants need to be preserved
+ assertThat(outputs.keySet()).containsExactly("TheEnum.class");
+
+ String text = textify(outputs.get("TheEnum.class"));
+ String[] expected = {
+ "// class version 51.0 (51)",
+ "// access flags 0x4031",
+ "// signature Ljava/lang/Enum<LTheEnum;>;",
+ "// declaration: TheEnum extends java.lang.Enum<TheEnum>",
+ "public final enum TheEnum extends java/lang/Enum {",
+ "",
+ "",
+ " // access flags 0x4019",
+ " public final static enum LTheEnum; ONE",
+ "",
+ " // access flags 0x4019",
+ " public final static enum LTheEnum; TWO",
+ "",
+ " // access flags 0x4019",
+ " public final static enum LTheEnum; THREE",
+ "",
+ " // access flags 0x101A",
+ " private final static synthetic [LTheEnum; $VALUES",
+ "",
+ " // access flags 0x9",
+ " public static values()[LTheEnum;",
+ "",
+ " // access flags 0x9",
+ " public static valueOf(Ljava/lang/String;)LTheEnum;",
+ "",
+ " // access flags 0x2",
+ " // signature ()V",
+ " // declaration: void <init>()",
+ " private <init>(Ljava/lang/String;I)V",
+ "",
+ " // access flags 0x8",
+ " static <clinit>()V",
+ "}",
+ ""
+ };
+ assertThat(text).isEqualTo(Joiner.on('\n').join(expected));
+ }
+
+ /**
+ * A sample annotation processor for testing.
+ *
+ * <p>Writes an output file that isn't valid UTF-8 to test handling of encoding errors.
+ */
+ @SupportedAnnotationTypes("MyAnnotation")
+ public static class MyBadEncodingProcessor extends AbstractProcessor {
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ boolean first = true;
+
+ @Override
+ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ if (!first) {
+ return false;
+ }
+ if (roundEnv.getRootElements().isEmpty()) {
+ return false;
+ }
+ first = false;
+ Element element = roundEnv.getRootElements().iterator().next();
+ try {
+ JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile("Generated", element);
+ try (OutputStream os = sourceFile.openOutputStream()) {
+ os.write(
+ "class Generated { public static String x = \"".getBytes(StandardCharsets.UTF_8));
+ os.write(0xc2); // write an unpaired surrogate
+ os.write("\";}}".getBytes(StandardCharsets.UTF_8));
+ }
+ } catch (IOException e) {
+ throw new IOError(e);
+ }
+ return false;
+ }
+ }
+
+ @Test
+ public void badEncoding() throws Exception {
+ addSourceLines("MyAnnotation.java", "public @interface MyAnnotation {}");
+ addSourceLines(
+ "Hello.java",
+ "@MyAnnotation",
+ "class Hello {",
+ " public static void main(String[] args) {",
+ " System.err.println(\"Hello World\");",
+ " }",
+ "}");
+
+ optionsBuilder.setProcessors(ImmutableList.of(MyBadEncodingProcessor.class.getName()));
+ optionsBuilder.addProcessorPathEntries(
+ ImmutableList.copyOf(Splitter.on(':').split(System.getProperty("java.class.path"))));
+ optionsBuilder.addClassPathEntries(
+ ImmutableList.copyOf(Splitter.on(':').split(System.getProperty("java.class.path"))));
+
+ optionsBuilder.addSources(ImmutableList.copyOf(Iterables.transform(sources, TO_STRING)));
+ try (StringWriter sw = new StringWriter();
+ JavacTurbine turbine =
+ new JavacTurbine(new PrintWriter(sw, true), optionsBuilder.build())) {
+ Result result = turbine.compile();
+ assertThat(result).isEqualTo(Result.ERROR);
+ assertThat(sw.toString()).contains("error reading");
+ }
+ }
+}