// 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.Strings.isNullOrEmpty; import static com.google.devtools.build.lib.syntax.Type.STRING; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.FileProvider; import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.analysis.actions.FileWriteAction; import com.google.devtools.build.lib.analysis.actions.SpawnAction; import com.google.devtools.build.lib.analysis.config.CompilationMode; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.rules.android.AndroidConfiguration.AndroidManifestMerger; import com.google.devtools.build.lib.rules.android.ResourceContainer.Builder.JavaPackageSource; import com.google.devtools.build.lib.rules.android.ResourceContainer.ResourceType; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.annotation.Nullable; /** Represents a AndroidManifest, that may have been merged from dependencies. */ public final class ApplicationManifest { public static ApplicationManifest fromResourcesRule(RuleContext ruleContext) { final AndroidResourcesProvider resources = AndroidCommon.getAndroidResources(ruleContext); if (resources == null) { ruleContext.attributeError("manifest", "a resources or manifest attribute is mandatory."); return null; } return new ApplicationManifest( ruleContext, Iterables.getOnlyElement(resources.getDirectAndroidResources()).getManifest()); } public ApplicationManifest createSplitManifest( RuleContext ruleContext, String splitName, boolean hasCode) { // aapt insists that manifests be called AndroidManifest.xml, even though they have to be // explicitly designated as manifests on the command line Artifact result = AndroidBinary.getDxArtifact( ruleContext, "split_" + splitName + "/AndroidManifest.xml"); SpawnAction.Builder builder = new SpawnAction.Builder() .setExecutable(ruleContext.getExecutablePrerequisite("$build_split_manifest", Mode.HOST)) .setProgressMessage("Creating manifest for split " + splitName) .setMnemonic("AndroidBuildSplitManifest") .addArgument("--main_manifest") .addInputArgument(manifest) .addArgument("--split_manifest") .addOutputArgument(result) .addArgument("--split") .addArgument(splitName) .addArgument(hasCode ? "--hascode" : "--nohascode"); String overridePackage = manifestValues.get("applicationId"); if (overridePackage != null) { builder .addArgument("--override_package") .addArgument(overridePackage); } ruleContext.registerAction(builder.build(ruleContext)); return new ApplicationManifest(ruleContext, result); } public ApplicationManifest addMobileInstallStubApplication(RuleContext ruleContext) throws InterruptedException { Artifact stubManifest = ruleContext.getImplicitOutputArtifact( AndroidRuleClasses.MOBILE_INSTALL_STUB_APPLICATION_MANIFEST); SpawnAction.Builder builder = new SpawnAction.Builder() .setExecutable(ruleContext.getExecutablePrerequisite("$stubify_manifest", Mode.HOST)) .setProgressMessage("Injecting mobile install stub application") .setMnemonic("InjectMobileInstallStubApplication") .addArgument("--mode=mobile_install") .addArgument("--input_manifest") .addInputArgument(manifest) .addArgument("--output_manifest") .addOutputArgument(stubManifest) .addArgument("--output_datafile") .addOutputArgument(ruleContext.getImplicitOutputArtifact( AndroidRuleClasses.MOBILE_INSTALL_STUB_APPLICATION_DATA)); String overridePackage = manifestValues.get("applicationId"); if (overridePackage != null) { builder.addArgument("--override_package"); builder.addArgument(overridePackage); } ruleContext.registerAction(builder.build(ruleContext)); return new ApplicationManifest(ruleContext, stubManifest); } public ApplicationManifest addInstantRunStubApplication(RuleContext ruleContext) throws InterruptedException { Artifact stubManifest = ruleContext.getImplicitOutputArtifact( AndroidRuleClasses.INSTANT_RUN_STUB_APPLICATION_MANIFEST); SpawnAction.Builder builder = new SpawnAction.Builder() .setExecutable(ruleContext.getExecutablePrerequisite("$stubify_manifest", Mode.HOST)) .setProgressMessage("Injecting instant run stub application") .setMnemonic("InjectInstantRunStubApplication") .addArgument("--mode=instant_run") .addArgument("--input_manifest") .addInputArgument(manifest) .addArgument("--output_manifest") .addOutputArgument(stubManifest); ruleContext.registerAction(builder.build(ruleContext)); return new ApplicationManifest(ruleContext, stubManifest); } public static ApplicationManifest fromRule(RuleContext ruleContext) { return new ApplicationManifest( ruleContext, ruleContext.getPrerequisiteArtifact("manifest", Mode.TARGET)); } public static ApplicationManifest fromExplicitManifest( RuleContext ruleContext, Artifact manifest) { return new ApplicationManifest(ruleContext, manifest); } /** * Generates an empty manifest for a rule that does not directly specify resources. * * <p><strong>Note:</strong> This generated manifest can then be used as the primary manifest * when merging with dependencies. * * @return the generated ApplicationManifest */ public static ApplicationManifest generatedManifest(RuleContext ruleContext) { Artifact generatedManifest = ruleContext.getUniqueDirectoryArtifact( ruleContext.getRule().getName() + "_generated", PathFragment.create("AndroidManifest.xml"), ruleContext.getBinOrGenfilesDirectory()); String manifestPackage = AndroidCommon.getJavaPackage(ruleContext); String contents = Joiner.on("\n").join( "<?xml version=\"1.0\" encoding=\"utf-8\"?>", "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"", " package=\"" + manifestPackage + "\">", " <application>", " </application>", "</manifest>"); ruleContext .getAnalysisEnvironment() .registerAction( FileWriteAction.create( ruleContext, generatedManifest, contents, /*makeExecutable=*/ false)); return new ApplicationManifest(ruleContext, generatedManifest); } private static ImmutableMap<String, String> getManifestValues(RuleContext context) { Map<String, String> manifestValues = new TreeMap<>(); // applicationId is set from manifest_values or android_resources.rename_manifest_package // with descending priority. AndroidResourcesProvider resourcesProvider = AndroidCommon.getAndroidResources(context); if (resourcesProvider != null) { ResourceContainer resourceContainer = Iterables.getOnlyElement( resourcesProvider.getDirectAndroidResources()); if (resourceContainer.getRenameManifestPackage() != null) { manifestValues.put("applicationId", resourceContainer.getRenameManifestPackage()); } } if (context.attributes().isAttributeValueExplicitlySpecified("manifest_values")) { manifestValues.putAll(context.attributes().get("manifest_values", Type.STRING_DICT)); } for (String variable : manifestValues.keySet()) { manifestValues.put( variable, context.expandMakeVariables("manifest_values", manifestValues.get(variable))); } return ImmutableMap.copyOf(manifestValues); } private final Artifact manifest; private final ImmutableMap<String, String> manifestValues; private ApplicationManifest(RuleContext ruleContext, Artifact manifest) { this.manifest = manifest; this.manifestValues = getManifestValues(ruleContext); } public ApplicationManifest mergeWith(RuleContext ruleContext, ResourceDependencies resourceDeps) { Map<Artifact, Label> mergeeManifests = getMergeeManifests(resourceDeps.getResources()); boolean legacy = true; if (ruleContext.isLegalFragment(AndroidConfiguration.class) && ruleContext.getRule().isAttrDefined("manifest_merger", STRING)) { AndroidManifestMerger merger = AndroidManifestMerger.fromString( ruleContext.attributes().get("manifest_merger", STRING)); if (merger == null) { merger = ruleContext.getFragment(AndroidConfiguration.class).getManifestMerger(); } legacy = merger == AndroidManifestMerger.LEGACY; } if (legacy) { if (!mergeeManifests.isEmpty()) { Artifact outputManifest = ruleContext.getUniqueDirectoryArtifact( ruleContext.getRule().getName() + "_merged", "AndroidManifest.xml", ruleContext.getBinOrGenfilesDirectory()); AndroidManifestMergeHelper.createMergeManifestAction(ruleContext, getManifest(), mergeeManifests.keySet(), ImmutableList.of("all"), outputManifest); return new ApplicationManifest(ruleContext, outputManifest); } } else { if (!mergeeManifests.isEmpty() || !manifestValues.isEmpty()) { Artifact outputManifest = ruleContext.getUniqueDirectoryArtifact( ruleContext.getRule().getName() + "_merged", "AndroidManifest.xml", ruleContext.getBinOrGenfilesDirectory()); Artifact mergeLog = ruleContext.getUniqueDirectoryArtifact( ruleContext.getRule().getName() + "_merged", "manifest_merger_log.txt", ruleContext.getBinOrGenfilesDirectory()); new ManifestMergerActionBuilder(ruleContext) .setManifest(getManifest()) .setMergeeManifests(mergeeManifests) .setLibrary(false) .setManifestValues(manifestValues) .setCustomPackage(AndroidCommon.getJavaPackage(ruleContext)) .setManifestOutput(outputManifest) .setLogOut(mergeLog) .build(ruleContext); return new ApplicationManifest(ruleContext, outputManifest); } } return this; } private static Map<Artifact, Label> getMergeeManifests( Iterable<ResourceContainer> resourceContainers) { ImmutableSortedMap.Builder<Artifact, Label> builder = ImmutableSortedMap.orderedBy(Artifact.EXEC_PATH_COMPARATOR); for (ResourceContainer r : resourceContainers) { if (r.isManifestExported()) { builder.put(r.getManifest(), r.getLabel()); } } return builder.build(); } public ApplicationManifest renamePackage(RuleContext ruleContext, String customPackage) { if (isNullOrEmpty(customPackage)) { return this; } Artifact outputManifest = ruleContext.getUniqueDirectoryArtifact( ruleContext.getRule().getName() + "_renamed", "AndroidManifest.xml", ruleContext.getBinOrGenfilesDirectory()); new ManifestMergerActionBuilder(ruleContext) .setManifest(getManifest()) .setLibrary(true) .setCustomPackage(customPackage) .setManifestOutput(outputManifest) .build(ruleContext); return new ApplicationManifest(ruleContext, outputManifest); } /** Packages up the manifest with assets from the rule and dependent resources. * @throws InterruptedException */ public ResourceApk packWithAssets( Artifact resourceApk, RuleContext ruleContext, ResourceDependencies resourceDeps, Artifact rTxt, boolean incremental, Artifact proguardCfg) throws InterruptedException { LocalResourceContainer data = new LocalResourceContainer.Builder(ruleContext) .withAssets( AndroidCommon.getAssetDir(ruleContext), ruleContext.getPrerequisitesIf( // TODO(bazel-team): Remove the ResourceType construct. ResourceType.ASSETS.getAttribute(), Mode.TARGET, FileProvider.class)).build(); return createApk( ruleContext, false, /* isLibrary */ resourceDeps, ResourceFilter.empty(ruleContext), ImmutableList.<String>of(), /* uncompressedExtensions */ true, /* crunchPng */ incremental, ResourceContainer.builderFromRule(ruleContext) .setAssetsAndResourcesFrom(data) .setManifest(getManifest()) .setRTxt(rTxt) .setApk(resourceApk) .build(), data, proguardCfg, null, /* Artifact mainDexProguardCfg */ null, /* Artifact manifestOut */ null, /* Artifact mergedResources */ null, /* Artifact dataBindingInfoZip */ null, /* featureOf */ null /* featureAfter */); } /** Packages up the manifest with resource and assets from the LocalResourceContainer. */ public ResourceApk packWithDataAndResources( RuleContext ruleContext, LocalResourceContainer data, ResourceDependencies resourceDeps, Artifact rTxt, Artifact symbols, Artifact manifestOut, Artifact mergedResources, boolean alwaysExportManifest) throws InterruptedException { if (ruleContext.hasErrors()) { return null; } ResourceContainer.Builder resourceContainer = ResourceContainer.builderFromRule(ruleContext) .setAssetsAndResourcesFrom(data) .setManifest(getManifest()) .setRTxt(rTxt) .setSymbols(symbols) .setJavaPackageFrom(JavaPackageSource.MANIFEST); if (alwaysExportManifest) { resourceContainer.setManifestExported(true); } return createApk( ruleContext, true, /* isLibrary */ resourceDeps, ResourceFilter.empty(ruleContext), ImmutableList.<String>of(), /* List<String> uncompressedExtensions */ false, /* crunchPng */ false, /* incremental */ resourceContainer.build(), data, null, /* Artifact proguardCfg */ null, /* Artifact mainDexProguardCfg */ manifestOut, mergedResources, null, /* Artifact dataBindingInfoZip */ null, /* Artifact featureOf */ null /* Artifact featureAfter */); } /** Packages up the manifest with resource and assets from the rule and dependent resources. */ // TODO(bazel-team): this method calls for some refactoring, 15+ params including some nullables. public ResourceApk packWithDataAndResources( @Nullable Artifact resourceApk, RuleContext ruleContext, boolean isLibrary, ResourceDependencies resourceDeps, Artifact rTxt, Artifact symbols, ResourceFilter resourceFilter, List<String> uncompressedExtensions, boolean crunchPng, boolean incremental, Artifact proguardCfg, @Nullable Artifact mainDexProguardCfg, Artifact manifestOut, Artifact mergedResources, Artifact dataBindingInfoZip, @Nullable Artifact featureOf, @Nullable Artifact featureAfter) throws InterruptedException { LocalResourceContainer data = new LocalResourceContainer.Builder(ruleContext) .withAssets( AndroidCommon.getAssetDir(ruleContext), ruleContext.getPrerequisitesIf( // TODO(bazel-team): Remove the ResourceType construct. ResourceType.ASSETS.getAttribute(), Mode.TARGET, FileProvider.class)) .withResources( ruleContext.getPrerequisites( "resource_files", Mode.TARGET, FileProvider.class)).build(); if (ruleContext.hasErrors()) { return null; } return createApk( ruleContext, isLibrary, resourceDeps, resourceFilter, uncompressedExtensions, crunchPng, incremental, ResourceContainer.builderFromRule(ruleContext) .setAssetsAndResourcesFrom(data) .setManifest(getManifest()) .setRTxt(rTxt) .setSymbols(symbols) .setApk(resourceApk) .build(), data, proguardCfg, mainDexProguardCfg, manifestOut, mergedResources, dataBindingInfoZip, featureOf, featureAfter); } private ResourceApk createApk( RuleContext ruleContext, boolean isLibrary, ResourceDependencies resourceDeps, ResourceFilter resourceFilter, List<String> uncompressedExtensions, boolean crunchPng, boolean incremental, ResourceContainer maybeInlinedResourceContainer, LocalResourceContainer data, Artifact proguardCfg, @Nullable Artifact mainDexProguardCfg, Artifact manifestOut, Artifact mergedResources, Artifact dataBindingInfoZip, @Nullable Artifact featureOf, @Nullable Artifact featureAfter) throws InterruptedException { ResourceContainer resourceContainer = checkForInlinedResources( maybeInlinedResourceContainer, resourceDeps.getResources(), // TODO(bazel-team): Figure out if we really need to check // the ENTIRE transitive closure, or just the direct dependencies. Given that each rule with // resources would check for inline resources, we can rely on the previous rule to have // checked its dependencies. ruleContext); if (ruleContext.hasErrors()) { return null; } // Filter the resources during analysis to prevent processing of and dependencies on unwanted // resources during execution. resourceContainer = resourceContainer.filter(ruleContext, resourceFilter); resourceDeps = resourceDeps.filter(ruleContext, resourceFilter); ResourceContainer processed; if (isLibrary && AndroidCommon.getAndroidConfig(ruleContext).useParallelResourceProcessing()) { // android_library should only build the APK one way (!incremental). Preconditions.checkArgument(!incremental); Artifact rJavaClassJar = ruleContext.getImplicitOutputArtifact( AndroidRuleClasses.ANDROID_RESOURCES_CLASS_JAR); if (resourceContainer.getSymbols() != null) { new AndroidResourceParsingActionBuilder(ruleContext) .withPrimary(resourceContainer) .setParse(data) .setOutput(resourceContainer.getSymbols()) .build(ruleContext); } AndroidResourceMergingActionBuilder resourcesMergerBuilder = new AndroidResourceMergingActionBuilder(ruleContext) .setJavaPackage(resourceContainer.getJavaPackage()) .withPrimary(resourceContainer) .withDependencies(resourceDeps) .setMergedResourcesOut(mergedResources) .setManifestOut(manifestOut) .setClassJarOut(rJavaClassJar) .setDataBindingInfoZip(dataBindingInfoZip); ResourceContainer merged = resourcesMergerBuilder.build(ruleContext); AndroidResourceValidatorActionBuilder validatorBuilder = new AndroidResourceValidatorActionBuilder(ruleContext) .setJavaPackage(merged.getJavaPackage()) .setDebug( ruleContext.getConfiguration().getCompilationMode() != CompilationMode.OPT) .setMergedResources(mergedResources) .withPrimary(merged) .setSourceJarOut(merged.getJavaSourceJar()) .setRTxtOut(merged.getRTxt()); processed = validatorBuilder.build(ruleContext); } else { AndroidResourcesProcessorBuilder builder = new AndroidResourcesProcessorBuilder(ruleContext) .setLibrary(isLibrary) .setApkOut(resourceContainer.getApk()) .setResourceFilter(resourceFilter) .setUncompressedExtensions(uncompressedExtensions) .setCrunchPng(crunchPng) .setJavaPackage(resourceContainer.getJavaPackage()) .setDebug(ruleContext.getConfiguration().getCompilationMode() != CompilationMode.OPT) .setManifestOut(manifestOut) .setMergedResourcesOut(mergedResources) .withPrimary(resourceContainer) .withDependencies(resourceDeps) .setProguardOut(proguardCfg) .setMainDexProguardOut(mainDexProguardCfg) .setDataBindingInfoZip(dataBindingInfoZip) .setApplicationId(manifestValues.get("applicationId")) .setVersionCode(manifestValues.get("versionCode")) .setVersionName(manifestValues.get("versionName")); builder.setFeatureOf(featureOf); builder.setFeatureAfter(featureAfter); if (!incremental) { builder .setRTxtOut(resourceContainer.getRTxt()) .setSymbols(resourceContainer.getSymbols()) .setSourceJarOut(resourceContainer.getJavaSourceJar()); } processed = builder.build(ruleContext); } return new ResourceApk( resourceContainer.getApk(), processed.getJavaSourceJar(), processed.getJavaClassJar(), resourceDeps, processed, processed.getManifest(), proguardCfg, mainDexProguardCfg, false); } private static ResourceContainer checkForInlinedResources(ResourceContainer resourceContainer, Iterable<ResourceContainer> resourceContainers, RuleContext ruleContext) { // Dealing with Android library projects if (Iterables.size(resourceContainers) > 1) { if (resourceContainer.getConstantsInlined() && !resourceContainer.getArtifacts(ResourceType.RESOURCES).isEmpty()) { ruleContext.ruleError("This android binary depends on an android " + "library project, so the resources '" + AndroidCommon.getAndroidResources(ruleContext).getLabel() + "' should have the attribute inline_constants set to 0"); return null; } } return resourceContainer; } /** Uses the resource apk from the resources attribute, as opposed to recompiling. */ public ResourceApk useCurrentResources( RuleContext ruleContext, Artifact proguardCfg, @Nullable Artifact mainDexProguardCfg) { ResourceContainer resourceContainer = Iterables.getOnlyElement( AndroidCommon.getAndroidResources(ruleContext).getDirectAndroidResources()); new AndroidAaptActionHelper( ruleContext, resourceContainer.getManifest(), Lists.newArrayList(resourceContainer)) .createGenerateProguardAction(proguardCfg, mainDexProguardCfg); return new ResourceApk( resourceContainer.getApk(), null /* javaSrcJar */, null /* javaClassJar */, ResourceDependencies.empty(), resourceContainer, manifest, proguardCfg, mainDexProguardCfg, false); } /** * Packages up the manifest with resources, and generates the R.java. * @throws InterruptedException * * @deprecated in favor of {@link ApplicationManifest#packWithDataAndResources}. */ @Deprecated public ResourceApk packWithResources( Artifact resourceApk, RuleContext ruleContext, ResourceDependencies resourceDeps, boolean createSource, Artifact proguardCfg, @Nullable Artifact mainDexProguardCfg) throws InterruptedException { TransitiveInfoCollection resourcesPrerequisite = ruleContext.getPrerequisite("resources", Mode.TARGET); ResourceContainer resourceContainer = Iterables.getOnlyElement( resourcesPrerequisite.getProvider(AndroidResourcesProvider.class) .getDirectAndroidResources()); // It's ugly, but flattening now is more performant given the rest of the checks. List<ResourceContainer> resourceContainers = ImmutableList.<ResourceContainer>builder() //.add(resourceContainer) .addAll(resourceDeps.getResources()).build(); // Dealing with Android library projects if (Iterables.size(resourceDeps.getResources()) > 1) { if (resourceContainer.getConstantsInlined() && !resourceContainer.getArtifacts(ResourceType.RESOURCES).isEmpty()) { ruleContext.ruleError("This android_binary depends on an android_library, so the" + " resources '" + AndroidCommon.getAndroidResources(ruleContext).getLabel() + "' should have the attribute inline_constants set to 0"); return null; } } // This binary depends on a library project, so we need to regenerate the // resources. The resulting sources and apk will combine all the resources // contained in the transitive closure of the binary. AndroidAaptActionHelper aaptActionHelper = new AndroidAaptActionHelper(ruleContext, getManifest(), Lists.newArrayList(resourceContainers)); ResourceFilter resourceFilter = ResourceFilter.fromRuleContext(ruleContext); List<String> uncompressedExtensions = ruleContext.getTokenizedStringListAttr("nocompress_extensions"); ImmutableList.Builder<String> additionalAaptOpts = ImmutableList.<String>builder(); for (String extension : uncompressedExtensions) { additionalAaptOpts.add("-0").add(extension); } if (resourceFilter.hasConfigurationFilters() && !resourceFilter.isPrefiltering()) { additionalAaptOpts.add("-c").add(resourceFilter.getConfigurationFilterString()); } Artifact javaSourcesJar = null; if (createSource) { javaSourcesJar = ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_JAVA_SOURCE_JAR); aaptActionHelper.createGenerateResourceSymbolsAction( javaSourcesJar, null, resourceContainer.getJavaPackage(), true); } aaptActionHelper.createGenerateApkAction( resourceApk, resourceContainer.getRenameManifestPackage(), additionalAaptOpts.build(), resourceFilter.getDensities()); ResourceContainer updatedResources = resourceContainer.toBuilder() .setLabel(ruleContext.getLabel()) .setApk(resourceApk) .setManifest(getManifest()) .setJavaSourceJar(javaSourcesJar) .setJavaClassJar(null) .setSymbols(null) .build(); aaptActionHelper.createGenerateProguardAction(proguardCfg, mainDexProguardCfg); return new ResourceApk(resourceApk, updatedResources.getJavaSourceJar(), updatedResources.getJavaClassJar(), resourceDeps, updatedResources, manifest, proguardCfg, mainDexProguardCfg, true); } public Artifact getManifest() { return manifest; } }