blob: e2a01112adf9d28a6e9ade1734234b0e4c985a71 [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.tasks
import com.android.SdkConstants
import com.android.build.api.component.impl.ComponentPropertiesImpl
import com.android.build.api.transform.TransformException
import com.android.build.gradle.internal.LoggerWrapper
import com.android.build.gradle.internal.crash.PluginCrashReporter
import com.android.build.gradle.internal.dependency.getDexingArtifactConfiguration
import com.android.build.gradle.internal.errors.MessageReceiverImpl
import com.android.build.gradle.internal.publishing.AndroidArtifacts
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.build.gradle.internal.scope.InternalMultipleArtifactType
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction
import com.android.build.gradle.internal.transforms.DexMergerTransformCallable
import com.android.build.gradle.internal.utils.setDisallowChanges
import com.android.build.gradle.options.BooleanOption
import com.android.build.gradle.options.SyncOptions
import com.android.builder.dexing.DexMergerTool
import com.android.builder.dexing.DexingType
import com.android.ide.common.blame.Message
import com.android.ide.common.blame.ParsingProcessOutputHandler
import com.android.ide.common.blame.parser.DexParser
import com.android.ide.common.blame.parser.ToolOutputParser
import com.android.ide.common.process.ProcessException
import com.android.ide.common.process.ProcessOutput
import com.android.utils.FileUtils
import com.android.utils.PathUtils.toSystemIndependentPath
import com.google.common.annotations.VisibleForTesting
import com.google.common.base.Throwables
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.logging.Logging
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
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.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskProvider
import org.gradle.work.InputChanges
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.Serializable
import java.nio.file.Files
import java.util.Collections
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
import javax.inject.Inject
/**
* Dex merging task. This task will merge all specified dex files, using the specified parameters.
*
* If handles all dexing types, as specified in [DexingType].
*
* One of the interesting properties is [mergingThreshold]. For dex file inputs, this property will
* determine if dex files can be just copied to output, or we have to merge them. If the number of
* dex files is at least [mergingThreshold], the files will be merged in a single invocation.
* Otherwise, we will just copy the dex files to the output directory.
*/
@CacheableTask
abstract class DexMergingTask : NewIncrementalTask() {
@get:Input
abstract val dexingType: Property<DexingType>
@get:Input
abstract val dexMerger: Property<DexMergerTool>
@get:Input
abstract val minSdkVersion: Property<Int>
@get:Input
abstract val debuggable: Property<Boolean>
@get:Input
abstract val mergingThreshold: Property<Int>
@get:Optional
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
abstract val mainDexListFile: RegularFileProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
lateinit var dexFiles: FileCollection
private set
@get:Optional
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
abstract val fileDependencyDexFiles: DirectoryProperty
// Dummy folder, used as a way to set up dependency
@get:Optional
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val duplicateClassesCheck: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Internal
abstract val errorFormatMode: Property<SyncOptions.ErrorFormatMode>
@get:Internal
abstract val dxStateBuildService: Property<DxStateBuildService>
override fun doTaskAction(inputChanges: InputChanges) {
// TODO(132615300) Make this task incremental
getWorkerFacadeWithWorkers().use {
it.submit(
DexMergingTaskRunnable::class.java,
DexMergingParams(
dexingType.get(),
errorFormatMode.get(),
dexMerger.get(),
minSdkVersion.get(),
debuggable.get(),
mergingThreshold.get(),
mainDexListFile.orNull?.asFile,
dexFiles.files,
fileDependencyDexFiles.orNull?.asFile,
outputDir.get().asFile
)
)
}
if (dexMerger.get() == DexMergerTool.DX) {
dxStateBuildService.get().clearStateAfterBuild()
}
}
class CreationAction @JvmOverloads constructor(
componentProperties: ComponentPropertiesImpl,
private val action: DexMergingAction,
private val dexingType: DexingType,
private val dexingUsingArtifactTransforms: Boolean = true,
private val separateFileDependenciesDexingTask: Boolean = false,
private val outputType: InternalMultipleArtifactType = InternalMultipleArtifactType.DEX
) : VariantTaskCreationAction<DexMergingTask, ComponentPropertiesImpl>(componentProperties) {
private val internalName: String = when (action) {
DexMergingAction.MERGE_LIBRARY_PROJECTS -> componentProperties.computeTaskName("mergeLibDex")
DexMergingAction.MERGE_EXTERNAL_LIBS -> componentProperties.computeTaskName("mergeExtDex")
DexMergingAction.MERGE_PROJECT -> componentProperties.computeTaskName("mergeProjectDex")
DexMergingAction.MERGE_ALL -> componentProperties.computeTaskName("mergeDex")
}
override val name = internalName
override val type = DexMergingTask::class.java
override fun handleProvider(taskProvider: TaskProvider<DexMergingTask>) {
super.handleProvider(taskProvider)
creationConfig.artifacts
.use(taskProvider)
.wiredWith(DexMergingTask::outputDir)
.toAppendTo(outputType)
}
override fun configure(task: DexMergingTask) {
super.configure(task)
task.dexFiles = getDexFiles(creationConfig, action)
task.mergingThreshold.setDisallowChanges(getMergingThreshold(action, creationConfig))
task.dexingType.setDisallowChanges(dexingType)
if (DexMergingAction.MERGE_ALL == action && dexingType === DexingType.LEGACY_MULTIDEX) {
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.LEGACY_MULTIDEX_MAIN_DEX_LIST,
task.mainDexListFile)
}
task.errorFormatMode.setDisallowChanges(
SyncOptions.getErrorFormatMode(creationConfig.services.projectOptions))
task.dexMerger.setDisallowChanges(creationConfig.variantScope.dexMerger)
task.minSdkVersion.setDisallowChanges(
creationConfig.variantDslInfo.minSdkVersionWithTargetDeviceApi.featureLevel)
task.debuggable.setDisallowChanges(creationConfig.variantDslInfo.isDebuggable)
if (creationConfig.services
.projectOptions[BooleanOption.ENABLE_DUPLICATE_CLASSES_CHECK]) {
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.DUPLICATE_CLASSES_CHECK,
task.duplicateClassesCheck
)
}
if (separateFileDependenciesDexingTask) {
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.EXTERNAL_FILE_LIB_DEX_ARCHIVES,
task.fileDependencyDexFiles
)
}
task.dxStateBuildService.setDisallowChanges(
DxStateBuildService.RegistrationAction(task.project).execute())
}
private fun getDexFiles(
component: ComponentPropertiesImpl,
action: DexMergingAction
): FileCollection {
val attributes = getDexingArtifactConfiguration(component).getAttributes()
fun forAction(action: DexMergingAction): FileCollection {
when (action) {
DexMergingAction.MERGE_EXTERNAL_LIBS -> {
return if (dexingUsingArtifactTransforms) {
// If the file dependencies are being dexed in a task, don't also include them here
val artifactScope: AndroidArtifacts.ArtifactScope = if (separateFileDependenciesDexingTask) {
AndroidArtifacts.ArtifactScope.REPOSITORY_MODULE
} else {
AndroidArtifacts.ArtifactScope.EXTERNAL
}
component.variantDependencies.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
artifactScope,
AndroidArtifacts.ArtifactType.DEX,
attributes
)
} else {
component.globalScope.project.files(
component.artifacts.get(InternalArtifactType.EXTERNAL_LIBS_DEX_ARCHIVE),
component.artifacts.get(InternalArtifactType.EXTERNAL_LIBS_DEX_ARCHIVE_WITH_ARTIFACT_TRANSFORMS)
)
}
}
DexMergingAction.MERGE_LIBRARY_PROJECTS -> {
return if (dexingUsingArtifactTransforms) {
component.variantDependencies.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
AndroidArtifacts.ArtifactScope.PROJECT,
AndroidArtifacts.ArtifactType.DEX,
attributes
)
} else {
component.globalScope.project.files(
component.artifacts.get(InternalArtifactType.SUB_PROJECT_DEX_ARCHIVE))
}
}
DexMergingAction.MERGE_PROJECT -> {
val files =
component.globalScope.project.files(
component.artifacts.get(InternalArtifactType.PROJECT_DEX_ARCHIVE),
component.artifacts.get(InternalArtifactType.MIXED_SCOPE_DEX_ARCHIVE)
)
val variantType = component.variantType
if (variantType.isApk) {
component.onTestedConfig {
if (dexingUsingArtifactTransforms && it.variantType.isAar) {
// If dexing using artifact transforms, library production code will
// be dex'ed in a task, so we need to fetch the output directly.
// Otherwise, it will be in the dex'ed in the dex builder transform.
files.from(it.artifacts.getAll(InternalMultipleArtifactType.DEX))
}
}
}
return files
}
DexMergingAction.MERGE_ALL -> {
// technically, the Provider<> may not be needed, but the code would
// then assume that EXTERNAL_LIBS_DEX has already been registered by a
// Producer. Better to execute as late as possible.
return component.globalScope.project.files(
forAction(DexMergingAction.MERGE_PROJECT),
forAction(DexMergingAction.MERGE_LIBRARY_PROJECTS),
if (dexingType == DexingType.LEGACY_MULTIDEX) {
// we have to dex it
forAction(DexMergingAction.MERGE_EXTERNAL_LIBS)
} else {
// we merge external dex in a separate task
component.artifacts.getAll(InternalMultipleArtifactType.EXTERNAL_LIBS_DEX)
})
}
}
}
return forAction(action)
}
/**
* Get the number of dex files that will trigger merging of those files in a single
* invocation. Project and external libraries dex files are always merged as much as possible,
* so this only matters for the library projects dex files. See [LIBRARIES_MERGING_THRESHOLD]
* for details.
*/
private fun getMergingThreshold(
action: DexMergingAction,
component: ComponentPropertiesImpl
): Int {
return when (action) {
DexMergingAction.MERGE_LIBRARY_PROJECTS ->
when {
component.variantDslInfo.minSdkVersionWithTargetDeviceApi.featureLevel < 23 ->
LIBRARIES_MERGING_THRESHOLD
else -> LIBRARIES_M_PLUS_MAX_THRESHOLD
}
else -> 0
}
}
}
}
/**
* This returns a list of files from a set of files. If a file is in the set of files it is
* added to the resulting set. If it is a directory, all files all collected recursively, and they
* are sorted. This ensures that files from a single directory are always in deterministic order.
*
* We do not sort all files from a file collection as Gradle ensures consistent ordering of file
* collection content across builds. This holds for artifact transform outputs that are in the file
* collection. In fact, sorting it means that artifact transform outputs for library projects will
* not be consistent across builds. See http://b/119064593#comment11 for details.
*/
private fun getAllRegularFiles(files: Iterable<File>): List<File> {
return files.flatMap {
if (it.isFile) listOf(it)
else {
it.walkTopDown()
.filter(File::isFile)
.sortedWith(
Comparator { left, right ->
val systemIndependentLeft = toSystemIndependentPath(left.toPath())
val systemIndependentRight = toSystemIndependentPath(right.toPath())
systemIndependentLeft.compareTo(systemIndependentRight)
}
)
.toList()
}
}
}
/**
* Native multidex mode on android L does not support more
* than 100 DEX files (see <a href="http://b.android.com/233093">http://b.android.com/233093</a>).
*
* We assume the maximum number of dexes that will be produced from the external dependencies and
* project dex files is 50. The remaining 50 dex file can be used for library project.
*
* This means that if the number of library dex files is 51+, we might merge all of them when minSdk
* is 21 or 22.
*/
internal const val LIBRARIES_MERGING_THRESHOLD = 51
/**
* Max number of DEX files to generate on 23+, above that dex2out might have issues. See
* http://b/110374966 for more info.
*/
internal const val LIBRARIES_M_PLUS_MAX_THRESHOLD = 500
enum class DexMergingAction {
/** Merge only external libraries' dex files. */
MERGE_EXTERNAL_LIBS,
/** Merge only library projects' dex files. */
MERGE_LIBRARY_PROJECTS,
/** Merge only project's dex files. */
MERGE_PROJECT,
/** Merge external libraries, library projects, and project dex files. */
MERGE_ALL,
}
/** Delegate for [DexMergingTask]. It contains all logic for merging dex files. */
@VisibleForTesting
class DexMergingTaskRunnable @Inject constructor(
private val params: DexMergingParams
) : Runnable {
override fun run() {
val logger = LoggerWrapper.getLogger(DexMergingTaskRunnable::class.java)
val messageReceiver = MessageReceiverImpl(
params.errorFormatMode,
Logging.getLogger(DexMergingTask::class.java)
)
val forkJoinPool = ForkJoinPool()
val outputHandler = ParsingProcessOutputHandler(
ToolOutputParser(DexParser(), Message.Kind.ERROR, logger),
ToolOutputParser(DexParser(), logger),
messageReceiver
)
var processOutput: ProcessOutput? = null
try {
processOutput = outputHandler.createOutput()
// Analyze top-level inputs, and find all jars in dirs. These jars will be part of the
// input dex files to merge.
val jarsInInput = params.getAllDexFiles().filter { it.isDirectory }.flatMap { dir ->
dir.walkTopDown()
.filter { it.isFile && it.extension == SdkConstants.EXT_JAR }
.toSet()
}
val dexFiles = params.getAllDexFiles() + jarsInInput
FileUtils.cleanOutputDir(params.outputDir)
if (dexFiles.isEmpty()) {
return
}
val allDexFiles = lazy { getAllRegularFiles(dexFiles) }
if (dexFiles.size >= params.mergingThreshold
|| allDexFiles.value.size >= params.mergingThreshold) {
DexMergerTransformCallable(
messageReceiver,
params.dexingType,
processOutput,
params.outputDir,
dexFiles.map { it.toPath() }.iterator(),
params.mainDexListFile?.toPath(),
forkJoinPool,
params.dexMerger,
params.minSdkVersion,
params.isDebuggable
).call()
} else {
val outputPath =
{ id: Int -> params.outputDir.resolve("classes_$id.${SdkConstants.EXT_DEX}") }
var index = 0
for (file in allDexFiles.value) {
if (file.extension == SdkConstants.EXT_JAR) {
// Dex files can also come from jars when dexing is not done in artifact
// transforms. See b/130965921 for details.
ZipFile(file).use {
for (entry in it.entries()) {
BufferedInputStream(it.getInputStream(entry)).use { inputStream ->
BufferedOutputStream(outputPath(index++).outputStream()).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
}
} else {
file.copyTo(outputPath(index++))
}
}
}
} catch (e: Exception) {
PluginCrashReporter.maybeReportException(e)
// Print the error always, even without --stacktrace
logger.error(null, Throwables.getStackTraceAsString(e))
throw TransformException(e)
} finally {
processOutput?.let {
try {
outputHandler.handleOutput(it)
processOutput.close()
} catch (ignored: ProcessException) {
}
}
forkJoinPool.shutdown()
forkJoinPool.awaitTermination(100, TimeUnit.SECONDS)
}
}
}
@VisibleForTesting
data class DexMergingParams(
val dexingType: DexingType,
val errorFormatMode: SyncOptions.ErrorFormatMode,
val dexMerger: DexMergerTool,
val minSdkVersion: Int,
val isDebuggable: Boolean,
val mergingThreshold: Int,
val mainDexListFile: File?,
private val dexFiles: Set<File>,
private val fileDependencyDexFiles: File?,
val outputDir: File
) : Serializable {
fun getAllDexFiles(): List<File> {
val allDexFiles = ArrayList<File>(dexFiles)
fileDependencyDexFiles?.let {
Files.list(it.toPath()).use { files ->
files.forEach { file ->
if (Files.isRegularFile(file) && file.toString().endsWith(".jar")) {
allDexFiles.add(file.toFile())
}
}
}
}
return Collections.unmodifiableList(allDexFiles)
}
}