/* * Copyright 2014-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.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.InitializableFromDisk; import com.facebook.buck.rules.OnDiskBuildInfo; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.args.Arg; import com.facebook.buck.shell.BashStep; import com.facebook.buck.step.AbstractExecutionStep; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.StepExecutionResult; import com.facebook.buck.util.Escaper; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.ObjectMappers; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import javax.annotation.Nullable; import org.apache.commons.compress.utils.IOUtils; /** * Buildable that is responsible for taking a set of res/ directories and applying an optional * resource filter to them, ultimately generating the final set of res/ directories whose contents * should be included in an APK. * * <p>Clients of this Buildable may need to know: * * <ul> * <li>The set of res/ directories that was used to calculate the R.java file. (These are needed * as arguments to aapt to create the unsigned APK, as well as arguments to create a ProGuard * config, if appropriate.) * <li>The set of non-english {@code strings.xml} files identified by the resource filter. * </ul> */ public class ResourcesFilter extends AbstractBuildRule implements FilteredResourcesProvider, InitializableFromDisk<ResourcesFilter.BuildOutput> { private static final String RES_DIRECTORIES_KEY = "res_directories"; private static final String STRING_FILES_KEY = "string_files"; enum ResourceCompressionMode { DISABLED(/* isCompressResources */ false, /* isStoreStringsAsAssets */ false), ENABLED(/* isCompressResources */ true, /* isStoreStringsAsAssets */ false), ENABLED_STRINGS_ONLY(/* isCompressResources */ false, /* isStoreStringsAsAssets */ true), ENABLED_WITH_STRINGS_AS_ASSETS( /* isCompressResources */ true, /* isStoreStringsAsAssets */ true), ; private final boolean isCompressResources; private final boolean isStoreStringsAsAssets; ResourceCompressionMode(boolean isCompressResources, boolean isStoreStringsAsAssets) { this.isCompressResources = isCompressResources; this.isStoreStringsAsAssets = isStoreStringsAsAssets; } public boolean isCompressResources() { return isCompressResources; } public boolean isStoreStringsAsAssets() { return isStoreStringsAsAssets; } } // Rule key correctness is ensured by depping on all android_resource rules in // Builder.setAndroidResourceDepsFinder() private final ImmutableList<SourcePath> resDirectories; private final ImmutableSet<SourcePath> whitelistedStringDirs; @AddToRuleKey private final ImmutableSet<String> locales; @AddToRuleKey private final ResourceCompressionMode resourceCompressionMode; @AddToRuleKey private final FilterResourcesStep.ResourceFilter resourceFilter; @AddToRuleKey private final Optional<Arg> postFilterResourcesCmd; private final BuildOutputInitializer<BuildOutput> buildOutputInitializer; public ResourcesFilter( BuildRuleParams params, ImmutableList<SourcePath> resDirectories, ImmutableSet<SourcePath> whitelistedStringDirs, ImmutableSet<String> locales, ResourceCompressionMode resourceCompressionMode, FilterResourcesStep.ResourceFilter resourceFilter, Optional<Arg> postFilterResourcesCmd) { super(params); this.resDirectories = resDirectories; this.whitelistedStringDirs = whitelistedStringDirs; this.locales = locales; this.resourceCompressionMode = resourceCompressionMode; this.resourceFilter = resourceFilter; this.buildOutputInitializer = new BuildOutputInitializer<>(params.getBuildTarget(), this); this.postFilterResourcesCmd = postFilterResourcesCmd; } @Override public ImmutableList<Path> getResDirectories() { return buildOutputInitializer.getBuildOutput().resDirectories; } @Override public ImmutableList<Path> getStringFiles() { return buildOutputInitializer.getBuildOutput().stringFiles; } @Override public Optional<BuildRule> getResourceFilterRule() { return Optional.of(this); } @Override public boolean hasResources() { return !resDirectories.isEmpty(); } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, final BuildableContext buildableContext) { ImmutableList.Builder<Step> steps = ImmutableList.builder(); final ImmutableList.Builder<Path> filteredResDirectoriesBuilder = ImmutableList.builder(); ImmutableSet<Path> whitelistedStringPaths = whitelistedStringDirs .stream() .map( sourcePath -> getProjectFilesystem() .relativize(context.getSourcePathResolver().getAbsolutePath(sourcePath))) .collect(MoreCollectors.toImmutableSet()); ImmutableList<Path> resPaths = resDirectories .stream() .map( sourcePath -> getProjectFilesystem() .relativize(context.getSourcePathResolver().getAbsolutePath(sourcePath))) .collect(MoreCollectors.toImmutableList()); ImmutableBiMap<Path, Path> inResDirToOutResDirMap = createInResDirToOutResDirMap(resPaths, filteredResDirectoriesBuilder); final FilterResourcesStep filterResourcesStep = createFilterResourcesStep(whitelistedStringPaths, locales, inResDirToOutResDirMap); steps.add(filterResourcesStep); final ImmutableList.Builder<Path> stringFilesBuilder = ImmutableList.builder(); // The list of strings.xml files is only needed to build string assets if (resourceCompressionMode.isStoreStringsAsAssets()) { GetStringsFilesStep getStringsFilesStep = new GetStringsFilesStep(getProjectFilesystem(), resPaths, stringFilesBuilder); steps.add(getStringsFilesStep); } final ImmutableList<Path> filteredResDirectories = filteredResDirectoriesBuilder.build(); for (Path outputResourceDir : filteredResDirectories) { buildableContext.recordArtifact(outputResourceDir); } postFilterResourcesCmd.ifPresent( cmd -> { OutputStream filterResourcesDataOutputStream = null; try { Path filterResourcesDataPath = getFilterResourcesDataPath(); getProjectFilesystem().createParentDirs(filterResourcesDataPath); filterResourcesDataOutputStream = getProjectFilesystem().newFileOutputStream(filterResourcesDataPath); writeFilterResourcesData(filterResourcesDataOutputStream, inResDirToOutResDirMap); buildableContext.recordArtifact(filterResourcesDataPath); addPostFilterCommandSteps( cmd, context.getSourcePathResolver(), steps, filterResourcesDataPath); } catch (IOException e) { throw new RuntimeException("Could not generate/save filter resources data json", e); } finally { IOUtils.closeQuietly(filterResourcesDataOutputStream); } }); steps.add( new AbstractExecutionStep("record_build_output") { @Override public StepExecutionResult execute(ExecutionContext context) { buildableContext.addMetadata( RES_DIRECTORIES_KEY, filteredResDirectories .stream() .map(Object::toString) .collect(MoreCollectors.toImmutableList())); buildableContext.addMetadata( STRING_FILES_KEY, stringFilesBuilder .build() .stream() .map(Object::toString) .collect(MoreCollectors.toImmutableList())); return StepExecutionResult.SUCCESS; } }); return steps.build(); } @VisibleForTesting void addPostFilterCommandSteps( Arg command, SourcePathResolver sourcePathResolver, ImmutableList.Builder<Step> steps, Path dataPath) { ImmutableList.Builder<String> commandLineBuilder = new ImmutableList.Builder<>(); command.appendToCommandLine(commandLineBuilder, sourcePathResolver); commandLineBuilder.add(Escaper.escapeAsBashString(dataPath)); String commandLine = Joiner.on(' ').join(commandLineBuilder.build()); steps.add(new BashStep(getProjectFilesystem().getRootPath(), commandLine)); } private Path getFilterResourcesDataPath() { return BuildTargets.getGenPath( getProjectFilesystem(), getBuildTarget(), "%s/post_filter_resources_data.json"); } @VisibleForTesting void writeFilterResourcesData( OutputStream outputStream, ImmutableBiMap<Path, Path> inResDirToOutResDirMap) throws IOException { ObjectMappers.WRITER.writeValue( outputStream, ImmutableMap.of("res_dir_map", inResDirToOutResDirMap)); } /** * Sets up filtering of resources, images/drawables and strings in particular, based on build rule * parameters {@link #resourceFilter} and {@link #resourceCompressionMode}. * * <p>{@link com.facebook.buck.android.FilterResourcesStep.ResourceFilter} {@code resourceFilter} * determines which drawables end up in the APK (based on density - mdpi, hdpi etc), and also * whether higher density drawables get scaled down to the specified density (if not present). * * <p>{@link #resourceCompressionMode} determines whether non-english string resources are * packaged separately as assets (and not bundled together into the {@code resources.arsc} file). * * @param whitelistedStringDirs overrides storing non-english strings as assets for resources * inside these directories. */ @VisibleForTesting FilterResourcesStep createFilterResourcesStep( ImmutableSet<Path> whitelistedStringDirs, ImmutableSet<String> locales, ImmutableBiMap<Path, Path> resSourceToDestDirMap) { FilterResourcesStep.Builder filterResourcesStepBuilder = FilterResourcesStep.builder() .setProjectFilesystem(getProjectFilesystem()) .setInResToOutResDirMap(resSourceToDestDirMap) .setResourceFilter(resourceFilter); if (resourceCompressionMode.isStoreStringsAsAssets()) { filterResourcesStepBuilder.enableStringWhitelisting(); filterResourcesStepBuilder.setWhitelistedStringDirs(whitelistedStringDirs); } filterResourcesStepBuilder.setLocales(locales); return filterResourcesStepBuilder.build(); } @VisibleForTesting ImmutableBiMap<Path, Path> createInResDirToOutResDirMap( ImmutableList<Path> resourceDirectories, ImmutableList.Builder<Path> filteredResDirectories) { ImmutableBiMap.Builder<Path, Path> filteredResourcesDirMapBuilder = ImmutableBiMap.builder(); String resDestinationBasePath = getResDestinationBasePath(); int count = 0; for (Path resDir : resourceDirectories) { Path filteredResourceDir = Paths.get(resDestinationBasePath, String.valueOf(count++)); filteredResourcesDirMapBuilder.put(resDir, filteredResourceDir); filteredResDirectories.add(filteredResourceDir); } return filteredResourcesDirMapBuilder.build(); } private String getResDestinationBasePath() { return BuildTargets.getScratchPath(getProjectFilesystem(), getBuildTarget(), "__filtered__%s__") .toString(); } @Override public BuildOutput initializeFromDisk(OnDiskBuildInfo onDiskBuildInfo) { ImmutableList<Path> resDirectories = onDiskBuildInfo .getValues(RES_DIRECTORIES_KEY) .get() .stream() .map(Paths::get) .collect(MoreCollectors.toImmutableList()); ImmutableList<Path> stringFiles = onDiskBuildInfo .getValues(STRING_FILES_KEY) .get() .stream() .map(Paths::get) .collect(MoreCollectors.toImmutableList()); return new BuildOutput(resDirectories, stringFiles); } @Override public BuildOutputInitializer<BuildOutput> getBuildOutputInitializer() { return buildOutputInitializer; } public static class BuildOutput { private final ImmutableList<Path> resDirectories; private final ImmutableList<Path> stringFiles; public BuildOutput(ImmutableList<Path> resDirectories, ImmutableList<Path> stringFiles) { this.resDirectories = resDirectories; this.stringFiles = stringFiles; } } @Nullable @Override public SourcePath getSourcePathToOutput() { return null; } }