summaryrefslogtreecommitdiff
path: root/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dependency/DexingTransform.kt
diff options
context:
space:
mode:
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.kt324
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"