aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
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");
+ }
+ }
+}