diff options
Diffstat (limited to 'build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/DexingTransform.kt')
-rw-r--r-- | build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/DexingTransform.kt | 324 |
1 files changed, 172 insertions, 152 deletions
diff --git a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/DexingTransform.kt b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/DexingTransform.kt index cd5e6841fc..7f38dd2370 100644 --- a/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/DexingTransform.kt +++ b/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/DexingTransform.kt @@ -42,18 +42,17 @@ import com.android.builder.files.SerializableFileChanges import com.android.sdklib.AndroidVersion import com.android.utils.FileUtils import com.google.common.io.Closer -import com.google.common.io.Files import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.artifacts.transform.CacheableTransform import org.gradle.api.artifacts.transform.InputArtifact import org.gradle.api.artifacts.transform.InputArtifactDependencies import org.gradle.api.artifacts.transform.TransformAction import org.gradle.api.artifacts.transform.TransformOutputs +import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE import org.gradle.api.attributes.Attribute import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileCollection import org.gradle.api.file.FileSystemLocation -import org.gradle.api.internal.artifacts.ArtifactAttributes.ARTIFACT_FORMAT import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.Classpath @@ -61,12 +60,10 @@ import org.gradle.api.tasks.CompileClasspath import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional -import org.gradle.work.FileChange import org.gradle.work.Incremental import org.gradle.work.InputChanges import org.slf4j.LoggerFactory import java.io.File -import java.nio.file.Path import javax.inject.Inject @CacheableTransform @@ -94,213 +91,178 @@ abstract class BaseDexingTransform<T : BaseDexingTransform.Parameters> : Transfo abstract val inputChanges: InputChanges @get:Classpath - @get:InputArtifact @get:Incremental - abstract val primaryInput: Provider<FileSystemLocation> + @get:InputArtifact + abstract val inputArtifact: Provider<FileSystemLocation> - protected abstract fun computeClasspathFiles(): List<Path> + /** Classpath used for dexing/desugaring, or null if it is not required. */ + protected abstract fun computeClasspathFiles(): List<File>? override fun transform(outputs: TransformOutputs) { //TODO(b/162813654) record transform execution span - val input = primaryInput.get().asFile - val outputDir = outputs.dir(Files.getNameWithoutExtension(input.name)) - doTransform(input, outputDir) - } - - private fun doTransform(inputFile: File, outputDir: File) { - val provideIncrementalSupport = !isJarFile(inputFile) - - val dexOutputDir = if (parameters.enableGlobalSynthetics.get()) { - outputDir.resolve(computeDexDirName(outputDir)) + val inputDirOrJar = inputArtifact.get().asFile.also { + check(it.isDirectory || isJarFile(it)) { + "Expected directory or jar but found: ${it.path}" + } + } + val classpath = computeClasspathFiles() + val outputDir = outputs.dir(inputDirOrJar.nameWithoutExtension) + + // Currently we are able to run incrementally only if + // - The input artifact is a directory (not jar). + // - Dexing/desugaring does not require a classpath. + // This is because the desugaring graph that is used for incremental dexing needs to be + // relocatable, which requires that all files in the graph share one single root directory + // so that they can be converted to relative paths (see `DesugarGraph`'s kdoc). + val provideIncrementalSupport = inputDirOrJar.isDirectory && classpath == null + + val (dexOutputDir, globalSyntheticsOutputDir) = if (parameters.enableGlobalSynthetics.get()) { + Pair( + outputDir.resolve(computeDexDirName(outputDir)), + outputDir.resolve(computeGlobalSyntheticsDirName(outputDir)) + ) } else { - outputDir + Pair(outputDir, null) } - val globalSyntheticsDir = if (parameters.enableGlobalSynthetics.get()) { - outputDir.resolve(computeGlobalSyntheticsDirName(outputDir)) + val desugarGraphOutputFile = if (provideIncrementalSupport) { + outputDir.resolve(DESUGAR_GRAPH_FILE_NAME) } else null - // desugarGraphFile != null iff provideIncrementalSupport == true - val desugarGraphFile = - if (provideIncrementalSupport) { - // The desugaring graph file is outside outputDir and is not registered as an - // output because the graph is not relocatable. - outputDir.resolve("../$DESUGAR_GRAPH_FILE_NAME") - } else null - if (provideIncrementalSupport && inputChanges.isIncremental) { - // Gradle API currently does not provide classpath changes. When the classpath changes, - // Gradle will run the transform non-incrementally in a new directory, so it is still - // correct, but not quite efficient yet. - // TODO(132615827): Update this code once Gradle provides classpath changes - // (https://github.com/gradle/gradle/issues/11794) - val classpathChanges = emptyList<FileChange>() + logger.verbose("Running dexing transform incrementally for '${inputDirOrJar.path}'") processIncrementally( - inputFile, - inputChanges.getFileChanges(primaryInput).toSerializable(), - classpathChanges.toSerializable(), + // Must be a directory (see the definition of `provideIncrementalSupport`) + inputDir = inputDirOrJar, + // `inputChanges` contains changes to the input artifact only, changes to the + // classpath are not available (https://github.com/gradle/gradle/issues/11794) + inputDirChanges = inputChanges.getFileChanges(inputArtifact).toSerializable(), dexOutputDir, - globalSyntheticsDir, - desugarGraphFile!! + globalSyntheticsOutputDir, + desugarGraphOutputFile!! ) } else { + logger.verbose("Running dexing transform non-incrementally for '${inputDirOrJar.path}'") processNonIncrementally( - inputFile, + inputDirOrJar, + classpath, dexOutputDir, - globalSyntheticsDir, + globalSyntheticsOutputDir, provideIncrementalSupport, - desugarGraphFile + desugarGraphOutputFile ) } } private fun processIncrementally( - input: File, - inputChanges: SerializableFileChanges, - classpathChanges: SerializableFileChanges, + inputDir: File, + inputDirChanges: SerializableFileChanges, dexOutputDir: File, - globalSyntheticsOutput: File?, - desugarGraphFile: File + globalSyntheticsOutputDir: File?, + desugarGraphOutputFile: File ) { - val desugarGraph = try { - readDesugarGraph(desugarGraphFile) - } catch (e: Exception) { - LoggerWrapper.getLogger(BaseDexingTransform::class.java).warning( - "Failed to read desugaring graph." + - " Cause: ${e.javaClass.simpleName}, message: ${e.message}.\n" + - "Fall back to non-incremental mode." - ) - processNonIncrementally( - input, - dexOutputDir, - globalSyntheticsOutput, - true, - desugarGraphFile - ) - return - } + val desugarGraph = DesugarGraph.read(desugarGraphOutputFile, rootDir = inputDir) // Compute impacted files based on the changed files and the desugaring graph - val removedFiles = - (inputChanges.removedFiles + classpathChanges.removedFiles).map { it.file }.toSet() - val modifiedFiles = - (inputChanges.modifiedFiles + classpathChanges.modifiedFiles).map { it.file }.toSet() - val addedFiles = - (inputChanges.addedFiles + classpathChanges.addedFiles).map { it.file }.toSet() + val removedFiles = inputDirChanges.removedFiles.map { it.file } + val modifiedFiles = inputDirChanges.modifiedFiles.map { it.file } + val addedFiles = inputDirChanges.addedFiles.map { it.file } val unchangedButImpactedFiles = desugarGraph.getAllDependents(removedFiles + modifiedFiles) - val modifiedImpactedOrAddedFiles = modifiedFiles + unchangedButImpactedFiles + addedFiles - val removedModifiedOrImpactedFiles = - removedFiles + modifiedFiles + unchangedButImpactedFiles // Remove outputs of removed, modified, and unchanged-but-impacted class files - check(input.isDirectory) { "In incremental mode, input must be a directory: ${input.path}" } - (inputChanges.removedFiles.map { it.file } + input.walk().toList()) - .filter { - ClassFileInput.CLASS_MATCHER.test(it.path) && it in removedModifiedOrImpactedFiles - } + (removedFiles + modifiedFiles + unchangedButImpactedFiles) + .filter { ClassFileInput.CLASS_MATCHER.test(it.path) } .forEach { classFile -> - // We are in DexFilePerClassFile output mode - DexFilePerClassFile.getDexOutputRelativePathsOfClassFile( - classFileRelativePath = classFile.toRelativeString(input) - ).forEach { outputRelativePath -> - FileUtils.deleteIfExists(dexOutputDir.resolve(outputRelativePath)) - } - globalSyntheticsOutput?.let { - DexFilePerClassFile.getGlobalOutputRelativePathOfClassFile( - classFile.toRelativeString(input) - ).let { outputRelativePath -> - FileUtils.deleteIfExists(it.resolve(outputRelativePath)) - } + val classFileRelativePath = classFile.toRelativeString(inputDir) + // Output mode must be DexFilePerClassFile (see the `process` method) + DexFilePerClassFile.getDexOutputRelativePath(classFileRelativePath) + .let { FileUtils.deleteIfExists(dexOutputDir.resolve(it)) } + globalSyntheticsOutputDir?.let { + DexFilePerClassFile.getGlobalSyntheticOutputRelativePath(classFileRelativePath) + .let { FileUtils.deleteIfExists(globalSyntheticsOutputDir.resolve(it)) } } + desugarGraph.removeNode(classFile) } - // Remove stale nodes in the desugaring graph - removedModifiedOrImpactedFiles.forEach { - desugarGraph.removeNode(it) - } - - // Process only input files that are modified, added, or unchanged-but-impacted - val filter: (File, String) -> Boolean = { inputDir: File, relativePath: String -> - check(inputDir.isDirectory) + // Process only class files that are modified, unchanged-but-impacted, or added + val modifiedImpactedOrAddedFiles = + (modifiedFiles + unchangedButImpactedFiles + addedFiles).toSet() + val inputFilter: (File, String) -> Boolean = { _, relativePath: String -> inputDir.resolve(relativePath) in modifiedImpactedOrAddedFiles } process( - input, - filter, + inputDir, + inputFilter, + // `classpath` must be null (see the definition of `provideIncrementalSupport`) + classpath = null, dexOutputDir, - globalSyntheticsOutput, - true, + globalSyntheticsOutputDir, + provideIncrementalSupport = true, desugarGraph ) - // Store the desugaring graph for use in the next build. If dexing failed earlier, it is - // intended that we will not store the graph as it is only meant to contain info about a - // previous successful build. - writeDesugarGraph(desugarGraphFile, desugarGraph) + desugarGraph.write(desugarGraphOutputFile) } private fun processNonIncrementally( - input: File, + inputDirOrJar: File, + classpath: List<File>?, dexOutputDir: File, - globalSyntheticsOutput: File?, + globalSyntheticsOutputDir: File?, provideIncrementalSupport: Boolean, - desugarGraphFile: File? // desugarGraphFile != null iff provideIncrementalSupport == true + desugarGraphOutputFile: File? // Not-null iff provideIncrementalSupport == true ) { - FileUtils.deleteRecursivelyIfExists(dexOutputDir) - FileUtils.mkdirs(dexOutputDir) - globalSyntheticsOutput?.let { - FileUtils.deleteRecursivelyIfExists(it) - FileUtils.mkdirs(it) - } - desugarGraphFile?.let { - FileUtils.deleteIfExists(it) - FileUtils.mkdirs(it.parentFile) - } - - val desugarGraph = desugarGraphFile?.let { - MutableDependencyGraph<File>() - } + FileUtils.cleanOutputDir(dexOutputDir) + globalSyntheticsOutputDir?.let { FileUtils.cleanOutputDir(it) } + desugarGraphOutputFile?.let { FileUtils.deleteIfExists(it) } + + val desugarGraph = if (provideIncrementalSupport) { + // `inputDirOrJar` must be a directory when `provideIncrementalSupport == true` + // (see the definition of `provideIncrementalSupport`) + DesugarGraph(rootDir = inputDirOrJar) + } else null process( - input, + inputDirOrJar, { _, _ -> true }, + classpath, dexOutputDir, - globalSyntheticsOutput, + globalSyntheticsOutputDir, provideIncrementalSupport, desugarGraph ) - // Store the desugaring graph for use in the next build. If dexing failed earlier, it is - // intended that we will not store the graph as it is only meant to contain info about a - // previous successful build. - desugarGraphFile?.let { - writeDesugarGraph(it, desugarGraph!!) + if (provideIncrementalSupport) { + desugarGraph!!.write(desugarGraphOutputFile!!) } } private fun process( - input: File, + inputDirOrJar: File, inputFilter: (File, String) -> Boolean, + classpath: List<File>?, dexOutputDir: File, - globalSyntheticsOutput: File?, + globalSyntheticsOutputDir: File?, provideIncrementalSupport: Boolean, - // desugarGraphUpdater != null iff provideIncrementalSupport == true - desugarGraphUpdater: DependencyGraphUpdater<File>? + desugarGraph: DesugarGraph? // Not-null iff provideIncrementalSupport == true ) { + @Suppress("UnstableApiUsage") Closer.create().use { closer -> val d8DexBuilder = DexArchiveBuilder.createD8DexBuilder( DexParameters( minSdkVersion = parameters.minSdkVersion.get(), debuggable = parameters.debuggable.get(), + // dexPerClass iff provideIncrementalSupport == true dexPerClass = provideIncrementalSupport, withDesugaring = parameters.enableDesugaring.get(), desugarBootclasspath = ClassFileProviderFactory( parameters.bootClasspath.files.map(File::toPath) ) .also { closer.register(it) }, - desugarClasspath = ClassFileProviderFactory(computeClasspathFiles()).also { - closer.register(it) - }, + desugarClasspath = ClassFileProviderFactory( + classpath?.map(File::toPath) ?: emptyList() + ) + .also { closer.register(it) }, coreLibDesugarConfig = parameters.libConfiguration.orNull, messageReceiver = MessageReceiverImpl( parameters.errorFormat.get(), @@ -309,15 +271,15 @@ abstract class BaseDexingTransform<T : BaseDexingTransform.Parameters> : Transfo ) ) - ClassFileInputs.fromPath(input.toPath()).use { classFileInput -> + ClassFileInputs.fromPath(inputDirOrJar.toPath()).use { classFileInput -> classFileInput.entries { rootPath, relativePath -> inputFilter(rootPath.toFile(), relativePath) }.use { classesInput -> d8DexBuilder.convert( classesInput, dexOutputDir.toPath(), - globalSyntheticsOutput?.toPath(), - desugarGraphUpdater + globalSyntheticsOutputDir?.toPath(), + desugarGraph ) } } @@ -325,23 +287,76 @@ abstract class BaseDexingTransform<T : BaseDexingTransform.Parameters> : Transfo } } +/** + * Desugaring graph used for incremental dexing. It contains class files and their dependencies. + * + * This graph handles files with absolute paths. To make it relocatable, it requires that all files + * in the graph share one single root directory ([rootDir]) so that they can be converted to + * relative paths. + * + * Internally, this graph maintains a [relocatableDesugarGraph] containing relative paths of the + * files. When writing this graph to disk, we will write the [relocatableDesugarGraph]. + */ +private class DesugarGraph( + + /** The root directory that is shared among all files in the desugaring graph. */ + private val rootDir: File, + + /** The relocatable desugaring graph, which contains relative paths of the files. */ + private val relocatableDesugarGraph: MutableDependencyGraph<File> = MutableDependencyGraph() + +) : DependencyGraphUpdater<File> { + + override fun addEdge(dependent: File, dependency: File) { + relocatableDesugarGraph.addEdge( + dependent.relativeTo(rootDir), + dependency.relativeTo(rootDir) + ) + } + + fun removeNode(nodeToRemove: File) { + relocatableDesugarGraph.removeNode(nodeToRemove.relativeTo(rootDir)) + } + + fun getAllDependents(nodes: Collection<File>): Set<File> { + val relativePaths = nodes.mapTo(mutableSetOf()) { it.relativeTo(rootDir) } + + val dependents = relocatableDesugarGraph.getAllDependents(relativePaths) + + return dependents.mapTo(mutableSetOf()) { rootDir.resolve(it) } + } + + fun write(relocatableDesugarGraphFile: File) { + writeDesugarGraph(relocatableDesugarGraphFile, relocatableDesugarGraph) + } + + companion object { + + fun read(relocatableDesugarGraphFile: File, rootDir: File): DesugarGraph { + return DesugarGraph( + rootDir = rootDir, + relocatableDesugarGraph = readDesugarGraph(relocatableDesugarGraphFile) + ) + } + } +} + @CacheableTransform abstract class DexingNoClasspathTransform : BaseDexingTransform<BaseDexingTransform.Parameters>() { - override fun computeClasspathFiles() = listOf<Path>() + + override fun computeClasspathFiles() = null } @CacheableTransform abstract class DexingWithClasspathTransform : BaseDexingTransform<BaseDexingTransform.Parameters>() { - /** - * Using compile classpath normalization is safe here due to the design of desugar: - * Method bodies are only moved to the companion class within the same artifact, - * not between artifacts. - */ + + // Use @CompileClasspath instead of @Classpath because non-ABI changes on the classpath do not + // impact dexing/desugaring of the artifact. @get:CompileClasspath @get:InputArtifactDependencies abstract val classpath: FileCollection - override fun computeClasspathFiles() = classpath.files.map(File::toPath) + override fun computeClasspathFiles() = classpath.files.toList() } fun getDexingArtifactConfigurations(components: Collection<ComponentCreationConfig>): Set<DexingArtifactConfiguration> { @@ -379,7 +394,7 @@ data class DexingArtifactConfiguration( ) { // If we want to do desugaring and our minSdk (or the API level of the device we're deploying - // to) is lower than N then we need additional classpaths in order to proper do the desugaring. + // to) is lower than N then we need a classpath in order to properly do the desugaring. private val needsClasspath = enableDesugaring && minSdk < AndroidVersion.VersionCodes.N fun registerTransform( @@ -453,14 +468,17 @@ data class DexingArtifactConfiguration( } } spec.from.attribute( - ARTIFACT_FORMAT, + ARTIFACT_TYPE_ATTRIBUTE, inputArtifact.type ) if (enableGlobalSynthetics) { - spec.to.attribute(ARTIFACT_FORMAT, AndroidArtifacts.ArtifactType.D8_OUTPUTS.type) + spec.to.attribute( + ARTIFACT_TYPE_ATTRIBUTE, + AndroidArtifacts.ArtifactType.D8_OUTPUTS.type + ) } else { - spec.to.attribute(ARTIFACT_FORMAT, AndroidArtifacts.ArtifactType.DEX.type) + spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, AndroidArtifacts.ArtifactType.DEX.type) } getAttributes().apply { @@ -499,4 +517,6 @@ val ATTR_ENABLE_DESUGARING: Attribute<String> = val ATTR_ENABLE_JACOCO_INSTRUMENTATION: Attribute<String> = Attribute.of("dexing-enable-jacoco-instrumentation", String::class.java) -const val DESUGAR_GRAPH_FILE_NAME = "desugar_graph.bin" +private val logger = LoggerWrapper.getLogger(BaseDexingTransform::class.java) + +private const val DESUGAR_GRAPH_FILE_NAME = "desugar_graph.bin" |