/* * 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.rules.AbstractBuildRule; import com.facebook.buck.rules.AddToRuleKey; 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.ExplicitBuildTargetSourcePath; import com.facebook.buck.rules.InitializableFromDisk; import com.facebook.buck.rules.OnDiskBuildInfo; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.keys.SupportsInputBasedRuleKey; import com.facebook.buck.step.Step; import com.facebook.buck.step.fs.MakeCleanDirectoryStep; import com.facebook.buck.step.fs.TouchStep; import com.facebook.buck.step.fs.WriteFileStep; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.MoreMaps; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedMap; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Ordering; import java.nio.file.Path; 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/BUCK</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 AndroidPackageable, HasAndroidResourceDeps, InitializableFromDisk<String>, SupportsInputBasedRuleKey { private static final BuildableProperties PROPERTIES = new BuildableProperties(ANDROID, LIBRARY); @VisibleForTesting static final String METADATA_KEY_FOR_R_DOT_JAVA_PACKAGE = "METADATA_KEY_FOR_R_DOT_JAVA_PACKAGE"; @AddToRuleKey @Nullable private final SourcePath res; @SuppressWarnings("PMD.UnusedPrivateField") @AddToRuleKey private final ImmutableSortedMap<String, SourcePath> resSrcs; @AddToRuleKey @Nullable private final SourcePath assets; @SuppressWarnings("PMD.UnusedPrivateField") @AddToRuleKey private final ImmutableSortedMap<String, SourcePath> assetsSrcs; private final Path pathToTextSymbolsDir; private final Path pathToTextSymbolsFile; private final Path pathToRDotJavaPackageFile; @AddToRuleKey @Nullable private final SourcePath manifestFile; @AddToRuleKey private final Supplier<ImmutableSortedSet<? extends SourcePath>> symbolsOfDeps; @AddToRuleKey private final boolean hasWhitelistedStrings; @AddToRuleKey private final boolean resourceUnion; private final boolean isGrayscaleImageProcessingEnabled; private final ImmutableSortedSet<BuildRule> deps; private final BuildOutputInitializer<String> buildOutputInitializer; /** This is the original {@code package} argument passed to this rule. */ @AddToRuleKey @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; public AndroidResource( BuildRuleParams buildRuleParams, SourcePathRuleFinder ruleFinder, final ImmutableSortedSet<BuildRule> deps, @Nullable SourcePath res, ImmutableSortedMap<Path, SourcePath> resSrcs, @Nullable String rDotJavaPackageArgument, @Nullable SourcePath assets, ImmutableSortedMap<Path, SourcePath> assetsSrcs, @Nullable SourcePath manifestFile, Supplier<ImmutableSortedSet<? extends SourcePath>> symbolFilesFromDeps, boolean hasWhitelistedStrings, boolean resourceUnion, boolean isGrayscaleImageProcessingEnabled) { super( buildRuleParams.copyAppendingExtraDeps( Suppliers.compose(ruleFinder::filterBuildRuleInputs, symbolFilesFromDeps))); 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 = MoreMaps.transformKeysAndSort(resSrcs, Path::toString); this.assets = assets; this.assetsSrcs = MoreMaps.transformKeysAndSort(assetsSrcs, Path::toString); this.manifestFile = manifestFile; this.symbolsOfDeps = symbolFilesFromDeps; this.hasWhitelistedStrings = hasWhitelistedStrings; this.resourceUnion = resourceUnion; BuildTarget buildTarget = buildRuleParams.getBuildTarget(); this.pathToTextSymbolsDir = BuildTargets.getGenPath(getProjectFilesystem(), buildTarget, "__%s_text_symbols__"); this.pathToTextSymbolsFile = pathToTextSymbolsDir.resolve("R.txt"); this.pathToRDotJavaPackageFile = pathToTextSymbolsDir.resolve("RDotJavaPackage.txt"); this.deps = deps; this.buildOutputInitializer = new BuildOutputInitializer<>(buildTarget, this); this.rDotJavaPackageArgument = rDotJavaPackageArgument; this.rDotJavaPackage = new AtomicReference<>(rDotJavaPackageArgument); this.rDotJavaPackageSupplier = () -> { String rDotJavaPackage1 = AndroidResource.this.rDotJavaPackage.get(); if (rDotJavaPackage1 != null) { return rDotJavaPackage1; } else { throw new RuntimeException( "rDotJavaPackage for " + AndroidResource.this.getBuildTarget().toString() + " was requested before it was made available."); } }; this.isGrayscaleImageProcessingEnabled = isGrayscaleImageProcessingEnabled; } public AndroidResource( final BuildRuleParams buildRuleParams, SourcePathRuleFinder ruleFinder, final ImmutableSortedSet<BuildRule> deps, @Nullable SourcePath res, ImmutableSortedMap<Path, SourcePath> resSrcs, @Nullable String rDotJavaPackageArgument, @Nullable SourcePath assets, ImmutableSortedMap<Path, SourcePath> assetsSrcs, @Nullable SourcePath manifestFile, boolean hasWhitelistedStrings) { this( buildRuleParams, ruleFinder, deps, res, resSrcs, rDotJavaPackageArgument, assets, assetsSrcs, manifestFile, hasWhitelistedStrings, /* resourceUnion */ false, /* isGrayscaleImageProcessingEnabled */ false); } public AndroidResource( final BuildRuleParams buildRuleParams, SourcePathRuleFinder ruleFinder, final ImmutableSortedSet<BuildRule> deps, @Nullable SourcePath res, ImmutableSortedMap<Path, SourcePath> resSrcs, @Nullable String rDotJavaPackageArgument, @Nullable SourcePath assets, ImmutableSortedMap<Path, SourcePath> assetsSrcs, @Nullable SourcePath manifestFile, boolean hasWhitelistedStrings, boolean resourceUnion, boolean isGrayscaleImageProcessingEnabled) { this( buildRuleParams, ruleFinder, deps, res, resSrcs, rDotJavaPackageArgument, assets, assetsSrcs, manifestFile, () -> FluentIterable.from(buildRuleParams.getBuildDeps()) .filter(HasAndroidResourceDeps.class) .filter(input -> input.getRes() != null) .transform(HasAndroidResourceDeps::getPathToTextSymbolsFile) .toSortedSet(Ordering.natural()), hasWhitelistedStrings, resourceUnion, isGrayscaleImageProcessingEnabled); } @Override @Nullable public SourcePath getRes() { return res; } @Override @Nullable public SourcePath getAssets() { return assets; } @Nullable public SourcePath getManifestFile() { return manifestFile; } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, final BuildableContext buildableContext) { buildableContext.recordArtifact(Preconditions.checkNotNull(pathToTextSymbolsFile)); buildableContext.recordArtifact(Preconditions.checkNotNull(pathToRDotJavaPackageFile)); ImmutableList.Builder<Step> steps = ImmutableList.builder(); steps.addAll( MakeCleanDirectoryStep.of( getProjectFilesystem(), Preconditions.checkNotNull(pathToTextSymbolsDir))); if (getRes() == null) { return steps .add(new TouchStep(getProjectFilesystem(), pathToTextSymbolsFile)) .add( new WriteFileStep( getProjectFilesystem(), rDotJavaPackageArgument == null ? "" : rDotJavaPackageArgument, pathToRDotJavaPackageFile, false /* executable */)) .build(); } // If the 'package' was not specified for this android_resource(), then attempt to parse it // from the AndroidManifest.xml. if (rDotJavaPackageArgument == null) { Preconditions.checkNotNull( manifestFile, "manifestFile cannot be null when res is non-null and rDotJavaPackageArgument is " + "null. This should already be enforced by the constructor."); steps.add( new ExtractFromAndroidManifestStep( context.getSourcePathResolver().getAbsolutePath(manifestFile), getProjectFilesystem(), buildableContext, METADATA_KEY_FOR_R_DOT_JAVA_PACKAGE, Preconditions.checkNotNull(pathToRDotJavaPackageFile))); } else { steps.add( new WriteFileStep( getProjectFilesystem(), rDotJavaPackageArgument, pathToRDotJavaPackageFile, false /* executable */)); } ImmutableSet<Path> pathsToSymbolsOfDeps = symbolsOfDeps .get() .stream() .map(context.getSourcePathResolver()::getAbsolutePath) .collect(MoreCollectors.toImmutableSet()); steps.add( new MiniAapt( context.getSourcePathResolver(), getProjectFilesystem(), Preconditions.checkNotNull(res), Preconditions.checkNotNull(pathToTextSymbolsFile), pathsToSymbolsOfDeps, resourceUnion, isGrayscaleImageProcessingEnabled)); return steps.build(); } @Override @Nullable public SourcePath getSourcePathToOutput() { return new ExplicitBuildTargetSourcePath(getBuildTarget(), pathToTextSymbolsDir); } @Override public SourcePath getPathToTextSymbolsFile() { return new ExplicitBuildTargetSourcePath(getBuildTarget(), pathToTextSymbolsFile); } @Override public SourcePath getPathToRDotJavaPackageFile() { return new ExplicitBuildTargetSourcePath(getBuildTarget(), pathToRDotJavaPackageFile); } @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 String initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) { String rDotJavaPackageFromFile = getProjectFilesystem().readFirstLine(pathToRDotJavaPackageFile).get(); if (rDotJavaPackageArgument != null && !rDotJavaPackageFromFile.equals(rDotJavaPackageArgument)) { throw new RuntimeException( String.format( "%s contains incorrect rDotJavaPackage (%s!=%s)", pathToRDotJavaPackageFile, rDotJavaPackageFromFile, rDotJavaPackageArgument)); } rDotJavaPackage.set(rDotJavaPackageFromFile); return rDotJavaPackageFromFile; } @Override public BuildOutputInitializer<String> 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); } } }