// Copyright 2018 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 com.google.common.collect.ImmutableSortedMap; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext; import com.google.devtools.build.lib.analysis.actions.SymlinkAction; import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException; import com.google.devtools.build.lib.packages.RuleErrorConsumer; import com.google.devtools.build.lib.rules.android.AndroidConfiguration.AndroidManifestMerger; import com.google.devtools.build.lib.rules.java.JavaUtil; import com.google.devtools.build.lib.syntax.Type; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; /** Wraps an Android Manifest and provides utilities for working with it */ @Immutable public class AndroidManifest { private static final String CUSTOM_PACKAGE_ATTR = "custom_package"; private final Artifact manifest; /** The Android package. Will be null if and only if this is an aar_import target. */ @Nullable private final String pkg; private final boolean exported; public static StampedAndroidManifest forAarImport(Artifact manifest) { return new StampedAndroidManifest(manifest, /* pkg = */ null, /* exported = */ true); } /** * Gets the manifest for this rule. * *

If no manifest is specified in the rule's attributes, an empty manifest will be generated. * *

Unlike {@link #fromAttributes(RuleContext, AndroidDataContext,AndroidSemantics)}, the * AndroidSemantics-specific manifest processing methods will not be applied in this method. The * manifest returned by this method will be the same regardless of the AndroidSemantics being * used. */ public static AndroidManifest fromAttributes( RuleContext ruleContext, AndroidDataContext dataContext) throws InterruptedException, RuleErrorException { return fromAttributes(ruleContext, dataContext, null); } /** * Gets the manifest for this rule. * *

If no manifest is specified in the rule's attributes, an empty manifest will be generated. * *

If a non-null {@link AndroidSemantics} is passed, AndroidSemantics-specific manifest * processing will be preformed on this manifest. Otherwise, basic manifest renaming will be * performed if needed. */ public static AndroidManifest fromAttributes( RuleContext ruleContext, AndroidDataContext dataContext, @Nullable AndroidSemantics androidSemantics) throws RuleErrorException, InterruptedException { Artifact rawManifest = null; if (AndroidResources.definesAndroidResources(ruleContext.attributes())) { AndroidResources.validateRuleContext(ruleContext); rawManifest = ruleContext.getPrerequisiteArtifact("manifest", Mode.TARGET); } return from( dataContext, ruleContext, rawManifest, androidSemantics, getAndroidPackage(ruleContext), AndroidCommon.getExportsManifest(ruleContext)); } /** * Creates an AndroidManifest object, with correct preprocessing, from explicit variables. * *

Attributes included in the RuleContext will not be used; use {@link * #fromAttributes(RuleContext, AndroidDataContext)} instead. * *

In addition, the AndroidSemantics-specific manifest processing methods will not be applied * in this method. The manifest returned by this method will be the same regardless of the * AndroidSemantics being used. use {@link #fromAttributes(RuleContext, AndroidDataContext, * AndroidSemantics)} instead if you want AndroidSemantics-specific behavior. */ public static AndroidManifest from( AndroidDataContext dataContext, RuleErrorConsumer errorConsumer, @Nullable Artifact rawManifest, @Nullable String pkg, boolean exportsManifest) throws InterruptedException { return from(dataContext, errorConsumer, rawManifest, null, pkg, exportsManifest); } /** * Inner method to create an AndroidManifest. * * @param rawManifest If non-null, the returned object will wrap this manifest. Otherwise, the * returned object will wrap a generated dummy manifest. * @param androidSemantics If non-null, will invoke * AndroidSemantics#renameManifest(AndroidDataContext, AndroidManifest)} to do * platform-specific processing on the manifest. * @param pkg If non-null, this Android package will be used for the manifest, and the manifest * will be stamped with it when the {@link #stamp(AndroidDataContext)} method is called. * Otherwise, the default package, based on the current target's Bazel package, will be used. */ public static AndroidManifest from( AndroidDataContext dataContext, RuleErrorConsumer errorConsumer, @Nullable Artifact rawManifest, @Nullable AndroidSemantics androidSemantics, @Nullable String pkg, boolean exportsManifest) throws InterruptedException { if (pkg == null) { pkg = getDefaultPackage( dataContext.getLabel(), dataContext.getActionConstructionContext(), errorConsumer); } if (rawManifest == null) { // Generate a dummy manifest return StampedAndroidManifest.createEmpty( dataContext.getActionConstructionContext(), pkg, exportsManifest); } AndroidManifest raw = new AndroidManifest(rawManifest, pkg, exportsManifest); if (androidSemantics != null) { return androidSemantics.renameManifest(dataContext, raw); } return raw.renameManifestIfNeeded(dataContext); } AndroidManifest renameManifestIfNeeded(AndroidDataContext dataContext) throws InterruptedException { if (manifest.getFilename().equals("AndroidManifest.xml")) { return this; } else { /* * If the manifest file is not named AndroidManifest.xml, we create a symlink named * AndroidManifest.xml to it. aapt requires the manifest to be named as such. */ Artifact manifestSymlink = dataContext.createOutputArtifact(AndroidRuleClasses.ANDROID_SYMLINKED_MANIFEST); dataContext.registerAction( new SymlinkAction( dataContext.getActionConstructionContext().getActionOwner(), manifest, manifestSymlink, "Renaming Android manifest for " + dataContext.getLabel())); return updateManifest(manifestSymlink); } } public AndroidManifest updateManifest(Artifact manifest) { return new AndroidManifest(manifest, pkg, exported); } /** * Creates a manifest wrapper without doing any processing. From within a rule, use {@link * #fromAttributes(RuleContext, AndroidDataContext, AndroidSemantics)} instead. */ public AndroidManifest(Artifact manifest, @Nullable String pkg, boolean exported) { this.manifest = manifest; this.pkg = pkg; this.exported = exported; } /** If needed, stamps the manifest with the correct Java package */ public StampedAndroidManifest stamp(AndroidDataContext dataContext) { Artifact outputManifest = getManifest(); if (!isNullOrEmpty(pkg)) { outputManifest = dataContext.getUniqueDirectoryArtifact("_renamed", "AndroidManifest.xml"); new ManifestMergerActionBuilder() .setManifest(manifest) .setLibrary(true) .setCustomPackage(pkg) .setManifestOutput(outputManifest) .build(dataContext); } return new StampedAndroidManifest(outputManifest, pkg, exported); } /** * Merges the manifest with any dependent manifests * *

The manifest will also be stamped with any manifest values specified * *

If there is no merging to be done and no manifest values are specified, the manifest will * remain unstamped. * * @param manifestMerger if not null, a string dictating which manifest merger to use */ public StampedAndroidManifest mergeWithDeps( AndroidDataContext dataContext, AndroidSemantics androidSemantics, RuleErrorConsumer errorConsumer, ResourceDependencies resourceDeps, Map manifestValues, @Nullable String manifestMerger) { Map mergeeManifests = getMergeeManifests(resourceDeps.getResourceContainers()); Artifact newManifest; if (useLegacyMerging(errorConsumer, dataContext.getAndroidConfig(), manifestMerger)) { newManifest = androidSemantics .maybeDoLegacyManifestMerging(mergeeManifests, dataContext, manifest) .orElse(manifest); } else if (!mergeeManifests.isEmpty() || !manifestValues.isEmpty()) { newManifest = dataContext.getUniqueDirectoryArtifact("_merged", "AndroidManifest.xml"); new ManifestMergerActionBuilder() .setManifest(manifest) .setMergeeManifests(mergeeManifests) .setLibrary(false) .setManifestValues(manifestValues) .setCustomPackage(pkg) .setManifestOutput(newManifest) .setLogOut(dataContext.getUniqueDirectoryArtifact("_merged", "manifest_merger_log.txt")) .build(dataContext); } else { newManifest = manifest; } return new StampedAndroidManifest(newManifest, pkg, exported); } /** * Checks if the legacy manifest merger should be used, based on an optional string specifying the * merger to use. */ private static boolean useLegacyMerging( RuleErrorConsumer errorConsumer, AndroidConfiguration androidConfig, @Nullable String mergerString) { AndroidManifestMerger merger = AndroidManifestMerger.fromString(mergerString); if (merger == null) { merger = androidConfig.getManifestMerger(); } if (merger == AndroidManifestMerger.LEGACY) { errorConsumer.ruleWarning( "manifest_merger 'legacy' is deprecated. Please update to 'android'.\n" + "See https://developer.android.com/studio/build/manifest-merge.html for more " + "information about the manifest merger."); } return merger == AndroidManifestMerger.LEGACY; } private static Map getMergeeManifests( Iterable transitiveData) { ImmutableSortedMap.Builder builder = ImmutableSortedMap.orderedBy(Artifact.EXEC_PATH_COMPARATOR); for (ValidatedAndroidResources d : transitiveData) { if (d.isManifestExported()) { builder.put(d.getManifest(), d.getLabel()); } } return builder.build(); } public Artifact getManifest() { return manifest; } @Nullable String getPackage() { return pkg; } boolean isExported() { return exported; } /** Gets the Android package for this target, from either rule configuration or Java package */ private static String getAndroidPackage(RuleContext ruleContext) { if (ruleContext.attributes().isAttributeValueExplicitlySpecified(CUSTOM_PACKAGE_ATTR)) { return ruleContext.attributes().get(CUSTOM_PACKAGE_ATTR, Type.STRING); } return getDefaultPackage(ruleContext.getLabel(), ruleContext, ruleContext); } /** * Gets the default Java package for this target, based on the path to it. * *

For example, target "//some/path/java/com/foo/bar:baz" will have the default Java package of * "com.foo.bar". * *

A rule error will be registered if this path does not contain a "java" or "javatests" * segment indicating where the package begins. * *

This method should not be called if the target specifies a custom package; in that case, * that package should be used instead. */ public static String getDefaultPackage( Label label, ActionConstructionContext context, RuleErrorConsumer errorConsumer) { PathFragment dummyJar = // For backwards compatibility, also include the target's name in case it contains multiple // directories - for example, target "//foo/bar:java/baz/quux" is a legal one and results in // Java path of "baz/quux" context.getPackageDirectory().getRelative(label.getName() + "Dummy.jar"); return getJavaPackageFromPath(context, errorConsumer, dummyJar); } /** * Gets the Java package of a JAR file based on it's path. * *

Bazel requires that all Java code (including Android code) be in a path prefixed with "java" * or "javatests" followed by the Java package; this method validates and takes advantage of that * requirement. * * @param jarPathFragment The path to a JAR file contained in the current BUILD file's directory. * @return the Java package, as a String */ static String getJavaPackageFromPath( ActionConstructionContext context, RuleErrorConsumer errorConsumer, PathFragment jarPathFragment) { // TODO(bazel-team): JavaUtil.getJavaPackageName does not check to see if the path is valid. // So we need to check for the JavaRoot. if (JavaUtil.getJavaRoot(jarPathFragment) == null) { errorConsumer.ruleError( "The location of your BUILD file determines the Java package used for " + "Android resource processing. A directory named \"java\" or \"javatests\" will " + "be used as your Java source root and the path of your BUILD file relative to " + "the Java source root will be used as the package for Android resource " + "processing. The Java source root could not be determined for \"" + context.getPackageDirectory() + "\". Move your BUILD file under a java or javatests directory, or set the " + "'custom_package' attribute."); } return JavaUtil.getJavaPackageName(jarPathFragment); } @Override public boolean equals(Object object) { if (!(object instanceof AndroidManifest)) { return false; } AndroidManifest other = (AndroidManifest) object; return manifest.equals(other.manifest) && Objects.equals(pkg, other.pkg) && exported == other.exported; } @Override public int hashCode() { // Hash the current class with the other fields to distinguish between this AndroidManifest and // classes that extend it. return Objects.hash(manifest, pkg, exported, getClass()); } }