blob: b2872d3625f623ff242c0790c51a3db5f4e95ecb [file] [log] [blame]
/*
* Copyright 2012-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 static com.facebook.buck.rules.BuildableProperties.Kind.ANDROID;
import static com.facebook.buck.rules.BuildableProperties.Kind.LIBRARY;
import com.facebook.buck.android.aapt.MiniAapt;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.model.HasBuildTarget;
import com.facebook.buck.rules.AbiRule;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildOutputInitializer;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.BuildableProperties;
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.util.HumanReadableException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
/**
* An object that represents the resources of an android library.
* <p>
* Suppose this were a rule defined in <code>src/com/facebook/feed/BUILD</code>:
* <pre>
* android_resources(
* name = 'res',
* res = 'res',
* assets = 'buck-assets',
* deps = [
* '//first-party/orca/lib-ui:lib-ui',
* ],
* )
* </pre>
*/
public class AndroidResource extends AbstractBuildRule
implements AbiRule, HasAndroidResourceDeps, InitializableFromDisk<AndroidResource.BuildOutput>,
AndroidPackageable {
private static final BuildableProperties PROPERTIES = new BuildableProperties(ANDROID, LIBRARY);
@VisibleForTesting
static final String METADATA_KEY_FOR_ABI = "ANDROID_RESOURCE_ABI_KEY";
@VisibleForTesting
static final String METADATA_KEY_FOR_R_DOT_JAVA_PACKAGE = "METADATA_KEY_FOR_R_DOT_JAVA_PACKAGE";
@Nullable
private final Path res;
/** contents of {@code res} under version control (i.e., not generated by another rule). */
private final ImmutableSortedSet<Path> resSrcs;
@Nullable
private final Path assets;
/** contents of {@code assets} under version control (i.e., not generated by another rule). */
private final ImmutableSortedSet<Path> assetsSrcs;
@Nullable
private final Path pathToTextSymbolsDir;
@Nullable
private final Path pathToTextSymbolsFile;
@Nullable
private final SourcePath manifestFile;
private final boolean hasWhitelistedStrings;
private final ImmutableSortedSet<BuildRule> deps;
private final BuildOutputInitializer<BuildOutput> buildOutputInitializer;
/**
* This is the original {@code package} argument passed to this rule.
*/
@Nullable
private final String rDotJavaPackageArgument;
/**
* Supplier that returns the package for the Java class generated for the resources in
* {@link #res}, if any. The value for this supplier is determined, as follows:
* <ul>
* <li>If the user specified a {@code package} argument, the supplier will return that value.
* <li>Failing that, when the rule is built, it will parse the package from the file specified
* by the {@code manifest} so that it can be returned by this supplier. (Note this also
* needs to work correctly if the rule is initialized from disk.)
* <li>In all other cases (e.g., both {@code package} and {@code manifest} are unspecified), the
* behavior is undefined.
* </ul>
*/
private final Supplier<String> rDotJavaPackageSupplier;
private final AtomicReference<String> rDotJavaPackage;
protected AndroidResource(
BuildRuleParams buildRuleParams,
SourcePathResolver resolver,
final ImmutableSortedSet<BuildRule> deps,
@Nullable final Path res,
ImmutableSortedSet<Path> resSrcs,
@Nullable String rDotJavaPackageArgument,
@Nullable Path assets,
ImmutableSortedSet<Path> assetsSrcs,
@Nullable SourcePath manifestFile,
boolean hasWhitelistedStrings) {
super(buildRuleParams, resolver);
if (res != null && rDotJavaPackageArgument == null && manifestFile == null) {
throw new HumanReadableException(
"When the 'res' is specified for android_resource() %s, at least one of 'package' or " +
"'manifest' must be specified.",
getBuildTarget());
}
this.res = res;
this.resSrcs = resSrcs;
this.assets = assets;
this.assetsSrcs = assetsSrcs;
this.manifestFile = manifestFile;
this.hasWhitelistedStrings = hasWhitelistedStrings;
BuildTarget buildTarget = buildRuleParams.getBuildTarget();
if (res == null) {
pathToTextSymbolsDir = null;
pathToTextSymbolsFile = null;
} else {
pathToTextSymbolsDir = BuildTargets.getGenPath(buildTarget, "__%s_text_symbols__");
pathToTextSymbolsFile = pathToTextSymbolsDir.resolve("R.txt");
}
this.deps = deps;
this.buildOutputInitializer = new BuildOutputInitializer<>(buildTarget, this);
this.rDotJavaPackageArgument = rDotJavaPackageArgument;
this.rDotJavaPackage = new AtomicReference<>();
if (rDotJavaPackageArgument != null) {
this.rDotJavaPackage.set(rDotJavaPackageArgument);
}
this.rDotJavaPackageSupplier = new Supplier<String>() {
@Override
public String get() {
String rDotJavaPackage = AndroidResource.this.rDotJavaPackage.get();
if (rDotJavaPackage != null) {
return rDotJavaPackage;
} else {
throw new RuntimeException(String.format(
"rDotJavaPackage for %s was requested before it was made available.",
AndroidResource.this.getBuildTarget()));
}
}
};
}
@Override
public ImmutableCollection<Path> getInputsToCompareToOutput() {
ImmutableSortedSet.Builder<Path> inputs = ImmutableSortedSet.naturalOrder();
// This should include the res/ and assets/ folders.
inputs.addAll(resSrcs);
inputs.addAll(assetsSrcs);
// manifest file is optional.
if (manifestFile != null) {
inputs.addAll(
getResolver().filterInputsToCompareToOutput(Collections.singleton(manifestFile)));
}
return inputs.build();
}
@Override
@Nullable
public Path getRes() {
return res;
}
@Override
@Nullable
public Path getAssets() {
return assets;
}
@Nullable
public SourcePath getManifestFile() {
return manifestFile;
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context,
final BuildableContext buildableContext) {
// If there is no res directory, then there is no R.java to generate.
// TODO(mbolin): Change android_resources() so that 'res' is required.
if (getRes() == null) {
// Normally, the ABI key of a resource rule is a hash of the R.txt file that it creates.
// That R.txt essentially combines all the R.txt files generated by transitive dependencies of
// this rule. So, in this case (when we don't have the R.txt), the right ABI key is the
// ABI key for deps.
buildableContext.addMetadata(METADATA_KEY_FOR_ABI, getAbiKeyForDeps().getHash());
return ImmutableList.of();
}
ImmutableList.Builder<Step> steps = ImmutableList.builder();
steps.add(new MakeCleanDirectoryStep(pathToTextSymbolsDir));
// If the 'package' was not specified for this android_resource(), then attempt to parse it
// from the AndroidManifest.xml.
if (rDotJavaPackageArgument == null) {
steps.add(new AbstractExecutionStep("extract_android_package") {
@Override
public int execute(ExecutionContext context) {
Preconditions.checkNotNull(
manifestFile,
"manifestFile cannot be null when res is non-null and rDotJavaPackageArgument is " +
"null. This should already be enforced by the constructor.");
AndroidManifestReader androidManifestReader;
try {
androidManifestReader = DefaultAndroidManifestReader.forPath(
getResolver().getPath(manifestFile),
context.getProjectFilesystem());
} catch (IOException e) {
context.logError(e, "Failed to create AndroidManifestReader for %s.", manifestFile);
return 1;
}
String rDotJavaPackageFromAndroidManifest = androidManifestReader.getPackage();
AndroidResource.this.rDotJavaPackage.set(rDotJavaPackageFromAndroidManifest);
buildableContext.addMetadata(
METADATA_KEY_FOR_R_DOT_JAVA_PACKAGE,
rDotJavaPackageFromAndroidManifest);
return 0;
}
});
}
ImmutableSet<Path> pathsToSymbolsOfDeps = FluentIterable.from(getNonEmptyResourceDeps())
.transform(GET_RES_SYMBOLS_TXT)
.toSet();
steps.add(new MiniAapt(res, pathToTextSymbolsFile, pathsToSymbolsOfDeps));
buildableContext.recordArtifact(pathToTextSymbolsFile);
steps.add(new RecordFileSha1Step(
pathToTextSymbolsFile,
METADATA_KEY_FOR_ABI,
buildableContext));
return steps.build();
}
@Override
public RuleKey.Builder appendDetailsToRuleKey(RuleKey.Builder builder) {
return builder
.setReflectively("rDotJavaPackage", rDotJavaPackageArgument)
.setReflectively("hasWhitelistedStrings", hasWhitelistedStrings);
}
@Override
@Nullable
public Path getPathToOutputFile() {
return Optional.fromNullable(pathToTextSymbolsFile).orNull();
}
@Override
@Nullable
public Path getPathToTextSymbolsFile() {
return pathToTextSymbolsFile;
}
@Override
public Sha1HashCode getTextSymbolsAbiKey() {
return buildOutputInitializer.getBuildOutput().textSymbolsAbiKey;
}
@Override
public String getRDotJavaPackage() {
String rDotJavaPackage = rDotJavaPackageSupplier.get();
if (rDotJavaPackage == null) {
throw new RuntimeException("No package for " + getBuildTarget());
}
return rDotJavaPackage;
}
@Override
public BuildableProperties getProperties() {
return PROPERTIES;
}
@Override
public Sha1HashCode getAbiKeyForDeps() {
return HasAndroidResourceDeps.ABI_HASHER.apply(
FluentIterable.from(getNonEmptyResourceDeps())
.toSortedSet(HasBuildTarget.BUILD_TARGET_COMPARATOR));
}
private Iterable<HasAndroidResourceDeps> getNonEmptyResourceDeps() {
return FluentIterable.from(getDeps())
.filter(HasAndroidResourceDeps.class)
.filter(NON_EMPTY_RESOURCE);
}
@Override
public BuildOutput initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) {
Sha1HashCode sha1HashCode = onDiskBuildInfo.getHash(METADATA_KEY_FOR_ABI).get();
Optional<String> rDotJavaPackageFromAndroidManifest = onDiskBuildInfo.getValue(
METADATA_KEY_FOR_R_DOT_JAVA_PACKAGE);
if (rDotJavaPackageFromAndroidManifest.isPresent()) {
rDotJavaPackage.set(rDotJavaPackageFromAndroidManifest.get());
}
return new BuildOutput(sha1HashCode);
}
@Override
public BuildOutputInitializer<BuildOutput> getBuildOutputInitializer() {
return buildOutputInitializer;
}
@Override
public Iterable<AndroidPackageable> getRequiredPackageables() {
return AndroidPackageableCollector.getPackageableRules(deps);
}
@Override
public void addToCollector(AndroidPackageableCollector collector) {
if (res != null) {
if (hasWhitelistedStrings) {
collector.addStringWhitelistedResourceDirectory(getBuildTarget(), res);
} else {
collector.addResourceDirectory(getBuildTarget(), res);
}
}
if (assets != null) {
collector.addAssetsDirectory(getBuildTarget(), assets);
}
if (manifestFile != null) {
collector.addManifestFile(getBuildTarget(), getResolver().getPath(manifestFile));
}
}
public static class BuildOutput {
private final Sha1HashCode textSymbolsAbiKey;
public BuildOutput(Sha1HashCode textSymbolsAbiKey) {
this.textSymbolsAbiKey = textSymbolsAbiKey;
}
}
}