blob: 34bcc3a06d4177ad7ad4fd5bf1eb85cda6ca7d19 [file] [log] [blame]
// 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.android;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
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.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.packages.Types;
import com.google.devtools.build.lib.rules.android.AndroidConfiguration.ApkSigningMethod;
import com.google.devtools.build.lib.rules.java.JavaToolchainProvider;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
/**
* A class for coordinating APK building, signing and zipaligning.
*
* <p>It is not always necessary to zip align APKs, for instance if the APK does not contain
* resources. Furthermore, we do not always care about the unsigned apk because it cannot be
* installed on a device until it is signed.
*/
public class ApkActionsBuilder {
private Artifact classesDex;
private ImmutableList.Builder<Artifact> inputZips = new ImmutableList.Builder<>();
private Artifact javaResourceZip;
private FilesToRunProvider resourceExtractor;
private Artifact javaResourceFile;
private NativeLibs nativeLibs = NativeLibs.EMPTY;
private Artifact unsignedApk;
private Artifact signedApk;
private boolean zipalignApk = false;
private List<Artifact> signingKeys;
private Artifact signingLineage;
private String artifactLocation;
private Artifact v4SignatureFile;
private boolean deterministicSigning;
private String signingKeyRotationMinSdk;
private final String apkName;
public static ApkActionsBuilder create(String apkName) {
return new ApkActionsBuilder(apkName);
}
private ApkActionsBuilder(String apkName) {
this.apkName = apkName;
}
/** Sets the native libraries to be included in the APK. */
@CanIgnoreReturnValue
public ApkActionsBuilder setNativeLibs(NativeLibs nativeLibs) {
this.nativeLibs = nativeLibs;
return this;
}
/**
* Sets the dex file to be included in the APK.
*
* <p>Can be either a plain classes.dex or a .zip file containing dexes.
*/
@CanIgnoreReturnValue
public ApkActionsBuilder setClassesDex(Artifact classesDex) {
Preconditions.checkArgument(
classesDex == null
|| classesDex.getFilename().endsWith(".zip")
|| classesDex.getFilename().equals("classes.dex"));
this.classesDex = classesDex;
return this;
}
/** Add a zip file that should be copied as is into the APK. */
@CanIgnoreReturnValue
public ApkActionsBuilder addInputZip(Artifact inputZip) {
this.inputZips.add(inputZip);
return this;
}
@CanIgnoreReturnValue
public ApkActionsBuilder addInputZips(Iterable<Artifact> inputZips) {
this.inputZips.addAll(inputZips);
return this;
}
/**
* Adds a zip to be added to the APK and an executable that filters the zip to extract the
* relevant contents first.
*/
@CanIgnoreReturnValue
public ApkActionsBuilder setJavaResourceZip(
Artifact javaResourceZip, FilesToRunProvider resourceExtractor) {
this.javaResourceZip = javaResourceZip;
this.resourceExtractor = resourceExtractor;
return this;
}
/**
* Adds an individual resource file to the root directory of the APK.
*
* <p>This provides the same functionality as {@code javaResourceZip}, except much more hacky.
* Will most probably won't work if there is an input artifact in the same directory as this file.
*/
@CanIgnoreReturnValue
public ApkActionsBuilder setJavaResourceFile(Artifact javaResourceFile) {
this.javaResourceFile = javaResourceFile;
return this;
}
/** Requests an unsigned APK be built at the specified artifact. */
@CanIgnoreReturnValue
public ApkActionsBuilder setUnsignedApk(Artifact unsignedApk) {
this.unsignedApk = unsignedApk;
return this;
}
/** Requests a signed APK be built at the specified artifact. */
@CanIgnoreReturnValue
public ApkActionsBuilder setSignedApk(Artifact signedApk) {
this.signedApk = signedApk;
return this;
}
@CanIgnoreReturnValue
public ApkActionsBuilder setV4Signature(Artifact v4SignatureFile) {
this.v4SignatureFile = v4SignatureFile;
return this;
}
/** Requests that signed APKs are zipaligned. */
@CanIgnoreReturnValue
public ApkActionsBuilder setZipalignApk(boolean zipalign) {
this.zipalignApk = zipalign;
return this;
}
/** Sets the signing keys that will be used to sign the APK. */
@CanIgnoreReturnValue
public ApkActionsBuilder setSigningKeys(List<Artifact> signingKeys) {
this.signingKeys = signingKeys;
return this;
}
/** Sets the signing lineage file used to sign the APK. */
@CanIgnoreReturnValue
public ApkActionsBuilder setSigningLineageFile(Artifact signingLineage) {
this.signingLineage = signingLineage;
return this;
}
@CanIgnoreReturnValue
public ApkActionsBuilder setSigningKeyRotationMinSdk(String minSdk) {
this.signingKeyRotationMinSdk = minSdk;
return this;
}
/** Sets the output APK instead of creating with a static/standard path. */
@CanIgnoreReturnValue
public ApkActionsBuilder setArtifactLocationDirectory(String artifactLocation) {
this.artifactLocation = artifactLocation;
return this;
}
@CanIgnoreReturnValue
public ApkActionsBuilder setDeterministicSigning(boolean deterministicSigning) {
this.deterministicSigning = deterministicSigning;
return this;
}
/** Registers the actions needed to build the requested APKs in the rule context. */
public void registerActions(RuleContext ruleContext)
throws InterruptedException, RuleErrorException {
// If the caller did not request an unsigned APK, we still need to construct one so that
// we can sign it. So we make up an intermediate artifact.
Artifact intermediateUnsignedApk =
unsignedApk != null
? unsignedApk
: getApkArtifact(ruleContext, "unsigned_" + signedApk.getFilename());
buildApk(ruleContext, intermediateUnsignedApk);
if (signedApk != null) {
Artifact apkToSign = intermediateUnsignedApk;
// Zipalignment is performed before signing. So if a zipaligned APK is requested, we need an
// intermediate zipaligned-but-not-signed apk artifact.
if (zipalignApk) {
apkToSign = getApkArtifact(ruleContext, "zipaligned_" + signedApk.getFilename());
zipalignApk(ruleContext, intermediateUnsignedApk, apkToSign);
}
signApk(ruleContext, apkToSign, signedApk);
}
}
/** Appends the --output_jar_creator flag to the singlejar command line. */
private void setSingleJarCreatedBy(RuleContext ruleContext, CustomCommandLine.Builder builder) {
if (ruleContext.getConfiguration().getFragment(BazelAndroidConfiguration.class) != null) {
// Only enabled for Bazel, not Blaze.
builder.add("--output_jar_creator");
builder.add("Bazel");
}
}
/** Registers generating actions for {@code outApk} that build an unsigned APK using SingleJar. */
private void buildApk(RuleContext ruleContext, Artifact outApk)
throws InterruptedException, RuleErrorException {
Artifact compressedApk = getApkArtifact(ruleContext, "compressed_" + outApk.getFilename());
SpawnAction.Builder compressedApkActionBuilder =
createSpawnActionBuilder(ruleContext)
.setMnemonic("ApkBuilder")
.setProgressMessage("Generating unsigned %s", apkName)
.addOutput(compressedApk);
CustomCommandLine.Builder compressedApkCommandLine =
CustomCommandLine.builder()
.add("--exclude_build_data")
.add("--compression")
.add("--normalize")
.addExecPath("--output", compressedApk);
setSingleJarCreatedBy(ruleContext, compressedApkCommandLine);
setSingleJarAsExecutable(ruleContext, compressedApkActionBuilder);
if (classesDex != null) {
compressedApkActionBuilder.addInput(classesDex);
if (classesDex.getFilename().endsWith(".zip")) {
compressedApkCommandLine.addExecPath("--sources", classesDex);
} else {
compressedApkCommandLine
.add("--resources")
.addFormatted("%s:%s", classesDex, classesDex.getFilename());
}
}
if (javaResourceFile != null) {
compressedApkActionBuilder.addInput(javaResourceFile);
compressedApkCommandLine
.add("--resources")
.addFormatted("%s:%s", javaResourceFile, javaResourceFile.getFilename());
}
for (String architecture : nativeLibs.getMap().keySet()) {
for (Artifact nativeLib : nativeLibs.getMap().get(architecture).toList()) {
compressedApkActionBuilder.addInput(nativeLib);
compressedApkCommandLine
.add("--resources")
.addFormatted("%s:lib/%s/%s", nativeLib, architecture, nativeLib.getFilename());
}
}
SpawnAction.Builder singleJarActionBuilder =
createSpawnActionBuilder(ruleContext)
.setMnemonic("ApkBuilder")
.setProgressMessage("Generating unsigned %s", apkName)
.addInput(compressedApk)
.addOutput(outApk);
CustomCommandLine.Builder singleJarCommandLine = CustomCommandLine.builder();
singleJarCommandLine
.add("--exclude_build_data")
.add("--dont_change_compression")
.add("--normalize")
.addExecPath("--sources", compressedApk)
.addExecPath("--output", outApk);
setSingleJarCreatedBy(ruleContext, singleJarCommandLine);
setSingleJarAsExecutable(ruleContext, singleJarActionBuilder);
if (javaResourceZip != null) {
// The javaResourceZip contains many files that are unwanted in the APK such as .class files.
Artifact extractedJavaResourceZip =
getApkArtifact(ruleContext, "extracted_" + javaResourceZip.getFilename());
ruleContext.registerAction(
createSpawnActionBuilder(ruleContext)
.setExecutable(resourceExtractor)
.setMnemonic("ResourceExtractor")
.setProgressMessage("Extracting Java resources from deploy jar for %s", apkName)
.addInput(javaResourceZip)
.addOutput(extractedJavaResourceZip)
.addCommandLine(
CustomCommandLine.builder()
.addExecPath(javaResourceZip)
.addExecPath(extractedJavaResourceZip)
.build())
.useDefaultShellEnvironment()
.build(ruleContext));
if (ruleContext.getFragment(AndroidConfiguration.class).compressJavaResources()) {
compressedApkActionBuilder.addInput(extractedJavaResourceZip);
compressedApkCommandLine.addExecPath("--sources", extractedJavaResourceZip);
} else {
singleJarActionBuilder.addInput(extractedJavaResourceZip);
singleJarCommandLine.addExecPath("--sources", extractedJavaResourceZip);
}
}
if (nativeLibs.getName() != null) {
singleJarActionBuilder.addInput(nativeLibs.getName());
singleJarCommandLine
.add("--resources")
.addFormatted("%s:%s", nativeLibs.getName(), nativeLibs.getName().getFilename());
}
for (Artifact inputZip : inputZips.build()) {
singleJarActionBuilder.addInput(inputZip);
singleJarCommandLine.addExecPath("--sources", inputZip);
}
List<String> noCompressExtensions;
if (ruleContext
.getRule()
.isAttrDefined(AndroidRuleClasses.NOCOMPRESS_EXTENSIONS_ATTR, Types.STRING_LIST)) {
noCompressExtensions =
ruleContext
.getExpander()
.withDataLocations()
.tokenized(AndroidRuleClasses.NOCOMPRESS_EXTENSIONS_ATTR);
} else {
// This code is also used by android_test, which doesn't have this attribute.
noCompressExtensions = ImmutableList.of();
}
if (!noCompressExtensions.isEmpty()) {
compressedApkCommandLine.addAll("--nocompress_suffixes", noCompressExtensions);
singleJarCommandLine.addAll("--nocompress_suffixes", noCompressExtensions);
}
compressedApkActionBuilder.addCommandLine(compressedApkCommandLine.build());
ruleContext.registerAction(compressedApkActionBuilder.build(ruleContext));
singleJarActionBuilder.addCommandLine(singleJarCommandLine.build());
ruleContext.registerAction(singleJarActionBuilder.build(ruleContext));
}
/** Uses the zipalign tool to align the zip boundaries for uncompressed resources by 4 bytes. */
private void zipalignApk(RuleContext ruleContext, Artifact inputApk, Artifact zipAlignedApk)
throws RuleErrorException {
ruleContext.registerAction(
createSpawnActionBuilder(ruleContext)
.addInput(inputApk)
.addOutput(zipAlignedApk)
.setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getZipalign())
.setProgressMessage("Zipaligning %s", apkName)
.setMnemonic("AndroidZipAlign")
.addInput(inputApk)
.addOutput(zipAlignedApk)
.addCommandLine(
CustomCommandLine.builder()
.add("-p") // memory page aligment for stored shared object files
.add("4")
.addExecPath(inputApk)
.addExecPath(zipAlignedApk)
.build())
.build(ruleContext));
}
/**
* Signs an APK using the ApkSignerTool. Supports both the jar signing scheme(v1) and the apk
* signing scheme v2. Note that zip alignment is preserved by this step. Furthermore, zip
* alignment cannot be performed after v2 signing without invalidating the signature.
*/
private void signApk(
RuleContext ruleContext, Artifact unsignedApk, Artifact signedAndZipalignedApk)
throws RuleErrorException {
ApkSigningMethod signingMethod =
ruleContext.getFragment(AndroidConfiguration.class).getApkSigningMethod();
SpawnAction.Builder actionBuilder =
createSpawnActionBuilder(ruleContext)
.setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getApkSigner())
.setProgressMessage("Signing %s", apkName)
.setMnemonic("ApkSignerTool")
.addOutput(signedAndZipalignedApk)
.addInput(unsignedApk);
CustomCommandLine.Builder commandLine = CustomCommandLine.builder().add("sign");
actionBuilder.addInputs(signingKeys);
if (signingLineage != null) {
actionBuilder.addInput(signingLineage);
commandLine.add("--lineage").addExecPath(signingLineage);
}
if (deterministicSigning) {
// Enable deterministic DSA signing to keep the output of apksigner deterministic.
// This requires including BouncyCastleProvider as a Security provider, since the standard
// JDK Security providers do not include support for deterministic DSA signing.
// Since this adds BouncyCastleProvider to the end of the Provider list, any non-DSA signing
// algorithms (such as RSA) invoked by apksigner will still use the standard JDK
// implementations and not Bouncy Castle.
commandLine.add("--deterministic-dsa-signing", "true");
commandLine.add("--provider-class", "org.bouncycastle.jce.provider.BouncyCastleProvider");
}
for (int i = 0; i < signingKeys.size(); i++) {
if (i > 0) {
commandLine.add("--next-signer");
}
commandLine.add("--ks").addExecPath(signingKeys.get(i)).add("--ks-pass", "pass:android");
}
commandLine
.add("--v1-signing-enabled", Boolean.toString(signingMethod.signV1()))
.add("--v1-signer-name", "CERT")
.add("--v2-signing-enabled", Boolean.toString(signingMethod.signV2()));
if (signingMethod.signV4() != null) {
commandLine.add("--v4-signing-enabled", Boolean.toString(signingMethod.signV4()));
}
if (!Strings.isNullOrEmpty(signingKeyRotationMinSdk)) {
commandLine.add("--rotation-min-sdk-version", signingKeyRotationMinSdk);
}
commandLine.add("--out").addExecPath(signedAndZipalignedApk).addExecPath(unsignedApk);
if (v4SignatureFile != null) {
actionBuilder.addOutput(v4SignatureFile);
}
ruleContext.registerAction(
actionBuilder.addCommandLine(commandLine.build()).build(ruleContext));
}
private static void setSingleJarAsExecutable(RuleContext ruleContext, SpawnAction.Builder builder)
throws RuleErrorException {
FilesToRunProvider singleJar = JavaToolchainProvider.from(ruleContext).getSingleJar();
builder.setExecutable(singleJar);
}
private Artifact getApkArtifact(RuleContext ruleContext, String baseName) {
if (artifactLocation != null) {
return ruleContext.getUniqueDirectoryArtifact(
artifactLocation, baseName, ruleContext.getBinOrGenfilesDirectory());
} else {
return AndroidBinary.getDxArtifact(ruleContext, baseName);
}
}
/** Adds execution info by propagating tags from the target */
private static SpawnAction.Builder createSpawnActionBuilder(RuleContext ruleContext) {
return new SpawnAction.Builder()
.setExecutionInfo(
TargetUtils.getExecutionInfo(
ruleContext.getRule(), ruleContext.isAllowTagsPropagation()));
}
}