blob: f74893c45a8a30ba007ecda4f6e5c5d6af5ae66a [file] [log] [blame]
/*
* Copyright (C) 2018 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.tasks;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.build.gradle.internal.LoggerWrapper;
import com.android.ide.common.workers.WorkerExecutorFacade;
import com.android.utils.FileUtils;
import com.android.utils.PathUtils;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.inject.Inject;
import org.gradle.api.file.Directory;
import org.gradle.api.file.FileCollection;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.incremental.IncrementalTaskInputs;
import org.jacoco.core.instr.Instrumenter;
import org.jacoco.core.runtime.OfflineInstrumentationAccessGenerator;
/** Delegate for {@link JacocoTask}. */
public class JacocoTaskDelegate {
private static final Pattern CLASS_PATTERN = Pattern.compile(".*\\.class$");
// META-INF/*.kotlin_module files need to be copied to output so they show up
// in the intermediate classes jar.
private static final Pattern KOTLIN_MODULE_PATTERN =
Pattern.compile("^META-INF/.*\\.kotlin_module$");
@NonNull private final FileCollection jacocoAntTaskConfiguration;
@NonNull private final Provider<Directory> output;
@NonNull private final FileCollection inputClasses;
@NonNull private final WorkerExecutorFacade.IsolationMode isolationMode;
@NonNull private final Provider<Directory> outputJars;
public JacocoTaskDelegate(
@NonNull FileCollection jacocoAntTaskConfiguration,
@NonNull Provider<Directory> output,
@NonNull Provider<Directory> outputJars,
@NonNull FileCollection inputClasses,
@NonNull WorkerExecutorFacade.IsolationMode isolationMode) {
this.jacocoAntTaskConfiguration = jacocoAntTaskConfiguration;
this.output = output;
this.outputJars = outputJars;
this.inputClasses = inputClasses;
this.isolationMode = isolationMode;
}
public static class WorkerItemParameter implements Serializable {
final Map<Action, List<File>> nonIncToProcess;
final File root;
final File output;
public WorkerItemParameter(
Map<Action, List<File>> nonIncToProcess, File root, File output) {
this.nonIncToProcess = nonIncToProcess;
this.root = root;
this.output = output;
}
}
public void run(@NonNull WorkerExecutorFacade executor, @NonNull IncrementalTaskInputs inputs)
throws IOException {
if (inputs.isIncremental()) {
processIncrementally(executor, inputs);
} else {
File outputJarsFolder = outputJars.get().getAsFile();
FileUtils.cleanOutputDir(output.get().getAsFile());
FileUtils.cleanOutputDir(outputJarsFolder);
for (File file : inputClasses.getFiles()) {
if (file.isDirectory()) {
Map<Action, List<File>> nonIncToProcess =
getFilesForInstrumentationNonIncrementally(file);
WorkerItemParameter parameter =
new WorkerItemParameter(
nonIncToProcess, file, output.get().getAsFile());
executor.submit(
JacocoWorkerAction.class,
new WorkerExecutorFacade.Configuration(
parameter,
isolationMode,
jacocoAntTaskConfiguration.getFiles(),
ImmutableList.of()));
} else { // We expect *.jar files here
if (!file.getName().endsWith(SdkConstants.DOT_JAR)) {
continue;
}
executor.submit(
JacocoJarWorkerAction.class,
new WorkerExecutorFacade.Configuration(
new WorkerItemParameter(null, file, outputJarsFolder),
isolationMode,
jacocoAntTaskConfiguration.getFiles(),
ImmutableList.of()));
}
}
}
}
private void processIncrementally(
@NonNull WorkerExecutorFacade executor, @NonNull IncrementalTaskInputs inputs)
throws IOException {
Multimap<Path, Path> basePathToRemove =
Multimaps.newSetMultimap(new HashMap<>(), HashSet::new);
Multimap<Path, Path> basePathToProcess =
Multimaps.newSetMultimap(new HashMap<>(), HashSet::new);
Set<Path> baseDirs = new HashSet<>(inputClasses.getFiles().size());
for (File file : inputClasses.getFiles()) {
if (file.isDirectory()) {
baseDirs.add(file.toPath());
}
}
Set<File> jarsToRemove = new HashSet<>();
Set<File> jarsToProcess = new HashSet<>();
inputs.outOfDate(
info -> {
File file = info.getFile();
if (file.getName().endsWith(SdkConstants.DOT_JAR)) {
if (info.isAdded()) {
jarsToProcess.add(file);
} else if (info.isModified()) {
jarsToRemove.add(file);
jarsToProcess.add(file);
} else if (info.isRemoved()) {
jarsToRemove.add(file);
}
} else {
Path filePath = file.toPath();
Path baseDir = findBase(baseDirs, filePath);
if (info.isAdded()) {
basePathToProcess.put(baseDir, filePath);
} else if (info.isModified()) {
basePathToRemove.put(baseDir, filePath);
basePathToProcess.put(baseDir, filePath);
} else if (info.isRemoved()) {
basePathToRemove.put(baseDir, filePath);
}
}
});
inputs.removed(
info -> {
File file = info.getFile();
if (file.getName().endsWith(SdkConstants.DOT_JAR)) {
jarsToRemove.add(file);
} else {
Path filePath = file.toPath();
Path baseDir = findBase(baseDirs, filePath);
basePathToRemove.put(baseDir, filePath);
}
});
// remove old output
for (Path basePath : basePathToRemove.keys()) {
for (Path toRemove : basePathToRemove.get(basePath)) {
Action action = calculateAction(toRemove.toFile(), basePath.toFile());
if (action == Action.IGNORE) {
continue;
}
Path outputPath =
getOutputPath(basePath, toRemove, output.get().getAsFile().toPath());
PathUtils.deleteRecursivelyIfExists(outputPath);
}
}
File outputJarsFolder = outputJars.get().getAsFile();
for (File jarToRemove : jarsToRemove) {
File instrumentedJar = getCorrespondingInstrumentedJar(outputJarsFolder, jarToRemove);
FileUtils.delete(instrumentedJar);
}
// process changes
for (Path basePath : basePathToProcess.keySet()) {
Map<Action, List<File>> toProcess = new EnumMap<>(Action.class);
for (Path changed : basePathToProcess.get(basePath)) {
Action action = calculateAction(changed.toFile(), basePath.toFile());
if (action == Action.IGNORE) {
continue;
}
List<File> byAction = toProcess.getOrDefault(action, new ArrayList<>());
byAction.add(changed.toFile());
toProcess.put(action, byAction);
}
executor.submit(
JacocoWorkerAction.class,
new WorkerExecutorFacade.Configuration(
new WorkerItemParameter(
toProcess, basePath.toFile(), output.get().getAsFile()),
isolationMode,
jacocoAntTaskConfiguration.getFiles(),
ImmutableList.of()));
}
for (File jarToProcess : jarsToProcess) {
executor.submit(
JacocoJarWorkerAction.class,
new WorkerExecutorFacade.Configuration(
new WorkerItemParameter(null, jarToProcess, outputJarsFolder),
isolationMode,
jacocoAntTaskConfiguration.getFiles(),
ImmutableList.of()));
}
}
@NonNull
private static Path findBase(@NonNull Set<Path> baseDirs, @NonNull Path file) {
for (Path baseDir : baseDirs) {
if (file.startsWith(baseDir)) {
return baseDir;
}
}
throw new RuntimeException(
String.format(
"Unable to find base directory for %s. List of base dirs: %s",
file,
baseDirs.stream().map(Path::toString).collect(Collectors.joining(","))));
}
@NonNull
private static Path getOutputPath(
@NonNull Path baseDir, @NonNull Path inputFile, @NonNull Path outputBaseDir) {
Path relativePath = baseDir.relativize(inputFile);
return outputBaseDir.resolve(relativePath);
}
@NonNull
private static Map<Action, List<File>> getFilesForInstrumentationNonIncrementally(
@NonNull File inputDir) {
Map<Action, List<File>> toProcess = Maps.newHashMap();
Iterable<File> files = FileUtils.getAllFiles(inputDir);
for (File inputFile : files) {
Action fileAction = calculateAction(inputFile, inputDir);
switch (fileAction) {
case COPY:
// fall through
case INSTRUMENT:
List<File> actionFiles = toProcess.getOrDefault(fileAction, new ArrayList<>());
actionFiles.add(inputFile);
toProcess.put(fileAction, actionFiles);
break;
case IGNORE:
// do nothing
break;
default:
throw new AssertionError("Unsupported Action: " + fileAction);
}
}
return toProcess;
}
private static Action calculateAction(@NonNull File inputFile, @NonNull File inputDir) {
final String inputRelativePath =
FileUtils.toSystemIndependentPath(
FileUtils.relativePossiblyNonExistingPath(inputFile, inputDir));
return calculateAction(inputRelativePath);
}
private static Action calculateAction(@NonNull String inputRelativePath) {
for (Pattern pattern : Action.COPY.getPatterns()) {
if (pattern.matcher(inputRelativePath).matches()) {
return Action.COPY;
}
}
for (Pattern pattern : Action.INSTRUMENT.getPatterns()) {
if (pattern.matcher(inputRelativePath).matches()) {
return Action.INSTRUMENT;
}
}
return Action.IGNORE;
}
/** The possible actions which can happen to an input file */
private enum Action {
/** The file is just copied to the transform output. */
COPY(KOTLIN_MODULE_PATTERN),
/** The file is ignored. */
IGNORE(),
/** The file is instrumented and added to the transform output. */
INSTRUMENT(CLASS_PATTERN);
private final ImmutableList<Pattern> patterns;
/**
* @param patterns Patterns are compared to files' relative paths to determine if they
* undergo the corresponding action.
*/
Action(@NonNull Pattern... patterns) {
ImmutableList.Builder<Pattern> builder = new ImmutableList.Builder<>();
for (Pattern pattern : patterns) {
Preconditions.checkNotNull(pattern);
builder.add(pattern);
}
this.patterns = builder.build();
}
@NonNull
ImmutableList<Pattern> getPatterns() {
return patterns;
}
}
private static class JacocoWorkerAction implements Runnable {
@NonNull
private static final LoggerWrapper logger =
LoggerWrapper.getLogger(JacocoWorkerAction.class);
@NonNull private Map<Action, List<File>> inputs;
@NonNull private File inputDir;
@NonNull private File outputDir;
@Inject
public JacocoWorkerAction(@NonNull WorkerItemParameter workerItemParameter) {
this.inputs = workerItemParameter.nonIncToProcess;
this.inputDir = workerItemParameter.root;
this.outputDir = workerItemParameter.output;
}
@Override
public void run() {
Instrumenter instrumenter =
new Instrumenter(new OfflineInstrumentationAccessGenerator());
logger.info("Processing entries from input dir: " + inputDir.getAbsolutePath());
for (File toInstrument : inputs.getOrDefault(Action.INSTRUMENT, ImmutableList.of())) {
logger.info("Instrumenting file: " + toInstrument.getAbsolutePath());
try (InputStream inputStream =
Files.asByteSource(toInstrument).openBufferedStream()) {
byte[] instrumented =
instrumenter.instrument(inputStream, toInstrument.toString());
File outputFile =
new File(outputDir, FileUtils.relativePath(toInstrument, inputDir));
Files.createParentDirs(outputFile);
Files.write(instrumented, outputFile);
} catch (IOException e) {
throw new UncheckedIOException(
"Unable to instrument file with Jacoco: " + toInstrument, e);
}
}
for (File toCopy : inputs.getOrDefault(Action.COPY, ImmutableList.of())) {
File outputFile = new File(outputDir, FileUtils.relativePath(toCopy, inputDir));
try {
Files.createParentDirs(outputFile);
Files.copy(toCopy, outputFile);
} catch (IOException e) {
throw new UncheckedIOException("Unable to copy file: " + toCopy, e);
}
}
}
}
private static class JacocoJarWorkerAction implements Runnable {
@NonNull private File inputJar;
@NonNull private File outputDir;
@Inject
public JacocoJarWorkerAction(@NonNull WorkerItemParameter workerItemParameter) {
this.inputJar = workerItemParameter.root;
this.outputDir = workerItemParameter.output;
}
@Override
public void run() {
Instrumenter instrumenter =
new Instrumenter(new OfflineInstrumentationAccessGenerator());
File instrumentedJar = getCorrespondingInstrumentedJar(outputDir, inputJar);
try (ZipOutputStream outputZip =
new ZipOutputStream(
new BufferedOutputStream(new FileOutputStream(instrumentedJar)))) {
try (ZipFile zipFile = new ZipFile(inputJar)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String entryName = entry.getName();
Action entryAction = calculateAction(entryName);
if (entryAction == Action.IGNORE) {
continue;
}
InputStream classInputStream = zipFile.getInputStream(entry);
byte[] data;
if (entryAction == Action.INSTRUMENT) {
data = instrumenter.instrument(classInputStream, entryName);
} else { // just copy
data = ByteStreams.toByteArray(classInputStream);
}
ZipEntry nextEntry = new ZipEntry(entryName);
// Any negative time value sets ZipEntry's xdostime to DOSTIME_BEFORE_1980
// constant.
nextEntry.setTime(-1L);
outputZip.putNextEntry(nextEntry);
outputZip.write(data);
outputZip.closeEntry();
}
}
} catch (IOException e) {
throw new UncheckedIOException(
"Unable to instrument file with Jacoco: " + inputJar, e);
}
}
}
private static File getCorrespondingInstrumentedJar(
@NonNull File outputFolder, @NonNull File file) {
return new File(
outputFolder,
Hashing.sha256()
.hashBytes(file.getAbsolutePath().getBytes(StandardCharsets.UTF_8))
.toString()
+ SdkConstants.DOT_JAR);
}
}