| // 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.ImmutableMap; |
| 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.collect.nestedset.NestedSet; |
| 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.packages.Type; |
| 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.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. |
| * |
| * <p>If no manifest is specified in the rule's attributes, an empty manifest will be generated. |
| * |
| * <p>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. |
| * |
| * <p>If no manifest is specified in the rule's attributes, an empty manifest will be generated. |
| * |
| * <p>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. |
| * |
| * <p>Attributes included in the RuleContext will not be used; use {@link |
| * #fromAttributes(RuleContext, AndroidDataContext)} instead. |
| * |
| * <p>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(SymlinkAction.toArtifact( |
| 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 |
| * |
| * <p>The manifest will also be stamped with any manifest values specified |
| * |
| * <p>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<String, String> manifestValues, |
| @Nullable String manifestMerger) { |
| Map<Artifact, Label> mergeeManifests = |
| getMergeeManifests( |
| resourceDeps.getResourceContainers(), |
| dataContext.getAndroidConfig().getManifestMergerOrder()); |
| |
| 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) { |
| if (androidConfig.getManifestMerger() == AndroidManifestMerger.FORCE_ANDROID) { |
| return false; |
| } |
| |
| 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<Artifact, Label> getMergeeManifests( |
| NestedSet<ValidatedAndroidResources> transitiveData, |
| AndroidConfiguration.ManifestMergerOrder manifestMergerOrder) { |
| ImmutableMap.Builder<Artifact, Label> builder = new ImmutableMap.Builder<>(); |
| for (ValidatedAndroidResources d : transitiveData.toList()) { |
| if (d.isManifestExported()) { |
| builder.put(d.getManifest(), d.getLabel()); |
| } |
| } |
| switch (manifestMergerOrder) { |
| case ALPHABETICAL: |
| return ImmutableSortedMap.copyOf(builder.build(), Artifact.EXEC_PATH_COMPARATOR); |
| case ALPHABETICAL_BY_CONFIGURATION: |
| return ImmutableSortedMap.copyOf(builder.build(), Artifact.ROOT_RELATIVE_PATH_COMPARATOR); |
| case DEPENDENCY: |
| return builder.build(); |
| } |
| throw new AssertionError(manifestMergerOrder); |
| } |
| |
| 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. |
| * |
| * <p>For example, target "//some/path/java/com/foo/bar:baz" will have the default Java package of |
| * "com.foo.bar". |
| * |
| * <p>A rule error will be registered if this path does not contain a "java" or "javatests" |
| * segment indicating where the package begins. |
| * |
| * <p>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. |
| * |
| * <p>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()); |
| } |
| } |