blob: df4321fca3a06dd623091e906bdb88bcd31b3033 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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.android.build.gradle.tasks;
import static com.android.SdkConstants.FD_RES_VALUES;
import static com.android.build.gradle.internal.TaskManager.MergeType.MERGE;
import static com.android.build.gradle.internal.scope.InternalArtifactType.DATA_BINDING_LAYOUT_INFO_TYPE_MERGE;
import static com.android.build.gradle.internal.scope.InternalArtifactType.DATA_BINDING_LAYOUT_INFO_TYPE_PACKAGE;
import static com.android.ide.common.resources.AndroidAaptIgnoreKt.ANDROID_AAPT_IGNORE;
import static com.google.common.base.Preconditions.checkState;
import android.databinding.tool.LayoutXmlProcessor;
import android.databinding.tool.util.RelativizableFile;
import android.databinding.tool.writer.JavaFileWriter;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.component.impl.ComponentPropertiesImpl;
import com.android.build.gradle.internal.LoggerWrapper;
import com.android.build.gradle.internal.TaskManager;
import com.android.build.gradle.internal.aapt.WorkerExecutorResourceCompilationService;
import com.android.build.gradle.internal.databinding.MergingFileLookup;
import com.android.build.gradle.internal.errors.MessageReceiverImpl;
import com.android.build.gradle.internal.res.Aapt2MavenUtils;
import com.android.build.gradle.internal.res.namespaced.NamespaceRemover;
import com.android.build.gradle.internal.scope.BuildFeatureValues;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.InternalArtifactType;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.services.Aapt2DaemonBuildService;
import com.android.build.gradle.internal.services.Aapt2DaemonServiceKey;
import com.android.build.gradle.internal.services.Aapt2WorkersBuildService;
import com.android.build.gradle.internal.services.BuildServicesKt;
import com.android.build.gradle.internal.tasks.Blocks;
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction;
import com.android.build.gradle.internal.utils.HasConfigurableValuesKt;
import com.android.build.gradle.internal.variant.VariantPathHelper;
import com.android.build.gradle.options.BooleanOption;
import com.android.build.gradle.options.SyncOptions;
import com.android.builder.model.VectorDrawablesOptions;
import com.android.builder.png.VectorDrawableRenderer;
import com.android.ide.common.blame.MergingLog;
import com.android.ide.common.resources.CopyToOutputDirectoryResourceCompilationService;
import com.android.ide.common.resources.FileStatus;
import com.android.ide.common.resources.FileValidity;
import com.android.ide.common.resources.GeneratedResourceSet;
import com.android.ide.common.resources.MergedResourceWriter;
import com.android.ide.common.resources.MergingException;
import com.android.ide.common.resources.NoOpResourcePreprocessor;
import com.android.ide.common.resources.ResourceCompilationService;
import com.android.ide.common.resources.ResourceMerger;
import com.android.ide.common.resources.ResourcePreprocessor;
import com.android.ide.common.resources.ResourceSet;
import com.android.ide.common.resources.SingleFileProcessor;
import com.android.ide.common.vectordrawable.ResourcesNotSupportedException;
import com.android.ide.common.workers.WorkerExecutorFacade;
import com.android.resources.Density;
import com.android.utils.FileUtils;
import com.android.utils.ILogger;
import com.google.common.collect.ImmutableSet;
import com.google.wireless.android.sdk.stats.GradleBuildProfileSpan;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBException;
import kotlin.Pair;
import org.gradle.api.GradleException;
import org.gradle.api.artifacts.ArtifactCollection;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.logging.Logger;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.SetProperty;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskProvider;
@CacheableTask
public abstract class MergeResources extends ResourceAwareTask {
// ----- PUBLIC TASK API -----
/**
* Directory to write the merged resources to
*/
private File generatedPngsOutputDir;
// ----- PRIVATE TASK API -----
/**
* Optional file to write any publicly imported resource types and names to
*/
private boolean processResources;
private boolean crunchPng;
private List<ResourceSet> processedInputs;
private final FileValidity<ResourceSet> fileValidity = new FileValidity<>();
private boolean disableVectorDrawables;
private boolean vectorSupportLibraryIsUsed;
private Collection<String> generatedDensities;
private String aapt2Version;
@Internal
public abstract ConfigurableFileCollection getAapt2FromMaven();
@Nullable private File mergedNotCompiledResourcesOutputDirectory;
private boolean pseudoLocalesEnabled;
private boolean precompileDependenciesResources;
private ImmutableSet<Flag> flags;
@Input
public abstract Property<Boolean> getDataBindingEnabled();
@Input
public abstract Property<Boolean> getViewBindingEnabled();
@Input
@Optional
public abstract Property<String> getPackageName();
@Input
@Optional
public abstract Property<Boolean> getUseAndroidX();
/**
* Set of absolute paths to resource directories that are located outside of the root project
* directory when data binding / view binding is enabled.
*
* <p>These absolute paths appear in the layout info files generated by data binding, so we have
* to mark them as @Input to ensure build correctness. (If this set is not empty, this task is
* still cacheable but not relocatable.)
*
* <p>If data binding / view binding is not enabled, this set is empty as it won't be used.
*/
@Input
public abstract SetProperty<String> getResourceDirsOutsideRootProjectDir();
@Internal
public abstract Property<Aapt2WorkersBuildService> getAapt2WorkersBuildService();
@Internal
public abstract Property<Aapt2DaemonBuildService> getAapt2DaemonBuildService();
private boolean useJvmResourceCompiler;
@NonNull
private static ResourceCompilationService getResourceProcessor(
String projectName,
String owner,
@Nullable FileCollection aapt2FromMaven,
@NonNull WorkerExecutorFacade workerExecutor,
SyncOptions.ErrorFormatMode errorFormatMode,
ImmutableSet<Flag> flags,
boolean processResources,
boolean useJvmResourceCompiler,
Logger logger,
Aapt2DaemonBuildService aapt2DaemonBuildService) {
// If we received the flag for removing namespaces we need to use the namespace remover to
// process the resources.
if (flags.contains(Flag.REMOVE_RESOURCE_NAMESPACES)) {
return NamespaceRemover.INSTANCE;
}
// If we're not removing namespaces and there's no need to compile the resources, return a
// no-op resource processor.
if (!processResources) {
return CopyToOutputDirectoryResourceCompilationService.INSTANCE;
}
Aapt2DaemonServiceKey aapt2ServiceKey =
aapt2DaemonBuildService.registerAaptService(
aapt2FromMaven.getSingleFile(), new LoggerWrapper(logger));
return new WorkerExecutorResourceCompilationService(
projectName,
owner,
workerExecutor,
aapt2ServiceKey,
errorFormatMode,
useJvmResourceCompiler);
}
@Override
protected boolean getIncremental() {
return true;
}
@Internal
@NonNull
public WorkerExecutorFacade getAaptWorkerFacade() {
return getAapt2WorkersBuildService()
.get()
.getWorkerForAapt2(
getProjectName(),
getPath(),
getWorkerExecutor(),
getEnableGradleWorkers().get());
}
@NonNull
@OutputDirectory
@Optional
public abstract DirectoryProperty getDataBindingLayoutInfoOutFolder();
private SyncOptions.ErrorFormatMode errorFormatMode;
@Internal
public abstract Property<String> getAaptEnv();
@Internal
public abstract RegularFileProperty getProjectRootDir();
@Override
protected void doFullTaskAction() throws IOException, JAXBException {
ResourcePreprocessor preprocessor = getPreprocessor();
// this is full run, clean the previous outputs
File destinationDir = getOutputDir().get().getAsFile();
FileUtils.cleanOutputDir(destinationDir);
if (getDataBindingLayoutInfoOutFolder().isPresent()) {
FileUtils.deleteDirectoryContents(
getDataBindingLayoutInfoOutFolder().get().getAsFile());
}
List<ResourceSet> resourceSets =
getConfiguredResourceSets(preprocessor, getAaptEnv().getOrNull());
// create a new merger and populate it with the sets.
ResourceMerger merger = new ResourceMerger(getMinSdk().get());
MergingLog mergingLog = null;
if (getBlameLogOutputFolder().isPresent()) {
File blameLogFolder = getBlameLogOutputFolder().get().getAsFile();
FileUtils.cleanOutputDir(blameLogFolder);
mergingLog = new MergingLog(blameLogFolder);
}
try (WorkerExecutorFacade workerExecutorFacade = getAaptWorkerFacade();
ResourceCompilationService resourceCompiler =
getResourceProcessor(
getProjectName(),
getPath(),
getAapt2FromMaven(),
workerExecutorFacade,
errorFormatMode,
flags,
processResources,
useJvmResourceCompiler,
getLogger(),
getAapt2DaemonBuildService().get())) {
SingleFileProcessor dataBindingLayoutProcessor = maybeCreateLayoutProcessor();
Blocks.recordSpan(
getProjectName(),
getPath(),
GradleBuildProfileSpan.ExecutionType.TASK_EXECUTION_PHASE_1,
() -> {
for (ResourceSet resourceSet : resourceSets) {
resourceSet.loadFromFiles(new LoggerWrapper(getLogger()));
merger.addDataSet(resourceSet);
}
});
File publicFile =
getPublicFile().isPresent() ? getPublicFile().get().getAsFile() : null;
MergedResourceWriter writer =
new MergedResourceWriter(
workerExecutorFacade,
destinationDir,
publicFile,
mergingLog,
preprocessor,
resourceCompiler,
getIncrementalFolder(),
dataBindingLayoutProcessor,
mergedNotCompiledResourcesOutputDirectory,
pseudoLocalesEnabled,
getCrunchPng());
Blocks.recordSpan(
getProjectName(),
getPath(),
GradleBuildProfileSpan.ExecutionType.TASK_EXECUTION_PHASE_2,
() -> merger.mergeData(writer, false /*doCleanUp*/));
Blocks.recordSpan(
getProjectName(),
getPath(),
GradleBuildProfileSpan.ExecutionType.TASK_EXECUTION_PHASE_3,
() -> {
if (dataBindingLayoutProcessor != null) {
dataBindingLayoutProcessor.end();
}
});
// No exception? Write the known state.
Blocks.recordSpan(
getProjectName(),
getPath(),
GradleBuildProfileSpan.ExecutionType.TASK_EXECUTION_PHASE_4,
() -> merger.writeBlobTo(getIncrementalFolder(), writer, false));
} catch (Exception e) {
MergingException.findAndReportMergingException(
e, new MessageReceiverImpl(errorFormatMode, getLogger()));
try {
throw e;
} catch (MergingException mergingException) {
merger.cleanBlob(getIncrementalFolder());
throw new ResourceException(mergingException.getMessage(), mergingException);
}
} finally {
cleanup();
}
}
/**
* Check if the changed file is filtered out from the input to be compiled in compile library
* resources task, if it is then it should be ignored.
*/
private boolean isFilteredOutLibraryResource(File changedFile) {
ArtifactCollection localLibraryResources = getResourcesComputer().getLibraries();
File parentFile = changedFile.getParentFile();
if (localLibraryResources == null || parentFile.getName().startsWith(FD_RES_VALUES)) {
return false;
}
for (File resDir : localLibraryResources.getArtifactFiles()) {
if (parentFile.getAbsolutePath().startsWith(resDir.getAbsolutePath())) {
return true;
}
}
return false;
}
@Override
protected void doIncrementalTaskAction(@NonNull Map<File, ? extends FileStatus> changedInputs)
throws IOException, JAXBException {
ResourcePreprocessor preprocessor = getPreprocessor();
// create a merger and load the known state.
ResourceMerger merger = new ResourceMerger(getMinSdk().get());
try {
if (!merger.loadFromBlob(
getIncrementalFolder(), true /*incrementalState*/, getAaptEnv().getOrNull())) {
doFullTaskAction();
return;
}
if (precompileDependenciesResources) {
changedInputs =
changedInputs
.entrySet()
.stream()
.filter(
fileEntry ->
!isFilteredOutLibraryResource(fileEntry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (changedInputs.isEmpty()) {
return;
}
}
for (ResourceSet resourceSet : merger.getDataSets()) {
resourceSet.setPreprocessor(preprocessor);
}
List<ResourceSet> resourceSets =
getConfiguredResourceSets(preprocessor, getAaptEnv().getOrNull());
// compare the known state to the current sets to detect incompatibility.
// This is in case there's a change that's too hard to do incrementally. In this case
// we'll simply revert to full build.
if (!merger.checkValidUpdate(resourceSets)) {
getLogger().info("Changed Resource sets: full task run!");
doFullTaskAction();
return;
}
// The incremental process is the following:
// Loop on all the changed files, find which ResourceSet it belongs to, then ask
// the resource set to update itself with the new file.
for (Map.Entry<File, ? extends FileStatus> entry : changedInputs.entrySet()) {
File changedFile = entry.getKey();
merger.findDataSetContaining(changedFile, fileValidity);
if (fileValidity.getStatus() == FileValidity.FileStatus.UNKNOWN_FILE) {
doFullTaskAction();
return;
} else if (fileValidity.getStatus() == FileValidity.FileStatus.VALID_FILE) {
if (!fileValidity
.getDataSet()
.updateWith(
fileValidity.getSourceFile(),
changedFile,
entry.getValue(),
new LoggerWrapper(getLogger()))) {
getLogger().info(
String.format("Failed to process %s event! Full task run",
entry.getValue()));
doFullTaskAction();
return;
}
}
}
MergingLog mergingLog =
getBlameLogOutputFolder().isPresent()
? new MergingLog(getBlameLogOutputFolder().get().getAsFile())
: null;
try (WorkerExecutorFacade workerExecutorFacade = getAaptWorkerFacade();
ResourceCompilationService resourceCompiler =
getResourceProcessor(
getProjectName(),
getPath(),
getAapt2FromMaven(),
workerExecutorFacade,
errorFormatMode,
flags,
processResources,
useJvmResourceCompiler,
getLogger(),
getAapt2DaemonBuildService().get())) {
SingleFileProcessor dataBindingLayoutProcessor = maybeCreateLayoutProcessor();
File publicFile =
getPublicFile().isPresent() ? getPublicFile().get().getAsFile() : null;
MergedResourceWriter writer =
new MergedResourceWriter(
workerExecutorFacade,
getOutputDir().get().getAsFile(),
publicFile,
mergingLog,
preprocessor,
resourceCompiler,
getIncrementalFolder(),
dataBindingLayoutProcessor,
mergedNotCompiledResourcesOutputDirectory,
pseudoLocalesEnabled,
getCrunchPng());
merger.mergeData(writer, false /*doCleanUp*/);
if (dataBindingLayoutProcessor != null) {
dataBindingLayoutProcessor.end();
}
// No exception? Write the known state.
merger.writeBlobTo(getIncrementalFolder(), writer, false);
}
} catch (Exception e) {
MergingException.findAndReportMergingException(
e, new MessageReceiverImpl(errorFormatMode, getLogger()));
try {
throw e;
} catch (MergingException mergingException) {
merger.cleanBlob(getIncrementalFolder());
throw new ResourceException(mergingException.getMessage(), mergingException);
}
} finally {
cleanup();
}
}
@Nullable
private SingleFileProcessor maybeCreateLayoutProcessor() {
if (!getDataBindingEnabled().get() && !getViewBindingEnabled().get()) {
return null;
}
LayoutXmlProcessor.OriginalFileLookup fileLookup;
if (getBlameLogOutputFolder().isPresent()) {
fileLookup = new MergingFileLookup(getBlameLogOutputFolder().get().getAsFile());
} else {
fileLookup = file -> null;
}
final LayoutXmlProcessor processor =
new LayoutXmlProcessor(
getPackageName().get(),
new JavaFileWriter() {
// These methods are not supposed to be used, they are here only because
// the superclass requires an implementation of it.
// Whichever is calling this method is probably using it incorrectly
// (see stacktrace).
@Override
public void writeToFile(String canonicalName, String contents) {
throw new UnsupportedOperationException(
"Not supported in this mode");
}
@Override
public void deleteFile(String canonicalName) {
throw new UnsupportedOperationException(
"Not supported in this mode");
}
},
fileLookup,
getUseAndroidX().get());
return new SingleFileProcessor() {
private LayoutXmlProcessor getProcessor() {
return processor;
}
@Override
public boolean processSingleFile(
@NonNull File inputFile,
@NonNull File outputFile,
@Nullable Boolean inputFileIsFromDependency)
throws Exception {
// Data binding doesn't need/want to process layout files that come
// from dependencies (see bug 132637061).
if (inputFileIsFromDependency == Boolean.TRUE) {
return false;
}
// For cache relocatability, we want to use relative paths, but we
// can do that only for resource files that are located within the
// root project directory.
File rootProjectDir = getProjectRootDir().getAsFile().get();
RelativizableFile normalizedInputFile;
if (FileUtils.isFileInDirectory(inputFile, rootProjectDir)) {
// Check that the input file's absolute path has NOT been
// annotated as @Input via this task's
// getResourceDirsOutsideRootProjectDir() input file property.
checkState(
!resourceIsInResourceDirs(
inputFile, getResourceDirsOutsideRootProjectDir().get()),
inputFile.getAbsolutePath() + " should not be annotated as @Input");
// The base directory of the relative path has to be the root
// project directory, not the current project directory, because
// the consumer on the IDE side relies on that---see bug
// 128643036.
normalizedInputFile =
RelativizableFile.fromAbsoluteFile(
inputFile.getCanonicalFile(), rootProjectDir);
checkState(normalizedInputFile.getRelativeFile() != null);
} else {
// Check that the input file's absolute path has been annotated
// as @Input via this task's
// getResourceDirsOutsideRootProjectDir() input file property.
checkState(
resourceIsInResourceDirs(
inputFile, getResourceDirsOutsideRootProjectDir().get()),
inputFile.getAbsolutePath() + " is not annotated as @Input");
normalizedInputFile =
RelativizableFile.fromAbsoluteFile(inputFile.getCanonicalFile(), null);
checkState(normalizedInputFile.getRelativeFile() == null);
}
return getProcessor()
.processSingleFile(
normalizedInputFile,
outputFile,
getViewBindingEnabled().get(),
getDataBindingEnabled().get());
}
/**
* Returns `true` if the given resource file is located inside one of the resource
* directories.
*/
private boolean resourceIsInResourceDirs(
@NonNull File resFile, @NonNull Set<String> resDirs) {
return resDirs.stream()
.anyMatch(resDir -> FileUtils.isFileInDirectory(resFile, new File(resDir)));
}
@Override
public void processRemovedFile(File file) {
getProcessor().processRemovedFile(file);
}
@Override
public void processFileWithNoDataBinding(@NonNull File file) {
getProcessor().processFileWithNoDataBinding(file);
}
@Override
public void end() throws JAXBException {
getProcessor()
.writeLayoutInfoFiles(
getDataBindingLayoutInfoOutFolder().get().getAsFile());
}
};
}
private static class MergeResourcesVectorDrawableRenderer extends VectorDrawableRenderer {
public MergeResourcesVectorDrawableRenderer(
int minSdk,
boolean supportLibraryIsUsed,
File outputDir,
Collection<Density> densities,
Supplier<ILogger> loggerSupplier) {
super(minSdk, supportLibraryIsUsed, outputDir, densities, loggerSupplier);
}
@Override
public void generateFile(@NonNull File toBeGenerated, @NonNull File original)
throws IOException {
try {
super.generateFile(toBeGenerated, original);
} catch (ResourcesNotSupportedException e) {
// Add gradle-specific error message.
throw new GradleException(
String.format(
"Can't process attribute %1$s=\"%2$s\": references to other"
+ " resources are not supported by build-time PNG"
+ " generation.\n"
+ "%3$s\n"
+ "See http://developer.android.com/tools/help/vector-asset-studio.html"
+ " for details.",
e.getName(),
e.getValue(),
getPreprocessingReasonDescription(original)));
}
}
}
/**
* Only one pre-processor for now. The code will need slight changes when we add more.
*/
@NonNull
private ResourcePreprocessor getPreprocessor() {
if (disableVectorDrawables) {
// If the user doesn't want any PNGs, leave the XML file alone as well.
return NoOpResourcePreprocessor.INSTANCE;
}
Collection<Density> densities =
getGeneratedDensities().stream().map(Density::getEnum).collect(Collectors.toList());
return new MergeResourcesVectorDrawableRenderer(
getMinSdk().get(),
vectorSupportLibraryIsUsed,
generatedPngsOutputDir,
densities,
LoggerWrapper.supplierFor(MergeResources.class));
}
@NonNull
private List<ResourceSet> getConfiguredResourceSets(
ResourcePreprocessor preprocessor, @Nullable String aaptEnv) {
// It is possible that this get called twice in case the incremental run fails and reverts
// back to full task run. Because the cached ResourceList is modified we don't want
// to recompute this twice (plus, why recompute it twice anyway?)
if (processedInputs == null) {
processedInputs =
getResourcesComputer().compute(precompileDependenciesResources, aaptEnv);
List<ResourceSet> generatedSets = new ArrayList<>(processedInputs.size());
for (ResourceSet resourceSet : processedInputs) {
resourceSet.setPreprocessor(preprocessor);
ResourceSet generatedSet = new GeneratedResourceSet(resourceSet, aaptEnv);
resourceSet.setGeneratedSet(generatedSet);
generatedSets.add(generatedSet);
}
// We want to keep the order of the inputs. Given inputs:
// (A, B, C, D)
// We want to get:
// (A-generated, A, B-generated, B, C-generated, C, D-generated, D).
// Therefore, when later in {@link DataMerger} we look for sources going through the
// list backwards, B-generated will take priority over A (but not B).
// A real life use-case would be if an app module generated resource overrode a library
// module generated resource (existing not in generated but bundled dir at this stage):
// (lib, app debug, app main)
// We will get:
// (lib generated, lib, app debug generated, app debug, app main generated, app main)
for (int i = 0; i < generatedSets.size(); ++i) {
processedInputs.add(2 * i, generatedSets.get(i));
}
}
return processedInputs;
}
/**
* Releases resource sets not needed anymore, otherwise they will waste heap space for the
* duration of the build.
*
* <p>This might be called twice when an incremental build falls back to a full one.
*/
private void cleanup() {
fileValidity.clear();
processedInputs = null;
}
@OutputDirectory
public abstract DirectoryProperty getOutputDir();
@Input
public boolean getCrunchPng() {
return crunchPng;
}
@Input
public boolean getProcessResources() {
return processResources;
}
@Optional
@OutputFile
public abstract RegularFileProperty getPublicFile();
// Synthetic input: the validation flag is set on the resource sets in CreationAction.execute.
@Input
public boolean isValidateEnabled() {
return getResourcesComputer().getValidateEnabled();
}
// the optional blame output folder for the case where the task generates it.
@OutputDirectory
@Optional
public abstract DirectoryProperty getBlameLogOutputFolder();
@Optional
@OutputDirectory
public File getGeneratedPngsOutputDir() {
return generatedPngsOutputDir;
}
@Input
public Collection<String> getGeneratedDensities() {
return generatedDensities;
}
@Input
public abstract Property<Integer> getMinSdk();
@Input
public boolean isVectorSupportLibraryUsed() {
return vectorSupportLibraryIsUsed;
}
@Input
public String getAapt2Version() {
return aapt2Version;
}
@Nullable
@OutputDirectory
@Optional
public abstract DirectoryProperty getMergedNotCompiledResourcesOutputDirectory();
@Input
public boolean isPseudoLocalesEnabled() {
return pseudoLocalesEnabled;
}
@Input
public String getFlags() {
return flags.stream().map(Enum::name).sorted().collect(Collectors.joining(","));
}
@Input
public boolean isJvmResourceCompilerEnabled() {
return useJvmResourceCompiler;
}
public static class CreationAction
extends VariantTaskCreationAction<MergeResources, ComponentPropertiesImpl> {
@NonNull private final TaskManager.MergeType mergeType;
@NonNull
private final String taskNamePrefix;
@Nullable private final File mergedNotCompiledOutputDirectory;
private final boolean includeDependencies;
private final boolean processResources;
private final boolean processVectorDrawables;
@NonNull private final ImmutableSet<Flag> flags;
private boolean isLibrary;
public CreationAction(
@NonNull ComponentPropertiesImpl componentProperties,
@NonNull TaskManager.MergeType mergeType,
@NonNull String taskNamePrefix,
@Nullable File mergedNotCompiledOutputDirectory,
boolean includeDependencies,
boolean processResources,
@NonNull ImmutableSet<Flag> flags,
boolean isLibrary) {
super(componentProperties);
this.mergeType = mergeType;
this.taskNamePrefix = taskNamePrefix;
this.mergedNotCompiledOutputDirectory = mergedNotCompiledOutputDirectory;
this.includeDependencies = includeDependencies;
this.processResources = processResources;
this.processVectorDrawables = flags.contains(Flag.PROCESS_VECTOR_DRAWABLES);
this.flags = flags;
this.isLibrary = isLibrary;
}
@NonNull
@Override
public String getName() {
return computeTaskName(taskNamePrefix, "Resources");
}
@NonNull
@Override
public Class<MergeResources> getType() {
return MergeResources.class;
}
@Override
public void handleProvider(@NonNull TaskProvider<MergeResources> taskProvider) {
super.handleProvider(taskProvider);
// In LibraryTaskManager#createMergeResourcesTasks, there are actually two
// MergeResources tasks sharing the same task type (MergeResources) and CreationAction
// code: packageResources with mergeType == PACKAGE, and mergeResources with
// mergeType == MERGE. Since the following line of code is called for each task, the
// latter one wins: The mergeResources task with mergeType == MERGE is the one that is
// finally registered in the current scope.
// Filed https://issuetracker.google.com//110412851 to clean this up at some point.
creationConfig.getTaskContainer().setMergeResourcesTask(taskProvider);
creationConfig
.getArtifacts()
.setInitialProvider(
taskProvider, MergeResources::getDataBindingLayoutInfoOutFolder)
.withName("out")
.on(
mergeType == MERGE
? DATA_BINDING_LAYOUT_INFO_TYPE_MERGE.INSTANCE
: DATA_BINDING_LAYOUT_INFO_TYPE_PACKAGE.INSTANCE);
// only the full run with dependencies generates the blame folder
if (includeDependencies) {
creationConfig
.getArtifacts()
.setInitialProvider(taskProvider, MergeResources::getBlameLogOutputFolder)
.withName("out")
.on(InternalArtifactType.MERGED_RES_BLAME_FOLDER.INSTANCE);
}
}
@Override
public void configure(@NonNull MergeResources task) {
super.configure(task);
VariantScope variantScope = creationConfig.getVariantScope();
GlobalScope globalScope = creationConfig.getGlobalScope();
VariantPathHelper paths = creationConfig.getPaths();
task.getMinSdk()
.set(
globalScope
.getProject()
.provider(
() -> creationConfig.getMinSdkVersion().getApiLevel()));
task.getMinSdk().disallowChanges();
Pair<FileCollection, String> aapt2AndVersion =
Aapt2MavenUtils.getAapt2FromMavenAndVersion(globalScope);
task.getAapt2FromMaven().from(aapt2AndVersion.getFirst());
task.aapt2Version = aapt2AndVersion.getSecond();
task.setIncrementalFolder(paths.getIncrementalDir(getName()));
task.processResources = processResources;
task.crunchPng = variantScope.isCrunchPngs();
VectorDrawablesOptions vectorDrawablesOptions =
creationConfig.getVariantDslInfo().getVectorDrawables();
task.generatedDensities = vectorDrawablesOptions.getGeneratedDensities();
if (task.generatedDensities == null) {
task.generatedDensities = Collections.emptySet();
}
task.disableVectorDrawables =
!processVectorDrawables || task.generatedDensities.isEmpty();
// TODO: When support library starts supporting gradients (http://b/62421666), remove
// the vectorSupportLibraryIsUsed field and set disableVectorDrawables when
// the getUseSupportLibrary method returns TRUE.
task.vectorSupportLibraryIsUsed =
Boolean.TRUE.equals(vectorDrawablesOptions.getUseSupportLibrary());
task.getResourcesComputer().initFromVariantScope(creationConfig, includeDependencies);
if (!task.disableVectorDrawables) {
task.generatedPngsOutputDir = paths.getGeneratedPngsOutputDir();
}
final BuildFeatureValues features = creationConfig.getBuildFeatures();
final boolean isDataBindingEnabled = features.getDataBinding();
boolean isViewBindingEnabled = features.getViewBinding();
HasConfigurableValuesKt.setDisallowChanges(
task.getDataBindingEnabled(), isDataBindingEnabled);
HasConfigurableValuesKt.setDisallowChanges(
task.getViewBindingEnabled(), isViewBindingEnabled);
if (isDataBindingEnabled || isViewBindingEnabled) {
HasConfigurableValuesKt.setDisallowChanges(
task.getPackageName(), creationConfig.getVariantDslInfo().getPackageName());
HasConfigurableValuesKt.setDisallowChanges(
task.getUseAndroidX(),
creationConfig
.getServices()
.getProjectOptions()
.get(BooleanOption.USE_ANDROID_X));
}
task.mergedNotCompiledResourcesOutputDirectory = mergedNotCompiledOutputDirectory;
task.pseudoLocalesEnabled = creationConfig.getVariantDslInfo().isPseudoLocalesEnabled();
task.flags = flags;
task.errorFormatMode =
SyncOptions.getErrorFormatMode(
creationConfig.getServices().getProjectOptions());
task.precompileDependenciesResources =
mergeType.equals(MERGE)
&& !isLibrary
&& variantScope.isPrecompileDependenciesResourcesEnabled();
task.getResourceDirsOutsideRootProjectDir().set(globalScope.getProject().provider(
() -> {
Set<String> resourceDirsOutsideRootProjectDir = new HashSet<>();
if (!isDataBindingEnabled && !isViewBindingEnabled) {
// This set is used only when data binding / view binding is enabled
return resourceDirsOutsideRootProjectDir;
}
// In this task, data binding doesn't process layout files that come from
// dependencies (see the code at processSingleFile()). Therefore, we'll look at
// resources in the current sub-project only (via task.getLocalResources()).
// These resources are usually located inside the root project directory, but
// some of them may come from custom resource sets that are outside of the root
// project directory, so we need to collect the latter set.
File rootProjectDir = task.getProject().getRootDir();
for (FileCollection resDirs : task.getLocalResources()) {
for (File resDir : resDirs.getFiles()) {
if (!FileUtils.isFileInDirectory(resDir, rootProjectDir)) {
resourceDirsOutsideRootProjectDir.add(resDir.getCanonicalPath());
}
}
}
return resourceDirsOutsideRootProjectDir;
}));
task.getResourceDirsOutsideRootProjectDir().disallowChanges();
task.dependsOn(creationConfig.getTaskContainer().getResourceGenTask());
// TODO(141301405): when we compile resources AAPT2 stores the absolute path of the raw
// resource in the proto (.flat) file, so we need to mark those inputs with absolute
// path sensitivity (e.g. big merge in app). Otherwise just using the relative path
// sensitivity is enough (e.g. merges in libraries).
task.getInputs()
.files(task.getLocalResources())
.withPathSensitivity(
processResources ? PathSensitivity.ABSOLUTE : PathSensitivity.RELATIVE)
.withPropertyName("rawLocalResources");
task.useJvmResourceCompiler =
creationConfig
.getServices()
.getProjectOptions()
.get(BooleanOption.ENABLE_JVM_RESOURCE_COMPILER);
HasConfigurableValuesKt.setDisallowChanges(
task.getAapt2WorkersBuildService(),
BuildServicesKt.getBuildService(
creationConfig.getServices().getBuildServiceRegistry(),
Aapt2WorkersBuildService.class));
HasConfigurableValuesKt.setDisallowChanges(
task.getAapt2DaemonBuildService(),
BuildServicesKt.getBuildService(
creationConfig.getServices().getBuildServiceRegistry(),
Aapt2DaemonBuildService.class));
task.getAaptEnv()
.set(
creationConfig
.getServices()
.getGradleEnvironmentProvider()
.getEnvVariable(ANDROID_AAPT_IGNORE));
task.getProjectRootDir().set(task.getProject().getRootDir());
}
}
public enum Flag {
REMOVE_RESOURCE_NAMESPACES,
PROCESS_VECTOR_DRAWABLES,
}
}