// 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.AndroidConfiguration.AndroidAaptVersion; 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. * *

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 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 " + "/{%s}/", 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 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 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 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. * *

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 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.getArtifactOwner().getLabel(), lastResourceDir, lastFile.getRootRelativePath())); throw new RuleErrorException(); } PathFragment packageFragment = file.getArtifactOwner().getLabel().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.getArtifactOwner().getLabel(), packageRelativePath, file.getRootRelativePath())); throw new RuleErrorException(); } return resourceDir; } /** * Finds and validates the resource directory PathFragment from the artifact Path. * *

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 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 resources; private final ImmutableList resourceRoots; AndroidResources(AndroidResources other) { this(other.resources, other.resourceRoots); } @VisibleForTesting public AndroidResources( ImmutableList resources, ImmutableList resourceRoots) { this.resources = resources; this.resourceRoots = resourceRoots; } private static ImmutableList getResources(Iterable targets) { ImmutableList.Builder builder = ImmutableList.builder(); for (FileProvider target : targets) { builder.addAll(target.getFilesToBuild()); } return builder.build(); } public ImmutableList 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 getResourceRoots( RuleErrorConsumer ruleErrorConsumer, Iterable files, String resourcesAttr) throws RuleErrorException { Artifact lastFile = null; PathFragment lastResourceDir = null; Set 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 getResourceRoots() { return resourceRoots; } /** * Filters this object, assuming it contains the resources of the current target. * *

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 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 maybeFilter( RuleErrorConsumer errorConsumer, ResourceFilter resourceFilter, boolean isDependency) throws RuleErrorException { Optional> 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, AndroidAaptVersion aaptVersion, DataBindingContext dataBindingContext) throws InterruptedException { return ParsedAndroidResources.parseFrom( dataContext, this, manifest, aaptVersion, 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, AndroidAaptVersion.chooseTargetAaptVersion(ruleContext)); } ValidatedAndroidResources process( AndroidDataContext dataContext, StampedAndroidManifest manifest, ResourceDependencies resourceDeps, DataBindingContext dataBindingContext, AndroidAaptVersion aaptVersion) throws InterruptedException { return parse(dataContext, manifest, aaptVersion, dataBindingContext) .merge(dataContext, resourceDeps, aaptVersion) .validate(dataContext, aaptVersion); } @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(); } }