// 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.lib.rules.java; import static com.google.devtools.build.lib.util.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.ISO_8859_1; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.devtools.build.lib.actions.ActionAnalysisMetadata; import com.google.devtools.build.lib.actions.ActionExecutionContext; import com.google.devtools.build.lib.actions.ActionInput; import com.google.devtools.build.lib.actions.ActionOwner; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.BaseSpawn; import com.google.devtools.build.lib.actions.ExecException; import com.google.devtools.build.lib.actions.Executor; import com.google.devtools.build.lib.actions.ParameterFile; import com.google.devtools.build.lib.actions.ResourceSet; import com.google.devtools.build.lib.actions.Spawn; import com.google.devtools.build.lib.actions.SpawnActionContext; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.actions.CommandLine; import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction; import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.analysis.config.BuildConfiguration; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.ImmutableIterable; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.util.Fingerprint; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.annotation.Nullable; /** * Action for Java header compilation, to be used if --java_header_compilation is enabled. * * <p>The header compiler consumes the inputs of a java compilation, and produces an interface jar * that can be used as a compile-time jar by upstream targets. The header interface jar is * equivalent to the output of ijar, but unlike ijar the header compiler operates directly on Java * source files instead post-processing the class outputs of the compilation. Compiling the * interface jar from source moves javac off the build's critical path. * * <p>The implementation of the header compiler tool can be found under {@code * //src/java_tools/buildjar/java/com/google/devtools/build/java/turbine}. */ public class JavaHeaderCompileAction extends SpawnAction { private static final String GUID = "952db158-2654-4ced-87e5-4646d50523cf"; private static final ResourceSet LOCAL_RESOURCES = ResourceSet.createWithRamCpuIo(/*memoryMb=*/ 750.0, /*cpuUsage=*/ 0.5, /*ioUsage=*/ 0.0); private final Iterable<Artifact> directInputs; @Nullable private final CommandLine directCommandLine; /** The command line for a direct classpath compilation, or {@code null} if disabled. */ @VisibleForTesting @Nullable public CommandLine directCommandLine() { return directCommandLine; } /** * Constructs an action to compile a set of Java source files to a header interface jar. * * @param owner the action owner, typically a java_* RuleConfiguredTarget * @param tools the set of files comprising the tool that creates the header interface jar * @param directInputs the set of direct input artifacts of the compile action * @param transitiveInputs the set of transitive input artifacts of the compile action * @param outputs the outputs of the action * @param directCommandLine the direct command line arguments for the java header compiler * @param transitiveCommandLine the transitive command line arguments for the java header compiler * @param progressMessage the message printed during the progression of the build */ protected JavaHeaderCompileAction( ActionOwner owner, Iterable<Artifact> tools, Iterable<Artifact> directInputs, Iterable<Artifact> transitiveInputs, Iterable<Artifact> outputs, CommandLine directCommandLine, CommandLine transitiveCommandLine, String progressMessage) { super( owner, tools, transitiveInputs, outputs, LOCAL_RESOURCES, transitiveCommandLine, false, JavaCompileAction.UTF8_ENVIRONMENT, /*executionInfo=*/ ImmutableSet.<String>of(), progressMessage, "Turbine"); this.directInputs = checkNotNull(directInputs); this.directCommandLine = checkNotNull(directCommandLine); } @Override protected String computeKey() { return new Fingerprint() .addString(GUID) .addString(super.computeKey()) .addStrings(directCommandLine.arguments()) .hexDigestAndReset(); } @Override protected void internalExecute(ActionExecutionContext actionExecutionContext) throws ExecException, InterruptedException { Executor executor = actionExecutionContext.getExecutor(); SpawnActionContext context = getContext(executor); try { context.exec(getDirectSpawn(), actionExecutionContext); } catch (ExecException e) { // if the direct input spawn failed, try again with transitive inputs to produce better // better messages context.exec(getSpawn(actionExecutionContext.getClientEnv()), actionExecutionContext); // The compilation should never fail with direct deps but succeed with transitive inputs // unless it failed due to a strict deps error, in which case fall back to the transitive // classpath may allow it to succeed (Strict Java Deps errors are reported by javac, // not turbine). } } private final Spawn getDirectSpawn() { return new BaseSpawn( ImmutableList.copyOf(directCommandLine.arguments()), ImmutableMap.<String, String>of() /*environment*/, ImmutableMap.<String, String>of() /*executionInfo*/, this, LOCAL_RESOURCES) { @Override public Iterable<? extends ActionInput> getInputFiles() { return directInputs; } }; } /** Builder class to construct Java header compilation actions. */ public static class Builder { private final RuleContext ruleContext; private Artifact outputJar; @Nullable private Artifact outputDepsProto; private ImmutableSet<Artifact> sourceFiles = ImmutableSet.of(); private final Collection<Artifact> sourceJars = new ArrayList<>(); private NestedSet<Artifact> classpathEntries = NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER); private ImmutableIterable<Artifact> bootclasspathEntries = ImmutableIterable.from(ImmutableList.<Artifact>of()); @Nullable private String ruleKind; @Nullable private Label targetLabel; private PathFragment tempDirectory; private BuildConfiguration.StrictDepsMode strictJavaDeps = BuildConfiguration.StrictDepsMode.OFF; private NestedSet<Artifact> directJars = NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER); private NestedSet<Artifact> compileTimeDependencyArtifacts = NestedSetBuilder.emptySet(Order.STABLE_ORDER); private ImmutableList<String> javacOpts; private NestedSet<Artifact> processorPath = NestedSetBuilder.emptySet(Order.STABLE_ORDER); private final List<String> processorNames = new ArrayList<>(); private final List<String> processorFlags = new ArrayList<>(); private NestedSet<Artifact> javabaseInputs; private Artifact javacJar; public Builder(RuleContext ruleContext) { this.ruleContext = ruleContext; } /** Sets the output jdeps file. */ public Builder setOutputDepsProto(@Nullable Artifact outputDepsProto) { this.outputDepsProto = outputDepsProto; return this; } /** Sets the direct dependency artifacts. */ public Builder setDirectJars(NestedSet<Artifact> directJars) { checkNotNull(directJars, "directJars must not be null"); this.directJars = directJars; return this; } /** Sets the .jdeps artifacts for direct dependencies. */ public Builder setCompileTimeDependencyArtifacts(NestedSet<Artifact> dependencyArtifacts) { checkNotNull(dependencyArtifacts, "dependencyArtifacts must not be null"); this.compileTimeDependencyArtifacts = dependencyArtifacts; return this; } /** Sets Java compiler flags. */ public Builder setJavacOpts(ImmutableList<String> javacOpts) { checkNotNull(javacOpts, "javacOpts must not be null"); this.javacOpts = javacOpts; return this; } /** Sets the output jar. */ public Builder setOutputJar(Artifact outputJar) { checkNotNull(outputJar, "outputJar must not be null"); this.outputJar = outputJar; return this; } /** Adds Java source files to compile. */ public Builder setSourceFiles(ImmutableSet<Artifact> sourceFiles) { checkNotNull(sourceFiles, "sourceFiles must not be null"); this.sourceFiles = sourceFiles; return this; } /** Adds a jar archive of Java sources to compile. */ public Builder addSourceJars(Collection<Artifact> sourceJars) { checkNotNull(sourceJars, "sourceJars must not be null"); this.sourceJars.addAll(sourceJars); return this; } /** Sets the compilation classpath entries. */ public Builder setClasspathEntries(NestedSet<Artifact> classpathEntries) { checkNotNull(classpathEntries, "classpathEntries must not be null"); this.classpathEntries = classpathEntries; return this; } /** Sets the compilation bootclasspath entries. */ public Builder setBootclasspathEntries(ImmutableIterable<Artifact> bootclasspathEntries) { checkNotNull(bootclasspathEntries, "bootclasspathEntries must not be null"); this.bootclasspathEntries = bootclasspathEntries; return this; } /** Sets the annotation processors classpath entries. */ public Builder setProcessorPaths(NestedSet<Artifact> processorPaths) { checkNotNull(processorPaths, "processorPaths must not be null"); this.processorPath = processorPaths; return this; } /** Sets the fully-qualified class names of annotation processors to run. */ public Builder addProcessorNames(Collection<String> processorNames) { checkNotNull(processorNames, "processorNames must not be null"); this.processorNames.addAll(processorNames); return this; } /** Sets annotation processor flags to pass to javac. */ public Builder addProcessorFlags(Collection<String> processorFlags) { checkNotNull(processorFlags, "processorFlags must not be null"); this.processorFlags.addAll(processorFlags); return this; } /** Sets the kind of the build rule being compiled (e.g. {@code java_library}). */ public Builder setRuleKind(@Nullable String ruleKind) { this.ruleKind = ruleKind; return this; } /** Sets the label of the target being compiled. */ public Builder setTargetLabel(@Nullable Label targetLabel) { this.targetLabel = targetLabel; return this; } /** * Sets the path to a temporary directory, e.g. for extracting sourcejar entries to before * compilation. */ public Builder setTempDirectory(PathFragment tempDirectory) { checkNotNull(tempDirectory, "tempDirectory must not be null"); this.tempDirectory = tempDirectory; return this; } /** Sets the Strict Java Deps mode. */ public Builder setStrictJavaDeps(BuildConfiguration.StrictDepsMode strictJavaDeps) { checkNotNull(strictJavaDeps, "strictJavaDeps must not be null"); this.strictJavaDeps = strictJavaDeps; return this; } /** Sets the javabase inputs. */ public Builder setJavaBaseInputs(NestedSet<Artifact> javabaseInputs) { checkNotNull(javabaseInputs, "javabaseInputs must not be null"); this.javabaseInputs = javabaseInputs; return this; } /** Sets the javac jar. */ public Builder setJavacJar(Artifact javacJar) { checkNotNull(javacJar, "javacJar must not be null"); this.javacJar = javacJar; return this; } /** Builds and registers the {@link JavaHeaderCompileAction} for a header compilation. */ public void build(JavaToolchainProvider javaToolchain) { ruleContext.registerAction(buildInternal(javaToolchain)); } private ActionAnalysisMetadata[] buildInternal(JavaToolchainProvider javaToolchain) { checkNotNull(outputDepsProto, "outputDepsProto must not be null"); checkNotNull(sourceFiles, "sourceFiles must not be null"); checkNotNull(sourceJars, "sourceJars must not be null"); checkNotNull(classpathEntries, "classpathEntries must not be null"); checkNotNull(bootclasspathEntries, "bootclasspathEntries must not be null"); checkNotNull(tempDirectory, "tempDirectory must not be null"); checkNotNull(strictJavaDeps, "strictJavaDeps must not be null"); checkNotNull(directJars, "directJars must not be null"); checkNotNull( compileTimeDependencyArtifacts, "compileTimeDependencyArtifacts must not be null"); checkNotNull(javacOpts, "javacOpts must not be null"); checkNotNull(processorPath, "processorPath must not be null"); checkNotNull(processorNames, "processorNames must not be null"); // Invariant: if strictJavaDeps is OFF, then directJars and // dependencyArtifacts are ignored if (strictJavaDeps == BuildConfiguration.StrictDepsMode.OFF) { directJars = NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER); compileTimeDependencyArtifacts = NestedSetBuilder.emptySet(Order.STABLE_ORDER); } boolean useDirectClasspath = useDirectClasspath(); boolean disableJavacFallback = ruleContext.getFragment(JavaConfiguration.class).headerCompilationDisableJavacFallback(); CommandLine directCommandLine = null; if (useDirectClasspath) { CustomCommandLine.Builder builder = baseCommandLine(getBaseArgs(javaToolchain)).addExecPaths("--classpath", directJars); if (disableJavacFallback) { builder.add("--nojavac_fallback"); } directCommandLine = builder.build(); } Iterable<Artifact> tools = ImmutableList.of(javacJar, javaToolchain.getHeaderCompiler()); ImmutableList<Artifact> outputs = ImmutableList.of(outputJar, outputDepsProto); NestedSet<Artifact> directInputs = NestedSetBuilder.<Artifact>stableOrder() .addTransitive(javabaseInputs) .addAll(bootclasspathEntries) .addAll(sourceJars) .addAll(sourceFiles) .addTransitive(directJars) .addAll(tools) .build(); if (useDirectClasspath && disableJavacFallback) { // use a regular SpawnAction to invoke turbine with direct deps only, // and no fallback to javac-turbine return new ActionAnalysisMetadata[] { new SpawnAction( ruleContext.getActionOwner(), tools, directInputs, outputs, LOCAL_RESOURCES, directCommandLine, false, JavaCompileAction.UTF8_ENVIRONMENT, /*executionInfo=*/ ImmutableSet.<String>of(), getProgressMessage(), "Turbine") }; } CommandLine transitiveParams = transitiveCommandLine(); PathFragment paramFilePath = ParameterFile.derivePath(outputJar.getRootRelativePath()); Artifact paramsFile = ruleContext .getAnalysisEnvironment() .getDerivedArtifact(paramFilePath, outputJar.getRoot()); ParameterFileWriteAction parameterFileWriteAction = new ParameterFileWriteAction( ruleContext.getActionOwner(), paramsFile, transitiveParams, ParameterFile.ParameterFileType.UNQUOTED, ISO_8859_1); CommandLine transitiveCommandLine = getBaseArgs(javaToolchain).addPaths("@%s", paramsFile.getExecPath()).build(); NestedSet<Artifact> transitiveInputs = NestedSetBuilder.<Artifact>stableOrder() .addTransitive(directInputs) .addTransitive(classpathEntries) .addTransitive(processorPath) .addTransitive(compileTimeDependencyArtifacts) .add(paramsFile) .build(); if (!useDirectClasspath) { // If direct classpaths are disabled (e.g. because the compilation uses API-generating // annotation processors) skip the custom action implementation and just use SpawnAction. return new ActionAnalysisMetadata[] { parameterFileWriteAction, new SpawnAction( ruleContext.getActionOwner(), tools, transitiveInputs, outputs, LOCAL_RESOURCES, transitiveCommandLine, false, JavaCompileAction.UTF8_ENVIRONMENT, /*executionInfo=*/ ImmutableSet.<String>of(), getProgressMessage(), "JavacTurbine") }; } return new ActionAnalysisMetadata[] { parameterFileWriteAction, new JavaHeaderCompileAction( ruleContext.getActionOwner(), tools, directInputs, transitiveInputs, outputs, directCommandLine, transitiveCommandLine, getProgressMessage()) }; } private String getProgressMessage() { return String.format( "Compiling Java headers %s (%d files)", outputJar.prettyPrint(), sourceFiles.size() + sourceJars.size()); } private CustomCommandLine.Builder getBaseArgs(JavaToolchainProvider javaToolchain) { return CustomCommandLine.builder() .addPath(ruleContext.getHostConfiguration().getFragment(Jvm.class).getJavaExecutable()) .add("-Xverify:none") .add(javaToolchain.getJvmOptions()) .addPaths("-Xbootclasspath/p:%s", javacJar.getExecPath()) .addExecPath("-jar", javaToolchain.getHeaderCompiler()); } /** * Adds the command line arguments shared by direct classpath and transitive classpath * invocations. */ private CustomCommandLine.Builder baseCommandLine(CustomCommandLine.Builder result) { result.addExecPath("--output", outputJar); if (outputDepsProto != null) { result.addExecPath("--output_deps", outputDepsProto); } result.add("--temp_dir").addPath(tempDirectory); result.addExecPaths("--bootclasspath", bootclasspathEntries); result.addExecPaths("--sources", sourceFiles); if (!sourceJars.isEmpty()) { result.addExecPaths("--source_jars", sourceJars); } result.add("--javacopts", javacOpts); if (ruleKind != null) { result.add("--rule_kind"); result.add(ruleKind); } if (targetLabel != null) { result.add("--target_label"); if (targetLabel.getPackageIdentifier().getRepository().isDefault() || targetLabel.getPackageIdentifier().getRepository().isMain()) { result.add(targetLabel.toString()); } else { // @-prefixed strings will be assumed to be params filenames and expanded, // so add an extra @ to escape it. result.add("@" + targetLabel); } } return result; } /** Builds a transitive classpath command line. */ private CommandLine transitiveCommandLine() { CustomCommandLine.Builder result = CustomCommandLine.builder(); baseCommandLine(result); if (!processorNames.isEmpty()) { result.add("--processors", processorNames); } if (!processorFlags.isEmpty()) { result.add("--javacopts", processorFlags); } if (!processorPath.isEmpty()) { result.addExecPaths("--processorpath", processorPath); } if (strictJavaDeps != BuildConfiguration.StrictDepsMode.OFF) { result.add(new JavaCompileAction.JarsToTargetsArgv(classpathEntries, directJars)); if (!compileTimeDependencyArtifacts.isEmpty()) { result.addExecPaths("--deps_artifacts", compileTimeDependencyArtifacts); } } result.addExecPaths("--classpath", classpathEntries); return result.build(); } /** Returns true if the header compilation classpath should only include direct deps. */ boolean useDirectClasspath() { if (directJars.isEmpty() && !classpathEntries.isEmpty()) { // the compilation doesn't distinguish direct deps, e.g. because it doesn't support strict // java deps return false; } if (!processorNames.isEmpty()) { // the compilation uses API-generating annotation processors and has to fall back to // javac-turbine, which doesn't support direct classpaths return false; } JavaConfiguration javaConfiguration = ruleContext.getFragment(JavaConfiguration.class); if (!javaConfiguration.headerCompilationDirectClasspath()) { // the experiment is disabled return false; } return true; } } }