blob: b8f620d904a51325db684436d1c724e4990638f4 [file] [log] [blame]
/*
* Copyright (C) 2015 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.internal.pipeline;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.transform.Context;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.SecondaryFile;
import com.android.build.api.transform.SecondaryInput;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.gradle.internal.profile.AnalyticsUtil;
import com.android.build.gradle.internal.tasks.factory.TaskCreationAction;
import com.android.builder.profile.Recorder;
import com.android.ide.common.util.ReferenceHolder;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.wireless.android.sdk.stats.GradleBuildProfileSpan.ExecutionType;
import com.google.wireless.android.sdk.stats.GradleTransformExecution;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.gradle.api.Project;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectories;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.OutputFiles;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.incremental.IncrementalTaskInputs;
import org.gradle.workers.WorkerExecutor;
/** A task running a transform. */
@CacheableTask
public abstract class TransformTask extends StreamBasedTask implements Context {
private Transform transform;
private Recorder recorder;
Collection<SecondaryFile> secondaryFiles = null;
List<FileCollection> secondaryInputFiles = null;
@NonNull private final WorkerExecutor workerExecutor;
public Transform getTransform() {
return transform;
}
@Inject
public TransformTask(@NonNull WorkerExecutor workerExecutor) {
this.workerExecutor = workerExecutor;
}
@Input
@NonNull
public Set<? super QualifiedContent.Scope> getScopes() {
return transform.getScopes();
}
@Input
@NonNull
public Set<? super QualifiedContent.Scope> getReferencedScopes() {
return transform.getReferencedScopes();
}
@Input
@NonNull
public Set<QualifiedContent.ContentType> getInputTypes() {
return transform.getInputTypes();
}
@InputFiles
@Optional
public Collection<File> getOldSecondaryInputs() {
//noinspection deprecation: Needed for backward compatibility.
return transform.getSecondaryFileInputs();
}
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public List<FileCollection> getSecondaryFileInputs() {
if (secondaryInputFiles == null) {
secondaryInputFiles = transform.getSecondaryFiles().stream()
.map(secondaryFile -> secondaryFile.getFileCollection(getProject()))
.collect(Collectors.toList());
}
return secondaryInputFiles;
}
@OutputFiles
public Map<String, File> getOtherFileOutputs() {
ImmutableMap.Builder<String, File> builder = new ImmutableMap.Builder<>();
int index = 0;
for (File outputFile : transform.getSecondaryFileOutputs()) {
builder.put("otherFileOutput" + Integer.toString(++index), outputFile);
}
return builder.build();
}
@OutputDirectory
@PathSensitive(PathSensitivity.RELATIVE)
@Optional
@NonNull
public abstract DirectoryProperty getOutputDirectory();
@OutputDirectories
public Map<String, File> getOtherFolderOutputs() {
ImmutableMap.Builder<String, File> builder = new ImmutableMap.Builder<>();
int index = 0;
for (File outputFolder : transform.getSecondaryDirectoryOutputs()) {
builder.put("otherFolderOutput" + Integer.toString(++index), outputFolder);
}
return builder.build();
}
@Input
public Map<String, Object> getOtherInputs() {
return transform.getParameterInputs();
}
@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs)
throws IOException, TransformException, InterruptedException {
final ReferenceHolder<List<TransformInput>> consumedInputs = ReferenceHolder.empty();
final ReferenceHolder<List<TransformInput>> referencedInputs = ReferenceHolder.empty();
final ReferenceHolder<Boolean> isIncremental = ReferenceHolder.empty();
final ReferenceHolder<Collection<SecondaryInput>> changedSecondaryInputs =
ReferenceHolder.empty();
isIncremental.setValue(transform.isIncremental() && incrementalTaskInputs.isIncremental());
GradleTransformExecution preExecutionInfo =
GradleTransformExecution.newBuilder()
.setType(AnalyticsUtil.getTransformType(transform.getClass()).getNumber())
.setIsIncremental(isIncremental.getValue())
.build();
recorder.record(
ExecutionType.TASK_TRANSFORM_PREPARATION,
preExecutionInfo,
getProject().getPath(),
getVariantName(),
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
Map<File, Status> changedMap = Maps.newHashMap();
Set<File> removedFiles = Sets.newHashSet();
if (isIncremental.getValue()) {
// gather the changed files first.
gatherChangedFiles(
getLogger(), incrementalTaskInputs, changedMap, removedFiles);
// and check against secondary files, which disables
// incremental mode.
isIncremental.setValue(checkSecondaryFiles(changedMap, removedFiles));
}
if (isIncremental.getValue()) {
// ok create temporary incremental data
List<IncrementalTransformInput> incInputs =
createIncrementalInputs(consumedInputStreams);
List<IncrementalTransformInput> incReferencedInputs =
createIncrementalInputs(referencedInputStreams);
// then compare to changed list and create final Inputs
if (isIncremental.setValue(
updateIncrementalInputsWithChangedFiles(
incInputs,
incReferencedInputs,
changedMap,
removedFiles))) {
consumedInputs.setValue(convertToImmutable(incInputs));
referencedInputs.setValue(convertToImmutable(incReferencedInputs));
}
}
// at this point if we do not have incremental mode, got with
// default TransformInput with no inc data.
if (!isIncremental.getValue()) {
consumedInputs.setValue(
computeNonIncTransformInput(consumedInputStreams));
referencedInputs.setValue(
computeNonIncTransformInput(referencedInputStreams));
changedSecondaryInputs.setValue(ImmutableList.of());
} else {
// gather all secondary input changes.
changedSecondaryInputs.setValue(
gatherSecondaryInputChanges(changedMap, removedFiles));
}
return null;
}
});
GradleTransformExecution executionInfo =
preExecutionInfo.toBuilder().setIsIncremental(isIncremental.getValue()).build();
recorder.record(
ExecutionType.TASK_TRANSFORM,
executionInfo,
getProject().getPath(),
getVariantName(),
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
transform.transform(
new TransformInvocationBuilder(TransformTask.this)
.addInputs(consumedInputs.getValue())
.addReferencedInputs(referencedInputs.getValue())
.addSecondaryInputs(changedSecondaryInputs.getValue())
.addOutputProvider(
outputStream != null
? outputStream.asOutput(
isIncremental.getValue())
: null)
.setIncrementalMode(isIncremental.getValue())
.build());
if (outputStream != null) {
outputStream.save();
}
return null;
}
});
}
private Collection<SecondaryInput> gatherSecondaryInputChanges(
Map<File, Status> changedMap, Set<File> removedFiles) {
final Project project = getProject();
ImmutableList.Builder<SecondaryInput> builder = ImmutableList.builder();
for (final SecondaryFile secondaryFile : getAllSecondaryInputs()) {
for (File file : secondaryFile.getFileCollection(project).getFiles()) {
final Status status = changedMap.containsKey(file)
? changedMap.get(file)
: removedFiles.contains(file)
? Status.REMOVED
: Status.NOTCHANGED;
builder.add(new SecondaryInput() {
@Override
public SecondaryFile getSecondaryInput() {
return secondaryFile;
}
@Override
public Status getStatus() {
return status;
}
});
}
}
return builder.build();
}
@Override
public String getProjectName() {
return getProject().getName();
}
/**
* Returns a list of non incremental TransformInput.
* @param streams the streams.
* @return a list of non-incremental TransformInput matching the content of the streams.
*/
@NonNull
private static List<TransformInput> computeNonIncTransformInput(
@NonNull Collection<TransformStream> streams) {
return streams.stream()
.map(TransformStream::asNonIncrementalInput)
.collect(Collectors.toList());
}
/**
* Returns a list of IncrementalTransformInput for all the inputs.
*/
@NonNull
private static List<IncrementalTransformInput> createIncrementalInputs(
@NonNull Collection<TransformStream> streams) {
return streams.stream()
.map(TransformStream::asIncrementalInput)
.collect(Collectors.toList());
}
@Internal
private synchronized Collection<SecondaryFile> getAllSecondaryInputs() {
if (secondaryFiles == null) {
ImmutableList.Builder<SecondaryFile> builder = ImmutableList.builder();
builder.addAll(transform.getSecondaryFiles());
//noinspection deprecation
builder.addAll(
transform
.getSecondaryFileInputs()
.stream()
.map(SecondaryFile::nonIncremental)
.iterator());
secondaryFiles = builder.build();
}
return secondaryFiles;
}
private static void gatherChangedFiles(
@NonNull Logger logger,
@NonNull IncrementalTaskInputs incrementalTaskInputs,
@NonNull final Map<File, Status> changedFileMap,
@NonNull final Set<File> removedFiles) {
logger.info("Transform inputs calculations based on following changes");
incrementalTaskInputs.outOfDate(inputFileDetails -> {
logger.info(inputFileDetails.getFile().getAbsolutePath() + ":"
+ IntermediateFolderUtils.inputFileDetailsToStatus(inputFileDetails));
if (inputFileDetails.isAdded()) {
changedFileMap.put(inputFileDetails.getFile(), Status.ADDED);
} else if (inputFileDetails.isModified()) {
changedFileMap.put(inputFileDetails.getFile(), Status.CHANGED);
}
});
incrementalTaskInputs.removed(
inputFileDetails -> {
logger.info(inputFileDetails.getFile().getAbsolutePath() + ":REMOVED");
removedFiles.add(inputFileDetails.getFile());
});
}
private boolean checkSecondaryFiles(
@NonNull Map<File, Status> changedMap,
@NonNull Set<File> removedFiles) {
final Project project = getProject();
for (SecondaryFile secondaryFile : getAllSecondaryInputs()) {
Set<File> files = secondaryFile.getFileCollection(project).getFiles();
if ((!Sets.intersection(files, changedMap.keySet()).isEmpty()
|| !Sets.intersection(files, removedFiles).isEmpty())
&& !secondaryFile.supportsIncrementalBuild()) {
return false;
}
}
return true;
}
private boolean isSecondaryFile(File file) {
final Project project = getProject();
for (SecondaryFile secondaryFile : getAllSecondaryInputs()) {
if (secondaryFile.getFileCollection(project).contains(file)) {
return true;
}
}
return false;
}
private boolean updateIncrementalInputsWithChangedFiles(
@NonNull List<IncrementalTransformInput> consumedInputs,
@NonNull List<IncrementalTransformInput> referencedInputs,
@NonNull Map<File, Status> changedFilesMap,
@NonNull Set<File> removedFiles) {
// we're going to concat both list multiple times, and the Iterators API ultimately put
// all the iterators to concat in a list. So let's reuse a list.
List<Iterator<IncrementalTransformInput>> iterators = Lists.newArrayListWithCapacity(2);
Splitter splitter = Splitter.on(File.separatorChar);
final Sets.SetView<? super QualifiedContent.Scope> scopes =
Sets.union(transform.getScopes(), transform.getReferencedScopes());
final Set<QualifiedContent.ContentType> inputTypes = transform.getInputTypes();
// start with the removed files as they carry the risk of removing incremental mode.
// If we detect such a case, we stop immediately.
for (File removedFile : removedFiles) {
List<String> removedFileSegments = Lists.newArrayList(
splitter.split(removedFile.getAbsolutePath()));
Iterator<IncrementalTransformInput> iterator = getConcatIterator(consumedInputs,
referencedInputs, iterators);
boolean found = false;
while (iterator.hasNext()) {
IncrementalTransformInput next = iterator.next();
if (next.checkRemovedJarFile(scopes, inputTypes, removedFile, removedFileSegments)
|| next.checkRemovedFolderFile(
scopes, inputTypes, removedFile, removedFileSegments)) {
found = true;
break;
}
}
if (!found && !isSecondaryFile(removedFile)) {
// this deleted file breaks incremental because we cannot figure out where it's
// coming from and what types/scopes is associated with it.
return false;
}
}
// now handle the added/changed files.
for (Map.Entry<File, Status> entry : changedFilesMap.entrySet()) {
File changedFile = entry.getKey();
Status changedStatus = entry.getValue();
// first go through the jars first as it's a faster check.
Iterator<IncrementalTransformInput> iterator = getConcatIterator(consumedInputs,
referencedInputs, iterators);
boolean found = false;
while (iterator.hasNext()) {
if (iterator.next().checkForJar(changedFile, changedStatus)) {
// we can skip to the next changed file.
found = true;
break;
}
}
if (found) {
continue;
}
// now go through the folders. First get a segment list for the path.
iterator = getConcatIterator(consumedInputs,
referencedInputs, iterators);
List<String> changedSegments = Lists.newArrayList(
splitter.split(changedFile.getAbsolutePath()));
while (iterator.hasNext()) {
if (iterator.next().checkForFolder(changedFile, changedSegments, changedStatus)) {
// we can skip to the next changed file.
break;
}
}
}
return true;
}
@NonNull
private static Iterator<IncrementalTransformInput> getConcatIterator(
@NonNull List<IncrementalTransformInput> consumedInputs,
@NonNull List<IncrementalTransformInput> referencedInputs,
List<Iterator<IncrementalTransformInput>> iterators) {
iterators.clear();
iterators.add(consumedInputs.iterator());
iterators.add(referencedInputs.iterator());
return Iterators.concat(iterators.iterator());
}
@NonNull
private static List<TransformInput> convertToImmutable(
@NonNull List<IncrementalTransformInput> inputs) {
return inputs.stream()
.map(IncrementalTransformInput::asImmutable)
.collect(Collectors.toList());
}
@NonNull
@Override
public WorkerExecutor getWorkerExecutor() {
return workerExecutor;
}
public static class CreationAction<T extends Transform>
extends TaskCreationAction<TransformTask> {
@NonNull
private final String variantName;
@NonNull
private final String taskName;
@NonNull
private final T transform;
@NonNull
private Collection<TransformStream> consumedInputStreams;
@NonNull
private Collection<TransformStream> referencedInputStreams;
@Nullable
private IntermediateStream outputStream;
@NonNull private final Recorder recorder;
CreationAction(
@NonNull String variantName,
@NonNull String taskName,
@NonNull T transform,
@NonNull Collection<TransformStream> consumedInputStreams,
@NonNull Collection<TransformStream> referencedInputStreams,
@Nullable IntermediateStream outputStream,
@NonNull Recorder recorder) {
this.variantName = variantName;
this.taskName = taskName;
this.transform = transform;
this.consumedInputStreams = consumedInputStreams;
this.referencedInputStreams = referencedInputStreams;
this.outputStream = outputStream;
this.recorder = recorder;
}
@NonNull
@Override
public String getName() {
return taskName;
}
@NonNull
@Override
public Class<TransformTask> getType() {
return TransformTask.class;
}
@Override
public void configure(@NonNull TransformTask task) {
task.transform = transform;
transform.setOutputDirectory(task.getOutputDirectory());
task.consumedInputStreams = consumedInputStreams;
task.referencedInputStreams = referencedInputStreams;
task.outputStream = outputStream;
task.setVariantName(variantName);
task.recorder = recorder;
task.getOutputs().cacheIf(t -> transform.isCacheable());
task.registerConsumedAndReferencedStreamInputs();
}
}
}