/* * Copyright 2013-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.RDotTxtEntry.RType; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.rules.AbstractBuildRule; import com.facebook.buck.rules.AddToRuleKey; import com.facebook.buck.rules.BuildContext; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildRuleResolver; import com.facebook.buck.rules.BuildRules; import com.facebook.buck.rules.BuildableContext; import com.facebook.buck.rules.ExplicitBuildTargetSourcePath; import com.facebook.buck.rules.RecordFileSha1Step; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.coercer.ManifestEntries; import com.facebook.buck.step.Step; import com.facebook.buck.step.fs.CopyStep; import com.facebook.buck.step.fs.MakeCleanDirectoryStep; import com.facebook.buck.step.fs.MkdirStep; import com.facebook.buck.step.fs.TouchStep; import com.facebook.buck.zip.ZipScrubberStep; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Suppliers; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Ordering; import java.nio.file.Path; import java.util.EnumSet; import java.util.Optional; import java.util.stream.Stream; /** Packages the resources using {@code aapt}. */ public class AaptPackageResources extends AbstractBuildRule { public static final String RESOURCE_PACKAGE_HASH_KEY = "resource_package_hash"; public static final String FILTERED_RESOURCE_DIRS_KEY = "filtered_resource_dirs"; public static final String RESOURCE_APK_PATH_FORMAT = "%s.unsigned.ap_"; @AddToRuleKey private final SourcePath manifest; private final FilteredResourcesProvider filteredResourcesProvider; @AddToRuleKey private final Optional<String> resourceUnionPackage; private final ImmutableList<HasAndroidResourceDeps> resourceDeps; @AddToRuleKey private final boolean shouldBuildStringSourceMap; @AddToRuleKey private final boolean skipCrunchPngs; @AddToRuleKey private final EnumSet<RType> bannedDuplicateResourceTypes; @AddToRuleKey private final ManifestEntries manifestEntries; @AddToRuleKey private final boolean includesVectorDrawables; static ImmutableSortedSet<BuildRule> getAllDeps( BuildTarget aaptTarget, SourcePathRuleFinder ruleFinder, BuildRuleResolver ruleResolver, SourcePath manifest, FilteredResourcesProvider filteredResourcesProvider, ImmutableList<HasAndroidResourceDeps> resourceDeps) { ImmutableSortedSet.Builder<BuildRule> depsBuilder = ImmutableSortedSet.naturalOrder(); Stream<BuildTarget> resourceTargets = resourceDeps.stream().map(HasAndroidResourceDeps::getBuildTarget); depsBuilder.addAll( BuildRules.toBuildRulesFor(aaptTarget, ruleResolver, resourceTargets::iterator)); Stream<SourcePath> resourceDirs = resourceDeps.stream().map(HasAndroidResourceDeps::getRes); depsBuilder.addAll(ruleFinder.filterBuildRuleInputs(resourceDirs)); ruleFinder.getRule(manifest).ifPresent(depsBuilder::add); filteredResourcesProvider.getResourceFilterRule().ifPresent(depsBuilder::add); return depsBuilder.build(); } AaptPackageResources( BuildRuleParams params, SourcePathRuleFinder ruleFinder, BuildRuleResolver ruleResolver, SourcePath manifest, FilteredResourcesProvider filteredResourcesProvider, ImmutableList<HasAndroidResourceDeps> resourceDeps, Optional<String> resourceUnionPackage, boolean shouldBuildStringSourceMap, boolean skipCrunchPngs, boolean includesVectorDrawables, EnumSet<RType> bannedDuplicateResourceTypes, ManifestEntries manifestEntries) { super( params.copyReplacingDeclaredAndExtraDeps( Suppliers.ofInstance( getAllDeps( params.getBuildTarget(), ruleFinder, ruleResolver, manifest, filteredResourcesProvider, resourceDeps)), Suppliers.ofInstance(ImmutableSortedSet.of()))); this.manifest = manifest; this.filteredResourcesProvider = filteredResourcesProvider; this.resourceDeps = resourceDeps; this.resourceUnionPackage = resourceUnionPackage; this.shouldBuildStringSourceMap = shouldBuildStringSourceMap; this.skipCrunchPngs = skipCrunchPngs; this.includesVectorDrawables = includesVectorDrawables; this.bannedDuplicateResourceTypes = bannedDuplicateResourceTypes; this.manifestEntries = manifestEntries; } @Override public SourcePath getSourcePathToOutput() { return new ExplicitBuildTargetSourcePath(getBuildTarget(), getResourceApkPath()); } private Path getPathToRDotTxtDir() { return BuildTargets.getScratchPath( getProjectFilesystem(), getBuildTarget(), "__%s_res_symbols__"); } private Path getPathToRDotTxtFile() { return getPathToRDotTxtDir().resolve("R.txt"); } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, final BuildableContext buildableContext) { ImmutableList.Builder<Step> steps = ImmutableList.builder(); prepareManifestForAapt( steps, getProjectFilesystem(), getAndroidManifestXml(), context.getSourcePathResolver().getAbsolutePath(manifest), manifestEntries); steps.add(MkdirStep.of(getProjectFilesystem(), getResourceApkPath().getParent())); Path rDotTxtDir = getPathToRDotTxtDir(); steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), rDotTxtDir)); Path pathToGeneratedProguardConfig = getPathToGeneratedProguardConfigFile(); steps.addAll( MakeCleanDirectoryStep.of( getProjectFilesystem(), pathToGeneratedProguardConfig.getParent())); buildableContext.recordArtifact(pathToGeneratedProguardConfig); steps.add( new AaptStep( getProjectFilesystem().getRootPath(), getAndroidManifestXml(), filteredResourcesProvider.getResDirectories(), ImmutableSortedSet.of(), getResourceApkPath(), rDotTxtDir, pathToGeneratedProguardConfig, /* * In practice, it appears that if --no-crunch is used, resources will occasionally * appear distorted in the APK produced by this command (and what's worse, a clean * reinstall does not make the problem go away). This is not reliably reproducible, so * for now, we categorically outlaw the use of --no-crunch so that developers do not get * stuck in the distorted image state. One would expect the use of --no-crunch to allow * for faster build times, so it would be nice to figure out a way to leverage it in * debug mode that never results in distorted images. */ !skipCrunchPngs /* && packageType.isCrunchPngFiles() */, includesVectorDrawables, manifestEntries), ZipScrubberStep.of( context.getSourcePathResolver().getAbsolutePath(getSourcePathToOutput()))); // If we had an empty res directory, we won't generate an R.txt file. This ensures that it // always exists. steps.add(new TouchStep(getProjectFilesystem(), getPathToRDotTxtFile())); if (hasRDotJava()) { generateRDotJavaFiles(steps, buildableContext, context); } // Record the filtered resources dirs, since when we initialize ourselves from disk, we'll // need to test whether this is empty or not without requiring the `ResourcesFilter` rule to // be available. buildableContext.addMetadata( FILTERED_RESOURCE_DIRS_KEY, FluentIterable.from(filteredResourcesProvider.getResDirectories()) .transform(Object::toString) .toSortedList(Ordering.natural())); buildableContext.recordArtifact(getAndroidManifestXml()); buildableContext.recordArtifact(getResourceApkPath()); steps.add( new RecordFileSha1Step( getProjectFilesystem(), getResourceApkPath(), RESOURCE_PACKAGE_HASH_KEY, buildableContext)); return steps.build(); } static void prepareManifestForAapt( ImmutableList.Builder<Step> stepBuilder, ProjectFilesystem projectFilesystem, Path finalManifestPath, Path rawManifestPath, ManifestEntries manifestEntries) { // Copy manifest to a path named AndroidManifest.xml after replacing the manifest placeholders // if needed. Do this before running any other commands to ensure that it is available at the // desired path. stepBuilder.add(MkdirStep.of(projectFilesystem, finalManifestPath.getParent())); Optional<ImmutableMap<String, String>> placeholders = manifestEntries.getPlaceholders(); if (placeholders.isPresent() && !placeholders.get().isEmpty()) { stepBuilder.add( new ReplaceManifestPlaceholdersStep( projectFilesystem, rawManifestPath, finalManifestPath, placeholders.get())); } else { stepBuilder.add(CopyStep.forFile(projectFilesystem, rawManifestPath, finalManifestPath)); } } /** True iff an app has resources with ids (after filtering (like for display density)). */ boolean hasRDotJava() { return filteredResourcesProvider.hasResources(); } private void generateRDotJavaFiles( ImmutableList.Builder<Step> steps, BuildableContext buildableContext, BuildContext buildContext) { // Merge R.txt of HasAndroidRes and generate the resulting R.java files per package. Path rDotJavaSrc = getRawPathToGeneratedRDotJavaSrcFiles(); steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), rDotJavaSrc)); Path rDotTxtDir = getPathToRDotTxtDir(); MergeAndroidResourcesStep mergeStep = MergeAndroidResourcesStep.createStepForUberRDotJava( getProjectFilesystem(), buildContext.getSourcePathResolver(), resourceDeps, getPathToRDotTxtFile(), rDotJavaSrc, bannedDuplicateResourceTypes, resourceUnionPackage); steps.add(mergeStep); if (shouldBuildStringSourceMap) { // Make sure we have an output directory Path outputDirPath = getPathForNativeStringInfoDirectory(); steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), outputDirPath)); // Add the step that parses R.txt and all the strings.xml files, and // produces a JSON with android resource id's and xml paths for each string resource. GenStringSourceMapStep genNativeStringInfo = new GenStringSourceMapStep( getProjectFilesystem(), rDotTxtDir, filteredResourcesProvider.getResDirectories(), outputDirPath); steps.add(genNativeStringInfo); // Cache the generated strings.json file, it will be stored inside outputDirPath buildableContext.recordArtifact(outputDirPath); } // Ensure the generated R.txt and R.java files are also recorded. buildableContext.recordArtifact(rDotTxtDir); buildableContext.recordArtifact(rDotJavaSrc); } /** * Buck does not require the manifest to be named AndroidManifest.xml, but commands such as aapt * do. For this reason, we symlink the path to {@link #manifest} to the path returned by this * method, whose name is always "AndroidManifest.xml". * * <p>Therefore, commands created by this buildable should use this method instead of {@link * #manifest}. */ private Path getAndroidManifestXml() { return BuildTargets.getScratchPath( getProjectFilesystem(), getBuildTarget(), "__manifest_%s__/AndroidManifest.xml"); } /** * @return Path to the unsigned APK generated by this {@link com.facebook.buck.rules.BuildRule}. */ private Path getResourceApkPath() { return BuildTargets.getGenPath( getProjectFilesystem(), getBuildTarget(), RESOURCE_APK_PATH_FORMAT); } /** * This directory contains both the generated {@code R.java} files under a directory path that * matches the corresponding package structure. */ SourcePath getPathToRDotJavaDir() { return new ExplicitBuildTargetSourcePath( getBuildTarget(), getRawPathToGeneratedRDotJavaSrcFiles()); } private Path getRawPathToGeneratedRDotJavaSrcFiles() { return getPathToGeneratedRDotJavaSrcFiles(getBuildTarget(), getProjectFilesystem()); } private Path getPathForNativeStringInfoDirectory() { return BuildTargets.getScratchPath( getProjectFilesystem(), getBuildTarget(), "__%s_string_source_map__"); } private Path getPathToGeneratedProguardConfigFile() { return BuildTargets.getGenPath( getProjectFilesystem(), getBuildTarget(), "%s/proguard/proguard.txt"); } @VisibleForTesting static Path getPathToGeneratedRDotJavaSrcFiles( BuildTarget buildTarget, ProjectFilesystem filesystem) { return BuildTargets.getScratchPath(filesystem, buildTarget, "__%s_rdotjava_src__"); } @VisibleForTesting FilteredResourcesProvider getFilteredResourcesProvider() { return filteredResourcesProvider; } public AaptOutputInfo getAaptOutputInfo() { BuildTarget target = getBuildTarget(); return AaptOutputInfo.builder() .setPathToRDotTxt(new ExplicitBuildTargetSourcePath(target, getPathToRDotTxtFile())) .setRDotJavaDir(hasRDotJava() ? Optional.of(getPathToRDotJavaDir()) : Optional.empty()) .setPrimaryResourcesApkPath(new ExplicitBuildTargetSourcePath(target, getResourceApkPath())) .setAndroidManifestXml(new ExplicitBuildTargetSourcePath(target, getAndroidManifestXml())) .setAaptGeneratedProguardConfigFile( new ExplicitBuildTargetSourcePath(target, getPathToGeneratedProguardConfigFile())) .build(); } }