| // Copyright 2015 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 static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.devtools.build.lib.packages.Type.STRING; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.base.Function; |
| import com.google.common.base.Functions; |
| import com.google.common.base.Preconditions; |
| import com.google.common.base.Predicates; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; |
| import com.google.devtools.build.lib.actions.Artifact.TreeFileArtifact; |
| import com.google.devtools.build.lib.actions.CommandLine; |
| import com.google.devtools.build.lib.actions.FailAction; |
| import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException; |
| import com.google.devtools.build.lib.actions.ParamFileInfo; |
| import com.google.devtools.build.lib.actions.ParameterFile; |
| import com.google.devtools.build.lib.actions.ParameterFile.ParameterFileType; |
| import com.google.devtools.build.lib.analysis.AnalysisUtils; |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.FileProvider; |
| import com.google.devtools.build.lib.analysis.FilesToRunProvider; |
| import com.google.devtools.build.lib.analysis.OutputGroupInfo; |
| import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder; |
| import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory; |
| import com.google.devtools.build.lib.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.RuleErrorConsumer; |
| import com.google.devtools.build.lib.analysis.Runfiles; |
| import com.google.devtools.build.lib.analysis.RunfilesProvider; |
| import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; |
| import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext; |
| import com.google.devtools.build.lib.analysis.actions.CustomCommandLine; |
| import com.google.devtools.build.lib.analysis.actions.CustomCommandLine.VectorArg; |
| import com.google.devtools.build.lib.analysis.actions.ParameterFileWriteAction; |
| import com.google.devtools.build.lib.analysis.actions.SpawnAction; |
| import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate; |
| import com.google.devtools.build.lib.analysis.actions.SpawnActionTemplate.OutputPathMapper; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; |
| import com.google.devtools.build.lib.collect.nestedset.Order; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; |
| import com.google.devtools.build.lib.packages.BuildType; |
| import com.google.devtools.build.lib.packages.RuleClass; |
| import com.google.devtools.build.lib.packages.TriState; |
| import com.google.devtools.build.lib.packages.Type; |
| import com.google.devtools.build.lib.rules.android.AndroidBinaryMobileInstall.MobileInstallResourceApks; |
| import com.google.devtools.build.lib.rules.android.AndroidRuleClasses.MultidexMode; |
| import com.google.devtools.build.lib.rules.android.ProguardHelper.ProguardOutput; |
| import com.google.devtools.build.lib.rules.android.ZipFilterBuilder.CheckHashMismatchMode; |
| import com.google.devtools.build.lib.rules.android.databinding.DataBinding; |
| import com.google.devtools.build.lib.rules.cpp.CppSemantics; |
| import com.google.devtools.build.lib.rules.java.DeployArchiveBuilder; |
| import com.google.devtools.build.lib.rules.java.JavaCommon; |
| import com.google.devtools.build.lib.rules.java.JavaConfiguration; |
| import com.google.devtools.build.lib.rules.java.JavaConfiguration.OneVersionEnforcementLevel; |
| import com.google.devtools.build.lib.rules.java.JavaRuntimeInfo; |
| import com.google.devtools.build.lib.rules.java.JavaSemantics; |
| import com.google.devtools.build.lib.rules.java.JavaSourceInfoProvider; |
| import com.google.devtools.build.lib.rules.java.JavaTargetAttributes; |
| import com.google.devtools.build.lib.rules.java.JavaToolchainProvider; |
| import com.google.devtools.build.lib.rules.java.OneVersionCheckActionBuilder; |
| import com.google.devtools.build.lib.rules.java.ProguardSpecProvider; |
| import com.google.devtools.build.lib.server.FailureDetails.FailAction.Code; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.io.Serializable; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import javax.annotation.Nullable; |
| import net.starlark.java.eval.StarlarkInt; |
| |
| /** An implementation for the "android_binary" rule. */ |
| public abstract class AndroidBinary implements RuleConfiguredTargetFactory { |
| |
| private static final String DX_MINIMAL_MAIN_DEX_OPTION = "--minimal-main-dex"; |
| |
| protected abstract JavaSemantics createJavaSemantics(); |
| |
| protected abstract AndroidSemantics createAndroidSemantics(); |
| |
| protected abstract CppSemantics createCppSemantics(); |
| |
| @Override |
| public ConfiguredTarget create(RuleContext ruleContext) |
| throws InterruptedException, RuleErrorException, ActionConflictException { |
| CppSemantics cppSemantics = createCppSemantics(); |
| JavaSemantics javaSemantics = createJavaSemantics(); |
| AndroidSemantics androidSemantics = createAndroidSemantics(); |
| androidSemantics.checkForMigrationTag(ruleContext); |
| androidSemantics.validateAndroidBinaryRuleContext(ruleContext); |
| AndroidSdkProvider.verifyPresence(ruleContext); |
| |
| NestedSetBuilder<Artifact> filesBuilder = NestedSetBuilder.stableOrder(); |
| RuleConfiguredTargetBuilder builder = |
| init(ruleContext, filesBuilder, cppSemantics, javaSemantics, androidSemantics); |
| return builder.build(); |
| } |
| |
| /** Checks expected rule invariants, throws rule errors if anything is set wrong. */ |
| private static void validateRuleContext(RuleContext ruleContext, AndroidDataContext dataContext) |
| throws RuleErrorException { |
| if (getMultidexMode(ruleContext) != MultidexMode.LEGACY |
| && ruleContext |
| .attributes() |
| .isAttributeValueExplicitlySpecified("main_dex_proguard_specs")) { |
| ruleContext.throwWithAttributeError( |
| "main_dex_proguard_specs", |
| "The 'main_dex_proguard_specs' attribute is only allowed if 'multidex' is" |
| + " set to 'legacy'"); |
| } |
| if (ruleContext.attributes().isAttributeValueExplicitlySpecified("proguard_apply_mapping")) { |
| if (dataContext.throwOnProguardApplyMapping()) { |
| ruleContext.throwWithAttributeError( |
| "proguard_apply_mapping", "This attribute is not supported"); |
| } |
| if (ruleContext |
| .attributes() |
| .get(ProguardHelper.PROGUARD_SPECS, BuildType.LABEL_LIST) |
| .isEmpty()) { |
| ruleContext.throwWithAttributeError( |
| "proguard_apply_mapping", |
| "'proguard_apply_mapping' can only be used when 'proguard_specs' is also set"); |
| } |
| } |
| if (ruleContext.attributes().isAttributeValueExplicitlySpecified("proguard_apply_dictionary")) { |
| if (dataContext.throwOnProguardApplyDictionary()) { |
| ruleContext.throwWithAttributeError( |
| "proguard_apply_dictionary", "This attribute is not supported"); |
| } |
| if (ruleContext |
| .attributes() |
| .get(ProguardHelper.PROGUARD_SPECS, BuildType.LABEL_LIST) |
| .isEmpty()) { |
| ruleContext.throwWithAttributeError( |
| "proguard_apply_dictionary", |
| "'proguard_apply_dictionary' can only be used when 'proguard_specs' is also set"); |
| } |
| } |
| if (ruleContext.attributes().isAttributeValueExplicitlySpecified("shrink_resources") |
| && dataContext.throwOnShrinkResources()) { |
| ruleContext.throwWithAttributeError("shrink_resources", "This attribute is not supported"); |
| } |
| |
| if (AndroidCommon.getAndroidConfig(ruleContext).desugarJava8Libs() |
| && getMultidexMode(ruleContext) == MultidexMode.OFF) { |
| // Multidex is required so we can include legacy libs as a separate .dex file. |
| ruleContext.throwWithAttributeError( |
| "multidex", "Support for Java 8 libraries on legacy devices requires multidex"); |
| } |
| |
| if (ruleContext.getFragment(JavaConfiguration.class).enforceProguardFileExtension() |
| && ruleContext.attributes().has(ProguardHelper.PROGUARD_SPECS)) { |
| List<PathFragment> pathsWithUnexpectedExtension = |
| ruleContext.getPrerequisiteArtifacts(ProguardHelper.PROGUARD_SPECS).list().stream() |
| .filter(Artifact::isSourceArtifact) |
| .map(Artifact::getRootRelativePath) |
| .filter( |
| // This checks the filename directly instead of using FileType because we want to |
| // exclude third_party/, but FileType is generally only given the basename. |
| // |
| // See e.g. RuleContext#validateDirectPrerequisiteType and |
| // PrerequisiteArtifacts#filter. |
| path -> |
| !path.getFileExtension().equals("pgcfg") |
| && !path.startsWith(RuleClass.THIRD_PARTY_PREFIX) |
| && !path.startsWith(RuleClass.EXPERIMENTAL_PREFIX)) |
| .collect(toImmutableList()); |
| if (!pathsWithUnexpectedExtension.isEmpty()) { |
| ruleContext.throwWithAttributeError( |
| ProguardHelper.PROGUARD_SPECS, |
| "Proguard spec files must use the .pgcfg extension. These files do not end in .pgcfg: " |
| + pathsWithUnexpectedExtension); |
| } |
| } |
| } |
| |
| private static RuleConfiguredTargetBuilder init( |
| RuleContext ruleContext, |
| NestedSetBuilder<Artifact> filesBuilder, |
| CppSemantics cppSemantics, |
| JavaSemantics javaSemantics, |
| AndroidSemantics androidSemantics) |
| throws InterruptedException, RuleErrorException { |
| |
| ResourceDependencies resourceDeps = |
| ResourceDependencies.fromRuleDeps(ruleContext, /* neverlink= */ false); |
| |
| AndroidDataContext dataContext = androidSemantics.makeContextForNative(ruleContext); |
| validateRuleContext(ruleContext, dataContext); |
| |
| NativeLibs nativeLibs = |
| NativeLibs.fromLinkedNativeDeps( |
| ruleContext, |
| ImmutableList.of("deps"), |
| androidSemantics.getNativeDepsFileName(), |
| cppSemantics); |
| |
| // Retrieve and compile the resources defined on the android_binary rule. |
| AndroidResources.validateRuleContext(ruleContext); |
| |
| Map<String, String> manifestValues = StampedAndroidManifest.getManifestValues(ruleContext); |
| |
| StampedAndroidManifest manifest; |
| if (isInstrumentation(ruleContext) |
| && dataContext.getAndroidConfig().disableInstrumentationManifestMerging()) { |
| manifest = |
| AndroidManifest.fromAttributes(ruleContext, dataContext, androidSemantics) |
| .stamp(dataContext); |
| } else { |
| manifest = |
| AndroidManifest.fromAttributes(ruleContext, dataContext, androidSemantics) |
| .mergeWithDeps( |
| dataContext, |
| androidSemantics, |
| ruleContext, |
| resourceDeps, |
| manifestValues, |
| ruleContext.getRule().isAttrDefined("manifest_merger", STRING) |
| ? ruleContext.attributes().get("manifest_merger", STRING) |
| : null); |
| } |
| |
| boolean shrinkResourceCycles = |
| shouldShrinkResourceCycles( |
| dataContext.getAndroidConfig(), ruleContext, dataContext.isResourceShrinkingEnabled()); |
| ProcessedAndroidData processedAndroidData = |
| ProcessedAndroidData.processBinaryDataFrom( |
| dataContext, |
| ruleContext, |
| manifest, |
| /* conditionalKeepRules= */ shrinkResourceCycles, |
| manifestValues, |
| AndroidResources.from(ruleContext, "resource_files"), |
| AndroidAssets.from(ruleContext), |
| resourceDeps, |
| AssetDependencies.fromRuleDeps(ruleContext, /* neverlink = */ false), |
| ResourceFilterFactory.fromRuleContextAndAttrs(ruleContext), |
| ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions"), |
| ruleContext.attributes().get("crunch_png", Type.BOOLEAN), |
| DataBinding.contextFrom(ruleContext, dataContext.getAndroidConfig())); |
| |
| AndroidApplicationResourceInfo androidApplicationResourceInfo = |
| ruleContext.getPrerequisite( |
| "application_resources", |
| AndroidApplicationResourceInfo.PROVIDER); |
| |
| final ResourceApk resourceApk; |
| if (androidApplicationResourceInfo == null) { |
| resourceApk = |
| new RClassGeneratorActionBuilder() |
| .withDependencies(resourceDeps) |
| .finalFields(!shrinkResourceCycles) |
| .setClassJarOut( |
| dataContext.createOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCES_CLASS_JAR)) |
| .build(dataContext, processedAndroidData); |
| } else { |
| resourceApk = |
| ResourceApk.fromAndroidApplicationResourceInfo( |
| ruleContext, dataContext.getAndroidConfig(), androidApplicationResourceInfo); |
| } |
| |
| if (dataContext.useResourcePathShortening()) { |
| filesBuilder.add( |
| ruleContext.getImplicitOutputArtifact( |
| AndroidRuleClasses.ANDROID_RESOURCE_PATH_SHORTENING_MAP)); |
| } |
| |
| ruleContext.assertNoErrors(); |
| |
| JavaCommon javaCommon = |
| AndroidCommon.createJavaCommonWithAndroidDataBinding( |
| ruleContext, javaSemantics, resourceApk.asDataBindingContext(), /* isLibrary */ false); |
| javaSemantics.checkRule(ruleContext, javaCommon); |
| javaSemantics.checkForProtoLibraryAndJavaProtoLibraryOnSameProto(ruleContext, javaCommon); |
| |
| AndroidCommon androidCommon = new AndroidCommon(javaCommon, /* asNeverLink= */ true); |
| |
| // Remove the library resource JARs from the binary's runtime classpath. |
| // Resource classes from android_library dependencies are replaced by the binary's resource |
| // class. We remove them only at the top level so that resources included by a library that is |
| // a dependency of a java_library are still included, since these resources are propagated via |
| // android-specific providers and won't show up when we collect the library resource JARs. |
| // TODO(b/69552500): Instead, handle this properly so R JARs aren't put on the classpath for |
| // both binaries and libraries. |
| NestedSet<Artifact> excludedRuntimeArtifacts = getLibraryResourceJars(ruleContext); |
| |
| JavaTargetAttributes resourceClasses = |
| androidCommon.init( |
| javaSemantics, |
| androidSemantics, |
| resourceApk, |
| ruleContext.getConfiguration().isCodeCoverageEnabled(), |
| /* collectJavaCompilationArgs= */ true, |
| /* isBinary= */ true, |
| excludedRuntimeArtifacts, |
| /* generateExtensionRegistry= */ true); |
| ruleContext.assertNoErrors(); |
| |
| Function<Artifact, Artifact> derivedJarFunction = |
| collectDesugaredJars(ruleContext, androidCommon, androidSemantics, resourceClasses); |
| Artifact deployJar = |
| createDeployJar( |
| ruleContext, |
| javaSemantics, |
| androidCommon, |
| resourceClasses, |
| AndroidCommon.getAndroidConfig(ruleContext).checkDesugarDeps(), |
| derivedJarFunction); |
| |
| boolean isBinaryJarFiltered = isInstrumentation(ruleContext); |
| if (isBinaryJarFiltered) { |
| deployJar = getFilteredDeployJar(ruleContext, deployJar); |
| } |
| |
| OneVersionEnforcementLevel oneVersionEnforcementLevel = |
| ruleContext.getFragment(JavaConfiguration.class).oneVersionEnforcementLevel(); |
| Artifact oneVersionOutputArtifact = null; |
| if (oneVersionEnforcementLevel != OneVersionEnforcementLevel.OFF) { |
| NestedSet<Artifact> transitiveDependencies = |
| NestedSetBuilder.<Artifact>stableOrder() |
| .addAll( |
| Iterables.transform( |
| resourceClasses.getRuntimeClassPath().toList(), derivedJarFunction)) |
| .addAll( |
| Iterables.transform( |
| androidCommon.getJarsProducedForRuntime().toList(), derivedJarFunction)) |
| .build(); |
| |
| oneVersionOutputArtifact = |
| OneVersionCheckActionBuilder.newBuilder() |
| .withEnforcementLevel(oneVersionEnforcementLevel) |
| .outputArtifact( |
| ruleContext.getImplicitOutputArtifact(JavaSemantics.JAVA_ONE_VERSION_ARTIFACT)) |
| .useToolchain(JavaToolchainProvider.from(ruleContext)) |
| .checkJars(transitiveDependencies) |
| .build(ruleContext); |
| } |
| |
| Artifact proguardMapping = ruleContext.getPrerequisiteArtifact("proguard_apply_mapping"); |
| |
| MobileInstallResourceApks mobileInstallResourceApks = |
| AndroidBinaryMobileInstall.createMobileInstallResourceApks( |
| ruleContext, dataContext, manifest); |
| |
| return createAndroidBinary( |
| ruleContext, |
| dataContext, |
| filesBuilder, |
| deployJar, |
| derivedJarFunction, |
| isBinaryJarFiltered, |
| androidCommon, |
| javaSemantics, |
| androidSemantics, |
| nativeLibs, |
| resourceApk, |
| mobileInstallResourceApks, |
| resourceClasses, |
| ImmutableList.<Artifact>of(), |
| ImmutableList.<Artifact>of(), |
| proguardMapping, |
| oneVersionOutputArtifact); |
| } |
| |
| public static RuleConfiguredTargetBuilder createAndroidBinary( |
| RuleContext ruleContext, |
| AndroidDataContext dataContext, |
| NestedSetBuilder<Artifact> filesBuilder, |
| Artifact binaryJar, |
| Function<Artifact, Artifact> derivedJarFunction, |
| boolean isBinaryJarFiltered, |
| AndroidCommon androidCommon, |
| JavaSemantics javaSemantics, |
| AndroidSemantics androidSemantics, |
| NativeLibs nativeLibs, |
| ResourceApk resourceApk, |
| @Nullable MobileInstallResourceApks mobileInstallResourceApks, |
| JavaTargetAttributes resourceClasses, |
| ImmutableList<Artifact> apksUnderTest, |
| ImmutableList<Artifact> additionalMergedManifests, |
| Artifact proguardMapping, |
| @Nullable Artifact oneVersionEnforcementArtifact) |
| throws InterruptedException, RuleErrorException { |
| |
| List<ProguardSpecProvider> proguardDeps = new ArrayList<>(); |
| Iterables.addAll( |
| proguardDeps, ruleContext.getPrerequisites("deps", ProguardSpecProvider.PROVIDER)); |
| if (ruleContext.getConfiguration().isCodeCoverageEnabled() |
| && ruleContext.attributes().has("$jacoco_runtime", BuildType.LABEL)) { |
| proguardDeps.add( |
| ruleContext.getPrerequisite("$jacoco_runtime", ProguardSpecProvider.PROVIDER)); |
| } |
| ImmutableList<Artifact> proguardSpecs = |
| getProguardSpecs( |
| dataContext, |
| androidSemantics, |
| resourceApk.getResourceProguardConfig(), |
| resourceApk.getManifest(), |
| ruleContext.attributes().has(ProguardHelper.PROGUARD_SPECS, BuildType.LABEL_LIST) |
| ? ruleContext.getPrerequisiteArtifacts(ProguardHelper.PROGUARD_SPECS).list() |
| : ImmutableList.<Artifact>of(), |
| ruleContext.getPrerequisiteArtifacts(":extra_proguard_specs").list(), |
| proguardDeps); |
| boolean hasProguardSpecs = !proguardSpecs.isEmpty(); |
| |
| // TODO(bazel-team): Verify that proguard spec files don't contain -printmapping directions |
| // which this -printmapping command line flag will override. |
| Artifact proguardOutputMap = null; |
| if (ProguardHelper.genProguardMapping(ruleContext.attributes()) |
| || dataContext.isResourceShrinkingEnabled()) { |
| proguardOutputMap = androidSemantics.getProguardOutputMap(ruleContext); |
| } |
| |
| ProguardOutput proguardOutput = |
| applyProguard( |
| ruleContext, |
| androidCommon, |
| javaSemantics, |
| binaryJar, |
| proguardSpecs, |
| proguardMapping, |
| proguardOutputMap); |
| |
| if (dataContext.useResourceShrinking(hasProguardSpecs)) { |
| resourceApk = |
| shrinkResources( |
| ruleContext, |
| androidSemantics.makeContextForNative(ruleContext), |
| resourceApk, |
| proguardOutput, |
| filesBuilder); |
| } |
| |
| resourceApk = maybeOptimizeResources(dataContext, resourceApk, hasProguardSpecs); |
| |
| Artifact jarToDex = proguardOutput.getOutputJar(); |
| DexingOutput dexingOutput = |
| dex( |
| ruleContext, |
| androidSemantics, |
| binaryJar, |
| jarToDex, |
| isBinaryJarFiltered, |
| androidCommon, |
| resourceApk.getMainDexProguardConfig(), |
| resourceClasses, |
| derivedJarFunction, |
| proguardOutputMap); |
| |
| // Collect all native shared libraries across split transitions. Some AARs contain shared |
| // libraries across multiple architectures, e.g. x86 and armeabi-v7a, and need to be packed |
| // into the APK. |
| NestedSetBuilder<Artifact> transitiveNativeLibs = NestedSetBuilder.naiveLinkOrder(); |
| for (Map.Entry< |
| com.google.common.base.Optional<String>, |
| ? extends List<? extends TransitiveInfoCollection>> |
| entry : ruleContext.getSplitPrerequisites("deps").entrySet()) { |
| for (AndroidNativeLibsInfo provider : |
| AnalysisUtils.getProviders(entry.getValue(), AndroidNativeLibsInfo.PROVIDER)) { |
| transitiveNativeLibs.addTransitive(provider.getNativeLibs()); |
| } |
| } |
| NestedSet<Artifact> nativeLibsAar = transitiveNativeLibs.build(); |
| |
| DexPostprocessingOutput dexPostprocessingOutput = |
| androidSemantics.postprocessClassesDexZip( |
| ruleContext, filesBuilder, dexingOutput.classesDexZip, proguardOutput); |
| |
| if (!proguardSpecs.isEmpty()) { |
| proguardOutput.addAllToSet(filesBuilder, dexPostprocessingOutput.proguardMap()); |
| } |
| |
| Artifact unsignedApk = |
| ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_UNSIGNED_APK); |
| Artifact zipAlignedApk = |
| ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_APK); |
| ImmutableList<Artifact> signingKeys = AndroidCommon.getApkDebugSigningKeys(ruleContext); |
| Artifact signingLineage = ruleContext.getPrerequisiteArtifact("debug_signing_lineage_file"); |
| FilesToRunProvider resourceExtractor = |
| ruleContext.getExecutablePrerequisite("$resource_extractor"); |
| |
| Artifact finalClassesDex; |
| ImmutableList<Artifact> finalShardDexZips = dexingOutput.shardDexZips; |
| if (AndroidCommon.getAndroidConfig(ruleContext).desugarJava8Libs() |
| && dexPostprocessingOutput.classesDexZip().getFilename().endsWith(".zip")) { |
| Artifact java8LegacyDex; |
| if (binaryJar.equals(jarToDex)) { |
| // No Proguard: use canned Java 8 legacy .dex file |
| java8LegacyDex = ruleContext.getPrerequisiteArtifact("$java8_legacy_dex"); |
| } else { |
| // Proguard is used: build custom Java 8 legacy .dex file |
| java8LegacyDex = getDxArtifact(ruleContext, "_java8_legacy.dex.zip"); |
| Artifact androidJar = AndroidSdkProvider.fromRuleContext(ruleContext).getAndroidJar(); |
| ruleContext.registerAction( |
| new SpawnAction.Builder() |
| .setExecutable(ruleContext.getExecutablePrerequisite("$build_java8_legacy_dex")) |
| .addInput(jarToDex) |
| .addInput(androidJar) |
| .addOutput(java8LegacyDex) |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .addExecPath("--binary", jarToDex) |
| .addExecPath("--android_jar", androidJar) |
| .addExecPath("--output", java8LegacyDex) |
| .build()) |
| .setMnemonic("BuildLegacyDex") |
| .setProgressMessage("Building Java 8 legacy library for %s", ruleContext.getLabel()) |
| .build(ruleContext)); |
| } |
| |
| // Append legacy .dex library to app's .dex files |
| finalClassesDex = getDxArtifact(ruleContext, "_final_classes.dex.zip"); |
| ruleContext.registerAction( |
| new SpawnAction.Builder() |
| .useDefaultShellEnvironment() |
| .setMnemonic("AppendJava8LegacyDex") |
| .setProgressMessage("Adding Java 8 legacy library for %s", ruleContext.getLabel()) |
| .setExecutable(ruleContext.getExecutablePrerequisite("$merge_dexzips")) |
| .addInput(dexPostprocessingOutput.classesDexZip()) |
| .addInput(java8LegacyDex) |
| .addOutput(finalClassesDex) |
| // Order matters here: we want java8LegacyDex to be the highest-numbered classesN.dex |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .addExecPath("--input_zip", dexPostprocessingOutput.classesDexZip()) |
| .addExecPath("--input_zip", java8LegacyDex) |
| .addExecPath("--output_zip", finalClassesDex) |
| .build()) |
| .build(ruleContext)); |
| finalShardDexZips = |
| ImmutableList.<Artifact>builder().addAll(finalShardDexZips).add(java8LegacyDex).build(); |
| } else { |
| finalClassesDex = dexPostprocessingOutput.classesDexZip(); |
| } |
| |
| ApkActionsBuilder.create("apk") |
| .setClassesDex(finalClassesDex) |
| .addInputZip(resourceApk.getArtifact()) |
| .setJavaResourceZip(dexingOutput.javaResourceJar, resourceExtractor) |
| .addInputZips(nativeLibsAar.toList()) |
| .setNativeLibs(nativeLibs) |
| .setUnsignedApk(unsignedApk) |
| .setSignedApk(zipAlignedApk) |
| .setSigningKeys(signingKeys) |
| .setSigningLineageFile(signingLineage) |
| .setZipalignApk(true) |
| .registerActions(ruleContext); |
| |
| filesBuilder.add(binaryJar); |
| filesBuilder.add(unsignedApk); |
| filesBuilder.add(zipAlignedApk); |
| NestedSet<Artifact> filesToBuild = filesBuilder.build(); |
| |
| Artifact deployInfo = ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.DEPLOY_INFO); |
| AndroidDeployInfoAction.createDeployInfoAction( |
| ruleContext, |
| deployInfo, |
| resourceApk.getManifest(), |
| additionalMergedManifests, |
| ImmutableList.<Artifact>builder().add(zipAlignedApk).addAll(apksUnderTest).build()); |
| |
| RuleConfiguredTargetBuilder builder = new RuleConfiguredTargetBuilder(ruleContext); |
| |
| // If this is an instrumentation APK, create the provider for android_instrumentation_test. |
| if (isInstrumentation(ruleContext)) { |
| ApkInfo targetApkProvider = ruleContext.getPrerequisite("instruments", ApkInfo.PROVIDER); |
| |
| AndroidInstrumentationInfo instrumentationProvider = |
| new AndroidInstrumentationInfo(targetApkProvider); |
| |
| builder.addNativeDeclaredProvider(instrumentationProvider); |
| |
| // At this point, the Android manifests of both target and instrumentation APKs are finalized. |
| FilesToRunProvider checker = |
| ruleContext.getExecutablePrerequisite("$instrumentation_test_check"); |
| Artifact targetManifest = targetApkProvider.getMergedManifest(); |
| Artifact instrumentationManifest = resourceApk.getManifest(); |
| Artifact checkOutput = |
| ruleContext.getImplicitOutputArtifact( |
| AndroidRuleClasses.INSTRUMENTATION_TEST_CHECK_RESULTS); |
| |
| SpawnAction.Builder checkAction = |
| new SpawnAction.Builder() |
| .setExecutable(checker) |
| .addInput(targetManifest) |
| .addInput(instrumentationManifest) |
| .addOutput(checkOutput) |
| .setProgressMessage( |
| "Validating the merged manifests of the target and instrumentation APKs") |
| .setMnemonic("AndroidManifestInstrumentationCheck"); |
| |
| CustomCommandLine commandLine = |
| CustomCommandLine.builder() |
| .addExecPath("--instrumentation_manifest", instrumentationManifest) |
| .addExecPath("--target_manifest", targetManifest) |
| .addExecPath("--output", checkOutput) |
| .build(); |
| |
| builder.addOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL, checkOutput); |
| checkAction.addCommandLine(commandLine); |
| ruleContext.registerAction(checkAction.build(ruleContext)); |
| } |
| |
| androidCommon.addTransitiveInfoProviders( |
| builder, |
| /* aar= */ null, |
| resourceApk, |
| zipAlignedApk, |
| apksUnderTest, |
| nativeLibs, |
| androidCommon.isNeverLink(), |
| /* isLibrary = */ false); |
| |
| if (dexPostprocessingOutput.proguardMap() != null) { |
| builder.addNativeDeclaredProvider( |
| new ProguardMappingProvider(dexPostprocessingOutput.proguardMap())); |
| } |
| |
| if (oneVersionEnforcementArtifact != null) { |
| builder.addOutputGroup(OutputGroupInfo.HIDDEN_TOP_LEVEL, oneVersionEnforcementArtifact); |
| } |
| |
| if (mobileInstallResourceApks != null) { |
| AndroidBinaryMobileInstall.addMobileInstall( |
| ruleContext, |
| builder, |
| dexingOutput.javaResourceJar, |
| finalShardDexZips, |
| javaSemantics, |
| nativeLibs, |
| resourceApk, |
| mobileInstallResourceApks, |
| resourceExtractor, |
| nativeLibsAar, |
| signingKeys, |
| signingLineage, |
| additionalMergedManifests); |
| } |
| |
| return builder |
| .setFilesToBuild(filesToBuild) |
| .addProvider( |
| RunfilesProvider.class, |
| RunfilesProvider.simple( |
| new Runfiles.Builder( |
| ruleContext.getWorkspaceName(), |
| ruleContext.getConfiguration().legacyExternalRunfiles()) |
| .addRunfiles(ruleContext, RunfilesProvider.DEFAULT_RUNFILES) |
| .addTransitiveArtifacts(filesToBuild) |
| .build())) |
| .addProvider( |
| JavaSourceInfoProvider.class, |
| JavaSourceInfoProvider.fromJavaTargetAttributes(resourceClasses, javaSemantics)) |
| .addNativeDeclaredProvider( |
| new ApkInfo( |
| zipAlignedApk, |
| unsignedApk, |
| binaryJar, |
| getCoverageInstrumentationJarForApk(ruleContext), |
| resourceApk.getManifest(), |
| signingKeys, |
| signingLineage)) |
| .addNativeDeclaredProvider(new AndroidPreDexJarProvider(jarToDex)) |
| .addNativeDeclaredProvider( |
| AndroidFeatureFlagSetProvider.create( |
| AndroidFeatureFlagSetProvider.getAndValidateFlagMapFromRuleContext(ruleContext))) |
| // Report set feature flags as required "config fragments". |
| // While these aren't technically fragments, in practice they're user-defined settings with |
| // the same meaning: pieces of configuration the rule requires to work properly. So it makes |
| // sense to treat them equivalently for "requirements" reporting purposes. |
| .addRequiredConfigFragments(AndroidFeatureFlagSetProvider.getFlagNames(ruleContext)) |
| .addOutputGroup("android_deploy_info", deployInfo); |
| } |
| |
| /** |
| * For coverage builds, this returns a Jar containing <b>un</b>instrumented bytecode for the |
| * coverage reporter's consumption. This method simply returns the deploy Jar. Note the deploy Jar |
| * is built anyway for Android binaries. |
| * |
| * @return A Jar containing uninstrumented bytecode or {@code null} for non-coverage builds |
| */ |
| @Nullable |
| private static Artifact getCoverageInstrumentationJarForApk(RuleContext ruleContext) |
| throws InterruptedException { |
| return ruleContext.getConfiguration().isCodeCoverageEnabled() |
| ? ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_DEPLOY_JAR) |
| : null; |
| } |
| |
| public static NestedSet<Artifact> getLibraryResourceJars(RuleContext ruleContext) { |
| Iterable<AndroidLibraryResourceClassJarProvider> libraryResourceJarProviders = |
| AndroidCommon.getTransitivePrerequisites( |
| ruleContext, AndroidLibraryResourceClassJarProvider.PROVIDER); |
| |
| NestedSetBuilder<Artifact> libraryResourceJarsBuilder = NestedSetBuilder.naiveLinkOrder(); |
| for (AndroidLibraryResourceClassJarProvider provider : libraryResourceJarProviders) { |
| libraryResourceJarsBuilder.addTransitive(provider.getResourceClassJars()); |
| } |
| return libraryResourceJarsBuilder.build(); |
| } |
| |
| /** Generates an uncompressed _deploy.jar of all the runtime jars. */ |
| public static Artifact createDeployJar( |
| RuleContext ruleContext, |
| JavaSemantics javaSemantics, |
| AndroidCommon common, |
| JavaTargetAttributes attributes, |
| boolean checkDesugarDeps, |
| Function<Artifact, Artifact> derivedJarFunction) |
| throws InterruptedException { |
| Artifact deployJar = |
| ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_DEPLOY_JAR); |
| new DeployArchiveBuilder(javaSemantics, ruleContext) |
| .setOutputJar(deployJar) |
| .setAttributes(attributes) |
| .addRuntimeJars(common.getRuntimeJars()) |
| .setDerivedJarFunction(derivedJarFunction) |
| .setCheckDesugarDeps(checkDesugarDeps) |
| .build(); |
| return deployJar; |
| } |
| |
| /** |
| * Applies the proguard specifications, and creates a ProguardedJar. Proguard's output artifacts |
| * are added to the given {@code filesBuilder}. |
| */ |
| private static ProguardOutput applyProguard( |
| RuleContext ruleContext, |
| AndroidCommon common, |
| JavaSemantics javaSemantics, |
| Artifact deployJarArtifact, |
| ImmutableList<Artifact> proguardSpecs, |
| Artifact proguardMapping, |
| @Nullable Artifact proguardOutputMap) |
| throws InterruptedException { |
| Artifact proguardOutputJar = |
| ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_BINARY_PROGUARD_JAR); |
| |
| // Proguard will be only used for binaries which specify a proguard_spec |
| if (proguardSpecs.isEmpty()) { |
| // Although normally the Proguard jar artifact is not needed for binaries which do not specify |
| // proguard_specs, targets which use a select to provide an empty list to proguard_specs will |
| // still have a Proguard jar implicit output, as it is impossible to tell what a select will |
| // produce at the time of implicit output determination. As a result, this artifact must |
| // always be created. |
| return createEmptyProguardAction( |
| ruleContext, javaSemantics, proguardOutputJar, deployJarArtifact, proguardOutputMap); |
| } |
| |
| AndroidSdkProvider sdk = AndroidSdkProvider.fromRuleContext(ruleContext); |
| NestedSetBuilder<Artifact> libraryJars = |
| NestedSetBuilder.<Artifact>naiveLinkOrder().add(sdk.getAndroidJar()); |
| if (AndroidCommon.getAndroidConfig(ruleContext).desugarJava8Libs()) { |
| // Proguard sees the desugared app, so it needs legacy APIs to resolve symbols |
| libraryJars.addTransitive( |
| ruleContext |
| .getPrerequisite("$desugared_java8_legacy_apis") |
| .getProvider(FileProvider.class) |
| .getFilesToBuild()); |
| } |
| libraryJars.addTransitive(common.getTransitiveNeverLinkLibraries()); |
| |
| Artifact proguardSeeds = |
| ruleContext.getImplicitOutputArtifact(JavaSemantics.JAVA_BINARY_PROGUARD_SEEDS); |
| Artifact proguardUsage = |
| ruleContext.getImplicitOutputArtifact(JavaSemantics.JAVA_BINARY_PROGUARD_USAGE); |
| Artifact proguardDictionary = ruleContext.getPrerequisiteArtifact("proguard_apply_dictionary"); |
| ProguardOutput result = |
| ProguardHelper.createOptimizationActions( |
| ruleContext, |
| sdk.getProguard(), |
| deployJarArtifact, |
| proguardSpecs, |
| proguardSeeds, |
| proguardUsage, |
| proguardMapping, |
| proguardDictionary, |
| libraryJars.build(), |
| proguardOutputJar, |
| javaSemantics, |
| getProguardOptimizationPasses(ruleContext), |
| proguardOutputMap); |
| return result; |
| } |
| |
| @Nullable |
| private static Integer getProguardOptimizationPasses(RuleContext ruleContext) { |
| if (ruleContext.attributes().has("proguard_optimization_passes", Type.INTEGER)) { |
| StarlarkInt i = ruleContext.attributes().get("proguard_optimization_passes", Type.INTEGER); |
| if (i != null) { |
| return i.toIntUnchecked(); |
| } |
| } |
| return null; |
| } |
| |
| private static ProguardOutput createEmptyProguardAction( |
| RuleContext ruleContext, |
| JavaSemantics semantics, |
| Artifact proguardOutputJar, |
| Artifact deployJarArtifact, |
| Artifact proguardOutputMap) |
| throws InterruptedException { |
| NestedSetBuilder<Artifact> failures = NestedSetBuilder.<Artifact>stableOrder(); |
| ProguardOutput outputs = |
| ProguardHelper.getProguardOutputs( |
| proguardOutputJar, |
| /* proguardSeeds */ (Artifact) null, |
| /* proguardUsage */ (Artifact) null, |
| ruleContext, |
| semantics, |
| proguardOutputMap); |
| outputs.addAllToSet(failures); |
| ruleContext.registerAction( |
| new FailAction( |
| ruleContext.getActionOwner(), |
| failures.build().toSet(), |
| "Can't run Proguard without proguard_specs", |
| Code.PROGUARD_SPECS_MISSING)); |
| return new ProguardOutput(deployJarArtifact, null, null, null, null, null, null); |
| } |
| |
| static ImmutableList<Artifact> getProguardSpecs( |
| AndroidDataContext dataContext, |
| AndroidSemantics androidSemantics, |
| Artifact resourceProguardConfig, |
| Artifact mergedManifest, |
| ImmutableList<Artifact> localProguardSpecs, |
| ImmutableList<Artifact> extraProguardSpecs, |
| Iterable<ProguardSpecProvider> proguardDeps) { |
| |
| ImmutableList<Artifact> proguardSpecs = |
| ProguardHelper.collectTransitiveProguardSpecs( |
| dataContext.getLabel(), |
| dataContext.getActionConstructionContext(), |
| Iterables.concat(ImmutableList.of(resourceProguardConfig), extraProguardSpecs), |
| localProguardSpecs, |
| proguardDeps); |
| |
| boolean assumeMinSdkVersion = dataContext.getAndroidConfig().assumeMinSdkVersion(); |
| if (!proguardSpecs.isEmpty() && assumeMinSdkVersion) { |
| // NB: Order here is important. We're including generated Proguard specs before the user's |
| // specs so that they can override values. |
| proguardSpecs = |
| ImmutableList.<Artifact>builder() |
| .addAll(androidSemantics.getProguardSpecsForManifest(dataContext, mergedManifest)) |
| .addAll(proguardSpecs) |
| .build(); |
| } |
| |
| return proguardSpecs; |
| } |
| |
| /** Returns {@code true} if resource shrinking should be performed. */ |
| static boolean shouldShrinkResourceCycles( |
| AndroidConfiguration androidConfig, RuleErrorConsumer errorConsumer, boolean shrinkResources) |
| throws RuleErrorException { |
| boolean global = androidConfig.useAndroidResourceCycleShrinking(); |
| if (global && !shrinkResources) { |
| throw errorConsumer.throwWithRuleError( |
| "resource cycle shrinking can only be enabled when resource shrinking is enabled"); |
| } |
| return global; |
| } |
| |
| private static ResourceApk shrinkResources( |
| RuleContext ruleContext, |
| AndroidDataContext dataContext, |
| ResourceApk resourceApk, |
| ProguardOutput proguardOutput, |
| NestedSetBuilder<Artifact> filesBuilder) |
| throws RuleErrorException, InterruptedException { |
| |
| Artifact shrunkApk = |
| shrinkResources( |
| dataContext, |
| resourceApk.getRTxt(), |
| resourceApk.getResourcesZip(), |
| proguardOutput.getOutputJar(), |
| proguardOutput.getMapping(), |
| ResourceFilterFactory.fromRuleContextAndAttrs(ruleContext), |
| ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions")); |
| |
| filesBuilder.add( |
| ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCE_SHRINKER_LOG)); |
| return resourceApk.withApk(shrunkApk); |
| } |
| |
| static Artifact shrinkResources( |
| AndroidDataContext dataContext, |
| ValidatedAndroidResources validatedResources, |
| Artifact proguardOutputJar, |
| Artifact proguardMapping, |
| ResourceFilterFactory resourceFilterFactory, |
| List<String> noCompressExtensions) |
| throws InterruptedException { |
| return shrinkResources( |
| dataContext, |
| Preconditions.checkNotNull(validatedResources).getRTxt(), |
| dataContext.createOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCES_ZIP), |
| proguardOutputJar, |
| proguardMapping, |
| resourceFilterFactory, |
| noCompressExtensions); |
| } |
| |
| static Artifact shrinkResources( |
| AndroidDataContext dataContext, |
| Artifact rTxt, |
| Artifact resourcesZip, |
| Artifact proguardOutputJar, |
| Artifact proguardMapping, |
| ResourceFilterFactory resourceFilterFactory, |
| List<String> noCompressExtensions) |
| throws InterruptedException { |
| |
| ResourceShrinkerActionBuilder resourceShrinkerActionBuilder = |
| new ResourceShrinkerActionBuilder() |
| .setResourceApkOut( |
| dataContext.createOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCES_SHRUNK_APK)) |
| .setShrunkResourcesOut( |
| dataContext.createOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCES_SHRUNK_ZIP)) |
| .setLogOut( |
| dataContext.createOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCE_SHRINKER_LOG)) |
| .withResourceFiles(resourcesZip) |
| .withShrunkJar(proguardOutputJar) |
| .withProguardMapping(proguardMapping) |
| .withRTxt(rTxt) |
| .setResourceFilterFactory(resourceFilterFactory) |
| .setUncompressedExtensions(noCompressExtensions) |
| .setResourceOptimizationConfigOut( |
| dataContext.createOutputArtifact( |
| AndroidRuleClasses.ANDROID_RESOURCE_OPTIMIZATION_CONFIG)); |
| return resourceShrinkerActionBuilder.build(dataContext); |
| } |
| |
| private static ResourceApk maybeOptimizeResources( |
| AndroidDataContext dataContext, ResourceApk resourceApk, boolean hasProguardSpecs) |
| throws InterruptedException { |
| boolean useResourcePathShortening = dataContext.useResourcePathShortening(); |
| boolean useResourceNameObfuscation = dataContext.useResourceNameObfuscation(hasProguardSpecs); |
| if (!useResourcePathShortening && !useResourceNameObfuscation) { |
| return resourceApk; |
| } |
| |
| Artifact optimizedApk = |
| dataContext.createOutputArtifact(AndroidRuleClasses.ANDROID_RESOURCES_OPTIMIZED_APK); |
| |
| Aapt2OptimizeActionBuilder.Builder builder = |
| Aapt2OptimizeActionBuilder.builder() |
| .setResourceApk(resourceApk.getArtifact()) |
| .setOptimizedApkOut(optimizedApk); |
| if (useResourcePathShortening) { |
| builder.setResourcePathShorteningMapOut( |
| dataContext.createOutputArtifact( |
| AndroidRuleClasses.ANDROID_RESOURCE_PATH_SHORTENING_MAP)); |
| } |
| if (useResourceNameObfuscation) { |
| builder.setResourceOptimizationConfig( |
| dataContext.createOutputArtifact( |
| AndroidRuleClasses.ANDROID_RESOURCE_OPTIMIZATION_CONFIG)); |
| } |
| builder.build().registerAction(dataContext); |
| |
| return resourceApk.withApk(optimizedApk); |
| } |
| |
| @Immutable |
| static final class DexingOutput { |
| private final Artifact classesDexZip; |
| final Artifact javaResourceJar; |
| final ImmutableList<Artifact> shardDexZips; |
| |
| private DexingOutput( |
| Artifact classesDexZip, Artifact javaResourceJar, ImmutableList<Artifact> shardDexZips) { |
| this.classesDexZip = classesDexZip; |
| this.javaResourceJar = javaResourceJar; |
| this.shardDexZips = Preconditions.checkNotNull(shardDexZips); |
| } |
| } |
| |
| /** All artifacts modified by any dex post-processing steps. */ |
| @AutoValue |
| public abstract static class DexPostprocessingOutput { |
| |
| public static DexPostprocessingOutput create(Artifact classesDexZip, Artifact proguardMap) { |
| return new AutoValue_AndroidBinary_DexPostprocessingOutput(classesDexZip, proguardMap); |
| } |
| |
| /** A .zip of .dex files to include in the APK. */ |
| abstract Artifact classesDexZip(); |
| |
| /** |
| * The proguard mapping corresponding to the post-processed dex files. This may be null if |
| * proguard was not run. |
| */ |
| @Nullable |
| abstract Artifact proguardMap(); |
| } |
| |
| /** Creates one or more classes.dex files that correspond to {@code proguardedJar}. */ |
| private static DexingOutput dex( |
| RuleContext ruleContext, |
| AndroidSemantics androidSemantics, |
| Artifact binaryJar, |
| Artifact proguardedJar, |
| boolean isBinaryJarFiltered, |
| AndroidCommon common, |
| @Nullable Artifact mainDexProguardSpec, |
| JavaTargetAttributes attributes, |
| Function<Artifact, Artifact> derivedJarFunction, |
| @Nullable Artifact proguardOutputMap) |
| throws InterruptedException, RuleErrorException { |
| List<String> dexopts = ruleContext.getExpander().withDataLocations().tokenized("dexopts"); |
| MultidexMode multidexMode = getMultidexMode(ruleContext); |
| if (!supportsMultidexMode(ruleContext, multidexMode)) { |
| ruleContext.throwWithRuleError( |
| "Multidex mode \"" |
| + multidexMode.getAttributeValue() |
| + "\" not supported by this version of the Android SDK"); |
| } |
| |
| int dexShards = ruleContext.attributes().get("dex_shards", Type.INTEGER).toIntUnchecked(); |
| if (dexShards > 1) { |
| if (multidexMode == MultidexMode.OFF) { |
| ruleContext.throwWithRuleError(".dex sharding is only available in multidex mode"); |
| } |
| |
| if (multidexMode == MultidexMode.MANUAL_MAIN_DEX) { |
| ruleContext.throwWithRuleError(".dex sharding is not available in manual multidex mode"); |
| } |
| } |
| |
| Artifact mainDexList = ruleContext.getPrerequisiteArtifact("main_dex_list"); |
| if ((mainDexList != null && multidexMode != MultidexMode.MANUAL_MAIN_DEX) |
| || (mainDexList == null && multidexMode == MultidexMode.MANUAL_MAIN_DEX)) { |
| ruleContext.throwWithRuleError( |
| "Both \"main_dex_list\" and \"multidex='manual_main_dex'\" must be specified."); |
| } |
| |
| boolean usesDexArchives = |
| getEffectiveIncrementalDexing( |
| ruleContext, dexopts, !Objects.equals(binaryJar, proguardedJar)); |
| Artifact inclusionFilterJar = |
| isBinaryJarFiltered && Objects.equals(binaryJar, proguardedJar) ? binaryJar : null; |
| Artifact singleJarToDex = !Objects.equals(binaryJar, proguardedJar) ? proguardedJar : null; |
| if (multidexMode == MultidexMode.OFF) { |
| // Single dex mode: generate classes.dex directly from the input jar. |
| if (usesDexArchives) { |
| Artifact classesDex = getDxArtifact(ruleContext, "classes.dex.zip"); |
| createIncrementalDexingActions( |
| ruleContext, |
| singleJarToDex, |
| common, |
| inclusionFilterJar, |
| dexopts, |
| androidSemantics, |
| attributes, |
| derivedJarFunction, |
| /*multidex=*/ false, |
| /*mainDexList=*/ null, |
| classesDex); |
| return new DexingOutput(classesDex, binaryJar, ImmutableList.of(classesDex)); |
| } else { |
| // By *not* writing a zip we get dx to drop resources on the floor. |
| Artifact classesDex = getDxArtifact(ruleContext, "classes.dex"); |
| AndroidCommon.createDexAction( |
| ruleContext, |
| proguardedJar, |
| classesDex, |
| dexopts, |
| /*multidex=*/ false, |
| /*mainDexList=*/ null); |
| return new DexingOutput(classesDex, binaryJar, ImmutableList.of(classesDex)); |
| } |
| } else { |
| // Multidex mode: generate classes.dex.zip, where the zip contains [classes.dex, |
| // classes2.dex, ... classesN.dex]. |
| |
| if (multidexMode == MultidexMode.LEGACY) { |
| // For legacy multidex, we need to generate a list for the dexer's --main-dex-list flag. |
| mainDexList = |
| createMainDexListAction( |
| ruleContext, |
| androidSemantics, |
| proguardedJar, |
| mainDexProguardSpec, |
| proguardOutputMap); |
| } else if (multidexMode == MultidexMode.MANUAL_MAIN_DEX) { |
| mainDexList = |
| transformDexListThroughProguardMapAction(ruleContext, proguardOutputMap, mainDexList); |
| } |
| |
| Artifact classesDex = getDxArtifact(ruleContext, "classes.dex.zip"); |
| if (dexShards > 1) { |
| ImmutableList<Artifact> shards = |
| makeShardArtifacts(ruleContext, dexShards, usesDexArchives ? ".jar.dex.zip" : ".jar"); |
| |
| Artifact javaResourceJar = |
| createShuffleJarActions( |
| ruleContext, |
| usesDexArchives, |
| singleJarToDex, |
| shards, |
| common, |
| inclusionFilterJar, |
| dexopts, |
| androidSemantics, |
| attributes, |
| derivedJarFunction, |
| mainDexList); |
| |
| ImmutableList.Builder<Artifact> shardDexesBuilder = ImmutableList.builder(); |
| for (int i = 1; i <= dexShards; i++) { |
| Artifact shard = shards.get(i - 1); |
| Artifact shardDex = getDxArtifact(ruleContext, "shard" + i + ".dex.zip"); |
| shardDexesBuilder.add(shardDex); |
| if (usesDexArchives) { |
| // If there's a main dex list then the first shard contains exactly those files. |
| // To work with devices that lack native multi-dex support we need to make sure that |
| // the main dex list becomes one dex file if at all possible. |
| // Note shard here (mostly) contains of .class.dex files from shuffled dex archives, |
| // instead of being a conventional Jar file with .class files. |
| createDexMergerAction( |
| ruleContext, |
| mainDexList != null && i == 1 ? "minimal" : "best_effort", |
| ImmutableList.of(shard), |
| shardDex, |
| /*mainDexList=*/ null, |
| dexopts); |
| } else { |
| AndroidCommon.createDexAction( |
| ruleContext, shard, shardDex, dexopts, /*multidex=*/ true, /*mainDexList=*/ null); |
| } |
| } |
| ImmutableList<Artifact> shardDexes = shardDexesBuilder.build(); |
| |
| CommandLine mergeCommandLine = |
| CustomCommandLine.builder() |
| .addExecPaths(VectorArg.addBefore("--input_zip").each(shardDexes)) |
| .addExecPath("--output_zip", classesDex) |
| .build(); |
| ruleContext.registerAction( |
| new SpawnAction.Builder() |
| .useDefaultShellEnvironment() |
| .setMnemonic("MergeDexZips") |
| .setProgressMessage("Merging dex shards for %s", ruleContext.getLabel()) |
| .setExecutable(ruleContext.getExecutablePrerequisite("$merge_dexzips")) |
| .addInputs(shardDexes) |
| .addOutput(classesDex) |
| .addCommandLine(mergeCommandLine) |
| .build(ruleContext)); |
| if (usesDexArchives) { |
| // Using the deploy jar for java resources gives better "bazel mobile-install" performance |
| // with incremental dexing b/c bazel can create the "incremental" and "split resource" |
| // APKs earlier (b/c these APKs don't depend on code being dexed here). This is also done |
| // for other multidex modes. |
| javaResourceJar = binaryJar; |
| } |
| return new DexingOutput(classesDex, javaResourceJar, shardDexes); |
| } else { |
| if (usesDexArchives) { |
| createIncrementalDexingActions( |
| ruleContext, |
| singleJarToDex, |
| common, |
| inclusionFilterJar, |
| dexopts, |
| androidSemantics, |
| attributes, |
| derivedJarFunction, |
| /*multidex=*/ true, |
| mainDexList, |
| classesDex); |
| } else { |
| // Because the dexer also places resources into this zip, we also need to create a cleanup |
| // action that removes all non-.dex files before staging for apk building. |
| // Create an artifact for the intermediate zip output that includes non-.dex files. |
| Artifact classesDexIntermediate = |
| AndroidBinary.getDxArtifact(ruleContext, "intermediate_classes.dex.zip"); |
| // Have the dexer generate the intermediate file and the "cleaner" action consume this to |
| // generate the final archive with only .dex files. |
| AndroidCommon.createDexAction( |
| ruleContext, |
| proguardedJar, |
| classesDexIntermediate, |
| dexopts, |
| /*multidex=*/ true, |
| mainDexList); |
| createCleanDexZipAction(ruleContext, classesDexIntermediate, classesDex); |
| } |
| return new DexingOutput(classesDex, binaryJar, ImmutableList.of(classesDex)); |
| } |
| } |
| } |
| |
| /** |
| * Helper that sets up dexbuilder/dexmerger actions when dex_shards attribute is not set, for use |
| * with or without multidex. |
| */ |
| private static void createIncrementalDexingActions( |
| RuleContext ruleContext, |
| @Nullable Artifact proguardedJar, |
| AndroidCommon common, |
| @Nullable Artifact inclusionFilterJar, |
| List<String> dexopts, |
| AndroidSemantics androidSemantics, |
| JavaTargetAttributes attributes, |
| Function<Artifact, Artifact> derivedJarFunction, |
| boolean multidex, |
| @Nullable Artifact mainDexList, |
| Artifact classesDex) |
| throws InterruptedException, RuleErrorException { |
| ImmutableList<Artifact> dexArchives; |
| if (proguardedJar == null |
| && (multidex || inclusionFilterJar == null) |
| && AndroidCommon.getAndroidConfig(ruleContext).incrementalDexingUseDexSharder()) { |
| dexArchives = |
| toDexedClasspath( |
| ruleContext, |
| collectRuntimeJars(common, attributes), |
| collectDexArchives( |
| ruleContext, common, dexopts, androidSemantics, derivedJarFunction)); |
| } else { |
| if (proguardedJar != null |
| && AndroidCommon.getAndroidConfig(ruleContext).incrementalDexingShardsAfterProguard() |
| > 1) { |
| // TODO(b/69816569): Also use this logic if #shards > #Jars on runtime classpath |
| dexArchives = |
| makeShardArtifacts( |
| ruleContext, |
| AndroidCommon.getAndroidConfig(ruleContext).incrementalDexingShardsAfterProguard(), |
| ".jar.dex.zip"); |
| } else { |
| dexArchives = ImmutableList.of(AndroidBinary.getDxArtifact(ruleContext, "classes.jar")); |
| } |
| |
| if (proguardedJar != null && dexArchives.size() == 1) { |
| // No need to shuffle, just run proguarded Jar through dexbuilder |
| DexArchiveAspect.createDexArchiveAction( |
| ruleContext, |
| "$dexbuilder_after_proguard", |
| proguardedJar, |
| DexArchiveAspect.topLevelDexbuilderDexopts(dexopts), |
| dexArchives.get(0)); |
| } else { |
| createShuffleJarActions( |
| ruleContext, |
| /*makeDexArchives=*/ true, |
| proguardedJar, |
| dexArchives, |
| common, |
| inclusionFilterJar, |
| dexopts, |
| androidSemantics, |
| attributes, |
| derivedJarFunction, |
| (Artifact) null); |
| inclusionFilterJar = null; |
| } |
| } |
| |
| if (dexArchives.size() == 1 || !multidex) { |
| checkState(inclusionFilterJar == null); |
| createDexMergerAction( |
| ruleContext, multidex ? "minimal" : "off", dexArchives, classesDex, mainDexList, dexopts); |
| } else { |
| SpecialArtifact shardsToMerge = |
| createSharderAction(ruleContext, dexArchives, mainDexList, dexopts, inclusionFilterJar); |
| Artifact multidexShards = createTemplatedMergerActions(ruleContext, shardsToMerge, dexopts); |
| // TODO(b/69431301): avoid this action and give the files to apk build action directly |
| createZipMergeAction(ruleContext, multidexShards, classesDex); |
| } |
| } |
| |
| private static ImmutableList<Artifact> makeShardArtifacts( |
| RuleContext ruleContext, int shardCount, String suffix) { |
| ImmutableList.Builder<Artifact> shardsBuilder = ImmutableList.builder(); |
| for (int i = 1; i <= shardCount; i++) { |
| shardsBuilder.add(getDxArtifact(ruleContext, "shard" + i + suffix)); |
| } |
| return shardsBuilder.build(); |
| } |
| |
| /** |
| * Returns whether incremental dexing should actually be used based on the --incremental_dexing |
| * flag, the incremental_dexing attribute and the target's dexopts. |
| */ |
| private static boolean getEffectiveIncrementalDexing( |
| RuleContext ruleContext, List<String> dexopts, boolean isBinaryProguarded) { |
| TriState override = ruleContext.attributes().get("incremental_dexing", BuildType.TRISTATE); |
| AndroidConfiguration config = AndroidCommon.getAndroidConfig(ruleContext); |
| // Ignore --incremental_dexing if the incremental_dexing attribute is set, but require the |
| // attribute to be YES for proguarded binaries and binaries with blacklisted dexopts. |
| if (isBinaryProguarded |
| && override == TriState.YES |
| && config.incrementalDexingShardsAfterProguard() <= 0) { |
| ruleContext.attributeError( |
| "incremental_dexing", "target cannot be incrementally dexed because it uses Proguard"); |
| return false; |
| } |
| |
| if (override == TriState.NO) { |
| return false; |
| } |
| if (override == TriState.YES || config.useIncrementalDexing()) { |
| if (isBinaryProguarded) { |
| return override == TriState.YES || config.incrementalDexingAfterProguardByDefault(); |
| } |
| Iterable<String> blacklistedDexopts = |
| DexArchiveAspect.blacklistedDexopts(ruleContext, dexopts); |
| if (Iterables.isEmpty(blacklistedDexopts)) { |
| // target's dexopts are all compatible with incremental dexing. |
| return true; |
| } else if (override == TriState.YES) { |
| // target's dexopts include flags blacklisted with --non_incremental_per_target_dexopts. If |
| // incremental_dexing attribute is explicitly set for this target then we'll warn and |
| // incrementally dex anyway. Otherwise, just don't incrementally dex. |
| Iterable<String> ignored = |
| Iterables.filter( |
| blacklistedDexopts, |
| Predicates.not(Predicates.in(config.getDexoptsSupportedInIncrementalDexing()))); |
| ruleContext.attributeWarning( |
| "incremental_dexing", |
| String.format( |
| "Using incremental dexing even though dexopts %s indicate this target " |
| + "may be unsuitable for incremental dexing for the moment.%s", |
| blacklistedDexopts, |
| Iterables.isEmpty(ignored) ? "" : " Ignored dexopts: " + ignored)); |
| return true; |
| } else { |
| // If there are incompatible dexopts and the attribute is not set, we silently don't run |
| // incremental dexing. |
| return false; |
| } |
| } else { |
| // attribute is auto and flag is false |
| return false; |
| } |
| } |
| |
| /** |
| * Sets up a {@code $dexsharder} action for the given {@code dexArchives} and returns the output |
| * tree artifact. |
| * |
| * @return Tree artifact containing dex archives to merge into exactly one .dex file each |
| */ |
| private static SpecialArtifact createSharderAction( |
| RuleContext ruleContext, |
| ImmutableList<Artifact> dexArchives, |
| @Nullable Artifact mainDexList, |
| Collection<String> dexopts, |
| @Nullable Artifact inclusionFilterJar) { |
| SpecialArtifact outputTree = |
| ruleContext.getTreeArtifact( |
| ruleContext.getUniqueDirectory("dexsplits"), ruleContext.getBinOrGenfilesDirectory()); |
| |
| SpawnAction.Builder shardAction = |
| new SpawnAction.Builder() |
| .useDefaultShellEnvironment() |
| .setMnemonic("ShardForMultidex") |
| .setProgressMessage( |
| "Assembling dex files for %s", ruleContext.getLabel().getCanonicalForm()) |
| .setExecutable(ruleContext.getExecutablePrerequisite("$dexsharder")) |
| .addInputs(dexArchives) |
| .addOutput(outputTree); |
| |
| CustomCommandLine.Builder shardCommandLine = |
| CustomCommandLine.builder() |
| .addExecPaths(VectorArg.addBefore("--input").each(dexArchives)) |
| .addExecPath("--output", outputTree); |
| |
| if (mainDexList != null) { |
| shardAction.addInput(mainDexList); |
| shardCommandLine.addExecPath("--main-dex-list", mainDexList); |
| } |
| shardCommandLine.addAll(DexArchiveAspect.sharderDexopts(ruleContext, dexopts)); |
| if (inclusionFilterJar != null) { |
| shardCommandLine.addExecPath("--inclusion_filter_jar", inclusionFilterJar); |
| shardAction.addInput(inclusionFilterJar); |
| } |
| ruleContext.registerAction( |
| shardAction |
| .addCommandLine( |
| shardCommandLine.build(), |
| // Classpaths can be long--overflow into @params file if necessary |
| ParamFileInfo.builder(ParameterFile.ParameterFileType.SHELL_QUOTED).build()) |
| .build(ruleContext)); |
| |
| return outputTree; |
| } |
| |
| /** |
| * Sets up a monodex {@code $dexmerger} actions for each dex archive in the given tree artifact |
| * and returns the output tree artifact. |
| * |
| * @return Tree artifact containing zips with final dex files named for inclusion in an APK. |
| */ |
| private static Artifact createTemplatedMergerActions( |
| RuleContext ruleContext, SpecialArtifact inputTree, Collection<String> dexopts) { |
| SpecialArtifact outputTree = |
| ruleContext.getTreeArtifact( |
| ruleContext.getUniqueDirectory("dexfiles"), ruleContext.getBinOrGenfilesDirectory()); |
| SpawnActionTemplate.Builder dexmerger = |
| new SpawnActionTemplate.Builder(inputTree, outputTree) |
| .setExecutable(ruleContext.getExecutablePrerequisite("$dexmerger")) |
| .setMnemonics("DexShardsToMerge", "DexMerger") |
| .setOutputPathMapper( |
| (OutputPathMapper & Serializable) TreeFileArtifact::getParentRelativePath); |
| CustomCommandLine.Builder commandLine = |
| CustomCommandLine.builder() |
| .addPlaceholderTreeArtifactExecPath("--input", inputTree) |
| .addPlaceholderTreeArtifactExecPath("--output", outputTree) |
| .add("--multidex=given_shard") |
| .addAll( |
| DexArchiveAspect.mergerDexopts( |
| ruleContext, |
| Iterables.filter( |
| dexopts, Predicates.not(Predicates.equalTo(DX_MINIMAL_MAIN_DEX_OPTION))))); |
| dexmerger.setCommandLineTemplate(commandLine.build()); |
| ruleContext.registerAction(dexmerger.build(ruleContext.getActionOwner())); |
| |
| return outputTree; |
| } |
| |
| private static void createZipMergeAction( |
| RuleContext ruleContext, Artifact inputTree, Artifact outputZip) { |
| CustomCommandLine args = |
| CustomCommandLine.builder() |
| .add("--normalize") |
| .add("--exclude_build_data") |
| .add("--dont_change_compression") |
| .add("--sources") |
| .addExpandedTreeArtifactExecPaths(inputTree) |
| .addExecPath("--output", outputZip) |
| .add("--no_duplicates") // safety: expect distinct entry names in all inputs |
| .build(); |
| // Must use params file as otherwise expanding the input tree artifact doesn't work |
| Artifact paramFile = |
| ruleContext.getDerivedArtifact( |
| ParameterFile.derivePath( |
| outputZip.getOutputDirRelativePath( |
| ruleContext.getConfiguration().isSiblingRepositoryLayout())), |
| outputZip.getRoot()); |
| ruleContext.registerAction( |
| new ParameterFileWriteAction( |
| ruleContext.getActionOwner(), |
| NestedSetBuilder.create(Order.STABLE_ORDER, inputTree), |
| paramFile, |
| args, |
| ParameterFile.ParameterFileType.SHELL_QUOTED)); |
| ruleContext.registerAction( |
| singleJarSpawnActionBuilder(ruleContext) |
| .setMnemonic("MergeDexZips") |
| .setProgressMessage("Merging dex shards for %s", ruleContext.getLabel()) |
| .addInput(inputTree) |
| .addInput(paramFile) |
| .addOutput(outputZip) |
| .addCommandLine(CustomCommandLine.builder().addPrefixedExecPath("@", paramFile).build()) |
| .build(ruleContext)); |
| } |
| |
| private static void createDexMergerAction( |
| RuleContext ruleContext, |
| String multidexStrategy, |
| ImmutableList<Artifact> dexArchives, |
| Artifact classesDex, |
| @Nullable Artifact mainDexList, |
| Collection<String> dexopts) { |
| SpawnAction.Builder dexmerger = |
| new SpawnAction.Builder() |
| .useDefaultShellEnvironment() |
| .setExecutable(ruleContext.getExecutablePrerequisite("$dexmerger")) |
| .setMnemonic("DexMerger") |
| .setProgressMessage("Assembling dex files into %s", classesDex.getRootRelativePath()) |
| .addInputs(dexArchives) |
| .addOutput(classesDex); |
| CustomCommandLine.Builder commandLine = |
| CustomCommandLine.builder() |
| .addExecPaths(VectorArg.addBefore("--input").each(dexArchives)) |
| .addExecPath("--output", classesDex) |
| .addAll(DexArchiveAspect.mergerDexopts(ruleContext, dexopts)) |
| .addPrefixed("--multidex=", multidexStrategy); |
| if (mainDexList != null) { |
| dexmerger.addInput(mainDexList); |
| commandLine.addExecPath("--main-dex-list", mainDexList); |
| } |
| dexmerger.addCommandLine( |
| commandLine.build(), |
| // Classpaths can be long--overflow into @params file if necessary |
| ParamFileInfo.builder(ParameterFile.ParameterFileType.SHELL_QUOTED).build()); |
| ruleContext.registerAction(dexmerger.build(ruleContext)); |
| } |
| |
| /** |
| * Returns a {@link DexArchiveProvider} of all transitively generated dex archives as well as dex |
| * archives for the Jars produced by the binary target itself. |
| */ |
| public static Function<Artifact, Artifact> collectDesugaredJars( |
| RuleContext ruleContext, |
| AndroidCommon common, |
| AndroidSemantics semantics, |
| JavaTargetAttributes attributes) { |
| if (!AndroidCommon.getAndroidConfig(ruleContext).desugarJava8()) { |
| return Functions.identity(); |
| } |
| AndroidRuntimeJarProvider.Builder result = |
| collectDesugaredJarsFromAttributes( |
| ruleContext, semantics.getAttributesWithJavaRuntimeDeps(ruleContext)); |
| for (Artifact jar : common.getJarsProducedForRuntime().toList()) { |
| // Create dex archives next to all Jars produced by AndroidCommon for this rule. We need to |
| // do this (instead of placing dex archives into the _dx subdirectory like DexArchiveAspect) |
| // because for "legacy" ResourceApks, AndroidCommon produces Jars per resource dependency that |
| // can theoretically have duplicate basenames, so they go into special directories, and we |
| // piggyback on that naming scheme here by placing dex archives into the same directories. |
| PathFragment jarPath = |
| jar.getOutputDirRelativePath(ruleContext.getConfiguration().isSiblingRepositoryLayout()); |
| Artifact desugared = |
| DexArchiveAspect.desugar( |
| ruleContext, |
| jar, |
| attributes.getBootClassPath().bootclasspath(), |
| attributes.getCompileTimeClassPath(), |
| ruleContext.getDerivedArtifact( |
| jarPath.replaceName(jarPath.getBaseName() + "_desugared.jar"), jar.getRoot())); |
| result.addDesugaredJar(jar, desugared); |
| } |
| return result.build().collapseToFunction(); |
| } |
| |
| static AndroidRuntimeJarProvider.Builder collectDesugaredJarsFromAttributes( |
| RuleContext ruleContext, ImmutableList<String> attributes) { |
| AndroidRuntimeJarProvider.Builder result = new AndroidRuntimeJarProvider.Builder(); |
| for (String attr : attributes) { |
| // Use all available AndroidRuntimeJarProvider from attributes that carry runtime dependencies |
| result.addTransitiveProviders( |
| ruleContext.getPrerequisites(attr, AndroidRuntimeJarProvider.class)); |
| } |
| return result; |
| } |
| |
| /** |
| * Returns a {@link Map} of all transitively generated dex archives as well as dex archives for |
| * the Jars produced by the binary target itself. |
| */ |
| private static Map<Artifact, Artifact> collectDexArchives( |
| RuleContext ruleContext, |
| AndroidCommon common, |
| List<String> dexopts, |
| AndroidSemantics semantics, |
| Function<Artifact, Artifact> derivedJarFunction) { |
| DexArchiveProvider.Builder result = new DexArchiveProvider.Builder(); |
| for (String attr : semantics.getAttributesWithJavaRuntimeDeps(ruleContext)) { |
| // Use all available DexArchiveProviders from attributes that carry runtime dependencies |
| result.addTransitiveProviders(ruleContext.getPrerequisites(attr, DexArchiveProvider.class)); |
| } |
| ImmutableSet<String> incrementalDexopts = |
| DexArchiveAspect.incrementalDexopts(ruleContext, dexopts); |
| for (Artifact jar : common.getJarsProducedForRuntime().toList()) { |
| // Create dex archives next to all Jars produced by AndroidCommon for this rule. We need to |
| // do this (instead of placing dex archives into the _dx subdirectory like DexArchiveAspect) |
| // because for "legacy" ResourceApks, AndroidCommon produces Jars per resource dependency that |
| // can theoretically have duplicate basenames, so they go into special directories, and we |
| // piggyback on that naming scheme here by placing dex archives into the same directories. |
| PathFragment jarPath = |
| jar.getOutputDirRelativePath(ruleContext.getConfiguration().isSiblingRepositoryLayout()); |
| Artifact dexArchive = |
| DexArchiveAspect.createDexArchiveAction( |
| ruleContext, |
| "$dexbuilder", |
| derivedJarFunction.apply(jar), |
| incrementalDexopts, |
| ruleContext.getDerivedArtifact( |
| jarPath.replaceName(jarPath.getBaseName() + ".dex.zip"), jar.getRoot())); |
| result.addDexArchive(incrementalDexopts, dexArchive, jar); |
| } |
| return result.build().archivesForDexopts(incrementalDexopts); |
| } |
| |
| private static Artifact createShuffleJarActions( |
| RuleContext ruleContext, |
| boolean makeDexArchives, |
| @Nullable Artifact proguardedJar, |
| ImmutableList<Artifact> shards, |
| AndroidCommon common, |
| @Nullable Artifact inclusionFilterJar, |
| List<String> dexopts, |
| AndroidSemantics semantics, |
| JavaTargetAttributes attributes, |
| Function<Artifact, Artifact> derivedJarFunction, |
| @Nullable Artifact mainDexList) |
| throws InterruptedException, RuleErrorException { |
| checkArgument(!shards.isEmpty()); |
| checkArgument(mainDexList == null || shards.size() > 1); |
| checkArgument(proguardedJar == null || inclusionFilterJar == null); |
| |
| Artifact javaResourceJar = |
| ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.JAVA_RESOURCES_JAR); |
| ImmutableList<Artifact> shuffleOutputs; |
| if (makeDexArchives && proguardedJar != null) { |
| checkArgument(shards.size() > 1); |
| // Split proguardedJar into N shards and run dexbuilder over each one below |
| shuffleOutputs = makeShardArtifacts(ruleContext, shards.size(), ".jar"); |
| } else { |
| shuffleOutputs = shards; |
| } |
| |
| SpawnAction.Builder shardAction = |
| new SpawnAction.Builder() |
| .useDefaultShellEnvironment() |
| .setMnemonic("ShardClassesToDex") |
| .setProgressMessage("Sharding classes for dexing for %s", ruleContext.getLabel()) |
| .setExecutable(ruleContext.getExecutablePrerequisite("$shuffle_jars")) |
| .addOutputs(shuffleOutputs) |
| .addOutput(javaResourceJar); |
| |
| CustomCommandLine.Builder shardCommandLine = |
| CustomCommandLine.builder() |
| .addExecPaths(VectorArg.addBefore("--output_jar").each(shuffleOutputs)) |
| .addExecPath("--output_resources", javaResourceJar); |
| |
| if (mainDexList != null) { |
| shardCommandLine.addExecPath("--main_dex_filter", mainDexList); |
| shardAction.addInput(mainDexList); |
| } |
| |
| // If we need to run Proguard, all the class files will be in the Proguarded jar, which has to |
| // be converted to dex. Otherwise we can use the transitive classpath directly and can leverage |
| // incremental dexing outputs for classpath Jars if applicable. |
| if (proguardedJar != null) { |
| shardCommandLine.addExecPath("--input_jar", proguardedJar); |
| shardAction.addInput(proguardedJar); |
| } else { |
| ImmutableList<Artifact> classpath = collectRuntimeJars(common, attributes); |
| // Check whether we can use dex archives. Besides the --incremental_dexing flag, also |
| // make sure the "dexopts" attribute on this target doesn't mention any problematic flags. |
| if (makeDexArchives) { |
| // Use dex archives instead of their corresponding Jars wherever we can. At this point |
| // there should be very few or no Jar files that still end up in shards. The dexing |
| // step below will have to deal with those in addition to merging .dex files together. |
| Map<Artifact, Artifact> dexArchives = |
| collectDexArchives(ruleContext, common, dexopts, semantics, derivedJarFunction); |
| classpath = toDexedClasspath(ruleContext, classpath, dexArchives); |
| shardCommandLine.add("--split_dexed_classes"); |
| } else { |
| classpath = classpath.stream().map(derivedJarFunction).collect(toImmutableList()); |
| } |
| shardCommandLine.addExecPaths(VectorArg.addBefore("--input_jar").each(classpath)); |
| shardAction.addInputs(classpath); |
| |
| if (inclusionFilterJar != null) { |
| shardCommandLine.addExecPath("--inclusion_filter_jar", inclusionFilterJar); |
| shardAction.addInput(inclusionFilterJar); |
| } |
| } |
| |
| shardAction.addCommandLine( |
| shardCommandLine.build(), ParamFileInfo.builder(ParameterFileType.SHELL_QUOTED).build()); |
| ruleContext.registerAction(shardAction.build(ruleContext)); |
| |
| if (makeDexArchives && proguardedJar != null) { |
| for (int i = 0; i < shards.size(); ++i) { |
| checkState(!shuffleOutputs.get(i).equals(shards.get(i))); |
| DexArchiveAspect.createDexArchiveAction( |
| ruleContext, |
| "$dexbuilder_after_proguard", |
| shuffleOutputs.get(i), |
| DexArchiveAspect.topLevelDexbuilderDexopts(dexopts), |
| shards.get(i)); |
| } |
| } |
| return javaResourceJar; |
| } |
| |
| private static ImmutableList<Artifact> collectRuntimeJars( |
| AndroidCommon common, JavaTargetAttributes attributes) { |
| return ImmutableList.<Artifact>builder() |
| .addAll(common.getRuntimeJars()) |
| .addAll(attributes.getRuntimeClassPathForArchive().toList()) |
| .build(); |
| } |
| |
| private static ImmutableList<Artifact> toDexedClasspath( |
| RuleContext ruleContext, |
| ImmutableList<Artifact> classpath, |
| Map<Artifact, Artifact> dexArchives) |
| throws RuleErrorException { |
| // This is a simple Iterables.transform but with useful error message in case of missed Jars. |
| ImmutableList.Builder<Artifact> dexedClasspath = ImmutableList.builder(); |
| for (Artifact jar : classpath) { |
| Artifact dexArchive = dexArchives.get(jar); |
| if (dexArchive == null) { |
| // Users can create this situation by directly depending on a .jar artifact (checked in |
| // or coming from a genrule or similar, b/11285003). This will also catch new implicit |
| // dependencies that incremental dexing would need to be extended to (b/34949364). |
| // Typically the fix for the latter involves propagating DexArchiveAspect along the |
| // attribute defining the new implicit dependency. |
| ruleContext.throwWithAttributeError( |
| "deps", |
| "Dependencies on .jar artifacts are not " |
| + "allowed in Android binaries, please use a java_import to depend on " |
| + jar.prettyPrint() |
| + ". If this is an implicit dependency then the rule that " |
| + "introduces it will need to be fixed to account for it correctly."); |
| } |
| dexedClasspath.add(dexArchive); |
| } |
| return dexedClasspath.build(); |
| } |
| |
| // Adds the appropriate SpawnAction options depending on if SingleJar is a jar or not. |
| private static SpawnAction.Builder singleJarSpawnActionBuilder(RuleContext ruleContext) { |
| Artifact singleJar = JavaToolchainProvider.from(ruleContext).getSingleJar(); |
| SpawnAction.Builder builder = new SpawnAction.Builder().useDefaultShellEnvironment(); |
| if (singleJar.getFilename().endsWith(".jar")) { |
| builder |
| .setJarExecutable( |
| JavaCommon.getHostJavaExecutable(ruleContext), |
| singleJar, |
| JavaToolchainProvider.from(ruleContext).getJvmOptions()) |
| .addTransitiveInputs(JavaRuntimeInfo.forHost(ruleContext).javaBaseInputsMiddleman()); |
| } else { |
| builder.setExecutable(singleJar); |
| } |
| return builder; |
| } |
| |
| /** |
| * Creates an action that copies a .zip file to a specified path, filtering all non-.dex files out |
| * of the output. |
| */ |
| static void createCleanDexZipAction( |
| RuleContext ruleContext, Artifact inputZip, Artifact outputZip) { |
| ruleContext.registerAction( |
| singleJarSpawnActionBuilder(ruleContext) |
| .setProgressMessage("Trimming %s", inputZip.getExecPath().getBaseName()) |
| .setMnemonic("TrimDexZip") |
| .addInput(inputZip) |
| .addOutput(outputZip) |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .add("--exclude_build_data") |
| .add("--dont_change_compression") |
| .addExecPath("--sources", inputZip) |
| .addExecPath("--output", outputZip) |
| .add("--include_prefixes") |
| .add("classes") |
| .build()) |
| .build(ruleContext)); |
| } |
| |
| /** |
| * Creates an action that generates a list of classes to be passed to the dexer's --main-dex-list |
| * flag (which specifies the classes that need to be directly in classes.dex). Returns the file |
| * containing the list. |
| */ |
| static Artifact createMainDexListAction( |
| RuleContext ruleContext, |
| AndroidSemantics androidSemantics, |
| Artifact jar, |
| @Nullable Artifact mainDexProguardSpec, |
| @Nullable Artifact proguardOutputMap) |
| throws InterruptedException { |
| AndroidConfiguration config = AndroidCommon.getAndroidConfig(ruleContext); |
| AndroidSdkProvider sdk = AndroidSdkProvider.fromRuleContext(ruleContext); |
| // Create the main dex classes list. |
| Artifact mainDexList = AndroidBinary.getDxArtifact(ruleContext, "main_dex_list.txt"); |
| |
| List<Artifact> proguardSpecs = new ArrayList<>(); |
| proguardSpecs.addAll(ruleContext.getPrerequisiteArtifacts("main_dex_proguard_specs").list()); |
| if (proguardSpecs.isEmpty()) { |
| proguardSpecs.add(sdk.getMainDexClasses()); |
| } |
| if (mainDexProguardSpec != null) { |
| proguardSpecs.add(mainDexProguardSpec); |
| } |
| |
| // If --legacy_main_dex_list_generator is not set, use ProGuard and the main dext list creator |
| // specified by the android_sdk rule. If --legacy_main_dex_list_generator is provided, use that |
| // tool instead. |
| // TODO(b/147692286): Remove the old main-dex list generation that relied on ProGuard. |
| if (config.getLegacyMainDexListGenerator() == null) { |
| // Process the input jar through Proguard into an intermediate, streamlined jar. |
| Artifact strippedJar = AndroidBinary.getDxArtifact(ruleContext, "main_dex_intermediate.jar"); |
| SpawnAction.Builder streamlinedBuilder = |
| new SpawnAction.Builder() |
| .useDefaultShellEnvironment() |
| .addOutput(strippedJar) |
| .setExecutable(sdk.getProguard()) |
| .setProgressMessage("Generating streamlined input jar for main dex classes list") |
| .setMnemonic("MainDexClassesIntermediate") |
| .addInput(jar) |
| .addInput(sdk.getShrinkedAndroidJar()); |
| CustomCommandLine.Builder streamlinedCommandLine = |
| CustomCommandLine.builder() |
| .add("-forceprocessing") |
| .addExecPath("-injars", jar) |
| .addExecPath("-libraryjars", sdk.getShrinkedAndroidJar()) |
| .addExecPath("-outjars", strippedJar) |
| .add("-dontwarn") |
| .add("-dontnote") |
| .add("-dontoptimize") |
| .add("-dontobfuscate"); |
| |
| for (Artifact spec : proguardSpecs) { |
| streamlinedBuilder.addInput(spec); |
| streamlinedCommandLine.addExecPath("-include", spec); |
| } |
| |
| androidSemantics.addMainDexListActionArguments( |
| ruleContext, streamlinedBuilder, streamlinedCommandLine, proguardOutputMap); |
| |
| streamlinedBuilder.addCommandLine(streamlinedCommandLine.build()); |
| ruleContext.registerAction(streamlinedBuilder.build(ruleContext)); |
| |
| SpawnAction.Builder builder = |
| new SpawnAction.Builder() |
| .setMnemonic("MainDexClasses") |
| .setProgressMessage("Generating main dex classes list"); |
| |
| ruleContext.registerAction( |
| builder |
| .setExecutable(sdk.getMainDexListCreator()) |
| .addOutput(mainDexList) |
| .addInput(strippedJar) |
| .addInput(jar) |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .addExecPath(mainDexList) |
| .addExecPath(strippedJar) |
| .addExecPath(jar) |
| .addAll( |
| ruleContext |
| .getExpander() |
| .withDataLocations() |
| .tokenized("main_dex_list_opts")) |
| .build()) |
| .build(ruleContext)); |
| } else { |
| FilesToRunProvider legacyMainDexListGenerator = |
| ruleContext.getExecutablePrerequisite(":legacy_main_dex_list_generator"); |
| // Use the newer legacy multidex main-dex list generation. |
| SpawnAction.Builder actionBuilder = |
| new SpawnAction.Builder() |
| .setMnemonic("MainDexClasses") |
| .setProgressMessage("Generating main dex classes list"); |
| |
| CustomCommandLine.Builder commandLineBuilder = |
| CustomCommandLine.builder() |
| .addExecPath("--main-dex-list-output", mainDexList) |
| .addExecPath("--lib", sdk.getAndroidJar()); |
| if (AndroidCommon.getAndroidConfig(ruleContext).desugarJava8Libs()) { |
| NestedSet<Artifact> legacyApis = |
| ruleContext |
| .getPrerequisite("$desugared_java8_legacy_apis") |
| .getProvider(FileProvider.class) |
| .getFilesToBuild(); |
| for (Artifact lib : legacyApis.toList()) { |
| actionBuilder.addInput(lib); |
| commandLineBuilder.addExecPath("--lib", lib); |
| } |
| } |
| for (Artifact spec : proguardSpecs) { |
| actionBuilder.addInput(spec); |
| commandLineBuilder.addExecPath("--main-dex-rules", spec); |
| } |
| |
| commandLineBuilder.addExecPath(jar); |
| |
| ruleContext.registerAction( |
| actionBuilder |
| .setExecutable(legacyMainDexListGenerator) |
| .addOutput(mainDexList) |
| .addInput(jar) |
| .addInput(sdk.getAndroidJar()) |
| .addCommandLine(commandLineBuilder.build()) |
| .build(ruleContext)); |
| } |
| return mainDexList; |
| } |
| |
| /** Transforms manual main_dex_list through proguard obfuscation map. */ |
| static Artifact transformDexListThroughProguardMapAction( |
| RuleContext ruleContext, @Nullable Artifact proguardOutputMap, Artifact mainDexList) |
| throws InterruptedException { |
| if (proguardOutputMap == null |
| || !ruleContext.attributes().get("proguard_generate_mapping", Type.BOOLEAN)) { |
| return mainDexList; |
| } |
| Artifact obfuscatedMainDexList = |
| AndroidBinary.getDxArtifact(ruleContext, "main_dex_list_obfuscated.txt"); |
| SpawnAction.Builder actionBuilder = |
| new SpawnAction.Builder() |
| .setMnemonic("MainDexProguardClasses") |
| .setProgressMessage("Obfuscating main dex classes list") |
| .setExecutable(ruleContext.getExecutablePrerequisite("$dex_list_obfuscator")) |
| .addInput(mainDexList) |
| .addInput(proguardOutputMap) |
| .addOutput(obfuscatedMainDexList) |
| .addCommandLine( |
| CustomCommandLine.builder() |
| .addExecPath("--input", mainDexList) |
| .addExecPath("--output", obfuscatedMainDexList) |
| .addExecPath("--obfuscation_map", proguardOutputMap) |
| .build()); |
| ruleContext.registerAction(actionBuilder.build(ruleContext)); |
| return obfuscatedMainDexList; |
| } |
| |
| public static Artifact createMainDexProguardSpec(Label label, ActionConstructionContext context) { |
| return ProguardHelper.getProguardConfigArtifact(label, context, "main_dex"); |
| } |
| |
| /** Returns the multidex mode to apply to this target. */ |
| public static MultidexMode getMultidexMode(RuleContext ruleContext) { |
| if (ruleContext.getRule().isAttrDefined("multidex", Type.STRING)) { |
| return Preconditions.checkNotNull( |
| MultidexMode.fromValue(ruleContext.attributes().get("multidex", Type.STRING))); |
| } else { |
| return MultidexMode.OFF; |
| } |
| } |
| |
| /** |
| * List of Android SDKs that contain runtimes that do not support the native multidexing |
| * introduced in Android L. If someone tries to build an android_binary that has multidex=native |
| * set with an old SDK, we will exit with an error to alert the developer that their application |
| * might not run on devices that the used SDK still supports. |
| */ |
| private static final ImmutableSet<String> RUNTIMES_THAT_DONT_SUPPORT_NATIVE_MULTIDEXING = |
| ImmutableSet.of( |
| "/android_sdk_linux/platforms/android_10/", "/android_sdk_linux/platforms/android_13/", |
| "/android_sdk_linux/platforms/android_15/", "/android_sdk_linux/platforms/android_16/", |
| "/android_sdk_linux/platforms/android_17/", "/android_sdk_linux/platforms/android_18/", |
| "/android_sdk_linux/platforms/android_19/", "/android_sdk_linux/platforms/android_20/"); |
| |
| /** |
| * Returns true if the runtime contained in the Android SDK used to build this rule supports the |
| * given version of multidex mode specified, false otherwise. |
| */ |
| public static boolean supportsMultidexMode(RuleContext ruleContext, MultidexMode mode) { |
| if (mode == MultidexMode.NATIVE) { |
| // Native mode is not supported by Android devices running Android before v21. |
| String runtime = |
| AndroidSdkProvider.fromRuleContext(ruleContext).getAndroidJar().getExecPathString(); |
| for (String blacklistedRuntime : RUNTIMES_THAT_DONT_SUPPORT_NATIVE_MULTIDEXING) { |
| if (runtime.contains(blacklistedRuntime)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| /** Returns an intermediate artifact used to support dex generation. */ |
| public static Artifact getDxArtifact(RuleContext ruleContext, String baseName) { |
| return ruleContext.getUniqueDirectoryArtifact( |
| "_dx", baseName, ruleContext.getBinOrGenfilesDirectory()); |
| } |
| |
| /** Returns true if this android_binary target is an instrumentation binary */ |
| private static boolean isInstrumentation(RuleContext ruleContext) { |
| return ruleContext.attributes().isAttributeValueExplicitlySpecified("instruments"); |
| } |
| |
| /** |
| * Perform class filtering using the target APK's predexed JAR. Filter duplicate .class and |
| * R.class files based on name. Prevents runtime crashes on ART. See b/19713845 for details. |
| */ |
| private static Artifact getFilteredDeployJar(RuleContext ruleContext, Artifact deployJar) |
| throws InterruptedException { |
| Artifact filterJar = |
| ruleContext |
| .getPrerequisite("instruments") |
| .get(AndroidPreDexJarProvider.PROVIDER) |
| .getPreDexJar(); |
| Artifact filteredDeployJar = |
| ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_TEST_FILTERED_JAR); |
| AndroidCommon.createZipFilterAction( |
| ruleContext, |
| deployJar, |
| filterJar, |
| filteredDeployJar, |
| CheckHashMismatchMode.NONE, |
| ruleContext |
| .getFragment(AndroidConfiguration.class) |
| .removeRClassesFromInstrumentationTestJar()); |
| return filteredDeployJar; |
| } |
| } |