blob: 296048383f61571c4006e28d2e93bae18f127a5d [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.SdkConstants.DOT_DEX
import com.android.SdkConstants.FD_ASSETS
import com.android.SdkConstants.FD_DEX
import com.android.build.api.artifact.SingleArtifact
import com.android.build.gradle.internal.component.ApkCreationConfig
import com.android.build.gradle.internal.component.ApplicationCreationConfig
import com.android.build.gradle.internal.component.ComponentCreationConfig
import com.android.build.gradle.internal.component.DynamicFeatureCreationConfig
import com.android.build.gradle.internal.dependency.AndroidAttributes
import com.android.build.gradle.internal.fusedlibrary.FusedLibraryInternalArtifactType
import com.android.build.gradle.internal.packaging.JarCreatorFactory
import com.android.build.gradle.internal.packaging.JarCreatorType
import com.android.build.gradle.internal.pipeline.StreamFilter
import com.android.build.gradle.internal.privaysandboxsdk.PrivacySandboxSdkInternalArtifactType
import com.android.build.gradle.internal.privaysandboxsdk.PrivacySandboxSdkVariantScope
import com.android.build.gradle.internal.publishing.AndroidArtifacts
import com.android.build.gradle.internal.publishing.AndroidArtifacts.MODULE_PATH
import com.android.build.gradle.internal.scope.InternalArtifactType
import com.android.build.gradle.internal.scope.InternalArtifactType.MERGED_NATIVE_LIBS
import com.android.build.gradle.internal.scope.InternalArtifactType.STRIPPED_NATIVE_LIBS
import com.android.build.gradle.internal.scope.InternalMultipleArtifactType
import com.android.build.gradle.internal.tasks.factory.TaskCreationAction
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.files.NativeLibraryAbiPredicate
import com.android.builder.packaging.JarCreator
import com.android.builder.packaging.JarMerger
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.model.ObjectFactory
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
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 org.gradle.work.DisableCachingByDefault
import java.io.File
import java.util.Locale
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Predicate
import java.util.zip.Deflater
import javax.inject.Inject
/**
* Task that zips a module's bundle elements into a zip file. This gets published
* so that the base app can package into the bundle.
*
*/
@DisableCachingByDefault
abstract class PerModuleBundleTask @Inject constructor(objects: ObjectFactory) :
NonIncrementalTask() {
@get:Optional
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@get:Optional
@get:OutputFile
abstract val outputFile: RegularFileProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val dexFiles: ConfigurableFileCollection
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val featureDexFiles: ConfigurableFileCollection
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val resFiles: RegularFileProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val javaResFiles: ConfigurableFileCollection
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val featureJavaResFiles: ConfigurableFileCollection
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val assetsFilesDirectory: DirectoryProperty
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val nativeLibsFiles: ConfigurableFileCollection
@get:Input
abstract val abiFilters: SetProperty<String>
@get:InputFiles
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:Optional
abstract val baseModuleMetadata: ConfigurableFileCollection
@get:InputFile
@get:PathSensitive(PathSensitivity.NAME_ONLY)
@get:Optional
abstract val appMetadata: RegularFileProperty
@get:Optional
@get:Input
abstract val fileName: Property<String>
@get:Optional
@get:InputFile
@get:PathSensitive(PathSensitivity.NAME_ONLY)
abstract val privacySandboxSdkRuntimeConfigFile: RegularFileProperty
@get:Input
val jarCreatorType: Property<JarCreatorType> = objects.property(JarCreatorType::class.java)
public override fun doTaskAction() {
val outputPath =
(outputFile.orNull?.asFile ?: File(outputDir.get().asFile, fileName.get())).toPath()
val jarCreator =
JarCreatorFactory.make(
jarFile = outputPath,
type = jarCreatorType.get()
)
// Disable compression for module zips, since this will only be used in bundletool and it
// will need to uncompress them anyway.
jarCreator.setCompressionLevel(Deflater.NO_COMPRESSION)
val filters = baseModuleMetadata.singleOrNull()?.let {
ModuleMetadata.load(it).abiFilters.toSet()
} ?: abiFilters.get()
val abiFilter = filters?.let { NativeLibraryAbiPredicate(it, false) }
// https://b.corp.google.com/issues/140219742
val excludeJarManifest =
Predicate { path: String ->
!path.uppercase(Locale.US).endsWith("MANIFEST.MF")
}
jarCreator.use {
it.addDirectory(
assetsFilesDirectory.get().asFile.toPath(),
null,
null,
Relocator(FD_ASSETS)
)
it.addJar(resFiles.get().asFile.toPath(), excludeJarManifest, ResRelocator())
// dex files
val dexFilesSet = if (hasFeatureDexFiles()) featureDexFiles.files else dexFiles.files
if (dexFilesSet.size == 1) {
// Don't rename if there is only one input folder
// as this might be the legacy multidex case.
addHybridFolder(it, dexFilesSet.sortedBy { it.name }, Relocator(FD_DEX), excludeJarManifest)
} else {
addHybridFolder(it, dexFilesSet.sortedBy { it.name }, DexRelocator(FD_DEX), excludeJarManifest)
}
// we check hasFeatureDexFiles() instead of checking if
// featureJavaResFiles.files.isNotEmpty() because we want to use featureJavaResFiles
// even if it's empty (which will be the case when using proguard)
val javaResFilesSet =
if (hasFeatureDexFiles()) featureJavaResFiles.files else javaResFiles.files
addHybridFolder(it, javaResFilesSet, Relocator("root"), JarMerger.EXCLUDE_CLASSES)
addHybridFolder(
it,
appMetadata.orNull?.asFile?.let { metadataFile -> setOf(metadataFile) }
?: setOf(),
Relocator("root/META-INF/com/android/build/gradle"),
JarMerger.EXCLUDE_CLASSES)
addHybridFolder(it, nativeLibsFiles.files, fileFilter = abiFilter)
if (privacySandboxSdkRuntimeConfigFile.isPresent) {
it.addFile("runtime_enabled_sdk_config.pb",
privacySandboxSdkRuntimeConfigFile.get().asFile.toPath())
}
}
}
private fun addHybridFolder(
jarCreator: JarCreator,
files: Iterable<File>,
relocator: JarCreator.Relocator? = null,
fileFilter: Predicate<String>? = null ) {
// in this case the artifact is a folder containing things to add
// to the zip. These can be file to put directly, jars to copy the content
// of, or folders
for (file in files) {
if (file.isFile) {
if (file.name.endsWith(SdkConstants.DOT_JAR)) {
jarCreator.addJar(file.toPath(), fileFilter, relocator)
} else if (fileFilter == null || fileFilter.test(file.name)) {
if (relocator != null) {
jarCreator.addFile(relocator.relocate(file.name), file.toPath())
} else {
jarCreator.addFile(file.name, file.toPath())
}
}
} else if (file.exists()){
jarCreator.addDirectory(
file.toPath(),
fileFilter,
null,
relocator)
}
}
}
private fun hasFeatureDexFiles() = featureDexFiles.files.isNotEmpty()
class PrivacySandboxSdkCreationAction(
private val creationConfig: PrivacySandboxSdkVariantScope
): TaskCreationAction<PerModuleBundleTask>() {
override val name: String = "buildModuleForBundle"
override val type: Class<PerModuleBundleTask> = PerModuleBundleTask::class.java
override fun handleProvider(taskProvider: TaskProvider<PerModuleBundleTask>) {
super.handleProvider(taskProvider)
creationConfig.artifacts.setInitialProvider(
taskProvider,
PerModuleBundleTask::outputFile
).withName("base.zip").on(PrivacySandboxSdkInternalArtifactType.MODULE_BUNDLE)
}
override fun configure(task: PerModuleBundleTask) {
task.configureVariantProperties("", task.project.gradle.sharedServices)
task.dexFiles.fromDisallowChanges(
creationConfig.artifacts.get(
PrivacySandboxSdkInternalArtifactType.DEX
)
)
task.assetsFilesDirectory.setDisallowChanges(
creationConfig.artifacts.get(FusedLibraryInternalArtifactType.MERGED_ASSETS).map {
it.dir(SdkConstants.FD_ASSETS)
}
)
task.resFiles.setDisallowChanges(
creationConfig.artifacts.get(
PrivacySandboxSdkInternalArtifactType.LINKED_MERGE_RES_FOR_ASB
)
)
task.javaResFiles.fromDisallowChanges(
creationConfig.artifacts.get(FusedLibraryInternalArtifactType.MERGED_JAVA_RES)
)
// Not applicable
task.featureDexFiles.fromDisallowChanges()
task.featureJavaResFiles.fromDisallowChanges()
task.nativeLibsFiles.fromDisallowChanges()
task.abiFilters.setDisallowChanges(emptySet())
task.jarCreatorType.setDisallowChanges(
if (creationConfig.services.projectOptions.get(BooleanOption.USE_NEW_JAR_CREATOR)) {
JarCreatorType.JAR_FLINGER
} else {
JarCreatorType.JAR_MERGER
}
)
}
}
class CreationAction(
creationConfig: ApkCreationConfig
) : VariantTaskCreationAction<PerModuleBundleTask, ApkCreationConfig>(
creationConfig
) {
override val name: String
get() = computeTaskName("build", "PreBundle")
override val type: Class<PerModuleBundleTask>
get() = PerModuleBundleTask::class.java
override fun handleProvider(
taskProvider: TaskProvider<PerModuleBundleTask>
) {
super.handleProvider(taskProvider)
creationConfig.artifacts.setInitialProvider(
taskProvider,
PerModuleBundleTask::outputDir
).on(InternalArtifactType.MODULE_BUNDLE)
}
override fun configure(
task: PerModuleBundleTask
) {
super.configure(task)
val artifacts = creationConfig.artifacts
if (creationConfig is DynamicFeatureCreationConfig) {
task.fileName.set(creationConfig.featureName.map { "$it.zip" })
} else {
task.fileName.set("base.zip")
}
task.fileName.disallowChanges()
task.assetsFilesDirectory.setDisallowChanges(
creationConfig.artifacts.get(SingleArtifact.ASSETS)
)
val legacyShrinkerEnabled = creationConfig.androidResourcesCreationConfig?.useResourceShrinker == true &&
!creationConfig.services.projectOptions[BooleanOption.ENABLE_NEW_RESOURCE_SHRINKER]
task.resFiles.set(
if (legacyShrinkerEnabled){
artifacts.get(InternalArtifactType.LEGACY_SHRUNK_LINKED_RES_FOR_BUNDLE)
} else {
artifacts.get(InternalArtifactType.LINKED_RES_FOR_BUNDLE)
}
)
task.resFiles.disallowChanges()
task.dexFiles.from(
if ((creationConfig as? ApplicationCreationConfig)?.consumesFeatureJars == true) {
artifacts.get(InternalArtifactType.BASE_DEX)
} else {
artifacts.getAll(InternalMultipleArtifactType.DEX)
}
)
if (creationConfig.shouldPackageDesugarLibDex) {
task.dexFiles.from(
artifacts.get(InternalArtifactType.DESUGAR_LIB_DEX)
)
}
task.featureDexFiles.from(
creationConfig.variantDependencies.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
AndroidArtifacts.ArtifactScope.PROJECT,
AndroidArtifacts.ArtifactType.FEATURE_DEX,
AndroidAttributes(MODULE_PATH to task.project.path)
)
)
task.javaResFiles.from(
if (creationConfig.minifiedEnabled) {
creationConfig.services.fileCollection(
artifacts.get(InternalArtifactType.SHRUNK_JAVA_RES)
)
} else if (creationConfig.needsMergedJavaResStream) {
creationConfig.transformManager
.getPipelineOutputAsFileCollection(StreamFilter.RESOURCES)
} else {
creationConfig.services.fileCollection(
artifacts.get(InternalArtifactType.MERGED_JAVA_RES)
)
}
)
task.nativeLibsFiles.from(getNativeLibsFiles(creationConfig))
task.featureJavaResFiles.from(
creationConfig.variantDependencies.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
AndroidArtifacts.ArtifactScope.PROJECT,
AndroidArtifacts.ArtifactType.FEATURE_SHRUNK_JAVA_RES,
AndroidAttributes(MODULE_PATH to task.project.path)
)
)
task.featureJavaResFiles.disallowChanges()
if (creationConfig.componentType.isDynamicFeature) {
// If this is a dynamic feature, we use the abiFilters published by the base module.
task.baseModuleMetadata.from(
creationConfig.variantDependencies.getArtifactFileCollection(
AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH,
AndroidArtifacts.ArtifactScope.PROJECT,
AndroidArtifacts.ArtifactType.BASE_MODULE_METADATA
)
)
} else {
task.abiFilters.set(creationConfig.supportedAbis)
}
task.abiFilters.disallowChanges()
task.baseModuleMetadata.disallowChanges()
if (creationConfig.componentType.isBaseModule) {
artifacts.setTaskInputToFinalProduct(
InternalArtifactType.APP_METADATA,
task.appMetadata
)
}
task.jarCreatorType.set(creationConfig.global.jarCreatorType)
task.jarCreatorType.disallowChanges()
if (creationConfig.services.projectOptions[BooleanOption.PRIVACY_SANDBOX_SDK_SUPPORT] && creationConfig.componentType.isBaseModule) {
artifacts.setTaskInputToFinalProduct(
InternalArtifactType.PRIVACY_SANDBOX_SDK_RUNTIME_CONFIG_FILE,
task.privacySandboxSdkRuntimeConfigFile
)
} else {
task.privacySandboxSdkRuntimeConfigFile.disallowChanges()
}
}
}
}
private class Relocator(private val prefix: String): JarCreator.Relocator {
override fun relocate(entryPath: String) = "$prefix/$entryPath"
}
/**
* Relocate the dex files into a single folder which might force renaming
* the dex files into a consistent scheme : classes.dex, classes2.dex, classes4.dex, ...
*
* <p>Note that classes1.dex is not a valid dex file name</p>
*
* When dealing with native multidex, we can have several folders each containing 1 to many
* dex files (with the same naming scheme). In that case, merge and rename accordingly, note
* that only one classes.dex can exist in the merged location so the others will be renamed.
*
* When dealing with a single feature (base) in legacy multidex, we must not rename the
* main dex file (classes.dex) as it contains the bootstrap code for the application.
*/
private class DexRelocator(private val prefix: String): JarCreator.Relocator {
// first valid classes.dex with an index starts at 2.
val index = AtomicInteger(2)
val classesDexNameUsed = AtomicBoolean(false)
override fun relocate(entryPath: String): String {
// Note that the dex file may be in a subdirectory (e.g.,
// `<dex_merging_task_output>/bucket_0/classes.dex`). Also, it may not have the name
// `classesXY.dex` (e.g., it could be `ExampleClass.dex`).
if (entryPath.endsWith(DOT_DEX, ignoreCase=true)) {
return if (entryPath.endsWith("classes.dex") && !classesDexNameUsed.get()) {
classesDexNameUsed.set(true)
"$prefix/classes.dex"
} else {
val entryIndex = index.getAndIncrement()
"$prefix/classes$entryIndex.dex"
}
}
return "$prefix/$entryPath"
}
}
private class ResRelocator : JarCreator.Relocator {
override fun relocate(entryPath: String) = when(entryPath) {
SdkConstants.FN_ANDROID_MANIFEST_XML -> "manifest/" + SdkConstants.FN_ANDROID_MANIFEST_XML
else -> entryPath
}
}
/**
* Returns a file collection containing all of the native libraries to be packaged.
*/
fun getNativeLibsFiles(creationConfig: ComponentCreationConfig): FileCollection {
val nativeLibs = creationConfig.services.fileCollection()
if (creationConfig.componentType.isForTesting) {
return nativeLibs.from(creationConfig.artifacts.get(MERGED_NATIVE_LIBS))
}
nativeLibs.from(creationConfig.artifacts.get(STRIPPED_NATIVE_LIBS))
return nativeLibs
}