blob: 301ea88ae0fb502df0d9c63a33195155ff83e6be [file] [log] [blame]
/*
* 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.AaptPackageResources.BuildOutput;
import com.facebook.buck.android.AndroidBinary.PackageType;
import com.facebook.buck.android.AndroidBinary.TargetCpuType;
import com.facebook.buck.dalvik.EstimateLinearAllocStep;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.java.AccumulateClassNamesStep;
import com.facebook.buck.java.HasJavaClassHashes;
import com.facebook.buck.java.JavacOptions;
import com.facebook.buck.java.JavacStep;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildOutputInitializer;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.InitializableFromDisk;
import com.facebook.buck.rules.OnDiskBuildInfo;
import com.facebook.buck.rules.RecordFileSha1Step;
import com.facebook.buck.rules.RuleKey;
import com.facebook.buck.rules.Sha1HashCode;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.step.AbstractExecutionStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirAndSymlinkFileStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
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.hash.HashCode;
import com.google.common.hash.Hashing;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Packages the resources using {@code aapt}.
*/
public class AaptPackageResources extends AbstractBuildRule
implements InitializableFromDisk<BuildOutput>, HasJavaClassHashes {
public static final String RESOURCE_PACKAGE_HASH_KEY = "resource_package_hash";
public static final String R_DOT_JAVA_LINEAR_ALLOC_SIZE = "r_dot_java_linear_alloc_size";
/** Options to use with {@link com.facebook.buck.android.DxStep} when dexing R.java. */
public static final EnumSet<DxStep.Option> DX_OPTIONS = EnumSet.of(
DxStep.Option.USE_CUSTOM_DX_IF_AVAILABLE,
DxStep.Option.RUN_IN_PROCESS,
DxStep.Option.NO_OPTIMIZE);
private final SourcePath manifest;
private final FilteredResourcesProvider filteredResourcesProvider;
private final ImmutableSet<Path> assetsDirectories;
private final PackageType packageType;
private final ImmutableSet<TargetCpuType> cpuFilters;
private final ImmutableList<HasAndroidResourceDeps> resourceDeps;
private final JavacOptions javacOptions;
private final boolean rDotJavaNeedsDexing;
private final boolean shouldBuildStringSourceMap;
private final boolean shouldWarnIfMissingResource;
private final boolean skipCrunchPngs;
private final BuildOutputInitializer<BuildOutput> buildOutputInitializer;
AaptPackageResources(
BuildRuleParams params,
SourcePathResolver resolver,
SourcePath manifest,
FilteredResourcesProvider filteredResourcesProvider,
ImmutableList<HasAndroidResourceDeps> resourceDeps,
ImmutableSet<Path> assetsDirectories,
PackageType packageType,
ImmutableSet<TargetCpuType> cpuFilters,
JavacOptions javacOptions,
boolean rDotJavaNeedsDexing,
boolean shouldBuildStringSourceMap,
boolean shouldWarnIfMissingResources,
boolean skipCrunchPngs) {
super(params, resolver);
this.manifest = manifest;
this.filteredResourcesProvider = filteredResourcesProvider;
this.resourceDeps = resourceDeps;
this.assetsDirectories = assetsDirectories;
this.packageType = packageType;
this.cpuFilters = cpuFilters;
this.javacOptions = javacOptions;
this.rDotJavaNeedsDexing = rDotJavaNeedsDexing;
this.shouldBuildStringSourceMap = shouldBuildStringSourceMap;
this.shouldWarnIfMissingResource = shouldWarnIfMissingResources;
this.skipCrunchPngs = skipCrunchPngs;
this.buildOutputInitializer = new BuildOutputInitializer<>(params.getBuildTarget(), this);
}
@Override
public ImmutableCollection<Path> getInputsToCompareToOutput() {
return getResolver().filterInputsToCompareToOutput(Collections.singleton(manifest));
}
@Override
public RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) {
return builder
.setReflectively("packageType", packageType.toString())
.setReflectively("cpuFilters", ImmutableSortedSet.copyOf(cpuFilters).toString())
.setReflectively("rDotJavaNeedsDexing", rDotJavaNeedsDexing)
.setReflectively("shouldBuildStringSourceMap", shouldBuildStringSourceMap)
.setReflectively("skipCrunchPngs", skipCrunchPngs);
}
@Override
public Path getPathToOutputFile() {
return getResourceApkPath();
}
public Optional<DexWithClasses> getRDotJavaDexWithClasses() {
Preconditions.checkState(rDotJavaNeedsDexing,
"Error trying to get R.java dex file: R.java is not supposed to be dexed.");
final Optional<Integer> linearAllocSizeEstimate =
buildOutputInitializer.getBuildOutput().rDotJavaDexLinearAllocEstimate;
if (!linearAllocSizeEstimate.isPresent()) {
return Optional.absent();
}
return Optional.<DexWithClasses>of(
new DexWithClasses() {
@Override
public Path getPathToDexFile() {
return getPathToRDotJavaDex();
}
@Override
public ImmutableSet<String> getClassNames() {
throw new RuntimeException(
"Since R.java is unconditionally packed in the primary dex, no" +
"one should call this method.");
}
@Override
public Sha1HashCode getClassesHash() {
return Sha1HashCode.fromHashCode(
Hashing.combineOrdered(getClassNamesToHashes().values()));
}
@Override
public int getSizeEstimate() {
return linearAllocSizeEstimate.get();
}
});
}
@Override
public ImmutableSortedMap<String, HashCode> getClassNamesToHashes() {
return buildOutputInitializer.getBuildOutput().rDotJavaClassesHash;
}
/**
* @return path to the directory where the {@code R.class} files can be found after this rule is
* built.
*/
public Path getPathToCompiledRDotJavaFiles() {
return BuildTargets.getBinPath(getBuildTarget(), "__%s_rdotjava_bin__");
}
public Path getPathToRDotTxtDir() {
return BuildTargets.getBinPath(getBuildTarget(), "__%s_res_symbols__");
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context,
final BuildableContext buildableContext) {
ImmutableList.Builder<Step> steps = ImmutableList.builder();
// Symlink the manifest to a path named AndroidManifest.xml. Do this before running any other
// commands to ensure that it is available at the desired path.
steps.add(
new MkdirAndSymlinkFileStep(getResolver().getPath(manifest), getAndroidManifestXml()));
// Copy the transitive closure of files in assets to a single directory, if any.
// TODO(mbolin): Older versions of aapt did not support multiple -A flags, so we can probably
// eliminate this now.
Step collectAssets = new Step() {
@Override
public int execute(ExecutionContext context) throws IOException, InterruptedException {
// This must be done in a Command because the files and directories that are specified may
// not exist at the time this Command is created because the previous Commands have not run
// yet.
ImmutableList.Builder<Step> commands = ImmutableList.builder();
try {
createAllAssetsDirectory(
assetsDirectories,
commands,
context.getProjectFilesystem());
} catch (IOException e) {
context.logError(e, "Error creating all assets directory in %s.", getBuildTarget());
return 1;
}
for (Step command : commands.build()) {
int exitCode = command.execute(context);
if (exitCode != 0) {
throw new HumanReadableException("Error running " + command.getDescription(context));
}
}
return 0;
}
@Override
public String getShortName() {
return "symlink_assets";
}
@Override
public String getDescription(ExecutionContext context) {
return getShortName();
}
};
steps.add(collectAssets);
Optional<Path> assetsDirectory;
if (assetsDirectories.isEmpty()) {
assetsDirectory = Optional.absent();
} else {
assetsDirectory = Optional.of(getPathToAllAssetsDirectory());
}
steps.add(new MkdirStep(getResourceApkPath().getParent()));
Path rDotTxtDir = getPathToRDotTxtDir();
steps.add(new MakeCleanDirectoryStep(rDotTxtDir));
Optional<Path> pathToGeneratedProguardConfig = Optional.absent();
if (packageType.isBuildWithObfuscation()) {
Path proguardConfigDir = getPathToGeneratedProguardConfigDir();
steps.add(new MakeCleanDirectoryStep(proguardConfigDir));
pathToGeneratedProguardConfig = Optional.of(proguardConfigDir.resolve("proguard.txt"));
buildableContext.recordArtifactsInDirectory(proguardConfigDir);
}
steps.add(
new AaptStep(
getAndroidManifestXml(),
filteredResourcesProvider.getResDirectories(),
assetsDirectory,
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() */));
if (!filteredResourcesProvider.getResDirectories().isEmpty()) {
generateAndCompileRDotJavaFiles(steps, buildableContext);
if (rDotJavaNeedsDexing) {
Path rDotJavaDexDir = getPathToRDotJavaDexFiles();
steps.add(new MakeCleanDirectoryStep(rDotJavaDexDir));
steps.add(new DxStep(
getPathToRDotJavaDex(),
Collections.singleton(getPathToCompiledRDotJavaFiles()),
DX_OPTIONS));
final EstimateLinearAllocStep estimateLinearAllocStep = new EstimateLinearAllocStep(
getPathToCompiledRDotJavaFiles());
steps.add(estimateLinearAllocStep);
buildableContext.recordArtifact(getPathToRDotJavaDex());
steps.add(
new AbstractExecutionStep("record_build_output") {
@Override
public int execute(ExecutionContext context) {
buildableContext.addMetadata(
R_DOT_JAVA_LINEAR_ALLOC_SIZE,
estimateLinearAllocStep.get().toString());
return 0;
}
});
}
}
buildableContext.recordArtifact(getAndroidManifestXml());
buildableContext.recordArtifact(getResourceApkPath());
steps.add(new RecordFileSha1Step(
getResourceApkPath(),
RESOURCE_PACKAGE_HASH_KEY,
buildableContext));
return steps.build();
}
private void generateAndCompileRDotJavaFiles(
ImmutableList.Builder<Step> steps,
BuildableContext buildableContext) {
// Merge R.txt of HasAndroidRes and generate the resulting R.java files per package.
Path rDotJavaSrc = getPathToGeneratedRDotJavaSrcFiles();
steps.add(new MakeCleanDirectoryStep(rDotJavaSrc));
Path rDotTxtDir = getPathToRDotTxtDir();
MergeAndroidResourcesStep mergeStep = MergeAndroidResourcesStep.createStepForUberRDotJava(
resourceDeps,
rDotTxtDir.resolve("R.txt"),
shouldWarnIfMissingResource,
rDotJavaSrc);
steps.add(mergeStep);
if (shouldBuildStringSourceMap) {
// Make sure we have an output directory
Path outputDirPath = getPathForNativeStringInfoDirectory();
steps.add(new MakeCleanDirectoryStep(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(
rDotTxtDir,
filteredResourcesProvider.getResDirectories(),
outputDirPath);
steps.add(genNativeStringInfo);
// Cache the generated strings.json file, it will be stored inside outputDirPath
buildableContext.recordArtifactsInDirectory(outputDirPath);
}
// Create the path where the R.java files will be compiled.
Path rDotJavaBin = getPathToCompiledRDotJavaFiles();
steps.add(new MakeCleanDirectoryStep(rDotJavaBin));
JavacStep javacStep = RDotJava.createJavacStepForUberRDotJavaFiles(
ImmutableSet.copyOf(mergeStep.getRDotJavaFiles()),
rDotJavaBin,
javacOptions,
getBuildTarget());
steps.add(javacStep);
Path rDotJavaClassesTxt = getPathToRDotJavaClassesTxt();
steps.add(new MakeCleanDirectoryStep(rDotJavaClassesTxt.getParent()));
steps.add(new AccumulateClassNamesStep(Optional.of(rDotJavaBin), rDotJavaClassesTxt));
// Ensure the generated R.txt, R.java, and R.class files are also recorded.
buildableContext.recordArtifactsInDirectory(rDotTxtDir);
buildableContext.recordArtifactsInDirectory(rDotJavaSrc);
buildableContext.recordArtifactsInDirectory(rDotJavaBin);
buildableContext.recordArtifact(rDotJavaClassesTxt);
}
/**
* 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}.
*/
Path getAndroidManifestXml() {
return BuildTargets.getBinPath(getBuildTarget(), "__manifest_%s__/AndroidManifest.xml");
}
/**
* Given a set of assets directories to include in the APK (which may be empty), return the path
* to the directory that contains the union of all the assets. If any work needs to be done to
* create such a directory, the appropriate commands should be added to the {@code commands}
* list builder.
* <p>
* If there are no assets (i.e., {@code assetsDirectories} is empty), then the return value will
* be an empty {@link Optional}.
*/
@VisibleForTesting
Optional<Path> createAllAssetsDirectory(
Set<Path> assetsDirectories,
ImmutableList.Builder<Step> steps,
ProjectFilesystem filesystem) throws IOException {
if (assetsDirectories.isEmpty()) {
return Optional.absent();
}
// Due to a limitation of aapt, only one assets directory can be specified, so if multiple are
// specified in Buck, then all of the contents must be symlinked to a single directory.
Path destination = getPathToAllAssetsDirectory();
steps.add(new MakeCleanDirectoryStep(destination));
final ImmutableMap.Builder<Path, Path> allAssets = ImmutableMap.builder();
for (final Path assetsDirectory : assetsDirectories) {
if (!filesystem.exists(assetsDirectory)) {
continue;
}
filesystem.walkRelativeFileTree(
assetsDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!AaptStep.isSilentlyIgnored(file)) {
allAssets.put(assetsDirectory.relativize(file), file);
}
return FileVisitResult.CONTINUE;
}
});
}
for (Map.Entry<Path, Path> entry : allAssets.build().entrySet()) {
steps.add(new MkdirAndSymlinkFileStep(
entry.getValue(),
destination.resolve(entry.getKey())));
}
return Optional.of(destination);
}
/**
* @return Path to the unsigned APK generated by this {@link com.facebook.buck.rules.BuildRule}.
*/
public Path getResourceApkPath() {
return BuildTargets.getGenPath(getBuildTarget(), "%s.unsigned.ap_");
}
@VisibleForTesting
Path getPathToAllAssetsDirectory() {
return BuildTargets.getBinPath(getBuildTarget(), "__assets_%s__");
}
public Sha1HashCode getResourcePackageHash() {
return buildOutputInitializer.getBuildOutput().resourcePackageHash;
}
static class BuildOutput {
private final Sha1HashCode resourcePackageHash;
private final Optional<Integer> rDotJavaDexLinearAllocEstimate;
private final ImmutableSortedMap<String, HashCode> rDotJavaClassesHash;
BuildOutput(
Sha1HashCode resourcePackageHash,
Optional<Integer> rDotJavaDexLinearAllocEstimate,
ImmutableSortedMap<String, HashCode> rDotJavaClassesHash) {
this.resourcePackageHash = resourcePackageHash;
this.rDotJavaDexLinearAllocEstimate = rDotJavaDexLinearAllocEstimate;
this.rDotJavaClassesHash = rDotJavaClassesHash;
}
}
@Override
public BuildOutput initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
Optional<Sha1HashCode> resourcePackageHash = onDiskBuildInfo.getHash(RESOURCE_PACKAGE_HASH_KEY);
Preconditions.checkState(
resourcePackageHash.isPresent(),
"Should not be initializing %s from disk if the resource hash is not written.",
getBuildTarget());
ImmutableSortedMap<String, HashCode> classesHash = ImmutableSortedMap.of();
if (!filteredResourcesProvider.getResDirectories().isEmpty()) {
List<String> lines;
try {
lines = onDiskBuildInfo.getOutputFileContentsByLine(getPathToRDotJavaClassesTxt());
} catch (IOException e) {
throw new RuntimeException(e);
}
classesHash = AccumulateClassNamesStep.parseClassHashes(lines);
}
Optional<String> linearAllocSizeValue = onDiskBuildInfo.getValue(R_DOT_JAVA_LINEAR_ALLOC_SIZE);
Optional<Integer> linearAllocSize = linearAllocSizeValue.isPresent()
? Optional.of(Integer.parseInt(linearAllocSizeValue.get()))
: Optional.<Integer>absent();
return new BuildOutput(resourcePackageHash.get(), linearAllocSize, classesHash);
}
@Override
public BuildOutputInitializer<BuildOutput> getBuildOutputInitializer() {
return buildOutputInitializer;
}
private Path getPathToRDotJavaDexFiles() {
return BuildTargets.getBinPath(getBuildTarget(), "__%s_rdotjava_dex__");
}
private Path getPathToRDotJavaClassesTxt() {
return BuildTargets.getBinPath(getBuildTarget(), "__%s_rdotjava_classes__")
.resolve("classes.txt");
}
private Path getPathToRDotJavaDex() {
return getPathToRDotJavaDexFiles().resolve("classes.dex.jar");
}
/**
* This directory contains both the generated {@code R.java} files under a directory path that
* matches the corresponding package structure.
*/
private Path getPathToGeneratedRDotJavaSrcFiles() {
return getPathToGeneratedRDotJavaSrcFiles(getBuildTarget());
}
private Path getPathForNativeStringInfoDirectory() {
return BuildTargets.getBinPath(getBuildTarget(), "__%s_string_source_map__");
}
/**
* This is the path to the directory for generated files related to ProGuard. Ultimately, it
* should include:
* <ul>
* <li>proguard.txt
* <li>dump.txt
* <li>seeds.txt
* <li>usage.txt
* <li>mapping.txt
* <li>obfuscated.jar
* </ul>
* @return path to directory (will not include trailing slash)
*/
public Path getPathToGeneratedProguardConfigDir() {
return BuildTargets.getGenPath(getBuildTarget(), "__%s__proguard__").resolve(".proguard");
}
@VisibleForTesting
static Path getPathToGeneratedRDotJavaSrcFiles(BuildTarget buildTarget) {
return BuildTargets.getBinPath(buildTarget, "__%s_rdotjava_src__");
}
@VisibleForTesting
FilteredResourcesProvider getFilteredResourcesProvider() {
return filteredResourcesProvider;
}
}