blob: c92d7808252ca4af754e16e2a3c728b3a6ec34b0 [file] [log] [blame]
/*
* Copyright 2019 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.uptodatedness
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.kotlin.dsl.extra
import java.io.File
import java.util.Date
/**
* Validates that all tasks (except a temporary exception list) are considered up-to-date.
* The expected usage of this is that the user will invoke a build with the
* TaskUpToDateValidator disabled, and then reinvoke the same build with the TaskUpToDateValidator
* enabled. If the second build actually runs any tasks, then some tasks don't have the correct
* inputs/outputs declared and are running more often than necessary.
*/
const val DISALLOW_TASK_EXECUTION_FLAG_NAME = "disallowExecution"
const val RECORD_FLAG_NAME = "verifyUpToDate"
// Temporary set of exempt tasks that are known to still be out-of-date after running once
// Entries in this set may be task names (like assembleDebug) or task paths
// (like :core:core:assembleDebug)
// Entries in this set do still get rerun because they might produce files that are needed by
// subsequent tasks
val ALLOW_RERUNNING_TASKS = setOf(
"analyticsRecordingRelease",
"buildOnServer",
"checkExternalLicenses",
"createArchive",
"createDiffArchiveForAll",
"createProjectZip",
"externalNativeBuildDebug",
"externalNativeBuildRelease",
"generateJsonModelDebug",
"generateJsonModelRelease",
"generateMetadataFileForAndroidDebugPublication",
"generateMetadataFileForAndroidReleasePublication",
"generateMetadataFileForDesktopPublication",
"generateMetadataFileForJvmPublication",
"generateMetadataFileForJvmlinux-x64Publication",
"generateMetadataFileForJvmmacos-x64Publication",
"generateMetadataFileForJvmmacos-arm64Publication",
"generateMetadataFileForJvmwindows-x64Publication",
"generateMetadataFileForJvmallPublication",
"generateMetadataFileForMavenPublication",
"generateMetadataFileForMetadataPublication",
"generateMetadataFileForKotlinMultiplatformPublication",
"generateMetadataFileForPluginMavenPublication",
"generatePomFileForBenchmarkPluginMarkerMavenPublication",
"generatePomFileForAndroidDebugPublication",
"generatePomFileForAndroidReleasePublication",
"generatePomFileForDesktopPublication",
"generatePomFileForJvmlinux-x64Publication",
"generatePomFileForJvmmacos-x64Publication",
"generatePomFileForJvmmacos-arm64Publication",
"generatePomFileForJvmwindows-x64Publication",
"generatePomFileForJvmallPublication",
"generatePomFileForJvmPublication",
"generatePomFileForKotlinMultiplatformPublication",
"generatePomFileForMavenPublication",
"generatePomFileForPluginMavenPublication",
"generatePomFileForMetadataPublication",
"generatePomFileForSafeargsJavaPluginMarkerMavenPublication",
"generatePomFileForSafeargsKotlinPluginMarkerMavenPublication",
"jacocoPublicDebug",
"jacocoTipOfTreeDebug",
"partiallyDejetifyArchive",
"publishBenchmarkPluginMarkerMavenPublicationToMavenRepository",
"publishAndroidDebugPublicationToMavenRepository",
"publishAndroidReleasePublicationToMavenRepository",
"publishDesktopPublicationToMavenRepository",
"publishJvmPublicationToMavenRepository",
"publishJvmlinux-x64PublicationToMavenRepository",
"publishJvmmacos-x64PublicationToMavenRepository",
"publishJvmmacos-arm64PublicationToMavenRepository",
"publishJvmwindows-x64PublicationToMavenRepository",
"publishJvmallPublicationToMavenRepository",
"publishKotlinMultiplatformPublicationToMavenRepository",
"publishMavenPublicationToMavenRepository",
"publishMetadataPublicationToMavenRepository",
"publishPluginMavenPublicationToMavenRepository",
"publishSafeargsJavaPluginMarkerMavenPublicationToMavenRepository",
"publishSafeargsKotlinPluginMarkerMavenPublicationToMavenRepository",
/**
* relocateShadowJar is used to configure the ShadowJar hence it does not have any outputs.
* https://github.com/johnrengelman/shadow/issues/561
*/
"relocateShadowJar",
"stripArchiveForPartialDejetification",
"verifyDependencyVersions",
"zipTestConfigsWithApks",
":camera:integration-tests:camera-testapp-core:mergeLibDexDebug",
":camera:integration-tests:camera-testapp-core:packageDebug",
":camera:integration-tests:camera-testapp-uiwidgets:mergeLibDexDebug",
":camera:integration-tests:camera-testapp-uiwidgets:packageDebug",
":camera:integration-tests:camera-testapp-core:GenerateTestConfigurationdebug",
":camera:integration-tests:camera-testapp-core:GenerateTestConfigurationdebugAndroidTest",
":camera:integration-tests:camera-testapp-view:GenerateTestConfigurationdebug",
":camera:integration-tests:camera-testapp-view:GenerateTestConfigurationdebugAndroidTest",
":camera:integration-tests:camera-testapp-view:mergeLibDexDebug",
":camera:integration-tests:camera-testapp-view:packageDebug",
)
// Additional tasks that are expected to be temporarily out-of-date after running once
// Tasks in this set we don't even try to rerun, because they're known to be somewhat slow
// and also known to not generate any new, necessary files during subsequent runs
val DONT_TRY_RERUNNING_TASKS = setOf(
// More information about the fact that these dokka tasks rerun can be found at b/167569304
"dokkaKotlinDocs",
"zipDokkaDocs",
// We should be able to remove these entries when b/160392650 is fixed
"lint",
"lintDebug",
"lintVitalRelease",
)
class TaskUpToDateValidator {
companion object {
private val BUILD_START_TIME_KEY = "taskUpToDateValidatorSetupTime"
private fun shouldRecord(project: Project): Boolean {
return project.hasProperty(RECORD_FLAG_NAME)
}
private fun shouldValidate(project: Project): Boolean {
return project.hasProperty(DISALLOW_TASK_EXECUTION_FLAG_NAME)
}
private fun isAllowedToRerunTask(task: Task): Boolean {
return ALLOW_RERUNNING_TASKS.contains(task.name) ||
ALLOW_RERUNNING_TASKS.contains(task.path)
}
private fun shouldTryRerunningTask(task: Task): Boolean {
return !(
DONT_TRY_RERUNNING_TASKS.contains(task.name) ||
DONT_TRY_RERUNNING_TASKS.contains(task.path)
)
}
private fun recordBuildStartTime(rootProject: Project) {
rootProject.extra.set(BUILD_START_TIME_KEY, Date())
}
private fun getBuildStartTime(project: Project): Date {
return project.rootProject.extra.get(BUILD_START_TIME_KEY) as Date
}
fun setup(rootProject: Project) {
recordBuildStartTime(rootProject)
val taskGraph = rootProject.gradle.taskGraph
if (shouldValidate(rootProject)) {
taskGraph.beforeTask { task ->
if (!shouldTryRerunningTask(task)) {
task.enabled = false
}
}
}
if (shouldRecord(rootProject) || shouldValidate(rootProject)) {
taskGraph.afterTask { task ->
// In the second build, make sure that the task didn't rerun
if (shouldValidate(rootProject)) {
if (task.didWork) {
if (!isAllowedToRerunTask(task)) {
val message = "Ran two consecutive builds of the same tasks," +
" and in the second build, observed $task to be not " +
" UP-TO-DATE. This indicates that $task does not declare" +
" inputs and/or outputs correctly.\n" +
tryToExplainTaskExecution(task, taskGraph)
throw GradleException(message)
}
}
}
// In the first build, record the task's inputs so that if they change in
// the second build then we can compare.
// In the second build, also record the task's inputs because we recorded
// them in the first build, and we want the two builds to be as similar as
// possible
if (shouldTryRerunningTask(task) && !isAllowedToRerunTask(task)) {
recordTaskInputs(task)
}
}
}
}
fun recordTaskInputs(task: Task) {
val text = task.inputs.files.files.joinToString("\n")
val destFile = getTaskInputListPath(task)
destFile.parentFile.mkdirs()
destFile.writeText(text)
}
fun getTaskInputListPath(task: Task): File {
return File(getTasksInputListPath(task.project), task.name)
}
fun getTasksInputListPath(project: Project): File {
return File(project.buildDir, "TaskUpToDateValidator/inputs")
}
fun checkForChangingSetOfInputs(task: Task): String {
val previousInputsFile = getTaskInputListPath(task)
val previousInputs = previousInputsFile.readLines()
val currentInputs = task.inputs.files.files.map { f -> f.toString() }
val addedInputs = currentInputs.minus(previousInputs)
val removedInputs = previousInputs.minus(currentInputs)
val addedMessage = if (addedInputs.size > 0) {
"Added these " + addedInputs.size + " inputs: " +
addedInputs.joinToString("\n") + "\n"
} else {
""
}
val removedMessage = if (removedInputs.size > 0) {
"Removed these " + removedInputs.size + " inputs: " +
removedInputs.joinToString("\n") + "\n"
} else {
""
}
return addedMessage + removedMessage
}
fun tryToExplainTaskExecution(task: Task, taskGraph: TaskExecutionGraph): String {
val numOutputFiles = task.outputs.files.files.size
val outputsMessage = if (numOutputFiles > 0) {
task.path + " declares " + numOutputFiles + " output files. This seems fine.\n"
} else {
task.path + " declares " + numOutputFiles + " output files. This is probably " +
"an error.\n"
}
val inputFiles = task.inputs.files.files
var lastModifiedFile: File? = null
var lastModifiedWhen = Date(0)
for (inputFile in inputFiles) {
val modifiedWhen = Date(inputFile.lastModified())
if (modifiedWhen.compareTo(lastModifiedWhen) > 0) {
lastModifiedFile = inputFile
lastModifiedWhen = modifiedWhen
}
}
val inputSetModifiedMessage = checkForChangingSetOfInputs(task)
val inputsMessage = if (inputSetModifiedMessage != "") {
inputSetModifiedMessage
} else {
if (lastModifiedFile != null) {
task.path + " declares " + inputFiles.size + " input files. The " +
"last modified input file is\n" + lastModifiedFile + "\nmodified at " +
lastModifiedWhen + " (this build started at about " +
getBuildStartTime(task.project) + "). " +
tryToExplainFileModification(lastModifiedFile, taskGraph)
} else {
task.path + " declares " + inputFiles.size + " input files.\n"
}
}
val reproductionMessage = "\nTo reproduce this error you can try running " +
"`./gradlew ${task.path} -PverifyUpToDate`\n"
val readLogsMessage = "\nYou can check why Gradle executed ${task.path} by " +
"passing the '--info' flag to Gradle and then searching stdout for output " +
"generated immediately before the task began to execute.\n" +
"Our best guess for the reason that ${task.path} executed is below.\n"
return readLogsMessage + outputsMessage + inputsMessage + reproductionMessage
}
fun getTaskDeclaringFile(file: File, taskGraph: TaskExecutionGraph): Task? {
for (task in taskGraph.allTasks) {
if (task.outputs.files.files.contains(file)) {
return task
}
}
return null
}
fun tryToExplainFileModification(file: File, taskGraph: TaskExecutionGraph): String {
// Find the task declaring this file as an output,
// or the task declaring one of its parent dirs as an output
var createdByTask: Task? = null
var declaredFile: File? = file
while (createdByTask == null && declaredFile != null) {
createdByTask = getTaskDeclaringFile(declaredFile, taskGraph)
declaredFile = declaredFile.parentFile
}
if (createdByTask == null) {
return "This file is not declared as the output of any task in this build."
}
if (isAllowedToRerunTask(createdByTask)) {
return "This file is declared as an output of " + createdByTask +
", which is a task that is not yet validated by the TaskUpToDateValidator"
} else {
return "This file is decared as an output of " + createdByTask +
", which is a task that is validated by the TaskUpToDateValidator " +
"(and therefore must not have been out-of-date during this build)"
}
}
}
}