| /* |
| * Copyright 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 androidx.build |
| |
| import androidx.build.dependencyTracker.AffectedModuleDetector |
| import com.android.build.api.dsl.Lint |
| import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask |
| import com.android.build.gradle.internal.lint.AndroidLintTextOutputTask |
| import com.google.common.io.Files |
| import org.gradle.api.DefaultTask |
| import org.gradle.api.Project |
| import org.gradle.api.file.RegularFileProperty |
| import org.gradle.api.services.BuildService |
| import org.gradle.api.services.BuildServiceParameters |
| import org.gradle.api.tasks.InputFiles |
| import org.gradle.api.tasks.OutputFile |
| import org.gradle.api.tasks.TaskAction |
| import org.gradle.kotlin.dsl.getByType |
| import java.io.File |
| import java.util.Locale |
| |
| /** |
| * Setting this property means that lint will update lint-baseline.xml if it exists. |
| */ |
| private const val UPDATE_LINT_BASELINE = "updateLintBaseline" |
| |
| /** |
| * Name of the service we use to limit the number of concurrent executions of lint |
| */ |
| public const val LINT_SERVICE_NAME = "androidxLintService" |
| |
| /** |
| * Property used by Lint to continue creating baselines without failing lint, normally set by: |
| * -Dlint.baselines.continue=true from command line. |
| */ |
| private const val LINT_BASELINE_CONTINUE = "lint.baselines.continue" |
| |
| // service for limiting the number of concurrent lint tasks |
| interface AndroidXLintService : BuildService<BuildServiceParameters.None> |
| |
| fun Project.configureRootProjectForLint() { |
| // determine many lint tasks to run in parallel |
| val memoryPerTask = 512 * 1024 * 1024 |
| val maxLintMemory = Runtime.getRuntime().maxMemory() * 0.75 // save memory for other things too |
| val maxNumParallelUsages = Math.max(1, (maxLintMemory / memoryPerTask).toInt()) |
| |
| project.gradle.sharedServices.registerIfAbsent( |
| LINT_SERVICE_NAME, |
| AndroidXLintService::class.java |
| ) { spec -> |
| spec.maxParallelUsages.set(maxNumParallelUsages) |
| } |
| } |
| |
| fun Project.configureNonAndroidProjectForLint(extension: AndroidXExtension) { |
| apply(mapOf("plugin" to "com.android.lint")) |
| |
| // Create fake variant tasks since that is what is invoked by developers. |
| val lintTask = tasks.named("lint") |
| lintTask.configure { task -> |
| task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText")) |
| AffectedModuleDetector.configureTaskGuard(task) |
| } |
| afterEvaluate { |
| tasks.named("lintAnalyze").configure { task -> |
| AffectedModuleDetector.configureTaskGuard(task) |
| } |
| /* TODO: uncomment when we upgrade to AGP 7.1.0-alpha04 |
| tasks.named("lintReport").configure { task -> |
| AffectedModuleDetector.configureTaskGuard(task) |
| }*/ |
| } |
| tasks.register("lintDebug") { |
| it.dependsOn(lintTask) |
| it.enabled = false |
| } |
| tasks.register("lintAnalyzeDebug") { |
| it.enabled = false |
| } |
| tasks.register("lintRelease") { |
| it.dependsOn(lintTask) |
| it.enabled = false |
| } |
| addToBuildOnServer(lintTask) |
| |
| val lint = extensions.getByType<Lint>() |
| // Support the lint standalone plugin case which, as yet, lacks AndroidComponents finalizeDsl |
| afterEvaluate { configureLint(lint, extension) } |
| } |
| |
| fun Project.configureAndroidProjectForLint(lint: Lint, extension: AndroidXExtension) { |
| project.afterEvaluate { |
| // makes sure that the lintDebug task will exist, so we can find it by name |
| setUpLintDebugIfNeeded() |
| } |
| tasks.register("lintAnalyze") { |
| it.enabled = false |
| } |
| configureLint(lint, extension) |
| tasks.named("lint").configure { task -> |
| // We already run lintDebug, we don't need to run lint which lints the release variant |
| task.enabled = false |
| } |
| afterEvaluate { |
| for (variant in project.agpVariants) { |
| tasks.named( |
| "lint${variant.name.replaceFirstChar { |
| if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() |
| }}" |
| ).configure { task -> |
| AffectedModuleDetector.configureTaskGuard(task) |
| task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText")) |
| } |
| tasks.named( |
| "lintAnalyze${variant.name.replaceFirstChar { |
| if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() |
| }}" |
| ).configure { task -> |
| AffectedModuleDetector.configureTaskGuard(task) |
| task.dependsOn(tasks.named("exportAtomicLibraryGroupsToText")) |
| } |
| /* TODO: uncomment when we upgrade to AGP 7.1.0-alpha04 |
| tasks.named("lintReport${variant.name.capitalize(Locale.US)}").configure { task -> |
| AffectedModuleDetector.configureTaskGuard(task) |
| }*/ |
| } |
| } |
| } |
| |
| private fun Project.setUpLintDebugIfNeeded() { |
| val variants = project.agpVariants |
| val variantNames = variants.map { v -> v.name } |
| if (!variantNames.contains("debug")) { |
| tasks.register("lintDebug") { |
| for (variantName in variantNames) { |
| if (variantName.lowercase(Locale.US).contains("debug")) { |
| it.dependsOn( |
| tasks.named( |
| "lint${variantName.replaceFirstChar { |
| if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() |
| }}" |
| ) |
| ) |
| } |
| } |
| } |
| } |
| } |
| |
| fun Project.configureLint(lint: Lint, extension: AndroidXExtension) { |
| project.dependencies.add( |
| "lintChecks", |
| project.rootProject.project(":lint-checks") |
| ) |
| |
| // The purpose of this specific project is to test that lint is running, so |
| // it contains expected violations that we do not want to trigger a build failure |
| val isTestingLintItself = (project.path == ":lint-checks:integration-tests") |
| |
| // If -PupdateLintBaseline was set we should update the baseline if it exists |
| // forUseAtConfigurationTime() is deprecated in Gradle 7.4, but we still use 7.3 |
| @Suppress("DEPRECATION") |
| val updateLintBaseline = project.providers.gradleProperty(UPDATE_LINT_BASELINE) |
| .forUseAtConfigurationTime().isPresent && !isTestingLintItself |
| |
| lint.apply { |
| // Skip lintVital tasks on assemble. We explicitly run lintRelease for libraries. |
| checkReleaseBuilds = false |
| } |
| |
| // task to copy autogenerated baselines back into the source tree |
| val updateBaselineTask = project.tasks.register( |
| "updateLintBaseline", |
| UpdateBaselineTask::class.java, |
| ) { copyTask -> |
| copyTask.source.set(generatedLintBaseline) |
| copyTask.dest.set(lintBaseline) |
| } |
| |
| val projectGeneratedLintBaseline = generatedLintBaseline |
| |
| tasks.withType(AndroidLintAnalysisTask::class.java).configureEach { task -> |
| // don't run too many copies of lint at once due to memory limitations |
| task.usesService( |
| task.project.gradle.sharedServices.registrations.getByName(LINT_SERVICE_NAME).service |
| ) |
| if (updateLintBaseline) { |
| // Remove the generated baseline before running lint, so that lint won't try to use it |
| task.doFirst { |
| if (projectGeneratedLintBaseline.exists()) { |
| projectGeneratedLintBaseline.delete() |
| } |
| } |
| } |
| } |
| tasks.withType(AndroidLintTextOutputTask::class.java).configureEach { task -> |
| if (updateLintBaseline) { |
| task.finalizedBy(updateBaselineTask) |
| } |
| } |
| |
| // Lint is configured entirely in finalizeDsl so that individual projects cannot easily |
| // disable individual checks in the DSL for any reason. |
| lint.apply { |
| if (!isTestingLintItself) { |
| abortOnError = true |
| } |
| ignoreWarnings = true |
| |
| // Run lint on tests. Uses top-level lint.xml to specify checks. |
| checkTestSources = true |
| |
| // Write output directly to the console (and nowhere else). |
| textReport = true |
| htmlReport = false |
| |
| // Format output for convenience. |
| explainIssues = true |
| noLines = false |
| quiet = true |
| |
| // We run lint on each library, so we don't want transitive checking of each dependency |
| checkDependencies = false |
| |
| if ( |
| extension.type == LibraryType.PUBLISHED_TEST_LIBRARY || |
| extension.type == LibraryType.INTERNAL_TEST_LIBRARY |
| ) { |
| // Test libraries are allowed to call @VisibleForTests code |
| disable.add("VisibleForTests") |
| } else { |
| fatal.add("VisibleForTests") |
| } |
| |
| // Disable dependency checks that suggest to change them. We want libraries to be |
| // intentional with their dependency version bumps. |
| disable.add("KtxExtensionAvailable") |
| disable.add("GradleDependency") |
| |
| // Disable a check that's only relevant for real apps. For our test apps we're not |
| // concerned with drawables potentially being a little bit blurry |
| disable.add("IconMissingDensityFolder") |
| |
| // Disable a check that's only triggered by translation updates which are |
| // outside of library owners' control, b/174655193 |
| disable.add("UnusedQuantity") |
| |
| // Disable until it works for our projects, b/171986505 |
| disable.add("JavaPluginLanguageLevel") |
| |
| // Disable the TODO check until we have a policy that requires it. |
| disable.add("StopShip") |
| |
| // Broken in 7.0.0-alpha15 due to b/180408990 |
| disable.add("RestrictedApi") |
| |
| // Broken in 7.0.0-alpha15 due to b/187508590 |
| disable.add("InvalidPackage") |
| |
| // Reenable after upgradingto 7.1.0-beta01 |
| disable.add("SupportAnnotationUsage") |
| |
| // Provide stricter enforcement for project types intended to run on a device. |
| if (extension.type.compilationTarget == CompilationTarget.DEVICE) { |
| fatal.add("Assert") |
| fatal.add("NewApi") |
| fatal.add("ObsoleteSdkInt") |
| fatal.add("NoHardKeywords") |
| fatal.add("UnusedResources") |
| fatal.add("KotlinPropertyAccess") |
| fatal.add("LambdaLast") |
| fatal.add("UnknownNullness") |
| |
| // Only override if not set explicitly. |
| // Some Kotlin projects may wish to disable this. |
| if ( |
| !disable.contains("SyntheticAccessor") && |
| extension.type != LibraryType.SAMPLES |
| ) { |
| fatal.add("SyntheticAccessor") |
| } |
| |
| // Only check for missing translations in finalized (beta and later) modules. |
| if (extension.mavenVersion?.isFinalApi() == true) { |
| fatal.add("MissingTranslation") |
| } else { |
| disable.add("MissingTranslation") |
| } |
| } else { |
| disable.add("BanUncheckedReflection") |
| } |
| |
| // Broken in 7.0.0-alpha15 due to b/187343720 |
| disable.add("UnusedResources") |
| |
| if (extension.type == LibraryType.SAMPLES) { |
| // TODO: b/190833328 remove if / when AGP will analyze dependencies by default |
| // This is needed because SampledAnnotationDetector uses partial analysis, and |
| // hence requires dependencies to be analyzed. |
| checkDependencies = true |
| } |
| |
| // Only run certain checks where API tracking is important. |
| if (extension.type.checkApi is RunApiTasks.No) { |
| disable.add("IllegalExperimentalApiUsage") |
| } |
| |
| // If the project has not overridden the lint config, set the default one. |
| if (lintConfig == null) { |
| val lintXmlPath = if (extension.type == LibraryType.SAMPLES) { |
| "buildSrc/lint_samples.xml" |
| } else { |
| "buildSrc/lint.xml" |
| } |
| // suppress warnings more specifically than issue-wide severity (regexes) |
| // Currently suppresses warnings from baseline files working as intended |
| lintConfig = File(project.getSupportRootFolder(), lintXmlPath) |
| } |
| |
| // Ideally, teams aren't able to add new violations to a baseline file; they should only |
| // be able to burn down existing violations. That's hard to enforce, though, so we'll |
| // generally allow teams to update their baseline files with a publicly-known flag. |
| if (updateLintBaseline) { |
| // Continue generating baselines regardless of errors. |
| abortOnError = false |
| |
| // Avoid printing every single lint error to the terminal. |
| textReport = false |
| } |
| |
| // If we give lint the filepath of a baseline file, then: |
| // If the file does not exist, lint will write new violations to it |
| // If the file does exist, lint will read extemptions from it |
| |
| // So, if we want to update the baselines, we need to give lint an empty location |
| // to save to. |
| |
| // If we're not updating the baselines, then we want lint to check for new errors. |
| // This requires us only pass a baseline to lint if one already exists. |
| if (updateLintBaseline) { |
| baseline = generatedLintBaseline |
| } else if (lintBaseline.exists()) { |
| baseline = lintBaseline |
| } |
| } |
| } |
| |
| val Project.lintBaseline get() = File(projectDir, "/lint-baseline.xml") |
| val Project.generatedLintBaseline get() = File(project.buildDir, "/generated-lint-baseline.xml") |
| |
| /** |
| * Task that copies the generated line baseline |
| * If an input baseline file has no issues, it is considered to be nonexistent |
| */ |
| abstract class UpdateBaselineTask : DefaultTask() { |
| @get:InputFiles // allows missing input file |
| abstract val source: RegularFileProperty |
| |
| @get:OutputFile |
| abstract val dest: RegularFileProperty |
| |
| @TaskAction |
| fun copyBaseline() { |
| val source = source.get().asFile |
| val dest = dest.get().asFile |
| val sourceText = if (source.exists()) { |
| source.readText() |
| } else { |
| "" |
| } |
| var sourceHasIssues = |
| if (source.exists()) { |
| // Does the baseline contain any issues? |
| source.reader().useLines { lines -> |
| lines.any { line -> |
| line.endsWith("<issue") |
| } |
| } |
| } else { |
| false |
| } |
| val destNonempty = dest.exists() |
| val changing = (sourceHasIssues != destNonempty) || |
| (sourceHasIssues && sourceText != dest.readText()) |
| if (changing) { |
| if (sourceHasIssues) { |
| Files.copy(source, dest) |
| println("Updated baseline file ${dest.path}") |
| } else { |
| dest.delete() |
| println("Deleted baseline file ${dest.path}") |
| } |
| } |
| } |
| } |