blob: 84a23829599586b62381f0e165ee0ea052e74deb [file] [log] [blame]
// 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 com.android.resources.ResourceFolderType;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.packages.AttributeMap;
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.databinding.DataBindingContext;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
/**
* The collected resources artifacts and roots.
*
* <p>This is used to encapsulate the logic and the data associated with the resources derived from
* an appropriate android rule in a reusable instance.
*/
public class AndroidResources {
private static final String DEFAULT_RESOURCES_ATTR = "resource_files";
public static final String[] RESOURCES_ATTRIBUTES =
new String[] {
"manifest",
DEFAULT_RESOURCES_ATTR,
"local_resource_files",
"assets",
"assets_dir",
"inline_constants",
"exports_manifest"
};
/** Set of allowable android directories prefixes. */
public static final ImmutableSet<String> RESOURCE_DIRECTORY_TYPES =
Arrays.stream(ResourceFolderType.values())
.map(ResourceFolderType::getName)
.collect(ImmutableSet.toImmutableSet());
public static final String INCORRECT_RESOURCE_LAYOUT_MESSAGE =
String.format(
"'%%s' is not in the expected resource directory structure of "
+ "<resource directory>/{%s}/<file>",
Joiner.on(',').join(RESOURCE_DIRECTORY_TYPES));
/**
* Determines if the attributes contain resource and asset attributes.
*
* @deprecated We are moving towards processing Android assets, resources, and manifests
* separately. Use a separate method that just checks the attributes you need.
*/
@Deprecated
public static boolean definesAndroidResources(AttributeMap attributes) {
for (String attribute : RESOURCES_ATTRIBUTES) {
if (attributes.isAttributeValueExplicitlySpecified(attribute)) {
return true;
}
}
return false;
}
/**
* Checks validity of a RuleContext to produce Android resources, assets, and manifests.
*
* @throws RuleErrorException if the RuleContext is invalid. Accumulated errors will be available
* via {@code ruleContext}
* @deprecated We are moving towards processing Android assets, resources, and manifests
* separately. Use a separate method that just checks the values you need.
*/
@Deprecated
public static void validateRuleContext(RuleContext ruleContext) throws RuleErrorException {
AndroidAssets.validateAssetsAndAssetsDir(ruleContext);
validateNoAndroidResourcesInSources(ruleContext);
validateManifest(ruleContext);
}
/**
* Validates that there are no targets with resources in the srcs, as they should not be used with
* the Android data logic.
*/
private static void validateNoAndroidResourcesInSources(RuleContext ruleContext)
throws RuleErrorException {
Iterable<AndroidResourcesInfo> resources =
ruleContext.getPrerequisites("srcs", Mode.TARGET, AndroidResourcesInfo.PROVIDER);
for (AndroidResourcesInfo info : resources) {
ruleContext.throwWithAttributeError(
"srcs",
String.format("srcs should not contain label with resources %s", info.getLabel()));
}
}
private static void validateManifest(RuleContext ruleContext) throws RuleErrorException {
if (ruleContext.getPrerequisiteArtifact("manifest", Mode.TARGET) == null) {
ruleContext.throwWithAttributeError(
"manifest", "manifest is required when resource_files or assets are defined.");
}
}
public static AndroidResources from(RuleContext ruleContext, String resourcesAttr)
throws RuleErrorException {
if (!hasLocalResourcesAttributes(ruleContext.attributes())) {
return empty();
}
return from(
ruleContext,
ruleContext.getPrerequisites(resourcesAttr, Mode.TARGET, FileProvider.class),
resourcesAttr);
}
public static AndroidResources from(
RuleErrorConsumer errorConsumer,
Iterable<FileProvider> resourcesTargets,
String resourcesAttr)
throws RuleErrorException {
return forResources(errorConsumer, getResources(resourcesTargets), resourcesAttr);
}
/** Returns an {@link AndroidResources} for a list of resource artifacts. */
@VisibleForTesting
public static AndroidResources forResources(
RuleErrorConsumer ruleErrorConsumer, ImmutableList<Artifact> resources, String resourcesAttr)
throws RuleErrorException {
return new AndroidResources(
resources, getResourceRoots(ruleErrorConsumer, resources, resourcesAttr));
}
/**
* TODO(b/76218640): Whether local resources are built into a target should depend on that
* target's resource attribute ("resource_files" in general, but local_resource_files for
* android_test), not any other attributes.
*/
private static boolean hasLocalResourcesAttributes(AttributeMap attrs) {
return attrs.has("assets") || attrs.has("resource_files");
}
static AndroidResources empty() {
return new AndroidResources(ImmutableList.of(), ImmutableList.of());
}
/**
* Creates a {@link AndroidResources} containing all the resources in directory artifacts, for use
* with AarImport rules.
*
* <p>In general, {@link #from(RuleContext, String)} should be used instead, but it can't be for
* AarImport since we don't know about its individual assets at analysis time. No transitive
* resources will be included in the container produced by this method.
*
* @param resourcesDir the tree artifact containing a {@code res/} directory
*/
static AndroidResources forAarImport(SpecialArtifact resourcesDir) {
Preconditions.checkArgument(resourcesDir.isTreeArtifact());
return new AndroidResources(
ImmutableList.of(resourcesDir),
ImmutableList.of(resourcesDir.getExecPath().getChild("res")));
}
/**
* Inner method for adding resource roots to a collection. May fail and report to the {@link
* RuleErrorConsumer} if the input is invalid.
*
* @param file the file to add the resource directory for
* @param lastFile the last file this method was called on. May be null if this is the first call
* for this set of resources.
* @param lastResourceDir the resource directory of the last file, as returned by the most recent
* call to this method, or null if this is the first call.
* @param resourceRoots the collection to add resources to
* @param resourcesAttr the attribute used to refer to resources. While we're moving towards
* "resource_files" everywhere, there are still uses of other attributes for different kinds
* of rules.
* @param ruleErrorConsumer for reporting errors
* @return the resource root of {@code file}.
* @throws RuleErrorException if the current resource has no resource directory or if it is
* incompatible with {@code lastResourceDir}. An error will also be reported to the {@link
* RuleErrorConsumer} in this case.
*/
private static PathFragment addResourceDir(
Artifact file,
@Nullable Artifact lastFile,
@Nullable PathFragment lastResourceDir,
Set<PathFragment> resourceRoots,
String resourcesAttr,
RuleErrorConsumer ruleErrorConsumer)
throws RuleErrorException {
PathFragment resourceDir = findResourceDir(file);
if (resourceDir == null) {
throw ruleErrorConsumer.throwWithAttributeError(
resourcesAttr,
String.format(INCORRECT_RESOURCE_LAYOUT_MESSAGE, file.getRootRelativePath()));
}
if (lastResourceDir != null && !resourceDir.equals(lastResourceDir)) {
throw ruleErrorConsumer.throwWithAttributeError(
resourcesAttr,
String.format(
"'%s' (generated by '%s') is not in the same directory '%s' (derived from %s)."
+ " All resources must share a common directory.",
file.getRootRelativePath(),
file.getOwnerLabel(),
lastResourceDir,
lastFile.getRootRelativePath()));
}
PathFragment packageFragment = file.getOwnerLabel().getPackageIdentifier().getSourceRoot();
PathFragment packageRelativePath = file.getRootRelativePath().relativeTo(packageFragment);
try {
PathFragment path = file.getExecPath();
resourceRoots.add(
path.subFragment(
0,
path.segmentCount() - segmentCountAfterAncestor(resourceDir, packageRelativePath)));
} catch (IllegalArgumentException e) {
throw ruleErrorConsumer.throwWithAttributeError(
resourcesAttr,
String.format(
"'%s' (generated by '%s') is not under the directory '%s' (derived from %s).",
file.getRootRelativePath(),
file.getOwnerLabel(),
packageRelativePath,
file.getRootRelativePath()));
}
return resourceDir;
}
/**
* Finds and validates the resource directory PathFragment from the artifact Path.
*
* <p>If the artifact is not a Fileset, the resource directory is presumed to be the second
* directory from the end. Filesets are expect to have the last directory as the resource
* directory.
*/
public static PathFragment findResourceDir(Artifact artifact) {
PathFragment fragment = artifact.getExecPath();
int segmentCount = fragment.segmentCount();
if (segmentCount < 3) {
return null;
}
// TODO(bazel-team): Expand Fileset to verify, or remove Fileset as an option for resources.
if (artifact.isFileset() || artifact.isTreeArtifact()) {
return fragment.subFragment(segmentCount - 1);
}
// Check the resource folder type layout.
// get the prefix of the parent folder of the fragment.
String parentDirectory = fragment.getSegment(segmentCount - 2);
int dashIndex = parentDirectory.indexOf('-');
String androidFolder =
dashIndex == -1 ? parentDirectory : parentDirectory.substring(0, dashIndex);
if (!RESOURCE_DIRECTORY_TYPES.contains(androidFolder)) {
return null;
}
return fragment.subFragment(segmentCount - 3, segmentCount - 2);
}
private static int segmentCountAfterAncestor(PathFragment ancestor, PathFragment path) {
String cutAtSegment = ancestor.getSegment(ancestor.segmentCount() - 1);
int index = -1;
List<String> segments = path.getSegments();
for (int i = segments.size() - 1; i >= 0; i--) {
if (segments.get(i).equals(cutAtSegment)) {
index = i;
break;
}
}
if (index == -1) {
throw new IllegalArgumentException("PathFragment " + path + " is not beneath " + ancestor);
}
return segments.size() - index - 1;
}
private final ImmutableList<Artifact> resources;
private final ImmutableList<PathFragment> resourceRoots;
AndroidResources(AndroidResources other) {
this(other.resources, other.resourceRoots);
}
@VisibleForTesting
public AndroidResources(
ImmutableList<Artifact> resources, ImmutableList<PathFragment> resourceRoots) {
this.resources = resources;
this.resourceRoots = resourceRoots;
}
private static ImmutableList<Artifact> getResources(Iterable<FileProvider> targets) {
ImmutableList.Builder<Artifact> builder = ImmutableList.builder();
for (FileProvider target : targets) {
builder.addAll(target.getFilesToBuild().toList());
}
return builder.build();
}
public ImmutableList<Artifact> getResources() {
return resources;
}
/**
* Gets the roots of some resources.
*
* @return a list of roots, or an empty list of the passed resources cannot all be contained in a
* single {@link AndroidResources}. If that's the case, it will be reported to the {@link
* RuleErrorConsumer}.
*/
@VisibleForTesting
static ImmutableList<PathFragment> getResourceRoots(
RuleErrorConsumer ruleErrorConsumer, Iterable<Artifact> files, String resourcesAttr)
throws RuleErrorException {
Artifact lastFile = null;
PathFragment lastResourceDir = null;
Set<PathFragment> resourceRoots = new LinkedHashSet<>();
for (Artifact file : files) {
PathFragment resourceDir =
addResourceDir(
file, lastFile, lastResourceDir, resourceRoots, resourcesAttr, ruleErrorConsumer);
lastFile = file;
lastResourceDir = resourceDir;
}
return ImmutableList.copyOf(resourceRoots);
}
public ImmutableList<PathFragment> getResourceRoots() {
return resourceRoots;
}
/**
* Filters this object, assuming it contains the resources of the current target.
*
* <p>If this object contains the resources from a dependency of this target, use {@link
* #maybeFilter(RuleErrorConsumer, ResourceFilter, boolean)} instead.
*
* @return a filtered {@link AndroidResources} object. If no filtering was done, this object will
* be returned.
*/
public AndroidResources filterLocalResources(
RuleErrorConsumer errorConsumer, ResourceFilter resourceFilter) throws RuleErrorException {
Optional<? extends AndroidResources> filtered =
maybeFilter(errorConsumer, resourceFilter, /* isDependency = */ false);
return filtered.isPresent() ? filtered.get() : this;
}
/**
* Filters this object.
*
* @return an optional wrapping a new {@link AndroidResources} with resources filtered by the
* passed {@link ResourceFilter}, or {@link Optional#empty()} if no resources should be
* filtered.
*/
public Optional<? extends AndroidResources> maybeFilter(
RuleErrorConsumer errorConsumer, ResourceFilter resourceFilter, boolean isDependency)
throws RuleErrorException {
Optional<ImmutableList<Artifact>> filtered =
resourceFilter.maybeFilter(resources, /* isDependency= */ isDependency);
if (!filtered.isPresent()) {
// Nothing was filtered out
return Optional.empty();
}
return Optional.of(
new AndroidResources(
filtered.get(),
getResourceRoots(errorConsumer, filtered.get(), DEFAULT_RESOURCES_ATTR)));
}
/** Parses these resources. */
public ParsedAndroidResources parse(
AndroidDataContext dataContext,
StampedAndroidManifest manifest,
DataBindingContext dataBindingContext)
throws InterruptedException {
return ParsedAndroidResources.parseFrom(dataContext, this, manifest, dataBindingContext);
}
/**
* Performs the complete resource processing pipeline - parsing, merging, and validation - on
* these resources.
*/
public ValidatedAndroidResources process(
RuleContext ruleContext,
AndroidDataContext dataContext,
StampedAndroidManifest manifest,
DataBindingContext dataBindingContext,
boolean neverlink)
throws RuleErrorException, InterruptedException {
return process(
dataContext,
manifest,
ResourceDependencies.fromRuleDeps(ruleContext, neverlink),
dataBindingContext);
}
ValidatedAndroidResources process(
AndroidDataContext dataContext,
StampedAndroidManifest manifest,
ResourceDependencies resourceDeps,
DataBindingContext dataBindingContext)
throws InterruptedException {
return parse(dataContext, manifest, dataBindingContext)
.merge(dataContext, resourceDeps)
.validate(dataContext);
}
@Override
public boolean equals(Object object) {
if (!(object instanceof AndroidResources)) {
return false;
}
AndroidResources other = (AndroidResources) object;
return resources.equals(other.resources) && resourceRoots.equals(other.resourceRoots);
}
@Override
public int hashCode() {
return Objects.hash(resources, resourceRoots);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("resources", resources)
.add("resourceRoots", resourceRoots)
.toString();
}
}