blob: ff83277855e1ebfbc43a7fa11eca6bf0ca8d7d8a [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.tasks;
import static com.android.build.VariantOutput.OutputType.FULL_SPLIT;
import static com.android.build.VariantOutput.OutputType.MAIN;
import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.ALL;
import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.LINT;
import static com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH;
import static com.android.build.gradle.internal.scope.TaskOutputHolder.AnchorOutputType.ALL_CLASSES;
import static com.android.build.gradle.internal.scope.TaskOutputHolder.TaskOutputType.APK;
import static com.android.build.gradle.internal.scope.TaskOutputHolder.TaskOutputType.LIBRARY_MANIFEST;
import static com.android.build.gradle.internal.scope.TaskOutputHolder.TaskOutputType.LINT_JAR;
import static com.android.build.gradle.internal.scope.TaskOutputHolder.TaskOutputType.MANIFEST_MERGE_REPORT;
import static com.android.build.gradle.internal.scope.TaskOutputHolder.TaskOutputType.MERGED_MANIFESTS;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.gradle.internal.LintGradleClient;
import com.android.build.gradle.internal.dsl.LintOptions;
import com.android.build.gradle.internal.scope.BuildOutput;
import com.android.build.gradle.internal.scope.BuildOutputs;
import com.android.build.gradle.internal.scope.GlobalScope;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.scope.TaskOutputHolder;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.tasks.BaseTask;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.Variant;
import com.android.tools.lint.LintCliFlags;
import com.android.tools.lint.Reporter;
import com.android.tools.lint.Warning;
import com.android.tools.lint.checks.BuiltinIssueRegistry;
import com.android.tools.lint.checks.GradleDetector;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.LintBaseline;
import com.android.tools.lint.detector.api.Issue;
import com.android.utils.Pair;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.ExtraPropertiesExtension;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.tooling.provider.model.ToolingModelBuilder;
import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry;
public abstract class LintBaseTask extends BaseTask {
/**
* Whether lint should attempt to do deep analysis of libraries. E.g. when building up the
* project graph, when it encounters an AndroidLibrary or JavaLibrary dependency, it should
* check if it's a local project, and if so recursively initialize the project with the local
* source paths etc of the library (in the past, this was not the case: it would naively just
* point to the library's resources and class files, which were the compiled outputs.
*
* <p>The new behavior is clearly the correct behavior (see issue #194092), but since this is a
* risky fix, we're putting it behind a flag now and as soon as we get some real user testing,
* we should enable this by default and remove the old code.
*/
public static final boolean MODEL_LIBRARIES = true;
protected static final Logger LOG = Logging.getLogger(LintBaseTask.class);
@Nullable protected LintOptions lintOptions;
@Nullable protected File sdkHome;
private boolean fatalOnly;
protected ToolingModelBuilderRegistry toolingRegistry;
@Nullable protected File reportsDir;
@Nullable
public LintOptions getLintOptions() {
return lintOptions;
}
@Nullable
public File getSdkHome() {
return sdkHome;
}
public ToolingModelBuilderRegistry getToolingRegistry() {
return toolingRegistry;
}
protected void setFatalOnly(boolean fatalOnly) {
this.fatalOnly = fatalOnly;
}
public boolean isFatalOnly() {
return fatalOnly;
}
@Nullable
public File getReportsDir() {
return reportsDir;
}
protected void abort() {
String message;
if (fatalOnly) {
message =
""
+ "Lint found fatal errors while assembling a release target.\n"
+ "\n"
+ "To proceed, either fix the issues identified by lint, or modify your build script as follows:\n"
+ "...\n"
+ "android {\n"
+ " lintOptions {\n"
+ " checkReleaseBuilds false\n"
+ " // Or, if you prefer, you can continue to check for errors in release builds,\n"
+ " // but continue the build even when errors are found:\n"
+ " abortOnError false\n"
+ " }\n"
+ "}\n"
+ "...";
} else {
message =
""
+ "Lint found errors in the project; aborting build.\n"
+ "\n"
+ "Fix the issues identified by lint, or add the following to your build script to proceed with errors:\n"
+ "...\n"
+ "android {\n"
+ " lintOptions {\n"
+ " abortOnError false\n"
+ " }\n"
+ "}\n"
+ "...";
}
throw new GradleException(message);
}
/** Runs lint on the given variant and returns the set of warnings */
protected Pair<List<Warning>, LintBaseline> runLint(
/*
* Note that as soon as we disable {@link #MODEL_LIBRARIES} this is
* unused and we can delete it and all the callers passing it recursively
*/
@NonNull AndroidProject modelProject,
@NonNull Variant variant,
@NonNull VariantInputs variantInputs,
boolean report) {
IssueRegistry registry = createIssueRegistry();
LintCliFlags flags = new LintCliFlags();
LintGradleClient client =
new LintGradleClient(
registry,
flags,
getProject(),
modelProject,
sdkHome,
variant,
variantInputs,
getBuildTools());
if (fatalOnly) {
flags.setFatalOnly(true);
}
if (lintOptions != null) {
syncOptions(
lintOptions,
client,
flags,
variant,
getProject(),
reportsDir,
report,
fatalOnly);
}
if (!report || fatalOnly) {
flags.setQuiet(true);
}
flags.setWriteBaselineIfMissing(report && !fatalOnly);
Pair<List<Warning>, LintBaseline> warnings;
try {
warnings = client.run(registry);
} catch (IOException e) {
throw new GradleException("Invalid arguments.", e);
}
if (report && client.haveErrors() && flags.isSetExitCode()) {
abort();
}
return warnings;
}
protected static void syncOptions(
@NonNull LintOptions options,
@NonNull LintGradleClient client,
@NonNull LintCliFlags flags,
@Nullable Variant variant,
@NonNull Project project,
@Nullable File reportsDir,
boolean report,
boolean fatalOnly) {
options.syncTo(
client,
flags,
variant != null ? variant.getName() : null,
project,
reportsDir,
report);
boolean displayEmpty = !(fatalOnly || flags.isQuiet());
for (Reporter reporter : flags.getReporters()) {
reporter.setDisplayEmpty(displayEmpty);
}
}
protected AndroidProject createAndroidProject(@NonNull Project gradleProject) {
String modelName = AndroidProject.class.getName();
ToolingModelBuilder modelBuilder = toolingRegistry.getBuilder(modelName);
assert modelBuilder != null;
// setup the level 3 sync.
final ExtraPropertiesExtension ext = gradleProject.getExtensions().getExtraProperties();
ext.set(
AndroidProject.PROPERTY_BUILD_MODEL_ONLY_VERSIONED,
Integer.toString(AndroidProject.MODEL_LEVEL_3_VARIANT_OUTPUT_POST_BUILD));
ext.set(AndroidProject.PROPERTY_BUILD_MODEL_DISABLE_SRC_DOWNLOAD, true);
try {
return (AndroidProject) modelBuilder.buildAll(modelName, gradleProject);
} finally {
ext.set(AndroidProject.PROPERTY_BUILD_MODEL_ONLY_VERSIONED, null);
ext.set(AndroidProject.PROPERTY_BUILD_MODEL_DISABLE_SRC_DOWNLOAD, null);
}
}
private static BuiltinIssueRegistry createIssueRegistry() {
return new LintGradleIssueRegistry();
}
// Issue registry when Lint is run inside Gradle: we replace the Gradle
// detector with a local implementation which directly references Groovy
// for parsing. In Studio on the other hand, the implementation is replaced
// by a PSI-based check. (This is necessary for now since we don't have a
// tool-agnostic API for the Groovy AST and we don't want to add a 6.3MB dependency
// on Groovy itself quite yet.
private static class LintGradleIssueRegistry extends BuiltinIssueRegistry {
private boolean mInitialized;
public LintGradleIssueRegistry() {}
@NonNull
@Override
public List<Issue> getIssues() {
List<Issue> issues = super.getIssues();
if (!mInitialized) {
mInitialized = true;
for (Issue issue : issues) {
if (issue.getImplementation().getDetectorClass() == GradleDetector.class) {
issue.setImplementation(GroovyGradleDetector.IMPLEMENTATION);
}
}
}
return issues;
}
}
public static class VariantInputs {
@NonNull private final String name;
@NonNull private final FileCollection localLintJarCollection;
@NonNull private final FileCollection dependencyLintJarCollection;
@NonNull private final FileCollection mergedManifest;
@Nullable private final FileCollection mergedManifestReport;
private List<File> lintRuleJars;
private final ConfigurableFileCollection allInputs;
public VariantInputs(@NonNull VariantScope variantScope) {
name = variantScope.getFullVariantName();
allInputs = variantScope.getGlobalScope().getProject().files();
allInputs.from(
localLintJarCollection = variantScope.getGlobalScope().getOutput(LINT_JAR));
allInputs.from(
dependencyLintJarCollection =
variantScope.getArtifactFileCollection(RUNTIME_CLASSPATH, ALL, LINT));
if (variantScope.hasOutput(MERGED_MANIFESTS)) {
mergedManifest = variantScope.getOutput(MERGED_MANIFESTS);
} else if (variantScope.hasOutput(LIBRARY_MANIFEST)) {
mergedManifest = variantScope.getOutput(LIBRARY_MANIFEST);
} else {
throw new RuntimeException(
"VariantInputs initialized with no merged manifest on: "
+ variantScope.getVariantConfiguration().getType());
}
allInputs.from(mergedManifest);
if (variantScope.hasOutput(MANIFEST_MERGE_REPORT)) {
allInputs.from(
mergedManifestReport = variantScope.getOutput(MANIFEST_MERGE_REPORT));
} else {
throw new RuntimeException(
"VariantInputs initialized with no merged manifest report on: "
+ variantScope.getVariantConfiguration().getType());
}
// these inputs are only there to ensure that the lint task runs after these build
// intermediates/outputs are built.
allInputs.from(variantScope.getOutput(ALL_CLASSES));
if (variantScope.hasOutput(APK)) {
allInputs.from(variantScope.getOutput(APK));
}
}
@NonNull
public String getName() {
return name;
}
@NonNull
public FileCollection getAllInputs() {
return allInputs;
}
/** the lint rule jars */
@NonNull
public List<File> getRuleJars() {
if (lintRuleJars == null) {
lintRuleJars =
Streams.concat(
dependencyLintJarCollection.getFiles().stream(),
localLintJarCollection.getFiles().stream())
.filter(File::isFile)
.collect(Collectors.toList());
}
return lintRuleJars;
}
/** the merged manifest of the current module */
@NonNull
public File getMergedManifest() {
File file = mergedManifest.getSingleFile();
if (file.isFile()) {
return file;
}
Collection<BuildOutput> manifests =
BuildOutputs.load(
ImmutableList.of(TaskOutputHolder.TaskOutputType.MERGED_MANIFESTS),
file);
if (manifests.isEmpty()) {
throw new RuntimeException("Can't find any manifest in folder: " + file);
}
// first search for a main manifest
Optional<File> mainManifest =
manifests
.stream()
.filter(buildOutput -> buildOutput.getApkInfo().getType() == MAIN)
.map(BuildOutput::getOutputFile)
.findFirst();
if (mainManifest.isPresent()) {
return mainManifest.get();
}
// else search for a full_split with no filters.
Optional<File> universalSplit =
manifests
.stream()
.filter(
output ->
output.getApkInfo().getType() == FULL_SPLIT
&& output.getFilters().isEmpty())
.map(BuildOutput::getOutputFile)
.findFirst();
// return the universal Manifest, or a random one if not found.
return universalSplit.orElseGet(() -> manifests.iterator().next().getOutputFile());
}
@Nullable
public File getManifestMergeReport() {
if (mergedManifestReport == null) {
return null;
}
return mergedManifestReport.getSingleFile();
}
}
public abstract static class BaseConfigAction<T extends LintBaseTask>
implements TaskConfigAction<T> {
@NonNull private final GlobalScope globalScope;
public BaseConfigAction(@NonNull GlobalScope globalScope) {
this.globalScope = globalScope;
}
@NonNull
protected GlobalScope getGlobalScope() {
return globalScope;
}
@Override
public void execute(@NonNull T lintTask) {
lintTask.setGroup(JavaBasePlugin.VERIFICATION_GROUP);
lintTask.lintOptions = globalScope.getExtension().getLintOptions();
File sdkFolder = globalScope.getSdkHandler().getSdkFolder();
if (sdkFolder != null) {
lintTask.sdkHome = sdkFolder;
}
lintTask.toolingRegistry = globalScope.getToolingRegistry();
lintTask.reportsDir = globalScope.getReportsDir();
lintTask.setAndroidBuilder(globalScope.getAndroidBuilder());
}
}
}