| // 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) { |
| ruleErrorConsumer.attributeError( |
| resourcesAttr, |
| String.format(INCORRECT_RESOURCE_LAYOUT_MESSAGE, file.getRootRelativePath())); |
| throw new RuleErrorException(); |
| } |
| |
| if (lastResourceDir != null && !resourceDir.equals(lastResourceDir)) { |
| ruleErrorConsumer.attributeError( |
| 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())); |
| throw new RuleErrorException(); |
| } |
| |
| 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) { |
| ruleErrorConsumer.attributeError( |
| resourcesAttr, |
| String.format( |
| "'%s' (generated by '%s') is not under the directory '%s' (derived from %s).", |
| file.getRootRelativePath(), |
| file.getOwnerLabel(), |
| packageRelativePath, |
| file.getRootRelativePath())); |
| throw new RuleErrorException(); |
| } |
| 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(); |
| } |
| } |