blob: 80e5208357842888c23fbbfc457c898593cf0f69 [file] [log] [blame]
// 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());
}
}