| // 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.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.RunfilesSupplierImpl; |
| import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; |
| import com.google.devtools.build.lib.analysis.actions.SpawnAction; |
| import com.google.devtools.build.lib.rules.android.AndroidConfiguration.ApkSigningMethod; |
| import com.google.devtools.build.lib.rules.java.JavaCommon; |
| import com.google.devtools.build.lib.rules.java.JavaRuntimeInfo; |
| import com.google.devtools.build.lib.rules.java.JavaToolchainProvider; |
| import com.google.devtools.build.lib.syntax.Type; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| 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 Artifact signingKey; |
| private String artifactLocation; |
| |
| 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. */ |
| 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; |
| } |
| |
| /** Add a zip file that should be copied as is into the APK. */ |
| public ApkActionsBuilder addInputZip(Artifact inputZip) { |
| this.inputZips.add(inputZip); |
| return this; |
| } |
| |
| 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. |
| */ |
| 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. |
| */ |
| public ApkActionsBuilder setJavaResourceFile(Artifact javaResourceFile) { |
| this.javaResourceFile = javaResourceFile; |
| 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; |
| } |
| |
| /** Sets the signing key that will be used to sign the APK. */ |
| public ApkActionsBuilder setSigningKey(Artifact signingKey) { |
| this.signingKey = signingKey; |
| return this; |
| } |
| |
| /** Sets the output APK instead of creating with a static/standard path. */ |
| public ApkActionsBuilder setArtifactLocationDirectory(String artifactLocation) { |
| this.artifactLocation = artifactLocation; |
| return this; |
| } |
| |
| /** Registers the actions needed to build the requested APKs in the rule context. */ |
| public void registerActions(RuleContext ruleContext) { |
| 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 |
| : getApkArtifact(ruleContext, "unsigned_" + signedApk.getFilename()); |
| if (useSingleJarApkBuilder) { |
| buildApk(ruleContext, intermediateUnsignedApk); |
| } else { |
| legacyBuildApk(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); |
| } |
| } |
| |
| /** |
| * 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) { |
| SpawnAction.Builder actionBuilder = |
| new SpawnAction.Builder() |
| .setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getApkBuilder()) |
| .setProgressMessage("Generating unsigned %s", apkName) |
| .setMnemonic("AndroidApkBuilder") |
| .addOutput(outApk); |
| CustomCommandLine.Builder commandLine = CustomCommandLine.builder().addExecPath(outApk); |
| |
| if (javaResourceZip != null) { |
| actionBuilder.addInput(javaResourceZip); |
| commandLine.add("-rj").addExecPath(javaResourceZip); |
| } |
| |
| NativeLibs.ManifestAndRunfiles nativeSymlinksManifestAndRunfiles = |
| nativeLibs.createApkBuilderSymlinks(ruleContext); |
| if (nativeSymlinksManifestAndRunfiles != null) { |
| // This following is equal to AndroidBinary.getDxArtifact( |
| // ruleContext, "native_symlinks/MANIFEST").getExecPath().getParentDirectory(); |
| // However, that causes an artifact to be registered without a generating action under |
| // --nobuild_runfile_manifests, so instead, the following directly synthesizes the required |
| // path fragment. |
| PathFragment nativeSymlinksDir = |
| ruleContext |
| .getBinOrGenfilesDirectory() |
| .getExecPath() |
| .getRelative(ruleContext.getUniqueDirectory("_dx").getRelative("native_symlinks")); |
| |
| actionBuilder |
| .addRunfilesSupplier( |
| new RunfilesSupplierImpl( |
| nativeSymlinksDir, |
| nativeSymlinksManifestAndRunfiles.runfiles, |
| nativeSymlinksManifestAndRunfiles.manifest)) |
| .addInputs(nativeLibs.getAllNativeLibs()); |
| if (nativeSymlinksManifestAndRunfiles.manifest != null) { |
| actionBuilder.addInput(nativeSymlinksManifestAndRunfiles.manifest); |
| } |
| commandLine |
| .add("-nf") |
| // If the native libs are "foo/bar/x86/foo.so", we need to pass "foo/bar" here |
| .addPath(nativeSymlinksDir); |
| } |
| |
| if (nativeLibs.getName() != null) { |
| actionBuilder.addInput(nativeLibs.getName()); |
| commandLine.add("-rf").addPath(nativeLibs.getName().getExecPath().getParentDirectory()); |
| } |
| |
| if (javaResourceFile != null) { |
| actionBuilder.addInput(javaResourceFile); |
| commandLine.add("-rf").addPath(javaResourceFile.getExecPath().getParentDirectory()); |
| } |
| |
| commandLine.add("-u"); |
| |
| for (Artifact inputZip : inputZips.build()) { |
| actionBuilder.addInput(inputZip); |
| commandLine.addExecPath("-z", inputZip); |
| } |
| |
| if (classesDex != null) { |
| actionBuilder.addInput(classesDex); |
| if (classesDex.getFilename().endsWith(".dex")) { |
| commandLine.add("-f"); |
| } else { |
| commandLine.add("-z"); |
| } |
| commandLine.addExecPath(classesDex); |
| } |
| |
| actionBuilder.addCommandLine(commandLine.build()); |
| 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) { |
| Artifact compressedApk = getApkArtifact(ruleContext, "compressed_" + outApk.getFilename()); |
| |
| SpawnAction.Builder compressedApkActionBuilder = |
| new SpawnAction.Builder() |
| .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); |
| 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)) { |
| compressedApkActionBuilder.addInput(nativeLib); |
| compressedApkCommandLine |
| .add("--resources") |
| .addFormatted("%s:lib/%s/%s", nativeLib, architecture, nativeLib.getFilename()); |
| } |
| } |
| |
| SpawnAction.Builder singleJarActionBuilder = |
| new SpawnAction.Builder() |
| .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); |
| 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( |
| new SpawnAction.Builder() |
| .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, Type.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) { |
| ruleContext.registerAction( |
| new SpawnAction.Builder() |
| .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) { |
| ApkSigningMethod signingMethod = |
| ruleContext.getFragment(AndroidConfiguration.class).getApkSigningMethod(); |
| ruleContext.registerAction( |
| new SpawnAction.Builder() |
| .setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getApkSigner()) |
| .setProgressMessage("Signing %s", apkName) |
| .setMnemonic("ApkSignerTool") |
| .addInput(signingKey) |
| .addOutput(signedAndZipalignedApk) |
| .addInput(unsignedApk) |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .add("sign") |
| .add("--ks") |
| .addExecPath(signingKey) |
| .add("--ks-pass", "pass:android") |
| .add("--v1-signing-enabled", Boolean.toString(signingMethod.signV1())) |
| .add("--v1-signer-name", "CERT") |
| .add("--v2-signing-enabled", Boolean.toString(signingMethod.signV2())) |
| .add("--out") |
| .addExecPath(signedAndZipalignedApk) |
| .addExecPath(unsignedApk) |
| .build()) |
| .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.from(ruleContext).getSingleJar(); |
| if (singleJar.getFilename().endsWith(".jar")) { |
| builder |
| .setJarExecutable( |
| JavaCommon.getHostJavaExecutable(ruleContext), |
| singleJar, |
| JavaToolchainProvider.from(ruleContext).getJvmOptions()) |
| .addTransitiveInputs(JavaRuntimeInfo.forHost(ruleContext).javaBaseInputsMiddleman()); |
| } else { |
| 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); |
| } |
| } |
| } |