diff options
Diffstat (limited to 'src')
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"); + } + } +} |