blob: d5f25d5fca4c55b8e80d37e1580f1e5df08687ff [file] [log] [blame]
/*
* Copyright (C) 2017 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.transforms;
import static com.android.build.api.transform.QualifiedContent.DefaultContentType.CLASSES;
import static com.android.build.gradle.internal.pipeline.ExtendedContentType.NATIVE_LIBS;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.SecondaryFile;
import com.android.build.api.transform.Status;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager;
import com.android.builder.utils.ZipEntryUtils;
import com.android.utils.FileUtils;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
* A transform that provides a simple API for transforming all the user classes. It only requires a
* function to transform from an {@link InputStream} to an {@link OutputStream} which will be run
* for every class. This transform takes care of all the incremental and jar vs class details.
*
* <p>This transform is created with a path to the .jar that provides its actual implementation. The
* jar must follow the following convention:
*
* <p>1) Expose a service of type BiConsumer<InputStream, OutputStream>. This function will be used
* invoked to transform a .class file. 2) To output additional classes as a result of the output,
* they will be looked for as resources in the given jar. All jars found in the "dependencies"
* resource directory will be output.
*/
public class CustomClassTransform extends Transform {
@NonNull private final String name;
@NonNull private final String path;
private final boolean addDependencies;
public static final Set<QualifiedContent.Scope> SCOPE_EXTERNAL =
Sets.immutableEnumSet(QualifiedContent.Scope.EXTERNAL_LIBRARIES);
/**
* Creates the transform.
*
* @param path the path to the transform code jar.
* @param addDependencies whether to add the dependencies of the transform to the streams for
* inclusion in the APK.
*/
public CustomClassTransform(@NonNull String path, boolean addDependencies) {
this.name = Files.getNameWithoutExtension(path);
this.path = path;
this.addDependencies = addDependencies;
}
@NonNull
@Override
public String getName() {
return name;
}
@NonNull
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@NonNull
@Override
public Set<QualifiedContent.ContentType> getOutputTypes() {
return addDependencies ? ImmutableSet.of(CLASSES, NATIVE_LIBS) : ImmutableSet.of(CLASSES);
}
@NonNull
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@NonNull
@Override
public Collection<SecondaryFile> getSecondaryFiles() {
return ImmutableSet.of(SecondaryFile.nonIncremental(new File(path)));
}
@Override
public boolean isIncremental() {
return true;
}
@Override
public void transform(@NonNull TransformInvocation invocation)
throws InterruptedException, IOException {
final TransformOutputProvider outputProvider = invocation.getOutputProvider();
assert outputProvider != null;
// Output the resources, we only do this if this is not incremental,
// as the secondary file is will trigger a full build if modified.
if (!invocation.isIncremental()) {
outputProvider.deleteAll();
if (addDependencies) {
// To avoid https://bugs.openjdk.java.net/browse/JDK-7183373
// we extract the resources directly as a zip file.
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(path))) {
ZipEntry entry;
Pattern pattern = Pattern.compile("dependencies/(.*)\\.jar");
while ((entry = zis.getNextEntry()) != null
&& ZipEntryUtils.isValidZipEntryName(entry)) {
Matcher matcher = pattern.matcher(entry.getName());
if (matcher.matches()) {
String name = matcher.group(1);
File outputJar =
outputProvider.getContentLocation(
name, getOutputTypes(), SCOPE_EXTERNAL, Format.JAR);
Files.createParentDirs(outputJar);
try (FileOutputStream fos = new FileOutputStream(outputJar)) {
ByteStreams.copy(zis, fos);
}
}
zis.closeEntry();
}
}
}
}
URL url = new File(path).toURI().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[] {url})) {
BiConsumer<InputStream, OutputStream> function = loadTransformFunction(loader);
for (TransformInput ti : invocation.getInputs()) {
for (JarInput jarInput : ti.getJarInputs()) {
File inputJar = jarInput.getFile();
File outputJar =
outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
if (invocation.isIncremental()) {
switch (jarInput.getStatus()) {
case NOTCHANGED:
break;
case ADDED:
case CHANGED:
transformJar(function, inputJar, outputJar);
break;
case REMOVED:
FileUtils.delete(outputJar);
break;
}
} else {
transformJar(function, inputJar, outputJar);
}
}
for (DirectoryInput di : ti.getDirectoryInputs()) {
File inputDir = di.getFile();
File outputDir =
outputProvider.getContentLocation(
di.getName(),
di.getContentTypes(),
di.getScopes(),
Format.DIRECTORY);
if (invocation.isIncremental()) {
for (Map.Entry<File, Status> entry : di.getChangedFiles().entrySet()) {
File inputFile = entry.getKey();
switch (entry.getValue()) {
case NOTCHANGED:
break;
case ADDED:
case CHANGED:
if (!inputFile.isDirectory()
&& inputFile.getName()
.endsWith(SdkConstants.DOT_CLASS)) {
File out = toOutputFile(outputDir, inputDir, inputFile);
transformFile(function, inputFile, out);
}
break;
case REMOVED:
File outputFile = toOutputFile(outputDir, inputDir, inputFile);
FileUtils.deleteIfExists(outputFile);
break;
}
}
} else {
for (File in : FileUtils.getAllFiles(inputDir)) {
if (in.getName().endsWith(SdkConstants.DOT_CLASS)) {
File out = toOutputFile(outputDir, inputDir, in);
transformFile(function, in, out);
}
}
}
}
}
}
}
private BiConsumer<InputStream, OutputStream> loadTransformFunction(URLClassLoader loader) {
ServiceLoader<BiConsumer> serviceLoader = ServiceLoader.load(BiConsumer.class, loader);
ArrayList<BiConsumer> functions = Lists.newArrayList(serviceLoader.iterator());
if (functions.isEmpty()) {
throw new IllegalStateException(
"Custom transform does not provide a BiConsumer to apply");
} else if (functions.size() > 1) {
throw new IllegalStateException(
"Custom transform provides more than one BiConsumer to apply");
}
BiConsumer uncheckedFunction = functions.get(0);
// Validate the generic arguments are valid:
Type[] types = uncheckedFunction.getClass().getGenericInterfaces();
for (Type type : types) {
if (type instanceof ParameterizedType) {
ParameterizedType generic = (ParameterizedType) type;
Type[] args = generic.getActualTypeArguments();
if (generic.getRawType().equals(BiConsumer.class)
&& args.length == 2
&& args[0].equals(InputStream.class)
&& args[1].equals(OutputStream.class)) {
return (BiConsumer<InputStream, OutputStream>) uncheckedFunction;
}
}
}
throw new IllegalStateException(
"Custom transform must provide a BiConsumer<InputStream, OutputStream>");
}
private void transformJar(
BiConsumer<InputStream, OutputStream> function, File inputJar, File outputJar)
throws IOException {
Files.createParentDirs(outputJar);
try (FileInputStream fis = new FileInputStream(inputJar);
ZipInputStream zis = new ZipInputStream(fis);
FileOutputStream fos = new FileOutputStream(outputJar);
ZipOutputStream zos = new ZipOutputStream(fos)) {
ZipEntry entry = zis.getNextEntry();
while (entry != null && ZipEntryUtils.isValidZipEntryName(entry)) {
if (!entry.isDirectory() && entry.getName().endsWith(SdkConstants.DOT_CLASS)) {
ZipEntry nextEntry = new ZipEntry(entry.getName());
// Any negative time value sets ZipEntry's xdostime to DOSTIME_BEFORE_1980
// constant.
nextEntry.setTime(-1L);
zos.putNextEntry(nextEntry);
apply(function, zis, zos);
} else {
// Do not copy resources
}
entry = zis.getNextEntry();
}
}
}
private void transformFile(
BiConsumer<InputStream, OutputStream> function, File inputFile, File outputFile)
throws IOException {
Files.createParentDirs(outputFile);
try (FileInputStream fis = new FileInputStream(inputFile);
FileOutputStream fos = new FileOutputStream(outputFile)) {
apply(function, fis, fos);
}
}
@NonNull
private static File toOutputFile(File outputDir, File inputDir, File inputFile) {
return new File(outputDir, FileUtils.relativePossiblyNonExistingPath(inputFile, inputDir));
}
private void apply(
BiConsumer<InputStream, OutputStream> function, InputStream in, OutputStream out)
throws IOException {
try {
function.accept(in, out);
} catch (UncheckedIOException e) {
throw e.getCause();
}
}
}