// 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.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesSupplierImpl;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.rules.android.AndroidConfiguration.ApkSigningMethod;
import com.google.devtools.build.lib.rules.java.JavaHelper;
import com.google.devtools.build.lib.rules.java.JavaToolchainProvider;
import com.google.devtools.build.lib.rules.java.Jvm;
import com.google.devtools.build.lib.util.Pair;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.Map;
/**
* 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 Artifact resourceApk;
private Artifact javaResourceZip;
private Artifact javaResourceFile;
private NestedSet<Artifact> nativeLibsZips;
private NativeLibs nativeLibs = NativeLibs.EMPTY;
private Artifact unsignedApk;
private Artifact signedApk;
private boolean zipalignApk = false;
private final String apkName;
private final ApkSigningMethod signingMethod;
public static ApkActionsBuilder create(String apkName, ApkSigningMethod signingMethod) {
return new ApkActionsBuilder(apkName, signingMethod);
}
private ApkActionsBuilder(String apkName, ApkSigningMethod signingMethod) {
this.apkName = apkName;
this.signingMethod = signingMethod;
}
/** Sets the native libraries to be included in the APK. */
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.
*/
public ApkActionsBuilder setClassesDex(Artifact classesDex) {
Preconditions.checkArgument(
classesDex.getFilename().endsWith(".zip")
|| classesDex.getFilename().equals("classes.dex"));
this.classesDex = classesDex;
return this;
}
/** Sets the resource APK that contains the Android resources to be bundled into the output. */
public ApkActionsBuilder setResourceApk(Artifact resourceApk) {
this.resourceApk = resourceApk;
return this;
}
/**
* Sets the file where Java resources are taken.
*
* <p>The contents of this zip will will be put directly into the APK except for files that are
* filtered out by the {@link com.android.sdklib.build.ApkBuilder} which seem to not be resources,
* e.g. files with the extension {@code .class}.
*/
public ApkActionsBuilder setJavaResourceZip(Artifact javaResourceZip) {
this.javaResourceZip = javaResourceZip;
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.
*/
public ApkActionsBuilder setJavaResourceFile(Artifact javaResourceFile) {
this.javaResourceFile = javaResourceFile;
return this;
}
public ApkActionsBuilder setNativeLibsZips(NestedSet<Artifact> nativeLibsZips) {
this.nativeLibsZips = nativeLibsZips;
return this;
}
/** Requests an unsigned APK be built at the specified artifact. */
public ApkActionsBuilder setUnsignedApk(Artifact unsignedApk) {
this.unsignedApk = unsignedApk;
return this;
}
/** Requests a signed APK be built at the specified artifact. */
public ApkActionsBuilder setSignedApk(Artifact signedApk) {
this.signedApk = signedApk;
return this;
}
/** Requests that signed APKs are zipaligned. */
public ApkActionsBuilder setZipalignApk(boolean zipalign) {
this.zipalignApk = zipalign;
return this;
}
/** Registers the actions needed to build the requested APKs in the rule context. */
public void registerActions(RuleContext ruleContext, AndroidSemantics semantics) {
boolean useSingleJarApkBuilder =
ruleContext.getFragment(AndroidConfiguration.class).useSingleJarApkBuilder();
// 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
: AndroidBinary.getDxArtifact(ruleContext, "unsigned_" + signedApk.getFilename());
if (useSingleJarApkBuilder) {
buildApk(ruleContext, intermediateUnsignedApk, "Generating unsigned " + apkName);
} else {
legacyBuildApk(ruleContext, intermediateUnsignedApk, null, "Generating unsigned " + apkName);
}
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 =
AndroidBinary.getDxArtifact(ruleContext, "zipaligned_" + signedApk.getFilename());
zipalignApk(ruleContext, intermediateUnsignedApk, apkToSign);
}
signApk(ruleContext, semantics.getApkDebugSigningKey(ruleContext), apkToSign, signedApk);
}
}
/**
* Registers generating actions for {@code outApk} that builds the APK specified.
*
* <p>If {@code signingKey} is not null, the apk will be signed with it using the V1 signature
* scheme.
*/
private void legacyBuildApk(RuleContext ruleContext, Artifact outApk, Artifact signingKey,
String message) {
SpawnAction.Builder actionBuilder = new SpawnAction.Builder()
.setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getApkBuilder())
.setProgressMessage(message)
.setMnemonic("AndroidApkBuilder")
.addOutputArgument(outApk);
if (javaResourceZip != null) {
actionBuilder
.addArgument("-rj")
.addInputArgument(javaResourceZip);
}
Pair<Artifact, Runfiles> nativeSymlinksManifestAndRunfiles =
nativeLibs.createApkBuilderSymlinks(ruleContext);
if (nativeSymlinksManifestAndRunfiles != null) {
Artifact nativeSymlinksManifest = nativeSymlinksManifestAndRunfiles.first;
Runfiles nativeSymlinksRunfiles = nativeSymlinksManifestAndRunfiles.second;
PathFragment nativeSymlinksDir = nativeSymlinksManifest.getExecPath().getParentDirectory();
actionBuilder
.addRunfilesSupplier(
new RunfilesSupplierImpl(
nativeSymlinksDir,
nativeSymlinksRunfiles,
nativeSymlinksManifest))
.addInput(nativeSymlinksManifest)
.addInputs(nativeLibs.getAllNativeLibs())
.addArgument("-nf")
// If the native libs are "foo/bar/x86/foo.so", we need to pass "foo/bar" here
.addArgument(nativeSymlinksDir.getPathString());
}
if (nativeLibs.getName() != null) {
actionBuilder
.addArgument("-rf")
.addArgument(nativeLibs.getName().getExecPath().getParentDirectory().getPathString())
.addInput(nativeLibs.getName());
}
if (nativeLibsZips != null) {
for (Artifact nativeLibsZip : nativeLibsZips) {
actionBuilder
.addArgument("-z")
.addInputArgument(nativeLibsZip);
}
}
if (javaResourceFile != null) {
actionBuilder
.addArgument("-rf")
.addArgument((javaResourceFile.getExecPath().getParentDirectory().getPathString()))
.addInput(javaResourceFile);
}
if (signingKey == null) {
actionBuilder.addArgument("-u");
} else {
actionBuilder.addArgument("-ks").addArgument(signingKey.getExecPathString());
actionBuilder.addInput(signingKey);
}
actionBuilder
.addArgument("-z")
.addInputArgument(resourceApk);
if (classesDex != null) {
actionBuilder
.addArgument(classesDex.getFilename().endsWith(".dex") ? "-f" : "-z")
.addInputArgument(classesDex);
}
ruleContext.registerAction(actionBuilder.build(ruleContext));
}
/**
* Registers generating actions for {@code outApk} that build an unsigned APK using SingleJar.
*/
private void buildApk(RuleContext ruleContext, Artifact outApk, String message) {
Map<String, String> executionInfo = ImmutableMap.of("supports-workers", "1");
Artifact compressedApk =
AndroidBinary.getDxArtifact(ruleContext, "compressed_" + outApk.getFilename());
SpawnAction.Builder compressedApkActionBuilder = new SpawnAction.Builder()
.setMnemonic("ApkBuilder")
.setProgressMessage(message)
.setExecutionInfo(executionInfo)
.addArgument("--exclude_build_data")
.addArgument("--compression")
.addArgument("--normalize")
.addArgument("--output")
.addOutputArgument(compressedApk);
setSingleJarAsExecutable(ruleContext, compressedApkActionBuilder);
if (classesDex != null) {
if (classesDex.getFilename().endsWith(".zip")) {
compressedApkActionBuilder
.addArgument("--sources")
.addInputArgument(classesDex);
} else {
compressedApkActionBuilder
.addInput(classesDex)
.addArgument("--resources")
.addArgument(
singleJarResourcesArgument(
classesDex.getExecPathString(),
classesDex.getFilename()));
}
}
if (javaResourceFile != null) {
compressedApkActionBuilder
.addInput(javaResourceFile)
.addArgument("--resources")
.addArgument(
singleJarResourcesArgument(
javaResourceFile.getExecPathString(),
javaResourceFile.getFilename()));
}
for (String architecture : nativeLibs.getMap().keySet()) {
for (Artifact nativeLib : nativeLibs.getMap().get(architecture)) {
compressedApkActionBuilder
.addArgument("--resources")
.addArgument(
singleJarResourcesArgument(
nativeLib.getExecPathString(),
"lib/" + architecture + "/" + nativeLib.getFilename()))
.addInput(nativeLib);
}
}
SpawnAction.Builder singleJarActionBuilder = new SpawnAction.Builder()
.setMnemonic("ApkBuilder")
.setProgressMessage(message)
.setExecutionInfo(executionInfo)
.addArgument("--exclude_build_data")
.addArgument("--dont_change_compression")
.addArgument("--normalize")
.addArgument("--sources")
.addInputArgument(compressedApk)
.addArgument("--output")
.addOutputArgument(outApk);
setSingleJarAsExecutable(ruleContext, singleJarActionBuilder);
if (javaResourceZip != null) {
// The javaResourceZip contains many files that are unwanted in the APK such as .class files.
Artifact extractedJavaResourceZip =
AndroidBinary.getDxArtifact(ruleContext, "extracted_" + javaResourceZip.getFilename());
ruleContext.registerAction(new SpawnAction.Builder()
.setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getResourceExtractor())
.setMnemonic("ResourceExtractor")
.setProgressMessage("Extracting Java resources from deploy jar for " + apkName)
.addInputArgument(javaResourceZip)
.addOutputArgument(extractedJavaResourceZip)
.build(ruleContext));
if (ruleContext.getFragment(AndroidConfiguration.class).compressJavaResources()) {
compressedApkActionBuilder
.addArgument("--sources")
.addInputArgument(extractedJavaResourceZip);
} else {
singleJarActionBuilder
.addArgument("--sources")
.addInputArgument(extractedJavaResourceZip);
}
}
if (nativeLibs.getName() != null) {
singleJarActionBuilder
.addArgument("--resources")
.addArgument(
singleJarResourcesArgument(
nativeLibs.getName().getExecPathString(),
nativeLibs.getName().getFilename()))
.addInput(nativeLibs.getName());
}
if (resourceApk != null) {
singleJarActionBuilder
.addArgument("--sources")
.addInputArgument(resourceApk);
}
if (nativeLibsZips != null) {
for (Artifact nativeLibsZip : nativeLibsZips) {
singleJarActionBuilder
.addArgument("--sources")
.addInputArgument(nativeLibsZip);
}
}
ImmutableList<String> noCompressExtensions =
ruleContext.getTokenizedStringListAttr("nocompress_extensions");
if (ruleContext.getFragment(AndroidConfiguration.class).useNocompressExtensionsOnApk()
&& !noCompressExtensions.isEmpty()) {
compressedApkActionBuilder
.addArgument("--nocompress_suffixes")
.addArguments(noCompressExtensions);
singleJarActionBuilder
.addArgument("--nocompress_suffixes")
.addArguments(noCompressExtensions);
}
ruleContext.registerAction(compressedApkActionBuilder.build(ruleContext));
ruleContext.registerAction(singleJarActionBuilder.build(ruleContext));
}
/**
* The --resources flag to singlejar can have either of the following forms:
* <ul>
* <li>The path to the input file. In this case the file is placed at the same path in the APK.
* <li>{@code from}:{@code to} where {@code from} is that path to the input file and {@code to} is
* the location in the APK to put it.
* </ul>
* This method creates the syntax for the second form.
*/
private static String singleJarResourcesArgument(String from, String to) {
return from + ":" + to;
}
/** Uses the zipalign tool to align the zip boundaries for uncompressed resources by 4 bytes. */
private void zipalignApk(RuleContext ruleContext, Artifact inputApk, Artifact zipAlignedApk) {
ruleContext.registerAction(new SpawnAction.Builder()
.addInput(inputApk)
.addOutput(zipAlignedApk)
.setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getZipalign())
.addArgument("4")
.addInputArgument(inputApk)
.addOutputArgument(zipAlignedApk)
.setProgressMessage("Zipaligning " + apkName)
.setMnemonic("AndroidZipAlign")
.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 signingKey,
Artifact unsignedApk, Artifact signedAndZipalignedApk) {
ruleContext.registerAction(new SpawnAction.Builder()
.setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getApkSigner())
.setProgressMessage("Signing " + apkName)
.setMnemonic("ApkSignerTool")
.addArgument("sign")
.addArgument("--ks")
.addInputArgument(signingKey)
.addArguments("--ks-pass", "pass:android")
.addArguments("--v1-signing-enabled", Boolean.toString(signingMethod.signV1()))
.addArguments("--v1-signer-name", "CERT")
.addArguments("--v2-signing-enabled", Boolean.toString(signingMethod.signV2()))
.addArgument("--out")
.addOutputArgument(signedAndZipalignedApk)
.addInputArgument(unsignedApk)
.build(ruleContext));
}
// Adds the appropriate SpawnAction options depending on if SingleJar is a jar or not.
private static void setSingleJarAsExecutable(
RuleContext ruleContext, SpawnAction.Builder builder) {
Artifact singleJar = JavaToolchainProvider.fromRuleContext(ruleContext).getSingleJar();
if (singleJar.getFilename().endsWith(".jar")) {
builder
.setJarExecutable(
ruleContext.getHostConfiguration().getFragment(Jvm.class).getJavaExecutable(),
singleJar,
JavaToolchainProvider.fromRuleContext(ruleContext).getJvmOptions())
.addTransitiveInputs(JavaHelper.getHostJavabaseInputs(ruleContext));
} else {
builder.setExecutable(singleJar);
}
}
}