// 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.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; 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.analysis.FileProvider; import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; import com.google.devtools.build.lib.analysis.RuleContext; import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable; import com.google.devtools.build.lib.packages.AttributeMap; import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException; import com.google.devtools.build.lib.rules.android.ResourceContainer.ResourceType; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.Collection; import java.util.LinkedHashSet; /** * The collected resources and assets artifacts and roots. * * <p>This is used to encapsulate the logic and the data associated with the resources and assets * derived from an appropriate android rule in a reusable instance. */ @Immutable public final class LocalResourceContainer { public static final String[] RESOURCES_ATTRIBUTES = new String[] { "manifest", "resource_files", "assets", "assets_dir", "inline_constants", "exports_manifest" }; /** * Determines if the attributes contain resource and asset attributes. */ 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 an AndroidData. * * @throws RuleErrorException if the RuleContext is invalid. Accumulated errors will be available * via {@code ruleContext} */ public static void validateRuleContext(RuleContext ruleContext) throws RuleErrorException { validateAssetsAndAssetsDir(ruleContext); validateNoResourcesAttribute(ruleContext); validateNoAndroidResourcesInSources(ruleContext); validateManifest(ruleContext); } private static void validateAssetsAndAssetsDir(RuleContext ruleContext) throws RuleErrorException { if (ruleContext.attributes().isAttributeValueExplicitlySpecified("assets") ^ ruleContext.attributes().isAttributeValueExplicitlySpecified("assets_dir")) { ruleContext.throwWithRuleError( "'assets' and 'assets_dir' should be either both empty or both non-empty"); } } /** * Validates that there are no resources defined if there are resource attributes defined. */ private static void validateNoResourcesAttribute(RuleContext ruleContext) throws RuleErrorException { if (ruleContext.attributes().isAttributeValueExplicitlySpecified("resources")) { ruleContext.throwWithAttributeError("resources", String.format("resources cannot be set when any of %s are defined.", Joiner.on(", ").join(RESOURCES_ATTRIBUTES))); } } /** * Validates that there are no android_resources srcjars in the srcs, as android_resource rules * should not be used with the Android data logic. */ private static void validateNoAndroidResourcesInSources(RuleContext ruleContext) throws RuleErrorException { Iterable<AndroidResourcesProvider> resources = ruleContext.getPrerequisites("srcs", Mode.TARGET, AndroidResourcesProvider.class); for (AndroidResourcesProvider provider : resources) { ruleContext.throwWithAttributeError("srcs", String.format("srcs should not contain android_resource label %s", provider.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 class Builder { /** * Set of allowable android directories prefixes. */ // TODO(bazel-team): Pull from AOSP constant? public static final ImmutableSet<String> RESOURCE_DIRECTORY_TYPES = ImmutableSet.of( "animator", "anim", "color", "drawable", "interpolator", "layout", "menu", "mipmap", "raw", "transition", "values", "xml"); 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)); private RuleContext ruleContext; private Collection<Artifact> assets = new LinkedHashSet<>(); private Collection<Artifact> resources = new LinkedHashSet<>(); private Collection<PathFragment> resourceRoots = new LinkedHashSet<>(); private Collection<PathFragment> assetRoots = new LinkedHashSet<>(); public Builder(RuleContext ruleContext) { this.ruleContext = ruleContext; } /** * Retrieves the asset {@link Artifact} and asset root {@link PathFragment}. * @param assetsDir The common directory for the assets, used to get the directory roots and * verify the artifacts are located beneath the assetsDir * @param targets {@link FileProvider}s for a given set of assets. * @return The Builder. */ public LocalResourceContainer.Builder withAssets( PathFragment assetsDir, Iterable<? extends TransitiveInfoCollection> targets) { for (TransitiveInfoCollection target : targets) { for (Artifact file : target.getProvider(FileProvider.class).getFilesToBuild()) { PathFragment packageFragment = file.getArtifactOwner().getLabel() .getPackageIdentifier().getSourceRoot(); PathFragment packageRelativePath = file.getRootRelativePath().relativeTo(packageFragment); if (packageRelativePath.startsWith(assetsDir)) { PathFragment relativePath = packageRelativePath.relativeTo(assetsDir); assetRoots.add(trimTail(file.getExecPath(), relativePath)); } else { ruleContext.attributeError(ResourceType.ASSETS.getAttribute(), String.format( "'%s' (generated by '%s') is not beneath '%s'", file.getRootRelativePath(), target.getLabel(), assetsDir)); return this; } assets.add(file); } } return this; } /** * Retrieves the resource {@link Artifact} and resource root {@link PathFragment}. * @param targets {@link FileProvider}s for a given set of resource. * @return The Builder. */ public LocalResourceContainer.Builder withResources(Iterable<FileProvider> targets) { PathFragment lastResourceDir = null; Artifact lastFile = null; for (FileProvider target : targets) { for (Artifact file : target.getFilesToBuild()) { PathFragment packageFragment = file.getArtifactOwner().getLabel() .getPackageIdentifier().getSourceRoot(); PathFragment packageRelativePath = file.getRootRelativePath().relativeTo(packageFragment); PathFragment resourceDir = findResourceDir(file); if (resourceDir == null) { ruleContext.attributeError(ResourceType.RESOURCES.getAttribute(), String.format( INCORRECT_RESOURCE_LAYOUT_MESSAGE, file.getRootRelativePath())); return this; } if (lastResourceDir == null || resourceDir.equals(lastResourceDir)) { resourceRoots.add( trimTail(file.getExecPath(), makeRelativeTo(resourceDir, packageRelativePath))); } else { ruleContext.attributeError(ResourceType.RESOURCES.getAttribute(), 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())); return this; } resources.add(file); lastFile = file; lastResourceDir = resourceDir; } } return this; } /** * 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.getPath().asFragment(); 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, segmentCount); } // 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); } /** * Returns the root-part of a given path by trimming off the end specified by * a given tail. Assumes that the tail is known to match, and simply relies on * the segment lengths. */ private static PathFragment trimTail(PathFragment path, PathFragment tail) { return path.subFragment(0, path.segmentCount() - tail.segmentCount()); } private static PathFragment makeRelativeTo(PathFragment ancestor, PathFragment path) { String cutAtSegment = ancestor.getSegment(ancestor.segmentCount() - 1); int totalPathSegments = path.segmentCount() - 1; for (int i = totalPathSegments; i >= 0; i--) { if (path.getSegment(i).equals(cutAtSegment)) { return path.subFragment(i, totalPathSegments); } } throw new IllegalArgumentException("PathFragment " + path + " is not beneath " + ancestor); } public LocalResourceContainer build() { return new LocalResourceContainer( ImmutableList.copyOf(resources), ImmutableList.copyOf(resourceRoots), ImmutableList.copyOf(assets), ImmutableList.copyOf(assetRoots)); } } private final ImmutableList<Artifact> resources; private final ImmutableList<Artifact> assets; private final ImmutableList<PathFragment> assetRoots; private final ImmutableList<PathFragment> resourceRoots; @VisibleForTesting public LocalResourceContainer( ImmutableList<Artifact> resources, ImmutableList<PathFragment> resourceRoots, ImmutableList<Artifact> assets, ImmutableList<PathFragment> assetRoots) { this.resources = resources; this.resourceRoots = resourceRoots; this.assets = assets; this.assetRoots = assetRoots; } public ImmutableList<Artifact> getResources() { return resources; } public ImmutableList<Artifact> getAssets() { return assets; } public ImmutableList<PathFragment> getAssetRoots() { return assetRoots; } public ImmutableList<PathFragment> getResourceRoots() { return resourceRoots; } }