blob: 19e798192f804240bc850abafe7b70a87730fa9b [file] [log] [blame]
/*
* Copyright (C) 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 com.android.build.gradle.internal.dependency
import com.android.build.gradle.internal.LoggerWrapper
import com.android.build.gradle.internal.component.ApkCreationConfig
import com.android.build.gradle.internal.component.ComponentCreationConfig
import com.android.build.gradle.internal.errors.MessageReceiverImpl
import com.android.build.gradle.internal.publishing.AndroidArtifacts
import com.android.build.gradle.internal.scope.Java8LangSupport
import com.android.build.gradle.internal.utils.DesugarConfigJson.Companion.combineFileContents
import com.android.build.gradle.options.BooleanOption
import com.android.build.gradle.options.SyncOptions.ErrorFormatMode
import com.android.build.gradle.tasks.toSerializable
import com.android.builder.dexing.ClassFileInput
import com.android.builder.dexing.ClassFileInputs
import com.android.builder.dexing.DependencyGraphUpdater
import com.android.builder.dexing.DexArchiveBuilder
import com.android.builder.dexing.DexFilePerClassFile
import com.android.builder.dexing.DexParameters
import com.android.builder.dexing.MutableDependencyGraph
import com.android.builder.dexing.isJarFile
import com.android.builder.dexing.r8.ClassFileProviderFactory
import com.android.builder.files.SerializableFileChanges
import com.android.utils.FileUtils
import com.google.common.annotations.VisibleForTesting
import com.google.common.io.Closer
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.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.CompileClasspath
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.work.Incremental
import org.gradle.work.InputChanges
import org.slf4j.LoggerFactory
import java.io.File
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import javax.inject.Inject
@CacheableTransform
abstract class BaseDexingTransform<T : BaseDexingTransform.Parameters> : TransformAction<T> {
interface Parameters : GenericTransformParameters {
@get:Input
val minSdkVersion: Property<Int>
@get:Input
val debuggable: Property<Boolean>
@get:Input
val enableDesugaring: Property<Boolean>
@get:Classpath
val bootClasspath: ConfigurableFileCollection
@get:Internal
val errorFormat: Property<ErrorFormatMode>
@get:Optional
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
val desugarLibConfigFiles: ConfigurableFileCollection
@get:Input
val enableGlobalSynthetics: Property<Boolean>
@get:Input
val enableApiModeling: Property<Boolean>
}
@get:Inject
abstract val inputChanges: InputChanges
@get:Classpath
@get:Incremental
@get:InputArtifact
abstract val inputArtifact: Provider<FileSystemLocation>
/** 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 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 {
Pair(outputDir, null)
}
val desugarGraphOutputFile = if (provideIncrementalSupport) {
outputDir.resolve(DESUGAR_GRAPH_FILE_NAME)
} else null
if (provideIncrementalSupport && inputChanges.isIncremental) {
logger.verbose("Running dexing transform incrementally for '${inputDirOrJar.path}'")
processIncrementally(
// 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,
globalSyntheticsOutputDir,
desugarGraphOutputFile!!
)
} else {
logger.verbose("Running dexing transform non-incrementally for '${inputDirOrJar.path}'")
processNonIncrementally(
inputDirOrJar,
classpath,
dexOutputDir,
globalSyntheticsOutputDir,
provideIncrementalSupport,
desugarGraphOutputFile
)
}
}
private fun processIncrementally(
inputDir: File,
inputDirChanges: SerializableFileChanges,
dexOutputDir: File,
globalSyntheticsOutputDir: File?,
desugarGraphOutputFile: File
) {
val desugarGraph = DesugarGraph.read(desugarGraphOutputFile, rootDir = inputDir)
// Compute impacted files based on the changed files and the desugaring graph
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)
// Remove outputs of removed, modified, and unchanged-but-impacted class files
(removedFiles + modifiedFiles + unchangedButImpactedFiles)
.filter { ClassFileInput.CLASS_MATCHER.test(it.path) }
.forEach { classFile ->
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)
}
// 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(
inputDir,
inputFilter,
// `classpath` must be null (see the definition of `provideIncrementalSupport`)
classpath = null,
dexOutputDir,
globalSyntheticsOutputDir,
provideIncrementalSupport = true,
desugarGraph
)
desugarGraph.write(desugarGraphOutputFile)
}
private fun processNonIncrementally(
inputDirOrJar: File,
classpath: List<File>?,
dexOutputDir: File,
globalSyntheticsOutputDir: File?,
provideIncrementalSupport: Boolean,
desugarGraphOutputFile: File? // Not-null iff provideIncrementalSupport == true
) {
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(
inputDirOrJar,
{ _, _ -> true },
classpath,
dexOutputDir,
globalSyntheticsOutputDir,
provideIncrementalSupport,
desugarGraph
)
if (provideIncrementalSupport) {
desugarGraph!!.write(desugarGraphOutputFile!!)
}
}
private fun process(
inputDirOrJar: File,
inputFilter: (File, String) -> Boolean,
classpath: List<File>?,
dexOutputDir: File,
globalSyntheticsOutputDir: File?,
provideIncrementalSupport: Boolean,
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(
classpath?.map(File::toPath) ?: emptyList()
)
.also { closer.register(it) },
coreLibDesugarConfig = combineFileContents(parameters.desugarLibConfigFiles.files),
enableApiModeling = parameters.enableApiModeling.get(),
messageReceiver = MessageReceiverImpl(
parameters.errorFormat.get(),
LoggerFactory.getLogger(BaseDexingTransform::class.java)
)
)
)
ClassFileInputs.fromPath(inputDirOrJar.toPath()).use { classFileInput ->
classFileInput.entries { rootPath, relativePath ->
inputFilter(rootPath.toFile(), relativePath)
}.use { classesInput ->
d8DexBuilder.convert(
classesInput,
dexOutputDir.toPath(),
globalSyntheticsOutputDir?.toPath(),
desugarGraph
)
}
}
}
}
}
/**
* Desugaring graph used for incremental dexing. It contains class files and their dependencies.
*
* This graph handles files with absolute paths. To make it relocatable, this graph internally
* maintains a [relocatableDesugarGraph] which contains Unix-style relative paths of the files
* (the paths need to be *relative* and in *Unix style* so that they can be used across
* filesystems). When writing this graph to disk, we will write the [relocatableDesugarGraph].
*
* To make it easier to convert absolute paths to relative paths, we currently require that all
* files in the graph share one single root directory ([rootDir]).
*/
@VisibleForTesting
internal 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 Unix-style relative paths of the files. */
private val relocatableDesugarGraph: MutableDependencyGraph<String> = MutableDependencyGraph()
) : DependencyGraphUpdater<File> {
override fun addEdge(dependent: File, dependency: File) {
relocatableDesugarGraph.addEdge(
dependent.toUnixStyleRelativePath(),
dependency.toUnixStyleRelativePath()
)
}
fun removeNode(nodeToRemove: File) {
relocatableDesugarGraph.removeNode(nodeToRemove.toUnixStyleRelativePath())
}
fun getAllDependents(nodes: Collection<File>): Set<File> {
val relativePaths = nodes.mapTo(mutableSetOf()) { it.toUnixStyleRelativePath() }
val dependents = relocatableDesugarGraph.getAllDependents(relativePaths)
return dependents.mapTo(mutableSetOf()) { rootDir.resolve(it) }
}
private fun File.toUnixStyleRelativePath(): String {
val unixStyleRelativePath = relativeTo(rootDir).invariantSeparatorsPath
check(!unixStyleRelativePath.startsWith("..")) {
"The given file '$path' is located outside the root directory '${rootDir.path}'"
}
return unixStyleRelativePath
}
fun write(desugarGraphFile: File) {
ObjectOutputStream(desugarGraphFile.outputStream().buffered()).use {
it.writeObject(relocatableDesugarGraph)
}
}
companion object {
fun read(desugarGraphFile: File, rootDir: File): DesugarGraph {
return DesugarGraph(
rootDir = rootDir,
relocatableDesugarGraph =
ObjectInputStream(desugarGraphFile.inputStream().buffered()).use {
@Suppress("UNCHECKED_CAST")
it.readObject() as MutableDependencyGraph<String>
}
)
}
}
}
@CacheableTransform
abstract class DexingNoClasspathTransform : BaseDexingTransform<BaseDexingTransform.Parameters>() {
override fun computeClasspathFiles() = null
}
@CacheableTransform
abstract class DexingWithClasspathTransform : BaseDexingTransform<BaseDexingTransform.Parameters>() {
// 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.toList()
}
/**
* Dexing transform which uses the full classpath (to address bugs such as b/230454566).
*
* Normally we can obtain this full classpath by querying for CLASSES with scope
* [com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.ALL]. However, this
* would impact build performance, especially for incremental builds where `PROJECT` classes often
* change, causing all dexing transforms to rerun.
*
* To mitigate this performance impact while keeping the build correct, the full classpath will
* consist of the following instead:
* 1. All external artifacts ([Parameters.externalArtifacts])
* 2. The input artifact's dependencies provided by Gradle ([inputArtifactDependencies]) -- This
* will ensure that if some of the input artifact's dependencies have `PROJECT` scope (i.e., they
* are not part of [Parameters.externalArtifacts]), they will still be included in the full
* classpath. (The downside is that if some of the input artifact's dependencies have `EXTERNAL`
* scope, they will overlap with [Parameters.externalArtifacts], but that is fine.)
*/
@CacheableTransform
abstract class DexingWithFullClasspathTransform :
BaseDexingTransform<DexingWithFullClasspathTransform.Parameters>() {
@get:CompileClasspath
@get:InputArtifactDependencies
abstract val inputArtifactDependencies: FileCollection
interface Parameters : BaseDexingTransform.Parameters {
@get:CompileClasspath
val externalArtifacts: ConfigurableFileCollection
}
override fun computeClasspathFiles() =
inputArtifactDependencies.files.toList() + parameters.externalArtifacts.files
}
object DexingRegistration {
/** Parameters that are shared across all [ComponentCreationConfig]s. */
class ComponentAgnosticParameters(
val projectName: String,
val dependencyHandler: DependencyHandler,
val bootClasspath: ConfigurableFileCollection,
val errorFormat: ErrorFormatMode,
val desugarLibConfigFiles: FileCollection,
val disableIncrementalDexing: Boolean,
val components: List<ComponentCreationConfig>
)
/**
* Parameters that are specific to a given [ComponentCreationConfig].
*
* Note: This class is a data class so that we can identify equivalent instances of this class
* (see [registerTransforms]).
*
* IMPORTANT: The properties of this class must be of primitive types (e.g., [Boolean], [Int],
* [String]) because the [getAttributes] method relies on [toString], and the implementation of
* [toString] on non-primitive types are not well-defined and subject to change (i.e., it can't
* be used to uniquely represent an object).
*/
data class ComponentSpecificParameters(
val minSdkVersion: Int,
val debuggable: Boolean,
val enableCoreLibraryDesugaring: Boolean,
val enableGlobalSynthetics: Boolean,
val enableApiModeling: Boolean,
val dependenciesClassesAreInstrumented: Boolean,
val asmTransformComponent: String?, // Not-null iff dependenciesClassesAreInstrumented == true
val useJacocoTransformInstrumentation: Boolean,
val enableDesugaring: Boolean,
val needsClasspath: Boolean,
val useFullClasspath: Boolean,
val componentIfUsingFullClasspath: String? // Not-null iff useFullClasspath == true
) {
constructor(creationConfig: ApkCreationConfig) : this(
minSdkVersion = creationConfig.dexing.minSdkVersionForDexing,
debuggable = creationConfig.debuggable,
enableCoreLibraryDesugaring = creationConfig.dexing.isCoreLibraryDesugaringEnabled,
enableGlobalSynthetics = creationConfig.enableGlobalSynthetics,
enableApiModeling = creationConfig.enableApiModeling,
dependenciesClassesAreInstrumented = creationConfig.instrumentationCreationConfig?.dependenciesClassesAreInstrumented == true,
asmTransformComponent = creationConfig.name.takeIf { creationConfig.instrumentationCreationConfig?.dependenciesClassesAreInstrumented == true },
useJacocoTransformInstrumentation = creationConfig.useJacocoTransformInstrumentation,
enableDesugaring = needsDesugaring(creationConfig),
needsClasspath = needsClasspath(creationConfig),
useFullClasspath = useFullClasspath(creationConfig),
componentIfUsingFullClasspath = creationConfig.name.takeIf { useFullClasspath(creationConfig) }
)
companion object {
private fun needsDesugaring(creationConfig: ApkCreationConfig): Boolean =
creationConfig.dexing.java8LangSupportType == Java8LangSupport.D8
private fun needsClasspath(creationConfig: ApkCreationConfig): Boolean =
needsDesugaring(creationConfig) &&
creationConfig.dexing.minSdkVersionForDexing < 24
private fun useFullClasspath(creationConfig: ApkCreationConfig): Boolean =
needsClasspath(creationConfig) &&
creationConfig.services.projectOptions.get(BooleanOption.USE_FULL_CLASSPATH_FOR_DEXING_TRANSFORM)
}
/**
* Returns [AndroidAttributes] that uniquely represent the contents of this object.
*
* These attributes will be used when registering the transforms and when consuming the
* artifacts to ensure correct artifact production/consumption.
*/
fun getAttributes() =
AndroidAttributes(
Attribute.of("dexing-component-attributes", String::class.java) to this.toString()
) + asmTransformComponent?.let {
// When asmTransformComponent != null, the consumed artifacts have the attribute
// below, so we need to specify it here as well to allow unambiguous artifact
// selection.
AndroidAttributes(AsmClassesTransform.ATTR_ASM_TRANSFORMED_VARIANT to asmTransformComponent)
}
}
fun registerTransforms(
components: List<ComponentCreationConfig>,
parameters: ComponentAgnosticParameters
) {
// To improve performance and avoid duplicate registrations, instead of setting up one
// transform per component, we will set up one transform per group of equivalent components
// (components whose registered transforms would be identical if registered separately).
components.filterIsInstance<ApkCreationConfig>()
.mapTo(linkedSetOf()) { ComponentSpecificParameters(it) }
.forEach {
registerTransform(parameters, it)
}
}
fun registerTransform(
allComponents: ComponentAgnosticParameters,
component: ComponentSpecificParameters
) {
@Suppress("UNCHECKED_CAST")
val transformClass = when {
!component.needsClasspath -> DexingNoClasspathTransform::class.java
!component.useFullClasspath -> DexingWithClasspathTransform::class.java
else -> DexingWithFullClasspathTransform::class.java as Class<BaseDexingTransform<BaseDexingTransform.Parameters>>
}
allComponents.dependencyHandler.registerTransform(transformClass) { spec ->
spec.parameters.apply {
projectName.set(allComponents.projectName)
minSdkVersion.set(component.minSdkVersion)
debuggable.set(component.debuggable)
enableDesugaring.set(component.enableDesugaring)
// bootclasspath is required by d8 to do API conversion for library desugaring
if (component.needsClasspath || component.enableCoreLibraryDesugaring) {
bootClasspath.from(allComponents.bootClasspath)
}
errorFormat.set(allComponents.errorFormat)
if (component.enableCoreLibraryDesugaring) {
desugarLibConfigFiles.setFrom(allComponents.desugarLibConfigFiles)
}
enableGlobalSynthetics.set(component.enableGlobalSynthetics)
enableApiModeling.set(component.enableApiModeling)
}
// There are 2 transform flows for DEX:
// 1. (JACOCO_)CLASSES_DIR -> (JACOCO_)CLASSES -> DEX
// 2. (JACOCO_)CLASSES_JAR -> (JACOCO_)CLASSES -> DEX
//
// For incremental dexing, when requesting DEX the consumer will indicate a
// preference for CLASSES_DIR over CLASSES_JAR (see DexMergingTask), otherwise
// Gradle will select CLASSES_JAR by default.
//
// However, there could be an issue if CLASSES_DIR is selected: For Java libraries
// using Kotlin, CLASSES_DIR has two separate directories: one for compiled Java
// classes and one for compiled Kotlin classes. Classes in one directory may
// reference classes in the other directory, but each directory is transformed to
// DEX independently. Therefore, if dexing requires a classpath (desugaring is
// enabled and minSdk < 24), desugaring may not work correctly.
//
// Android libraries do not have this issue, as their CLASSES_DIR is one directory
// containing both Java and Kotlin classes.
//
// Therefore, to ensure correctness in all cases, we transform CLASSES to DEX only
// when dexing does not require a classpath, and it is not for main and androidTest
// components in dynamic feature module(b/246326007). Otherwise, we transform
// CLASSES_JAR to DEX directly so that CLASSES_DIR will not be selected.
//
// In the case that the JacocoTransform is executed, the Jacoco equivalent artifact is
// used. These artifacts are the same as CLASSES, CLASSES_JAR and ASM_INSTRUMENTED_JARS,
// but they have been offline instrumented by Jacoco and include Jacoco dependencies.
val inputArtifactType: AndroidArtifacts.ArtifactType =
if (component.useJacocoTransformInstrumentation) {
when {
component.dependenciesClassesAreInstrumented ->
AndroidArtifacts.ArtifactType.JACOCO_ASM_INSTRUMENTED_JARS
!component.needsClasspath && !allComponents.disableIncrementalDexing ->
AndroidArtifacts.ArtifactType.JACOCO_CLASSES
else ->
AndroidArtifacts.ArtifactType.JACOCO_CLASSES_JAR
}
} else {
when {
component.dependenciesClassesAreInstrumented ->
AndroidArtifacts.ArtifactType.ASM_INSTRUMENTED_JARS
!component.needsClasspath && !allComponents.disableIncrementalDexing ->
AndroidArtifacts.ArtifactType.CLASSES
else ->
AndroidArtifacts.ArtifactType.CLASSES_JAR
}
}
if (component.useFullClasspath) {
val componentName = component.componentIfUsingFullClasspath!!
val creationConfig = allComponents.components.first { it.name == componentName }
(spec.parameters as DexingWithFullClasspathTransform.Parameters).externalArtifacts.from(
creationConfig.variantDependencies.getArtifactCollection(
AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
AndroidArtifacts.ArtifactScope.EXTERNAL,
inputArtifactType,
attributes = AsmClassesTransform.getAttributesForConfig(creationConfig).takeIf {
component.dependenciesClassesAreInstrumented
}
).artifactFiles
)
}
spec.from.attribute(ARTIFACT_TYPE_ATTRIBUTE, inputArtifactType.type)
if (component.enableGlobalSynthetics) {
spec.to.attribute(
ARTIFACT_TYPE_ATTRIBUTE,
AndroidArtifacts.ArtifactType.D8_OUTPUTS.type
)
} else {
spec.to.attribute(ARTIFACT_TYPE_ATTRIBUTE, AndroidArtifacts.ArtifactType.DEX.type)
}
component.getAttributes().apply {
addAttributesToContainer(spec.from)
addAttributesToContainer(spec.to)
}
}
}
}
private val logger = LoggerWrapper.getLogger(BaseDexingTransform::class.java)
private const val DESUGAR_GRAPH_FILE_NAME = "desugar_graph.bin"