From 602f3ae70f49bd8bae6205a93f3df5918d3bacac Mon Sep 17 00:00:00 2001 From: Adam Michael Date: Wed, 31 Aug 2016 23:44:59 +0000 Subject: Adds support for ApkSignerTool and APK signature schema v2 behind --apk_signing_method flag. Default is legacy_v1 which is the already existing functionality. Promotes AndroidBinary.ApkActionBuilder to a toplevel abstract class ApkActionsBuilder with two implementations, one for default signing/zipaligning and one for ApkSignerTool based signing/zipaligning. In addition to build the action for constructing the APK, it now responsible for orchestrating the various tools to build, sign and zipalign the APK. -- MOS_MIGRATED_REVID=131889338 --- .../build/lib/rules/android/AndroidBinary.java | 305 +++++--------------- .../lib/rules/android/AndroidConfiguration.java | 65 +++++ .../lib/rules/android/AndroidRuleClasses.java | 1 + .../build/lib/rules/android/AndroidSdk.java | 9 + .../lib/rules/android/AndroidSdkProvider.java | 6 + .../build/lib/rules/android/ApkActionsBuilder.java | 317 +++++++++++++++++++++ 6 files changed, 464 insertions(+), 239 deletions(-) create mode 100644 src/main/java/com/google/devtools/build/lib/rules/android/ApkActionsBuilder.java (limited to 'src') diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java index 21ed2dd069..ee00868dc5 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java @@ -29,7 +29,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; -import com.google.devtools.build.lib.actions.Action; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.FailAction; import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType; @@ -55,7 +54,10 @@ import com.google.devtools.build.lib.packages.BuildType; import com.google.devtools.build.lib.packages.TriState; import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory; import com.google.devtools.build.lib.rules.android.AndroidConfiguration.AndroidBinaryType; +import com.google.devtools.build.lib.rules.android.AndroidConfiguration.ApkSigningMethod; import com.google.devtools.build.lib.rules.android.AndroidRuleClasses.MultidexMode; +import com.google.devtools.build.lib.rules.android.ApkActionsBuilder.LegacySignerApkActionsBuilder; +import com.google.devtools.build.lib.rules.android.ApkActionsBuilder.SignerToolApkActionsBuilder; import com.google.devtools.build.lib.rules.cpp.CcToolchainProvider; import com.google.devtools.build.lib.rules.cpp.CppHelper; import com.google.devtools.build.lib.rules.java.DeployArchiveBuilder; @@ -76,6 +78,7 @@ import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import javax.annotation.Nullable; @@ -430,30 +433,23 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { resourceApk.getMainDexProguardConfig(), resourceClasses); + ApkSigningMethod signingMethod = + ruleContext.getFragment(AndroidConfiguration.class).getApkSigningMethod(); + Artifact unsignedApk = ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_UNSIGNED_APK); - Artifact signedApk = - ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_SIGNED_APK); - - ApkActionBuilder apkBuilder = new ApkActionBuilder(ruleContext, androidSemantics) - .classesDex(dexingOutput.classesDexZip) - .resourceApk(resourceApk.getArtifact()) - .javaResourceZip(dexingOutput.javaResourceJar) - .nativeLibs(nativeLibs); - - ruleContext.registerAction(apkBuilder - .message("Generating unsigned apk") - .build(unsignedApk)); - - ruleContext.registerAction(apkBuilder - .message("Generating signed apk") - .sign(true) - .build(signedApk)); - - Artifact zipAlignedApk = zipalignApk( - ruleContext, - signedApk, - ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_APK)); + Artifact zipAlignedApk = + ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_APK); + + createApkActionsBuilder(signingMethod) + .setClassesDex(dexingOutput.classesDexZip) + .setResourceApk(resourceApk.getArtifact()) + .setJavaResourceZip(dexingOutput.javaResourceJar) + .setNativeLibs(nativeLibs) + .setUnsignedApk(unsignedApk) + .setSignedAndZipalignedApk(zipAlignedApk) + .setApkName("apk") + .registerActions(ruleContext, androidSemantics); // Don't add blacklistedApk, so it's only built if explicitly requested. filesBuilder.add(binaryJar); @@ -509,19 +505,19 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { Artifact stubDex = getStubDex(ruleContext, javaSemantics, false); ruleContext.assertNoErrors(); - ApkActionBuilder incrementalActionBuilder = new ApkActionBuilder(ruleContext, androidSemantics) - .classesDex(stubDex) - .resourceApk(incrementalResourceApk.getArtifact()) - .javaResourceZip(dexingOutput.javaResourceJar) - .sign(true) - .javaResourceFile(stubData) - .message("Generating incremental apk"); + ApkActionsBuilder incrementalActionsBuilder = createApkActionsBuilder(signingMethod) + .setClassesDex(stubDex) + .setResourceApk(incrementalResourceApk.getArtifact()) + .setJavaResourceZip(dexingOutput.javaResourceJar) + .setJavaResourceFile(stubData) + .setApkName("incremental apk") + .setSignedApk(incrementalApk); if (!ruleContext.getFragment(AndroidConfiguration.class).useIncrementalNativeLibs()) { - incrementalActionBuilder.nativeLibs(nativeLibs); + incrementalActionsBuilder.setNativeLibs(nativeLibs); } - ruleContext.registerAction(incrementalActionBuilder.build(incrementalApk)); + incrementalActionsBuilder.registerActions(ruleContext, androidSemantics); Artifact argsArtifact = ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.MOBILE_INSTALL_ARGS); @@ -569,11 +565,11 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { // installation of each split (said references would eventually get installed, but it cannot // know that in advance) Artifact resourceSplitApk = getDxArtifact(ruleContext, "android_resources.apk"); - ruleContext.registerAction(new ApkActionBuilder(ruleContext, androidSemantics) - .resourceApk(splitResourceApk.getArtifact()) - .sign(true) - .message("Generating split Android resource apk") - .build(resourceSplitApk)); + createApkActionsBuilder(signingMethod) + .setResourceApk(splitResourceApk.getArtifact()) + .setApkName("split Android resource apk") + .setSignedApk(resourceSplitApk) + .registerActions(ruleContext, androidSemantics); splitApkSetBuilder.add(resourceSplitApk); for (int i = 0; i < dexingOutput.shardDexZips.size(); i++) { @@ -581,35 +577,35 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { Artifact splitApkResources = createSplitApkResources( ruleContext, applicationManifest, splitName, true); Artifact splitApk = getDxArtifact(ruleContext, splitName + ".apk"); - ruleContext.registerAction(new ApkActionBuilder(ruleContext, androidSemantics) - .classesDex(dexingOutput.shardDexZips.get(i)) - .resourceApk(splitApkResources) - .sign(true) - .message("Generating split dex apk " + (i + 1)) - .build(splitApk)); + createApkActionsBuilder(signingMethod) + .setClassesDex(dexingOutput.shardDexZips.get(i)) + .setResourceApk(splitApkResources) + .setApkName("split dex apk " + (i + 1)) + .setSignedApk(splitApk) + .registerActions(ruleContext, androidSemantics); splitApkSetBuilder.add(splitApk); } Artifact nativeSplitApkResources = createSplitApkResources( ruleContext, applicationManifest, "native", false); Artifact nativeSplitApk = getDxArtifact(ruleContext, "native.apk"); - ruleContext.registerAction(new ApkActionBuilder(ruleContext, androidSemantics) - .resourceApk(nativeSplitApkResources) - .sign(true) - .message("Generating split native apk") - .nativeLibs(nativeLibs) - .build(nativeSplitApk)); + createApkActionsBuilder(signingMethod) + .setResourceApk(nativeSplitApkResources) + .setNativeLibs(nativeLibs) + .setApkName("split native apk") + .setSignedApk(nativeSplitApk) + .registerActions(ruleContext, androidSemantics); splitApkSetBuilder.add(nativeSplitApk); Artifact javaSplitApkResources = createSplitApkResources( ruleContext, applicationManifest, "java_resources", false); Artifact javaSplitApk = getDxArtifact(ruleContext, "java_resources.apk"); - ruleContext.registerAction(new ApkActionBuilder(ruleContext, androidSemantics) - .resourceApk(javaSplitApkResources) - .javaResourceZip(dexingOutput.javaResourceJar) - .sign(true) - .message("Generating split Java resource apk") - .build(javaSplitApk)); + createApkActionsBuilder(signingMethod) + .setResourceApk(javaSplitApkResources) + .setJavaResourceZip(dexingOutput.javaResourceJar) + .setApkName("split Java resource apk") + .setSignedApk(javaSplitApk) + .registerActions(ruleContext, androidSemantics); splitApkSetBuilder.add(javaSplitApk); Artifact splitMainApkResources = getDxArtifact(ruleContext, "split_main.ap_"); @@ -627,12 +623,12 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { Artifact splitMainApk = getDxArtifact(ruleContext, "split_main.apk"); Artifact splitStubDex = getStubDex(ruleContext, javaSemantics, true); ruleContext.assertNoErrors(); - ruleContext.registerAction(new ApkActionBuilder(ruleContext, androidSemantics) - .resourceApk(splitMainApkResources) - .classesDex(splitStubDex) - .sign(true) - .message("Generating split main apk") - .build(splitMainApk)); + createApkActionsBuilder(signingMethod) + .setClassesDex(splitStubDex) + .setResourceApk(splitMainApkResources) + .setApkName("split main apk") + .setSignedApk(splitMainApk) + .registerActions(ruleContext, androidSemantics); splitApkSetBuilder.add(splitMainApk); NestedSet allSplitApks = splitApkSetBuilder.build(); @@ -1091,9 +1087,10 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { // Always OFF if finalJarIsDerived ImmutableSet incrementalDexing = - getEffectiveIncrementalDexing(ruleContext, dexopts, binaryJar != proguardedJar); + getEffectiveIncrementalDexing( + ruleContext, dexopts, !Objects.equals(binaryJar, proguardedJar)); Artifact inclusionFilterJar = - isBinaryJarFiltered && binaryJar == proguardedJar ? binaryJar : null; + isBinaryJarFiltered && Objects.equals(binaryJar, proguardedJar) ? binaryJar : null; if (multidexMode == MultidexMode.OFF) { // Single dex mode: generate classes.dex directly from the input jar. if (incrementalDexing.contains(AndroidBinaryType.MONODEX)) { @@ -1131,7 +1128,7 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { createShuffleJarAction( ruleContext, incrementalDexing.contains(AndroidBinaryType.MULTIDEX_SHARDED), - /*proguardedJar*/ binaryJar != proguardedJar ? proguardedJar : null, + /*proguardedJar*/ !Objects.equals(binaryJar, proguardedJar) ? proguardedJar : null, shards, common, inclusionFilterJar, @@ -1490,184 +1487,14 @@ public abstract class AndroidBinary implements RuleConfiguredTargetFactory { : null; } - /** - * Builder class for {@link com.google.devtools.build.lib.analysis.actions.SpawnAction}s that - * generate APKs. - * - *

Instances of this class can be reused after calling {@code build()}. - */ - private static final class ApkActionBuilder { - private final RuleContext ruleContext; - private final AndroidSemantics semantics; - - private boolean sign; - private String message; - private Artifact classesDex; - private Artifact resourceApk; - private Artifact javaResourceZip; - // javaResourceFile adds Java resources just like javaResourceZip. We should make the stub - // manifest writer output a zip file, then we could do away with this input to APK building. - private Artifact javaResourceFile; - private NativeLibs nativeLibs = NativeLibs.EMPTY; - - private ApkActionBuilder( - RuleContext ruleContext, AndroidSemantics semantics) { - this.ruleContext = ruleContext; - this.semantics = semantics; - } - - /** - * Sets the user-visible message that is displayed when the action is running. - */ - public ApkActionBuilder message(String message) { - this.message = message; - return this; - } - - /** - * Sets the native libraries to be included in the APK. - */ - public ApkActionBuilder nativeLibs(NativeLibs nativeLibs) { - this.nativeLibs = nativeLibs; - return this; - } - - /** - * Sets the dex file to be included in the APK. - * - *

Can be either a plain .dex or a .zip file containing dexes. - */ - public ApkActionBuilder classesDex(Artifact classesDex) { - this.classesDex = classesDex; - return this; - } - - /** - * Sets the resource APK that contains the Android resources to be bundled into the output. - */ - public ApkActionBuilder resourceApk(Artifact resourceApk) { - this.resourceApk = resourceApk; - return this; - } - - /** - * Sets the file where Java resources are taken. - * - *

Everything in this will will be put directly into the APK except files with the extension - * {@code .class}. - */ - public ApkActionBuilder javaResourceZip(Artifact javaResourcezip) { - this.javaResourceZip = javaResourcezip; - return this; - } - - /** - * Adds an individual resource file to the root directory of the APK. - * - *

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 ApkActionBuilder javaResourceFile(Artifact javaResourceFile) { - this.javaResourceFile = javaResourceFile; - return this; - } - - /** - * Sets if the APK will be signed. By default, it won't be. - */ - public ApkActionBuilder sign(boolean sign) { - this.sign = sign; - return this; - } - - /** - * Creates a generating action for {@code outApk} that builds the APK specified. - */ - public Action[] build(Artifact outApk) { - Builder actionBuilder = new SpawnAction.Builder() - .setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getApkBuilder()) - .setProgressMessage(message) - .setMnemonic("AndroidApkBuilder") - .addOutputArgument(outApk); - - if (javaResourceZip != null) { - actionBuilder - .addArgument("-rj") - .addInputArgument(javaResourceZip); - } - - Artifact nativeSymlinks = nativeLibs.createApkBuilderSymlinks(ruleContext); - if (nativeSymlinks != null) { - PathFragment nativeSymlinksDir = nativeSymlinks.getExecPath().getParentDirectory(); - actionBuilder - .addInputManifest(nativeSymlinks, nativeSymlinksDir) - .addInput(nativeSymlinks) - .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 (javaResourceFile != null) { - actionBuilder - .addArgument("-rf") - .addArgument((javaResourceFile.getExecPath().getParentDirectory().getPathString())) - .addInput(javaResourceFile); - } - - if (sign) { - Artifact signingKey = semantics.getApkDebugSigningKey(ruleContext); - actionBuilder.addArgument("-ks").addArgument(signingKey.getExecPathString()); - actionBuilder.addInput(signingKey); - } else { - actionBuilder.addArgument("-u"); - } - - actionBuilder - .addArgument("-z") - .addInputArgument(resourceApk); - - if (classesDex != null) { - actionBuilder - .addArgument(classesDex.getFilename().endsWith(".dex") ? "-f" : "-z") - .addInputArgument(classesDex); - } - - return actionBuilder.build(ruleContext); + private static ApkActionsBuilder createApkActionsBuilder(ApkSigningMethod signingMethod) { + if (signingMethod.signLegacy()) { + return new LegacySignerApkActionsBuilder(); + } else { + return new SignerToolApkActionsBuilder(signingMethod); } } - /** Last step in buildings an apk: align the zip boundaries by 4 bytes. */ - static Artifact zipalignApk(RuleContext ruleContext, - Artifact signedApk, Artifact zipAlignedApk) { - List args = new ArrayList<>(); - // "4" is the only valid value for zipalign, according to: - // http://developer.android.com/guide/developing/tools/zipalign.html - args.add("4"); - args.add(signedApk.getExecPathString()); - args.add(zipAlignedApk.getExecPathString()); - - ruleContext.registerAction(new SpawnAction.Builder() - .addInput(signedApk) - .addOutput(zipAlignedApk) - .setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getZipalign()) - .addArguments(args) - .setProgressMessage("Zipaligning apk") - .setMnemonic("AndroidZipAlign") - .build(ruleContext)); - args.add(signedApk.getExecPathString()); - args.add(zipAlignedApk.getExecPathString()); - return zipAlignedApk; - } - /** * Tests if the resources need to be regenerated. * diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidConfiguration.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidConfiguration.java index 45d9492863..c7b3650796 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidConfiguration.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidConfiguration.java @@ -66,6 +66,15 @@ public class AndroidConfiguration extends BuildConfiguration.Fragment { } } + /** + * Converter for {@link ApkSigningMethod}. + */ + public static final class ApkSigningMethodConverter extends EnumConverter { + public ApkSigningMethodConverter() { + super(ApkSigningMethod.class, "apk signing method"); + } + } + /** * Converter for a set of {@link AndroidBinaryType}s. */ @@ -133,6 +142,49 @@ public class AndroidConfiguration extends BuildConfiguration.Fragment { MONODEX, MULTIDEX_UNSHARDED, MULTIDEX_SHARDED } + /** + * Which APK signing method to use with the debug key for rules that build APKs. + * + *

    + *
  • LEGACY_V1 uses the signer inside the deprecated apkbuilder tool. + *
  • V1 uses the apksigner attribute from the android_sdk and signs the APK as a JAR. + *
  • V2 uses the apksigner attribute from the android_sdk and signs the APK according to the APK + * Signing Schema V2 that is only supported on Android N and later. + *
+ */ + public enum ApkSigningMethod { + LEGACY_V1(true, false, false), + V1(false, true, false), + V2(false, false, true), + V1_V2(false, true, true); + + private final boolean signLegacy; + private final boolean signV1; + private final boolean signV2; + + ApkSigningMethod(boolean signLegacy, boolean signV1, boolean signV2) { + // If signLegacy is true, the other two values will be ignored. + this.signLegacy = signLegacy; + this.signV1 = signV1; + this.signV2 = signV2; + } + + /** Whether to sign with the signer inside the deprecated apkbuilder tool. */ + public boolean signLegacy() { + return signLegacy; + } + + /** Whether to JAR sign the APK with the apksigner tool. */ + public boolean signV1() { + return signV1; + } + + /** Wheter to sign the APK with the apksigner tool with APK Signature Schema V2. */ + public boolean signV2() { + return signV2; + } + } + /** When to use incremental dexing (using {@link DexArchiveProvider}). */ private enum IncrementalDexing { OFF(), @@ -343,6 +395,13 @@ public class AndroidConfiguration extends BuildConfiguration.Fragment { + "R classes from a merge action, separately from aapt.") public boolean useParallelResourceProcessing; + @Option(name = "apk_signing_method", + converter = ApkSigningMethodConverter.class, + defaultValue = "legacy_v1", + category = "undocumented", + help = "Implementation to use to sign APKs") + public ApkSigningMethod apkSigningMethod; + @Override public void addAllLabels(Multimap labelMap) { if (androidCrosstoolTop != null) { @@ -413,6 +472,7 @@ public class AndroidConfiguration extends BuildConfiguration.Fragment { private final boolean useRClassGenerator; private final boolean useParallelResourceProcessing; private final AndroidManifestMerger manifestMerger; + private final ApkSigningMethod apkSigningMethod; AndroidConfiguration(Options options, Label androidSdk) { this.sdk = androidSdk; @@ -436,6 +496,7 @@ public class AndroidConfiguration extends BuildConfiguration.Fragment { this.useRClassGenerator = options.useRClassGenerator; this.useParallelResourceProcessing = options.useParallelResourceProcessing; this.manifestMerger = options.manifestMerger; + this.apkSigningMethod = options.apkSigningMethod; } public String getCpu() { @@ -512,6 +573,10 @@ public class AndroidConfiguration extends BuildConfiguration.Fragment { return manifestMerger; } + public ApkSigningMethod getApkSigningMethod() { + return apkSigningMethod; + } + @Override public void addGlobalMakeVariables(ImmutableMap.Builder globalMakeEnvBuilder) { globalMakeEnvBuilder.put("ANDROID_CPU", cpu); diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java index 7582f81564..ac641ce419 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidRuleClasses.java @@ -359,6 +359,7 @@ public final class AndroidRuleClasses { .add(attr("annotations_jar", LABEL).mandatory().cfg(HOST).allowedFileTypes(ANY_FILE)) .add(attr("main_dex_classes", LABEL).mandatory().cfg(HOST).allowedFileTypes(ANY_FILE)) .add(attr("apkbuilder", LABEL).mandatory().cfg(HOST).allowedFileTypes(ANY_FILE).exec()) + .add(attr("apksigner", LABEL).cfg(HOST).allowedFileTypes(ANY_FILE).exec()) .add(attr("zipalign", LABEL).mandatory().cfg(HOST).allowedFileTypes(ANY_FILE).exec()) .add( attr("jack", LABEL) diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidSdk.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidSdk.java index da7b3b40a6..fc957f38f5 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidSdk.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidSdk.java @@ -29,6 +29,7 @@ import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.packages.AggregatingAttributeMapper; import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException; import com.google.devtools.build.lib.rules.RuleConfiguredTargetFactory; +import com.google.devtools.build.lib.rules.android.AndroidConfiguration.ApkSigningMethod; import com.google.devtools.build.lib.rules.java.BaseJavaCompilationHelper; import com.google.devtools.build.lib.rules.java.JavaConfiguration; import com.google.devtools.build.lib.rules.java.JavaToolchainProvider; @@ -67,6 +68,13 @@ public class AndroidSdk implements RuleConfiguredTargetFactory { FilesToRunProvider aapt = ruleContext.getExecutablePrerequisite("aapt", Mode.HOST); FilesToRunProvider apkBuilder = ruleContext.getExecutablePrerequisite( "apkbuilder", Mode.HOST); + FilesToRunProvider apkSigner = ruleContext.getExecutablePrerequisite("apksigner", Mode.HOST); + ApkSigningMethod apkSigningMethod = + ruleContext.getFragment(AndroidConfiguration.class).getApkSigningMethod(); + if (apkSigner == null && !apkSigningMethod.signLegacy()) { + ruleContext.throwWithRuleError( + "android_sdk attribute apksigner must be set to use signing method " + apkSigningMethod); + } FilesToRunProvider adb = ruleContext.getExecutablePrerequisite("adb", Mode.HOST); FilesToRunProvider dx = ruleContext.getExecutablePrerequisite("dx", Mode.HOST); FilesToRunProvider mainDexListCreator = ruleContext.getExecutablePrerequisite( @@ -120,6 +128,7 @@ public class AndroidSdk implements RuleConfiguredTargetFactory { aidl, aapt, apkBuilder, + apkSigner, proguard, zipalign, jack, diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidSdkProvider.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidSdkProvider.java index c790e30afb..da82a92d45 100644 --- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidSdkProvider.java +++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidSdkProvider.java @@ -22,6 +22,7 @@ import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.analysis.TransitiveInfoProvider; import com.google.devtools.build.lib.collect.nestedset.NestedSet; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; +import javax.annotation.Nullable; /** Description of the tools Blaze needs from an Android SDK. */ @AutoValue @@ -44,6 +45,7 @@ public abstract class AndroidSdkProvider implements TransitiveInfoProvider { FilesToRunProvider aidl, FilesToRunProvider aapt, FilesToRunProvider apkBuilder, + @Nullable FilesToRunProvider apkSigner, FilesToRunProvider proguard, FilesToRunProvider zipalign, FilesToRunProvider jack, @@ -66,6 +68,7 @@ public abstract class AndroidSdkProvider implements TransitiveInfoProvider { aidl, aapt, apkBuilder, + apkSigner, proguard, zipalign, jack, @@ -138,6 +141,9 @@ public abstract class AndroidSdkProvider implements TransitiveInfoProvider { public abstract FilesToRunProvider getApkBuilder(); + @Nullable + public abstract FilesToRunProvider getApkSigner(); + public abstract FilesToRunProvider getProguard(); public abstract FilesToRunProvider getZipalign(); diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ApkActionsBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/android/ApkActionsBuilder.java new file mode 100644 index 0000000000..5829294b2f --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/rules/android/ApkActionsBuilder.java @@ -0,0 +1,317 @@ +// 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.devtools.build.lib.actions.Action; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.analysis.RuleContext; +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.vfs.PathFragment; +import java.util.ArrayList; +import java.util.List; + +/** + * A class for coordinating APK building, signing and zipaligning. + * + *

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. + * + *

This interface allows the caller to specify their desired APKs and the implementations + * determines which tools to use to build actions to fulfill them. + */ +public abstract class ApkActionsBuilder { + private Artifact classesDex; + private Artifact resourceApk; + private Artifact javaResourceZip; + private Artifact javaResourceFile; + private NativeLibs nativeLibs = NativeLibs.EMPTY; + + Artifact unsignedApk; + Artifact signedApk; + Artifact signedAndZipalignedApk; + String apkName; + + /** Sets the user-visible apkName that is included in the action progress messages. */ + public ApkActionsBuilder setApkName(String apkName) { + this.apkName = apkName; + return this; + } + + /** Registers the actions needed to build the requested APKs in the rule context. */ + public abstract void registerActions(RuleContext ruleContext, AndroidSemantics semantics); + + /** 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. + * + *

Can be either a plain .dex or a .zip file containing dexes. + */ + public ApkActionsBuilder setClassesDex(Artifact classesDex) { + 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. + * + *

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. + * + *

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 but not necessarily zip aligned APK be built at the specified artifact. */ + public ApkActionsBuilder setSignedApk(Artifact signedApk) { + this.signedApk = signedApk; + return this; + } + + /** Requests a signed and zipaligned APK be built at the specified artifact. */ + public ApkActionsBuilder setSignedAndZipalignedApk(Artifact signedAndZipalignedApk) { + this.signedAndZipalignedApk = signedAndZipalignedApk; + return this; + } + + /** + * Creates a generating action for {@code outApk} that builds the APK specified. + * + *

If {@code signingKey} is not null, the apk will be signed with it using the V1 signature + * scheme. + */ + Action[] buildApk(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); + } + + Artifact nativeSymlinks = nativeLibs.createApkBuilderSymlinks(ruleContext); + if (nativeSymlinks != null) { + PathFragment nativeSymlinksDir = nativeSymlinks.getExecPath().getParentDirectory(); + actionBuilder + .addInputManifest(nativeSymlinks, nativeSymlinksDir) + .addInput(nativeSymlinks) + .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 (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); + } + + return actionBuilder.build(ruleContext); + } + + /** + * An implementation that uses ApkBuilderMain to both build and sign APKs and the Android SDK + * zipalign tool to zipalign. This implementation only supports V1 signature scheme (JAR signing). + */ + static class LegacySignerApkActionsBuilder extends ApkActionsBuilder { + + @Override + public void registerActions(RuleContext ruleContext, AndroidSemantics semantics) { + Preconditions.checkNotNull( + apkName, "APK name must be set to create progress messages for APK actions."); + + if (unsignedApk != null) { + ruleContext.registerAction( + buildApk(ruleContext, unsignedApk, null, "Generating unsigned " + apkName)); + } + + if (signedAndZipalignedApk != null) { + Artifact intermediateSignedApk = this.signedApk; + if (intermediateSignedApk == null) { + // If the caller requested a zipaligned APK but not a signed APK, we still need to build + // a signed APK as an intermediate so we construct an artifact. + intermediateSignedApk = AndroidBinary.getDxArtifact( + ruleContext, "signed_" + signedAndZipalignedApk.getFilename()); + } + ruleContext.registerAction(buildApk( + ruleContext, + intermediateSignedApk, + semantics.getApkDebugSigningKey(ruleContext), + "Generating signed " + apkName)); + ruleContext.registerAction( + zipalignApk(ruleContext, intermediateSignedApk, signedAndZipalignedApk)); + } else if (signedApk != null) { + ruleContext.registerAction(buildApk( + ruleContext, + signedApk, + semantics.getApkDebugSigningKey(ruleContext), + "Generating signed " + apkName)); + } + } + + /** Last step in buildings an apk: align the zip boundaries by 4 bytes. */ + private Action[] zipalignApk(RuleContext ruleContext, Artifact signedApk, + Artifact zipAlignedApk) { + List args = new ArrayList<>(); + // "4" is the only valid value for zipalign, according to: + // http://developer.android.com/guide/developing/tools/zipalign.html + args.add("4"); + args.add(signedApk.getExecPathString()); + args.add(zipAlignedApk.getExecPathString()); + + return new SpawnAction.Builder() + .addInput(signedApk) + .addOutput(zipAlignedApk) + .setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getZipalign()) + .addArguments(args) + .setProgressMessage("Zipaligning " + apkName) + .setMnemonic("AndroidZipAlign") + .build(ruleContext); + } + } + + /** + * This implementation uses the executables from the apkbuilder and apksigner attributes of + * android_sdk to build and sign the SDKs. Zipaligning is done by the apksigner + * {@link com.android.apksigner.ApkSignerTool} between the V1 and V2 signature steps. The signer + * supports both V1 and V2 signing signatures configured by the {@link ApkSigningMethod} + * parameter. + */ + static class SignerToolApkActionsBuilder extends ApkActionsBuilder { + private final ApkSigningMethod signingMethod; + + SignerToolApkActionsBuilder(ApkSigningMethod signingMethod) { + this.signingMethod = signingMethod; + } + + @Override + public void registerActions(RuleContext ruleContext, AndroidSemantics semantics) { + // Only one should ever be specified. The only reason that both options exist are as a slight + // optimization for the legacy code path in which we only zip align full APKs, not split APKs. + Preconditions.checkState( + signedApk == null || signedAndZipalignedApk == null, + "ApkSignerTool cannot generate separate signedApk and signedAndZipalignedApk because " + + "zipaligning is done between the v1 signing and v2 signing in the same action."); + Preconditions.checkNotNull( + apkName, "APK name must be set to create progress messages for APK actions."); + + Artifact finalApk = signedApk == null ? signedAndZipalignedApk : signedApk; + if (finalApk != null) { + Artifact intermediateUnsignedApk = this.unsignedApk; + if (intermediateUnsignedApk == null) { + // 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. + intermediateUnsignedApk = AndroidBinary.getDxArtifact( + ruleContext, "unsigned_" + finalApk.getFilename()); + } + ruleContext.registerAction( + buildApk(ruleContext, intermediateUnsignedApk, null, "Generating unsigned " + apkName)); + ruleContext.registerAction(sign( + ruleContext, + semantics.getApkDebugSigningKey(ruleContext), + intermediateUnsignedApk, + finalApk)); + } else if (unsignedApk != null) { + ruleContext.registerAction( + buildApk(ruleContext, unsignedApk, null, "Generating unsigned " + apkName)); + } + } + + /** + * Signs and zip aligns an APK using the ApkSignerTool. Supports both the jar signing schema + * (v1) and the apk signing schema v2. + */ + private Action[] sign(RuleContext ruleContext, Artifact signingKey, Artifact unsignedApk, + Artifact signedAndZipalignedApk) { + return new SpawnAction.Builder() + .setExecutable(AndroidSdkProvider.fromRuleContext(ruleContext).getApkSigner()) + .setProgressMessage("Signing and zipaligning " + 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); + } + } +} -- cgit v1.2.3