/*
* 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();
}
}