/* * 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 import java.io.File import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.file.RegularFileProperty import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.TaskAction /** Finds the outputs of every task and saves this mapping into a file */ @CacheableTask abstract class ListTaskOutputsTask : DefaultTask() { @OutputFile val outputFile: RegularFileProperty = project.objects.fileProperty() @Input val removePrefixes: MutableList = mutableListOf() @Input val tasks: MutableList = mutableListOf() @get:Input val outputText by lazy { computeOutputText() } init { group = "Help" // compute the output text when the taskgraph is ready so that the output text can be // saved in the configuration cache and not generate a configuration cache violation project.gradle.taskGraph.whenReady { outputText } } fun setOutput(f: File) { outputFile.set(f) description = "Finds the outputs of every task and saves the resulting mapping into $f" } fun removePrefix(prefix: String) { removePrefixes.add("$prefix/") } // Given a map from output file to Task, formats into a String private fun formatTasks(tasksByOutput: Map): String { val messages: MutableList = mutableListOf() for ((output, task) in tasksByOutput) { var filePath = output.path for (prefix in removePrefixes) { filePath = filePath.removePrefix(prefix) } messages.add( formatInColumns( listOf(filePath, " - " + task.path + " (" + task::class.qualifiedName + ")") ) ) } messages.sort() return messages.joinToString("\n") } // Given a list of columns, indents and joins them to be easy to read private fun formatInColumns(columns: List): String { val components = mutableListOf() var textLength = 0 for (column in columns) { val roundedTextLength = if (textLength == 0) { textLength } else { ((textLength / 32) + 1) * 32 } val extraSpaces = " ".repeat(roundedTextLength - textLength) components.add(extraSpaces) textLength = roundedTextLength components.add(column) textLength += column.length } return components.joinToString("") } fun computeOutputText(): String { val tasksByOutput = project.rootProject.findAllTasksByOutput() return formatTasks(tasksByOutput) } @TaskAction fun exec() { val outputFile = outputFile.get() outputFile.asFile.writeText(outputText) } } // TODO(149103692): remove all elements of this set val taskNamesKnownToDuplicateOutputs = setOf( // Instead of adding new elements to this set, prefer to disable unused tasks when possible // b/308798582 "transformNonJvmMainCInteropDependenciesMetadataForIde", "transformDarwinTestCInteropDependenciesMetadataForIde", "transformDarwinMainCInteropDependenciesMetadataForIde", "transformCommonMainCInteropDependenciesMetadataForIde", "transformCommonTestCInteropDependenciesMetadataForIde", "transformIosMainCInteropDependenciesMetadataForIde", "transformIosTestCInteropDependenciesMetadataForIde", "transformNativeTestCInteropDependenciesMetadataForIde", "transformNativeMainCInteropDependenciesMetadataForIde", "transformLinuxMainCInteropDependenciesMetadataForIde", "transformNonJvmCommonMainCInteropDependenciesMetadataForIde", // The following tests intentionally have the same output of golden images "updateGoldenDesktopTest", "updateGoldenDebugUnitTest", // The following tasks have the same output file: // ../../prebuilts/androidx/javascript-for-kotlin/yarn.lock "kotlinRestoreYarnLock", "kotlinNpmInstall", "kotlinUpgradePackageLock", "kotlinUpgradeYarnLock", "kotlinStorePackageLock", "kotlinStoreYarnLock", // The following tasks have the same output configFile file: // projectBuildDir/js/packages/projectName-wasm-js/webpack.config.js // Remove when https://youtrack.jetbrains.com/issue/KT-70029 / b/361319689 is resolved // and set configFile location for each task "wasmJsBrowserDevelopmentWebpack", "wasmJsBrowserDevelopmentRun", "wasmJsBrowserProductionWebpack", "wasmJsBrowserProductionRun", "jsTestTestDevelopmentExecutableCompileSync", // Remove when https://youtrack.jetbrains.com/issue/KT-71688 is resolved and set // destinationDirectory to the project's build directory "wasmJsTestTestDevelopmentExecutableCompileSync", "wasmJsTestTestProductionExecutableCompileSync", // TODO file a bug "kotlinNodeJsSetup", ) fun shouldValidateTaskOutput(task: Task): Boolean { if (!task.enabled) { return false } return !taskNamesKnownToDuplicateOutputs.contains(task.name) } // For this project and all subprojects, collects all tasks and creates a map keyed by their output // files fun Project.findAllTasksByOutput(): Map { // find list of all tasks val allTasks = mutableListOf() project.allprojects { otherProject -> otherProject.tasks.forEach { task -> allTasks.add(task) } } // group tasks by their outputs val tasksByOutput: MutableMap = hashMapOf() for (otherTask in allTasks) { for (otherTaskOutput in otherTask.outputs.files.files) { val existingTask = tasksByOutput[otherTaskOutput] if (existingTask != null) { if (shouldValidateTaskOutput(existingTask) && shouldValidateTaskOutput(otherTask)) { throw GradleException( "Output file " + otherTaskOutput + " was declared as an output of " + "multiple tasks: " + otherTask + " and " + existingTask ) } // if there is an exempt conflict, keep the alphabetically earlier task to ensure // consistency if (existingTask.path > otherTask.path) continue } tasksByOutput[otherTaskOutput] = otherTask } } return tasksByOutput }