blob: 3865d3cd71141cda3adf2f0d818ec8c54b9ef8b4 [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.build.api.variant.impl.ApplicationVariantPropertiesImpl
import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import com.android.build.gradle.internal.publishing.AndroidArtifacts
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction
import com.android.build.gradle.internal.utils.fromDisallowChanges
import com.android.build.gradle.internal.utils.setDisallowChanges
import com.android.build.gradle.options.BooleanOption
import com.android.builder.packaging.PackagingUtils
import com.android.bundle.Config
import com.android.tools.build.bundletool.commands.BuildBundleCommand
import com.android.utils.FileUtils
import com.google.common.base.Preconditions
import com.google.common.collect.ImmutableList
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskProvider
import java.io.File
import java.io.Serializable
import java.nio.file.Path
import javax.inject.Inject
/**
* Task that generates the bundle (.aab) with all the modules.
*/
abstract class PackageBundleTask : NonIncrementalTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val baseModuleZip: DirectoryProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.NAME_ONLY)
lateinit var featureZips: FileCollection
private set
@get:InputFiles
@get:Optional
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val assetPackZips: DirectoryProperty
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.NONE)
abstract val bundleDeps: RegularFileProperty
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val mainDexList: RegularFileProperty
@get:InputFiles
@get:Optional
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val obsfuscationMappingFile: RegularFileProperty
@get:InputFiles
@get:Optional
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val integrityConfigFile: RegularFileProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val nativeDebugMetadataFiles: ConfigurableFileCollection
@get:Input
abstract val aaptOptionsNoCompress: ListProperty<String>
@get:Nested
lateinit var bundleOptions: BundleOptions
private set
@get:Nested
lateinit var bundleFlags: BundleFlags
private set
@get:OutputFile
abstract val bundleFile: RegularFileProperty
@get:Input
val fileName: String
get() = bundleFile.get().asFile.name
@get:Input
var debuggable: Boolean = false
private set
@get:Input
abstract val bundleNeedsFusedStandaloneConfig: Property<Boolean>
companion object {
const val MIN_SDK_FOR_SPLITS = 21
}
override fun doTaskAction() {
getWorkerFacadeWithWorkers().use {
it.submit(
BundleToolRunnable::class.java,
Params(
baseModuleFile = baseModuleZip.get().asFileTree.singleFile,
featureFiles = featureZips.files,
assetPackFiles = if (!assetPackZips.isPresent) null else assetPackZips.get().asFileTree.files,
mainDexList = mainDexList.orNull?.asFile,
obfuscationMappingFile = if (obsfuscationMappingFile.isPresent) obsfuscationMappingFile.get().asFile else null,
integrityConfigFile = if (integrityConfigFile.isPresent) integrityConfigFile.get().asFile else null,
nativeDebugMetadataFiles = nativeDebugMetadataFiles.files,
aaptOptionsNoCompress = aaptOptionsNoCompress.get(),
bundleOptions = bundleOptions,
bundleFlags = bundleFlags,
bundleFile = bundleFile.get().asFile,
bundleDeps = if (bundleDeps.isPresent) bundleDeps.get().asFile else null,
// do not compress the bundle in debug builds where it will be only used as an
// intermediate artifact
uncompressBundle = debuggable,
bundleNeedsFusedStandaloneConfig = bundleNeedsFusedStandaloneConfig.get()
)
)
}
}
private data class Params(
val baseModuleFile: File,
val featureFiles: Set<File>,
val assetPackFiles: Set<File>?,
val mainDexList: File?,
val obfuscationMappingFile: File?,
val integrityConfigFile: File?,
val nativeDebugMetadataFiles: Set<File>,
val aaptOptionsNoCompress: Collection<String>,
val bundleOptions: BundleOptions,
val bundleFlags: BundleFlags,
val bundleFile: File,
val bundleDeps: File?,
val uncompressBundle: Boolean,
val bundleNeedsFusedStandaloneConfig: Boolean
) : Serializable
private class BundleToolRunnable @Inject constructor(private val params: Params) : Runnable {
override fun run() {
// BundleTool requires that the destination directory for the bundle file exists,
// and that the bundle file itself does not
val bundleFile = params.bundleFile
FileUtils.cleanOutputDir(bundleFile.parentFile)
val builder = ImmutableList.builder<Path>()
builder.add(params.baseModuleFile.toPath())
params.featureFiles.forEach { builder.add(getBundlePath(it)) }
// BundleTool needs directories, not zips.
params.assetPackFiles?.forEach { builder.add(getBundlePath(it.parentFile)) }
val noCompressGlobsForBundle =
PackagingUtils.getNoCompressGlobsForBundle(params.aaptOptionsNoCompress)
val splitsConfig = Config.SplitsConfig.newBuilder()
.splitBy(Config.SplitDimension.Value.ABI, params.bundleOptions.enableAbi)
.splitBy(
Config.SplitDimension.Value.SCREEN_DENSITY,
params.bundleOptions.enableDensity
)
.splitBy(Config.SplitDimension.Value.LANGUAGE, params.bundleOptions.enableLanguage)
params.bundleOptions.enableTexture?.let {
splitsConfig.addSplitDimension(
Config.SplitDimension.newBuilder()
.setValue(Config.SplitDimension.Value.TEXTURE_COMPRESSION_FORMAT)
.setSuffixStripping(
Config.SuffixStripping.newBuilder()
.setEnabled(true)
.setDefaultSuffix(params.bundleOptions.textureDefaultFormat ?: "")
)
.setNegate(!it)
)
}
val uncompressNativeLibrariesConfig = Config.UncompressNativeLibraries.newBuilder()
.setEnabled(params.bundleFlags.enableUncompressedNativeLibs)
val bundleOptimizations = Config.Optimizations.newBuilder()
.setSplitsConfig(splitsConfig)
.setUncompressNativeLibraries(uncompressNativeLibrariesConfig)
if (params.bundleNeedsFusedStandaloneConfig) {
bundleOptimizations.setStandaloneConfig(
Config.StandaloneConfig.newBuilder()
.addSplitDimension(
Config.SplitDimension.newBuilder()
.setValue(Config.SplitDimension.Value.ABI)
.setNegate(true)
)
.addSplitDimension(
Config.SplitDimension.newBuilder()
.setValue(Config.SplitDimension.Value.SCREEN_DENSITY)
.setNegate(true)
)
.addSplitDimension(
Config.SplitDimension.newBuilder()
.setValue(Config.SplitDimension.Value.LANGUAGE)
.setNegate(true)
)
.addSplitDimension(
Config.SplitDimension.newBuilder()
.setValue(Config.SplitDimension.Value.TEXTURE_COMPRESSION_FORMAT)
.setNegate(true)
)
.setStrip64BitLibraries(true)
)
}
val bundleConfig =
Config.BundleConfig.newBuilder()
.setCompression(
Config.Compression.newBuilder()
.addAllUncompressedGlob(noCompressGlobsForBundle)
)
.setOptimizations(bundleOptimizations)
val command = BuildBundleCommand.builder()
.setUncompressedBundle(params.uncompressBundle)
.setBundleConfig(bundleConfig.build())
.setOutputPath(bundleFile.toPath())
.setModulesPaths(builder.build())
params.bundleDeps?.let {
command.addMetadataFile(
"com.android.tools.build.libraries",
"dependencies.pb",
it.toPath()
)
}
params.mainDexList?.let {
command.setMainDexListFile(it.toPath())
}
params.obfuscationMappingFile?.let {
command.addMetadataFile(
"com.android.tools.build.obfuscation",
"proguard.map",
it.toPath()
)
}
params.integrityConfigFile?.let {
if (it.isFile) {
command.addMetadataFile(
"com.google.play.apps.integrity",
"AppIntegrityConfig.pb",
it.toPath()
)
}
}
params.nativeDebugMetadataFiles.forEach { file ->
command.addMetadataFile(
"com.android.tools.build.debugsymbols",
"${file.parentFile.name}/${file.name}",
file.toPath()
)
}
command.build().execute()
}
private fun getBundlePath(folder: File): Path {
val children = folder.listFiles()
Preconditions.checkNotNull(children)
Preconditions.checkState(children.size == 1)
return children[0].toPath()
}
}
data class BundleOptions(
@get:Input
@get:Optional
val enableAbi: Boolean?,
@get:Input
@get:Optional
val enableDensity: Boolean?,
@get:Input
@get:Optional
val enableLanguage: Boolean?,
@get:Input
@get:Optional
val enableTexture: Boolean?,
@get:Input
@get:Optional
val textureDefaultFormat: String?
) : Serializable
data class BundleFlags(
@get:Input
val enableUncompressedNativeLibs: Boolean
) : Serializable
/**
* CreateAction for a Task that will pack the bundle artifact.
*/
class CreationAction(componentProperties: ApplicationVariantPropertiesImpl) :
VariantTaskCreationAction<PackageBundleTask, ApplicationVariantPropertiesImpl>(
componentProperties
) {
override val name: String
get() = computeTaskName("package", "Bundle")
override val type: Class<PackageBundleTask>
get() = PackageBundleTask::class.java
override fun handleProvider(
taskProvider: TaskProvider<PackageBundleTask>
) {
super.handleProvider(taskProvider)
val bundleName =
"${creationConfig.globalScope.projectBaseName}-${creationConfig.baseName}.aab"
creationConfig.artifacts.setInitialProvider(
taskProvider,
PackageBundleTask::bundleFile
).withName(bundleName).on(InternalArtifactType.INTERMEDIARY_BUNDLE)
}
override fun configure(
task: PackageBundleTask
) {
super.configure(task)
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.MODULE_BUNDLE, task.baseModuleZip
)
task.featureZips = creationConfig.variantDependencies.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.REVERSE_METADATA_VALUES,
AndroidArtifacts.ArtifactScope.ALL,
AndroidArtifacts.ArtifactType.MODULE_BUNDLE
)
if (creationConfig.needAssetPackTasks.get()) {
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.ASSET_PACK_BUNDLE, task.assetPackZips
)
}
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.BUNDLE_DEPENDENCY_REPORT,
task.bundleDeps
)
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.APP_INTEGRITY_CONFIG,
task.integrityConfigFile
)
task.debuggable = creationConfig.variantDslInfo.isDebuggable
task.nativeDebugMetadataFiles.fromDisallowChanges(
MergeNativeDebugMetadataTask.getNativeDebugMetadataFiles(creationConfig)
)
task.aaptOptionsNoCompress.setDisallowChanges(creationConfig.globalScope.extension.aaptOptions.noCompress)
task.bundleOptions =
((creationConfig.globalScope.extension as BaseAppModuleExtension).bundle).convert()
task.bundleFlags = BundleFlags(
enableUncompressedNativeLibs = creationConfig.services.projectOptions[BooleanOption.ENABLE_UNCOMPRESSED_NATIVE_LIBS_IN_BUNDLE]
)
if (creationConfig.variantScope.needsMainDexListForBundle) {
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.MAIN_DEX_LIST_FOR_BUNDLE,
task.mainDexList
)
// The dex files from this application are still processed for legacy multidex
// in this case, as if none of the dynamic features are fused the bundle tool will
// not reprocess the dex files.
}
creationConfig.artifacts.setTaskInputToFinalProduct(
InternalArtifactType.APK_MAPPING,
task.obsfuscationMappingFile
)
task.bundleNeedsFusedStandaloneConfig.set(
creationConfig.globalScope.project.provider {
creationConfig.minSdkVersion.featureLevel < MIN_SDK_FOR_SPLITS
&& creationConfig.artifacts.get(InternalArtifactType.ASSET_PACK_BUNDLE).isPresent
}
)
}
}
}
private fun com.android.build.gradle.internal.dsl.BundleOptions.convert() =
PackageBundleTask.BundleOptions(
enableAbi = abi.enableSplit,
enableDensity = density.enableSplit,
enableLanguage = language.enableSplit,
enableTexture = texture.enableSplit,
textureDefaultFormat = texture.defaultFormat
)
/**
* convenience function to call [Config.SplitsConfig.Builder.addSplitDimension]
*
* @param flag the [Config.SplitDimension.Value] on which to set the value
* @param value if true, split is enbaled for the given flag. If null, no change is made and the
* bundle-tool will decide the value.
*/
private fun Config.SplitsConfig.Builder.splitBy(
flag: Config.SplitDimension.Value,
value: Boolean?
): Config.SplitsConfig.Builder {
value?.let {
addSplitDimension(Config.SplitDimension.newBuilder().setValue(flag).setNegate(!it))
}
return this
}