blob: 25b84a5c82c334425ff0fb2a88b7926b28a00eaa [file] [log] [blame]
// Copyright 2014 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.common.collect.ImmutableList.toImmutableList;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.CommandLine;
import com.google.devtools.build.lib.actions.ExecutionRequirements;
import com.google.devtools.build.lib.actions.ParamFileInfo;
import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType;
import com.google.devtools.build.lib.actions.ResourceSet;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.rules.cpp.CppHelper;
import com.google.devtools.build.lib.rules.java.JavaConfiguration.OneVersionEnforcementLevel;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/** Utility for configuring an action to generate a deploy archive. */
public class DeployArchiveBuilder {
/**
* Memory consumption of SingleJar is about 250 bytes per entry in the output file. Unfortunately,
* the JVM tends to kill the process with an OOM long before we're at the limit. In the most
* recent example, 400 MB of memory was enough for about 500,000 entries.
*/
private static final int SINGLEJAR_MEMORY_MB = 1600;
private static final String SINGLEJAR_MAX_MEMORY = "-Xmx" + SINGLEJAR_MEMORY_MB + "m";
private static final ResourceSet DEPLOY_ACTION_RESOURCE_SET =
ResourceSet.createWithRamCpu(/*memoryMb = */ SINGLEJAR_MEMORY_MB, /*cpuUsage = */ 1);
private final RuleContext ruleContext;
private final NestedSetBuilder<Artifact> runtimeJarsBuilder = NestedSetBuilder.stableOrder();
private final JavaSemantics semantics;
private JavaTargetAttributes attributes;
private boolean includeBuildData;
private Compression compression = Compression.UNCOMPRESSED;
@Nullable private Artifact runfilesMiddleman;
private Artifact outputJar;
@Nullable private String javaStartClass;
private ImmutableList<String> deployManifestLines = ImmutableList.of();
@Nullable private Artifact launcher;
@Nullable private Function<Artifact, Artifact> derivedJars = null;
private boolean checkDesugarDeps;
private OneVersionEnforcementLevel oneVersionEnforcementLevel = OneVersionEnforcementLevel.OFF;
@Nullable private Artifact oneVersionWhitelistArtifact;
@Nullable private Artifact sharedArchive;
/** Type of compression to apply to output archive. */
public enum Compression {
/** Output should be compressed */
COMPRESSED,
/** Output should not be compressed */
UNCOMPRESSED;
}
/** Creates a builder using the configuration of the rule as the action configuration. */
public DeployArchiveBuilder(JavaSemantics semantics, RuleContext ruleContext) {
this.ruleContext = ruleContext;
this.semantics = semantics;
}
/** Sets the processed attributes of the rule generating the deploy archive. */
public DeployArchiveBuilder setAttributes(JavaTargetAttributes attributes) {
this.attributes = attributes;
return this;
}
/** Sets whether to include build-data.properties in the deploy archive. */
public DeployArchiveBuilder setIncludeBuildData(boolean includeBuildData) {
this.includeBuildData = includeBuildData;
return this;
}
/** Sets whether to enable compression of the output deploy archive. */
public DeployArchiveBuilder setCompression(Compression compress) {
this.compression = Preconditions.checkNotNull(compress);
return this;
}
/**
* Sets additional dependencies to be added to the action that creates the deploy jar so that we
* force the runtime dependencies to be built.
*/
public DeployArchiveBuilder setRunfilesMiddleman(@Nullable Artifact runfilesMiddleman) {
this.runfilesMiddleman = runfilesMiddleman;
return this;
}
/** Sets the artifact to create with the action. */
public DeployArchiveBuilder setOutputJar(Artifact outputJar) {
this.outputJar = Preconditions.checkNotNull(outputJar);
return this;
}
/** Sets the class to launch the Java application. */
public DeployArchiveBuilder setJavaStartClass(@Nullable String javaStartClass) {
this.javaStartClass = javaStartClass;
return this;
}
/** Adds additional jars that should be on the classpath at runtime. */
public DeployArchiveBuilder addRuntimeJars(NestedSet<Artifact> jars) {
this.runtimeJarsBuilder.addTransitive(jars);
return this;
}
/** Adds additional jars that should be on the classpath at runtime. */
public DeployArchiveBuilder addRuntimeJars(Iterable<Artifact> jars) {
this.runtimeJarsBuilder.addAll(jars);
return this;
}
/** Sets the list of extra lines to add to the archive's MANIFEST.MF file. */
public DeployArchiveBuilder setDeployManifestLines(ImmutableList<String> deployManifestLines) {
this.deployManifestLines = Preconditions.checkNotNull(deployManifestLines);
return this;
}
/** Sets the optional launcher to be used as the executable for this deploy JAR */
public DeployArchiveBuilder setLauncher(@Nullable Artifact launcher) {
this.launcher = launcher;
return this;
}
public DeployArchiveBuilder setDerivedJarFunction(Function<Artifact, Artifact> derivedJars) {
this.derivedJars = derivedJars;
return this;
}
/** Whether singlejar should process META-INF/desugar_deps files and fail upon inconsistencies. */
public DeployArchiveBuilder setCheckDesugarDeps(boolean checkDesugarDeps) {
this.checkDesugarDeps = checkDesugarDeps;
return this;
}
/** Whether or not singlejar would attempt to enforce one version of java classes in the jar */
public DeployArchiveBuilder setOneVersionEnforcementLevel(
OneVersionEnforcementLevel oneVersionEnforcementLevel,
@Nullable Artifact oneVersionWhitelistArtifact) {
this.oneVersionEnforcementLevel = oneVersionEnforcementLevel;
this.oneVersionWhitelistArtifact = oneVersionWhitelistArtifact;
return this;
}
public DeployArchiveBuilder setSharedArchive(@Nullable Artifact sharedArchive) {
this.sharedArchive = sharedArchive;
return this;
}
public static CustomCommandLine.Builder defaultSingleJarCommandLineWithoutOneVersion(
Artifact outputJar,
String javaMainClass,
ImmutableList<String> deployManifestLines,
Iterable<Artifact> buildInfoFiles,
ImmutableList<Artifact> classpathResources,
NestedSet<Artifact> runtimeClasspath,
boolean includeBuildData,
Compression compress,
Artifact launcher,
boolean usingNativeSinglejar) {
return defaultSingleJarCommandLine(
outputJar,
javaMainClass,
deployManifestLines,
buildInfoFiles,
classpathResources,
runtimeClasspath,
includeBuildData,
compress,
launcher,
usingNativeSinglejar,
OneVersionEnforcementLevel.OFF,
null);
}
public static CustomCommandLine.Builder defaultSingleJarCommandLine(
Artifact outputJar,
String javaMainClass,
ImmutableList<String> deployManifestLines,
Iterable<Artifact> buildInfoFiles,
ImmutableList<Artifact> classpathResources,
NestedSet<Artifact> runtimeClasspath,
boolean includeBuildData,
Compression compress,
Artifact launcher,
boolean usingNativeSinglejar,
OneVersionEnforcementLevel oneVersionEnforcementLevel,
@Nullable Artifact oneVersionWhitelistArtifact) {
CustomCommandLine.Builder args = CustomCommandLine.builder();
args.addExecPath("--output", outputJar);
if (compress == Compression.COMPRESSED) {
args.add("--compression");
}
args.add("--normalize");
if (javaMainClass != null) {
args.add("--main_class", javaMainClass);
}
if (!deployManifestLines.isEmpty()) {
args.add("--deploy_manifest_lines");
args.addAll(deployManifestLines);
}
if (buildInfoFiles != null) {
for (Artifact artifact : buildInfoFiles) {
args.addExecPath("--build_info_file", artifact);
}
}
if (!includeBuildData) {
args.add("--exclude_build_data");
}
if (launcher != null) {
args.addExecPath("--java_launcher", launcher);
}
args.addExecPaths("--classpath_resources", classpathResources);
if (runtimeClasspath != null) {
if (usingNativeSinglejar) {
args.addAll(
"--sources", OneVersionCheckActionBuilder.jarAndTargetVectorArg(runtimeClasspath));
} else {
args.addExecPaths("--sources", runtimeClasspath);
}
}
if (oneVersionEnforcementLevel != OneVersionEnforcementLevel.OFF && usingNativeSinglejar) {
args.add("--enforce_one_version");
// RuleErrors should have been added in Builder.build() before this command
// line is invoked.
Preconditions.checkNotNull(oneVersionWhitelistArtifact);
args.addExecPath("--one_version_whitelist", oneVersionWhitelistArtifact);
if (oneVersionEnforcementLevel == OneVersionEnforcementLevel.WARNING) {
args.add("--succeed_on_found_violations");
}
}
return args;
}
private static NestedSet<Artifact> getArchiveInputs(
JavaTargetAttributes attributes,
NestedSet<Artifact> runtimeClasspathForArchive,
@Nullable Function<Artifact, Artifact> derivedJarFunction) {
NestedSetBuilder<Artifact> inputs = NestedSetBuilder.stableOrder();
if (derivedJarFunction != null) {
inputs.addAll(
runtimeClasspathForArchive.toList().stream()
.map(derivedJarFunction)
.collect(toImmutableList()));
} else {
inputs.addTransitive(runtimeClasspathForArchive);
}
// TODO(bazel-team): Remove? Resources not used as input to singlejar action
inputs.addAll(attributes.getResources().values());
inputs.addAll(attributes.getClassPathResources());
return inputs.build();
}
/** Builds the action as configured. */
public void build() throws InterruptedException {
ImmutableList<Artifact> classpathResources = attributes.getClassPathResources();
Set<String> classPathResourceNames = new HashSet<>();
for (Artifact artifact : classpathResources) {
String name = artifact.getExecPath().getBaseName();
if (!classPathResourceNames.add(name)) {
ruleContext.attributeError(
"classpath_resources",
"entries must have different file names (duplicate: " + name + ")");
return;
}
}
NestedSet<Artifact> runtimeJars = runtimeJarsBuilder.build();
NestedSet<Artifact> runtimeClasspathForArchive = attributes.getRuntimeClassPathForArchive();
// TODO(kmb): Consider not using getArchiveInputs, specifically because we don't want/need to
// transform anything but the runtimeClasspath and b/c we currently do it twice here and below
NestedSetBuilder<Artifact> inputs = NestedSetBuilder.stableOrder();
inputs.addTransitive(getArchiveInputs(attributes, runtimeClasspathForArchive, derivedJars));
if (derivedJars != null) {
inputs.addAll(Iterables.transform(runtimeJars.toList(), derivedJars));
} else {
inputs.addTransitive(runtimeJars);
}
if (runfilesMiddleman != null) {
inputs.add(runfilesMiddleman);
}
ImmutableList<Artifact> buildInfoArtifacts = ruleContext.getBuildInfo(JavaBuildInfoFactory.KEY);
inputs.addAll(buildInfoArtifacts);
NestedSetBuilder<Artifact> runtimeClasspath = NestedSetBuilder.stableOrder();
if (derivedJars != null) {
runtimeClasspath.addAll(Iterables.transform(runtimeJars.toList(), derivedJars));
runtimeClasspath.addAll(
Iterables.transform(runtimeClasspathForArchive.toList(), derivedJars));
} else {
runtimeClasspath.addTransitive(runtimeJars);
runtimeClasspath.addTransitive(runtimeClasspathForArchive);
}
if (launcher != null) {
inputs.add(launcher);
}
if (oneVersionEnforcementLevel != OneVersionEnforcementLevel.OFF) {
if (oneVersionWhitelistArtifact == null) {
OneVersionCheckActionBuilder.addRuleErrorForMissingArtifacts(
ruleContext, JavaToolchainProvider.from(ruleContext));
return;
}
inputs.add(oneVersionWhitelistArtifact);
}
if (sharedArchive != null) {
inputs.add(sharedArchive);
}
// If singlejar's name ends with .jar, it is Java application, otherwise it is native.
// TODO(asmundak): once https://github.com/bazelbuild/bazel/issues/2241 is fixed (that is,
// the native singlejar is used on windows) remove support for the Java implementation
Artifact singlejar = JavaToolchainProvider.from(ruleContext).getSingleJar();
boolean usingNativeSinglejar = !singlejar.getFilename().endsWith(".jar");
CommandLine commandLine =
semantics.buildSingleJarCommandLine(
CppHelper.getToolchainUsingDefaultCcToolchainAttribute(ruleContext)
.getToolchainIdentifier(),
outputJar,
javaStartClass,
deployManifestLines,
buildInfoArtifacts,
classpathResources,
runtimeClasspath.build(),
includeBuildData,
compression,
launcher,
usingNativeSinglejar,
oneVersionEnforcementLevel,
oneVersionWhitelistArtifact,
sharedArchive);
if (checkDesugarDeps) {
commandLine = CommandLine.concat(commandLine, ImmutableList.of("--check_desugar_deps"));
}
List<String> jvmArgs = ImmutableList.of(SINGLEJAR_MAX_MEMORY);
if (!usingNativeSinglejar) {
ruleContext.registerAction(
new SpawnAction.Builder()
.addTransitiveInputs(inputs.build())
.addTransitiveInputs(JavaRuntimeInfo.forHost(ruleContext).javaBaseInputsMiddleman())
.addOutput(outputJar)
.setResources(DEPLOY_ACTION_RESOURCE_SET)
.setJarExecutable(JavaCommon.getHostJavaExecutable(ruleContext), singlejar, jvmArgs)
.addCommandLine(
commandLine,
ParamFileInfo.builder(ParameterFileType.SHELL_QUOTED).setUseAlways(true).build())
.setProgressMessage("Building deploy jar %s", outputJar.prettyPrint())
.setMnemonic("JavaDeployJar")
.setExecutionInfo(ExecutionRequirements.WORKER_MODE_ENABLED)
.build(ruleContext));
} else {
ruleContext.registerAction(
new SpawnAction.Builder()
.addTransitiveInputs(inputs.build())
.addOutput(outputJar)
.setResources(DEPLOY_ACTION_RESOURCE_SET)
.setExecutable(singlejar)
.addCommandLine(
commandLine,
ParamFileInfo.builder(ParameterFileType.SHELL_QUOTED).setUseAlways(true).build())
.setProgressMessage("Building deploy jar %s", outputJar.prettyPrint())
.setMnemonic("JavaDeployJar")
.build(ruleContext));
}
}
}