/* * Copyright 2014-present Facebook, Inc. * * 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.facebook.buck.android; import com.facebook.buck.android.aapt.MiniAapt; import com.facebook.buck.io.MorePaths; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.model.Either; import com.facebook.buck.model.Flavor; import com.facebook.buck.model.Flavored; import com.facebook.buck.model.InternalFlavor; import com.facebook.buck.model.Pair; import com.facebook.buck.parser.NoSuchBuildTargetException; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.CellPathResolver; import com.facebook.buck.rules.CommonDescriptionArg; import com.facebook.buck.rules.Description; import com.facebook.buck.rules.HasDeclaredDeps; import com.facebook.buck.rules.PathSourcePath; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.SymlinkTree; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.rules.TargetNode; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.RichStream; import com.facebook.buck.util.immutables.BuckStyleImmutable; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.google.common.io.Files; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.AbstractMap; import java.util.Optional; import org.immutables.value.Value; public class AndroidResourceDescription implements Description<AndroidResourceDescriptionArg>, Flavored { private static final ImmutableSet<String> NON_ASSET_FILENAMES = ImmutableSet.of( ".gitkeep", ".svn", ".git", ".ds_store", ".scc", "cvs", "thumbs.db", "picasa.ini"); private final boolean isGrayscaleImageProcessingEnabled; @VisibleForTesting static final Flavor RESOURCES_SYMLINK_TREE_FLAVOR = InternalFlavor.of("resources-symlink-tree"); @VisibleForTesting static final Flavor ASSETS_SYMLINK_TREE_FLAVOR = InternalFlavor.of("assets-symlink-tree"); public static final Flavor AAPT2_COMPILE_FLAVOR = InternalFlavor.of("aapt2_compile"); public AndroidResourceDescription(boolean enableGrayscaleImageProcessing) { isGrayscaleImageProcessingEnabled = enableGrayscaleImageProcessing; } @Override public Class<AndroidResourceDescriptionArg> getConstructorArgType() { return AndroidResourceDescriptionArg.class; } @SuppressWarnings("PMD.PrematureDeclaration") @Override public BuildRule createBuildRule( TargetGraph targetGraph, BuildRuleParams params, final BuildRuleResolver resolver, CellPathResolver cellRoots, AndroidResourceDescriptionArg args) { SourcePathRuleFinder ruleFinder = new SourcePathRuleFinder(resolver); ImmutableSortedSet<Flavor> flavors = params.getBuildTarget().getFlavors(); if (flavors.contains(RESOURCES_SYMLINK_TREE_FLAVOR)) { return createSymlinkTree(ruleFinder, params, args.getRes(), "res"); } else if (flavors.contains(ASSETS_SYMLINK_TREE_FLAVOR)) { return createSymlinkTree(ruleFinder, params, args.getAssets(), "assets"); } // Only allow android resource and library rules as dependencies. Optional<BuildRule> invalidDep = params .getDeclaredDeps() .get() .stream() .filter(rule -> !(rule instanceof AndroidResource || rule instanceof AndroidLibrary)) .findFirst(); if (invalidDep.isPresent()) { throw new HumanReadableException( params.getBuildTarget() + " (android_resource): dependency " + invalidDep.get().getBuildTarget() + " (" + invalidDep.get().getType() + ") is not of type android_resource or android_library."); } // We don't handle the resources parameter well in `AndroidResource` rules, as instead of // hashing the contents of the entire resources directory, we try to filter out anything that // doesn't look like a resource. This means when our resources are supplied from another rule, // we have to resort to some hackery to make sure things work correctly. Pair<Optional<SymlinkTree>, Optional<SourcePath>> resInputs = collectInputSourcePaths( resolver, params.getBuildTarget(), RESOURCES_SYMLINK_TREE_FLAVOR, args.getRes()); Pair<Optional<SymlinkTree>, Optional<SourcePath>> assetsInputs = collectInputSourcePaths( resolver, params.getBuildTarget(), ASSETS_SYMLINK_TREE_FLAVOR, args.getAssets()); if (flavors.contains(AAPT2_COMPILE_FLAVOR)) { Optional<SourcePath> resDir = resInputs.getSecond(); Preconditions.checkArgument( resDir.isPresent(), "Tried to require rule %s, but no resource dir is preset.", params.getBuildTarget()); params = params.copyReplacingDeclaredAndExtraDeps( Suppliers.ofInstance( ImmutableSortedSet.copyOf(ruleFinder.filterBuildRuleInputs(resDir.get()))), Suppliers.ofInstance(ImmutableSortedSet.of())); return new Aapt2Compile(params, resDir.get()); } params = params.copyAppendingExtraDeps( Iterables.concat( resInputs .getSecond() .map(ruleFinder::filterBuildRuleInputs) .orElse(ImmutableSet.of()), assetsInputs .getSecond() .map(ruleFinder::filterBuildRuleInputs) .orElse(ImmutableSet.of()))); return new AndroidResource( // We only propagate other AndroidResource rule dependencies, as these are // the only deps which should control whether we need to re-run the aapt_package // step. params.copyReplacingDeclaredAndExtraDeps( Suppliers.ofInstance( AndroidResourceHelper.androidResOnly(params.getDeclaredDeps().get())), params.getExtraDeps()), ruleFinder, resolver.getAllRules(args.getDeps()), resInputs.getSecond().orElse(null), resInputs.getFirst().map(SymlinkTree::getLinks).orElse(ImmutableSortedMap.of()), args.getPackage().orElse(null), assetsInputs.getSecond().orElse(null), assetsInputs.getFirst().map(SymlinkTree::getLinks).orElse(ImmutableSortedMap.of()), args.getManifest().orElse(null), args.getHasWhitelistedStrings(), args.getResourceUnion(), isGrayscaleImageProcessingEnabled); } private SymlinkTree createSymlinkTree( SourcePathRuleFinder ruleFinder, BuildRuleParams params, Optional<Either<SourcePath, ImmutableSortedMap<String, SourcePath>>> symlinkAttribute, String outputDirName) { ImmutableMap<Path, SourcePath> links = ImmutableMap.of(); if (symlinkAttribute.isPresent()) { if (symlinkAttribute.get().isLeft()) { // If our resources are coming from a `PathSourcePath`, we collect only the inputs we care // about and pass those in separately, so that that `AndroidResource` rule knows to only // hash these into it's rule key. // TODO(jakubzika): This is deprecated and should be disabled or removed. // Accessing the filesystem during rule creation is problematic because the accesses are // not cached or tracked in any way. Preconditions.checkArgument( symlinkAttribute.get().getLeft() instanceof PathSourcePath, "Resource or asset symlink tree can only be built for a PathSourcePath"); PathSourcePath path = (PathSourcePath) symlinkAttribute.get().getLeft(); links = collectInputFiles(path.getFilesystem(), path.getRelativePath()); } else { links = RichStream.from(symlinkAttribute.get().getRight().entrySet()) .map(e -> new AbstractMap.SimpleEntry<>(Paths.get(e.getKey()), e.getValue())) .filter(e -> isPossibleResourcePath(e.getKey())) .collect(MoreCollectors.toImmutableMap(e -> e.getKey(), e -> e.getValue())); } } Path symlinkTreeRoot = BuildTargets.getGenPath(params.getProjectFilesystem(), params.getBuildTarget(), "%s") .resolve(outputDirName); return new SymlinkTree( params.getBuildTarget(), params.getProjectFilesystem(), symlinkTreeRoot, links, ruleFinder); } public static Optional<SourcePath> getResDirectoryForProject( BuildRuleResolver ruleResolver, TargetNode<AndroidResourceDescriptionArg, ?> node) { AndroidResourceDescriptionArg arg = node.getConstructorArg(); if (arg.getProjectRes().isPresent()) { return Optional.of(new PathSourcePath(node.getFilesystem(), arg.getProjectRes().get())); } if (!arg.getRes().isPresent()) { return Optional.empty(); } if (arg.getRes().get().isLeft()) { return Optional.of(arg.getRes().get().getLeft()); } else { return getResDirectory(ruleResolver, node); } } public static Optional<SourcePath> getAssetsDirectoryForProject( BuildRuleResolver ruleResolver, TargetNode<AndroidResourceDescriptionArg, ?> node) { AndroidResourceDescriptionArg arg = node.getConstructorArg(); if (arg.getProjectAssets().isPresent()) { return Optional.of(new PathSourcePath(node.getFilesystem(), arg.getProjectAssets().get())); } if (!arg.getAssets().isPresent()) { return Optional.empty(); } if (arg.getAssets().get().isLeft()) { return Optional.of(arg.getAssets().get().getLeft()); } else { return getAssetsDirectory(ruleResolver, node); } } private static Optional<SourcePath> getResDirectory( BuildRuleResolver ruleResolver, TargetNode<AndroidResourceDescriptionArg, ?> node) { return collectInputSourcePaths( ruleResolver, node.getBuildTarget(), RESOURCES_SYMLINK_TREE_FLAVOR, node.getConstructorArg().getRes()) .getSecond(); } private static Optional<SourcePath> getAssetsDirectory( BuildRuleResolver ruleResolver, TargetNode<AndroidResourceDescriptionArg, ?> node) { return collectInputSourcePaths( ruleResolver, node.getBuildTarget(), ASSETS_SYMLINK_TREE_FLAVOR, node.getConstructorArg().getAssets()) .getSecond(); } private static Pair<Optional<SymlinkTree>, Optional<SourcePath>> collectInputSourcePaths( BuildRuleResolver ruleResolver, BuildTarget resourceRuleTarget, Flavor symlinkTreeFlavor, Optional<Either<SourcePath, ImmutableSortedMap<String, SourcePath>>> attribute) { if (!attribute.isPresent()) { return new Pair<>(Optional.empty(), Optional.empty()); } if (attribute.get().isLeft()) { SourcePath inputSourcePath = attribute.get().getLeft(); if (!(inputSourcePath instanceof PathSourcePath)) { // If the resources are generated by a rule, we can't inspect the contents of the directory // in advance to create a symlink tree. Instead, we have to pass the source path as is. return new Pair<>(Optional.empty(), Optional.of(inputSourcePath)); } } BuildTarget symlinkTreeTarget = resourceRuleTarget.withFlavors(symlinkTreeFlavor); SymlinkTree symlinkTree; try { symlinkTree = (SymlinkTree) ruleResolver.requireRule(symlinkTreeTarget); } catch (NoSuchBuildTargetException e) { throw new RuntimeException(e); } return new Pair<>(Optional.of(symlinkTree), Optional.of(symlinkTree.getSourcePathToOutput())); } @VisibleForTesting ImmutableSortedMap<Path, SourcePath> collectInputFiles( final ProjectFilesystem filesystem, Path inputDir) { final ImmutableSortedMap.Builder<Path, SourcePath> paths = ImmutableSortedMap.naturalOrder(); // We ignore the same files that mini-aapt and aapt ignore. FileVisitor<Path> fileVisitor = new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) throws IOException { String dirName = dir.getFileName().toString(); // Special case: directory starting with '_' as per aapt. if (dirName.charAt(0) == '_' || !isPossibleResourceName(dirName)) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attr) throws IOException { String filename = file.getFileName().toString(); if (isPossibleResourceName(filename)) { paths.put(MorePaths.relativize(inputDir, file), new PathSourcePath(filesystem, file)); } return FileVisitResult.CONTINUE; } }; try { filesystem.walkRelativeFileTree(inputDir, fileVisitor); } catch (IOException e) { throw new HumanReadableException( e, "Error while searching for android resources in directory %s.", inputDir); } return paths.build(); } @VisibleForTesting static boolean isPossibleResourcePath(Path filePath) { for (Path component : filePath) { if (!isPossibleResourceName(component.toString())) { return false; } } Path parentPath = filePath.getParent(); if (parentPath != null) { for (Path component : parentPath) { if (component.toString().startsWith("_")) { return false; } } } return true; } private static boolean isPossibleResourceName(String fileOrDirName) { if (NON_ASSET_FILENAMES.contains(fileOrDirName.toLowerCase())) { return false; } if (fileOrDirName.charAt(fileOrDirName.length() - 1) == '~') { return false; } if (MiniAapt.IGNORED_FILE_EXTENSIONS.contains(Files.getFileExtension(fileOrDirName))) { return false; } return true; } @Override public boolean hasFlavors(ImmutableSet<Flavor> flavors) { if (flavors.isEmpty()) { return true; } if (flavors.size() == 1) { Flavor flavor = flavors.iterator().next(); if (flavor.equals(RESOURCES_SYMLINK_TREE_FLAVOR) || flavor.equals(ASSETS_SYMLINK_TREE_FLAVOR) || flavor.equals(AAPT2_COMPILE_FLAVOR)) { return true; } } return false; } @BuckStyleImmutable @Value.Immutable interface AbstractAndroidResourceDescriptionArg extends CommonDescriptionArg, HasDeclaredDeps { Optional<Either<SourcePath, ImmutableSortedMap<String, SourcePath>>> getRes(); Optional<Either<SourcePath, ImmutableSortedMap<String, SourcePath>>> getAssets(); Optional<Path> getProjectRes(); Optional<Path> getProjectAssets(); @Value.Default default boolean getHasWhitelistedStrings() { return false; } // For R.java. Optional<String> getPackage(); Optional<SourcePath> getManifest(); @Value.Default default boolean getResourceUnion() { return false; } } }