blob: 2c5079bb12ae55638f75948d3c9be3a20c9261a5 [file] [log] [blame]
/*
* Copyright (C) 2014 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.DOT_JAVA;
import static com.android.SdkConstants.INT_DEF_ANNOTATION;
import static com.android.SdkConstants.LONG_DEF_ANNOTATION;
import static com.android.SdkConstants.STRING_DEF_ANNOTATION;
import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.EXTERNAL;
import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.CLASSES_JAR;
import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.api.component.impl.ComponentPropertiesImpl;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.InternalArtifactType;
import com.android.build.gradle.internal.tasks.NonIncrementalTask;
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction;
import com.android.build.gradle.internal.utils.AndroidXDependency;
import com.android.builder.packaging.TypedefRemover;
import com.android.tools.lint.gradle.api.ExtractAnnotationRequest;
import com.android.tools.lint.gradle.api.ReflectiveLintRunner;
import com.android.utils.FileUtils;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Stream;
import org.gradle.api.artifacts.ArtifactCollection;
import org.gradle.api.artifacts.component.ComponentIdentifier;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.file.EmptyFileVisitor;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.file.RelativePath;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.CompileClasspath;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskProvider;
/**
* Task which extracts annotations from the source files, and writes them to one of two possible
* destinations:
*
* <ul>
* <li>A "external annotations" file (pointed to by {@link ExtractAnnotations#getOutput()}) which
* records the annotations in a zipped XML format for use by the IDE and by lint to associate
* the (source retention) annotations back with the compiled code
* </ul>
*
* We typically only extract external annotations when building libraries; ProGuard annotations are
* extracted when building libraries (to record in the AAR), <b>or</b> when building an app module
* where ProGuarding is enabled.
*/
@CacheableTask
public abstract class ExtractAnnotations extends NonIncrementalTask {
@NonNull
private static final AndroidXDependency ANDROIDX_ANNOTATIONS =
AndroidXDependency.fromPreAndroidXDependency(
"com.android.support", "support-annotations");
private FileCollection bootClasspath;
private String encoding;
private FileCollection classDir;
private ArtifactCollection libraries;
@Nullable FileCollection lintClassPath;
private final List<Object> sources = new ArrayList<>();
private FileTree sourcesFileTree;
private FileCollection classpath;
/** Lint classpath */
@InputFiles
@Nullable
@PathSensitive(PathSensitivity.RELATIVE)
public FileCollection getLintClassPath() {
return lintClassPath;
}
@NonNull
@PathSensitive(PathSensitivity.NAME_ONLY)
@InputFiles
public FileTree getSource() {
return sourcesFileTree;
}
@CompileClasspath
public FileCollection getClasspath() {
return classpath;
}
/** Used by the variant API */
public void source(Object source) {
sources.add(source);
}
/** Boot classpath: typically android.jar */
@CompileClasspath
public FileCollection getBootClasspath() {
return bootClasspath;
}
public void setBootClasspath(FileCollection bootClasspath) {
this.bootClasspath = bootClasspath;
}
@CompileClasspath
public FileCollection getLibraries() {
return libraries.getArtifactFiles();
}
/** The output .zip file to write the annotations database to, if any */
@OutputFile
public abstract RegularFileProperty getOutput();
/**
* The output .txt file to write the typedef recipe file to. A "recipe" file is a file which
* describes typedef classes, typically ones that should be deleted. It is generated by this
* {@link ExtractAnnotations} task and consumed by the {@link TypedefRemover}.
*/
@OutputFile
public abstract RegularFileProperty getTypedefFile();
/**
* The encoding to use when reading source files. The output file will ignore this and will
* always be a UTF-8 encoded .xml file inside the annotations zip file.
*/
@NonNull
@Input
public String getEncoding() {
return encoding;
}
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Location of class files. If set, any non-public typedef source retention annotations will be
* removed prior to .jar packaging.
*/
@Optional
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public FileCollection getClassDir() {
return classDir;
}
public void setClassDir(FileCollection classDir) {
this.classDir = classDir;
}
@Override
protected void doTaskAction() {
SourceFileVisitor fileVisitor = new SourceFileVisitor();
getSource().visit(fileVisitor);
List<File> sourceFiles = fileVisitor.sourceUnits;
if (!containsTypeDefs(sourceFiles)) {
writeEmptyTypeDefFile(getTypedefFile().get().getAsFile());
return;
}
List<File> roots = fileVisitor.getSourceRoots();
FileCollection classpath = getClasspath();
if (classpath != null) {
for (File jar : classpath) {
roots.add(jar);
}
}
roots.addAll(getBootClasspath().getFiles());
ExtractAnnotationRequest request =
new ExtractAnnotationRequest(
getTypedefFile().get().getAsFile(),
getLogger(),
getClassDir(),
getOutput().get().getAsFile(),
sourceFiles,
roots);
FileCollection lintClassPath = getLintClassPath();
if (lintClassPath != null) {
new ReflectiveLintRunner().extractAnnotations(getProject().getGradle(),
request, lintClassPath.getFiles());
}
}
private static void writeEmptyTypeDefFile(@Nullable File file) {
if (file == null) {
return;
}
try {
FileUtils.deleteIfExists(file);
Files.createParentDirs(file);
Files.asCharSink(file, Charsets.UTF_8).write("");
} catch (IOException ignore) {
}
}
/**
* Returns true if the given set of source files contain any typedef references.
*/
private static boolean containsTypeDefs(@NonNull List<File> sourceFiles) {
for (File file : sourceFiles) {
if (containsTypeDefs(file)) {
return true;
}
}
return false;
}
/**
* Returns true if the given source file contains any typedef references.
*/
private static boolean containsTypeDefs(@NonNull File file) {
try {
// TODO: Perform faster checks, for example converting the target annotations
// to byte arrays and then scanning through the files on disk (without reading
// them into memory) looking for those byte sequences. Possibly using memory
// mapped buffers.
try (Stream<String> lines = Files.asCharSource(file, UTF_8).lines()) {
return lines.anyMatch(
line ->
line.contains("Def")
&& (line.contains(INT_DEF_ANNOTATION.oldName())
|| line.contains(INT_DEF_ANNOTATION.newName())
|| line.contains(LONG_DEF_ANNOTATION.oldName())
|| line.contains(LONG_DEF_ANNOTATION.newName())
|| line.contains(STRING_DEF_ANNOTATION.oldName())
|| line.contains(STRING_DEF_ANNOTATION.newName())));
}
} catch (IOException e) {
return false;
}
}
@Input
public boolean getHasAndroidAnnotations() {
for (ResolvedArtifactResult artifact : libraries.getArtifacts()) {
ComponentIdentifier id = artifact.getId()
.getComponentIdentifier();
// because we only ask for external dependencies, we should be able to cast
// this always
if (id instanceof ModuleComponentIdentifier) {
ModuleComponentIdentifier moduleId = (ModuleComponentIdentifier) id;
// Search in both AndroidX and pre-AndroidX libraries
if (moduleId.getGroup().equals(ANDROIDX_ANNOTATIONS.getGroup())
&& moduleId.getModule().equals(ANDROIDX_ANNOTATIONS.getModule())
|| moduleId.getGroup().equals(ANDROIDX_ANNOTATIONS.getOldGroup())
&& moduleId.getModule()
.equals(ANDROIDX_ANNOTATIONS.getOldModule())) {
return true;
}
}
}
return false;
}
public static class CreationAction
extends VariantTaskCreationAction<ExtractAnnotations, ComponentPropertiesImpl> {
public CreationAction(@NonNull ComponentPropertiesImpl componentProperties) {
super(componentProperties);
}
@NonNull
@Override
public String getName() {
return computeTaskName("extract", "Annotations");
}
@NonNull
@Override
public Class<ExtractAnnotations> getType() {
return ExtractAnnotations.class;
}
@Override
public void handleProvider(@NonNull TaskProvider<ExtractAnnotations> taskProvider) {
super.handleProvider(taskProvider);
creationConfig.getTaskContainer().setGenerateAnnotationsTask(taskProvider);
creationConfig
.getArtifacts()
.setInitialProvider(taskProvider, ExtractAnnotations::getOutput)
.withName(SdkConstants.FN_ANNOTATIONS_ZIP)
.on(InternalArtifactType.ANNOTATIONS_ZIP.INSTANCE);
creationConfig
.getArtifacts()
.setInitialProvider(taskProvider, ExtractAnnotations::getTypedefFile)
.withName("typedefs.txt")
.on(InternalArtifactType.ANNOTATIONS_TYPEDEF_FILE.INSTANCE);
}
@Override
public void configure(@NonNull ExtractAnnotations task) {
super.configure(task);
task.setDescription(
"Extracts Android annotations for the "
+ creationConfig.getName()
+ " variant into the archive file");
task.setGroup(BasePlugin.BUILD_GROUP);
task.setClassDir(creationConfig.getArtifacts().getAllClasses());
task.source(creationConfig.getJavaSources());
task.setEncoding(
creationConfig
.getGlobalScope()
.getExtension()
.getCompileOptions()
.getEncoding());
task.classpath = creationConfig.getJavaClasspath(COMPILE_CLASSPATH, CLASSES_JAR);
task.libraries =
creationConfig
.getVariantDependencies()
.getArtifactCollection(COMPILE_CLASSPATH, EXTERNAL, CLASSES_JAR);
GlobalScope globalScope = creationConfig.getGlobalScope();
// Setup the boot classpath just before the task actually runs since this will
// force the sdk to be parsed. (Same as in compileTask)
task.setBootClasspath(
creationConfig
.getServices()
.fileCollection(globalScope.getFilteredBootClasspath()));
task.lintClassPath =
globalScope
.getProject()
.getConfigurations()
.getByName(LintBaseTask.LINT_CLASS_PATH);
task.sourcesFileTree =
task.getProject()
.files((Callable<List<Object>>) () -> task.sources)
.getAsFileTree();
}
}
/**
* Visitor which gathers a series of individual source files as well as inferring the set of
* source roots
*/
private static class SourceFileVisitor extends EmptyFileVisitor {
private final List<File> sourceUnits = Lists.newArrayListWithExpectedSize(100);
private final List<File> sourceRoots = Lists.newArrayList();
private String mostRecentRoot = "\000";
public SourceFileVisitor() {
}
public List<File> getSourceFiles() {
return sourceUnits;
}
public List<File> getSourceRoots() {
return sourceRoots;
}
private static final String BUILD_GENERATED = File.separator + "build" + File.separator
+ "generated" + File.separator;
@Override
public void visitFile(FileVisitDetails details) {
File file = details.getFile();
String path = file.getPath();
if (path.endsWith(DOT_JAVA) && !path.contains(BUILD_GENERATED)) {
// Infer the source roots. These are available as relative paths
// on the file visit details.
if (!path.startsWith(mostRecentRoot)) {
RelativePath relativePath = details.getRelativePath();
String pathString = relativePath.getPathString();
// The above method always uses / as a file separator but for
// comparisons with the path we need to use the native separator:
pathString = pathString.replace('/', File.separatorChar);
if (path.endsWith(pathString)) {
String root = path.substring(0, path.length() - pathString.length());
File rootFile = new File(root);
if (!sourceRoots.contains(rootFile)) {
mostRecentRoot = rootFile.getPath();
sourceRoots.add(rootFile);
}
}
}
sourceUnits.add(file);
}
}
}
}