| /* |
| * 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.FD_ASSETS |
| import com.android.SdkConstants.FD_DEX |
| import com.android.build.api.artifact.BuildableArtifact |
| import com.android.build.gradle.internal.pipeline.StreamFilter |
| 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.BuildArtifactsHolder |
| 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.VariantScope |
| import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction |
| import com.android.build.gradle.internal.tasks.featuresplit.FeatureSetMetadata |
| import com.android.builder.files.NativeLibraryAbiPredicate |
| import com.android.builder.packaging.JarMerger |
| import com.android.builder.packaging.JarCreator |
| import com.android.utils.FileUtils |
| import org.gradle.api.file.Directory |
| import org.gradle.api.file.DirectoryProperty |
| import org.gradle.api.file.FileCollection |
| import org.gradle.api.file.RegularFile |
| import org.gradle.api.file.RegularFileProperty |
| import org.gradle.api.tasks.Input |
| 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.PathSensitive |
| import org.gradle.api.tasks.PathSensitivity |
| import org.gradle.api.tasks.TaskProvider |
| import java.io.File |
| import java.util.concurrent.atomic.AtomicBoolean |
| import java.util.concurrent.atomic.AtomicInteger |
| import java.util.function.Predicate |
| import java.util.function.Supplier |
| |
| /** |
| * 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. |
| * |
| */ |
| abstract class PerModuleBundleTask : NonIncrementalTask() { |
| |
| @get:OutputDirectory |
| @get:PathSensitive(PathSensitivity.RELATIVE) |
| abstract val outputDir: DirectoryProperty |
| |
| @get:InputFiles |
| @get:PathSensitive(PathSensitivity.RELATIVE) |
| lateinit var dexFiles: FileCollection |
| private set |
| |
| @get:InputFiles |
| @get:PathSensitive(PathSensitivity.RELATIVE) |
| lateinit var featureDexFiles: FileCollection |
| private set |
| |
| @get:InputFile |
| @get:PathSensitive(PathSensitivity.RELATIVE) |
| abstract val resFiles: RegularFileProperty |
| |
| @get:InputFiles |
| @get:PathSensitive(PathSensitivity.RELATIVE) |
| lateinit var javaResFiles: FileCollection |
| private set |
| |
| @get:InputFiles |
| @get:PathSensitive(PathSensitivity.RELATIVE) |
| abstract val assetsFiles: DirectoryProperty |
| |
| @get:InputFiles |
| @get:PathSensitive(PathSensitivity.RELATIVE) |
| lateinit var nativeLibsFiles: FileCollection |
| private set |
| |
| @get:Input |
| @get:Optional |
| var abiFilters: Set<String>? = null |
| private set |
| |
| private lateinit var fileNameSupplier: Supplier<String> |
| |
| @get:Input |
| val fileName: String |
| get() = fileNameSupplier.get() |
| |
| public override fun doTaskAction() { |
| FileUtils.cleanOutputDir(outputDir.get().asFile) |
| val jarMerger = JarMerger(File(outputDir.get().asFile, fileName).toPath()) |
| |
| // Disable compression for module zips, since this will only be used in bundletool and it |
| // will need to uncompress them anyway. |
| jarMerger.setCompressionLevel(0) |
| |
| val filters = abiFilters |
| val abiFilter: Predicate<String>? = if (filters != null) NativeLibraryAbiPredicate(filters, false) else null |
| |
| jarMerger.use { |
| it.addDirectory( |
| assetsFiles.get().asFile.toPath(), |
| null, |
| null, |
| Relocator(FD_ASSETS) |
| ) |
| |
| it.addJar(resFiles.get().asFile.toPath(), null, 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), null) |
| } else { |
| addHybridFolder(it, dexFilesSet.sortedBy { it.name }, DexRelocator(FD_DEX), null) |
| } |
| |
| val javaResFilesSet = if (hasFeatureDexFiles()) setOf<File>() else javaResFiles.files |
| addHybridFolder(it, javaResFilesSet, |
| Relocator("root"), |
| JarMerger.EXCLUDE_CLASSES) |
| |
| addHybridFolder(it, nativeLibsFiles.files, fileFilter = abiFilter) |
| } |
| } |
| |
| private fun addHybridFolder( |
| jarMerger: JarMerger, |
| 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)) { |
| jarMerger.addJar(file.toPath(), fileFilter, relocator) |
| } else if (fileFilter == null || fileFilter.test(file.name)) { |
| if (relocator != null) { |
| jarMerger.addFile(relocator.relocate(file.name), file.toPath()) |
| } else { |
| jarMerger.addFile(file.name, file.toPath()) |
| } |
| } |
| } else { |
| jarMerger.addDirectory( |
| file.toPath(), |
| fileFilter, |
| null, |
| relocator) |
| } |
| } |
| } |
| |
| private fun hasFeatureDexFiles() = featureDexFiles.files.isNotEmpty() |
| |
| class CreationAction( |
| variantScope: VariantScope, |
| private val packageCustomClassDependencies: Boolean |
| ) : VariantTaskCreationAction<PerModuleBundleTask>(variantScope) { |
| |
| override val name: String |
| get() = variantScope.getTaskName("build", "PreBundle") |
| override val type: Class<PerModuleBundleTask> |
| get() = PerModuleBundleTask::class.java |
| |
| override fun handleProvider(taskProvider: TaskProvider<out PerModuleBundleTask>) { |
| super.handleProvider(taskProvider) |
| variantScope.artifacts.producesDir(InternalArtifactType.MODULE_BUNDLE, |
| BuildArtifactsHolder.OperationType.INITIAL, |
| taskProvider, |
| PerModuleBundleTask::outputDir) |
| } |
| |
| override fun configure(task: PerModuleBundleTask) { |
| super.configure(task) |
| |
| val artifacts = variantScope.artifacts |
| task.fileNameSupplier = if (variantScope.type.isBaseModule) |
| Supplier { "base.zip"} |
| else { |
| val featureName: Supplier<String> = FeatureSetMetadata.getInstance() |
| .getFeatureNameSupplierForTask(variantScope, task) |
| Supplier { "${featureName.get()}.zip"} |
| } |
| |
| artifacts.setTaskInputToFinalProduct( |
| InternalArtifactType.MERGED_ASSETS, task.assetsFiles) |
| artifacts.setTaskInputToFinalProduct( |
| if (variantScope.useResourceShrinker()) { |
| InternalArtifactType.SHRUNK_LINKED_RES_FOR_BUNDLE |
| } else { |
| InternalArtifactType.LINKED_RES_FOR_BUNDLE |
| }, task.resFiles) |
| task.dexFiles = variantScope.transformManager.getPipelineOutputAsFileCollection( |
| StreamFilter.DEX) |
| task.featureDexFiles = |
| variantScope.getArtifactFileCollection( |
| AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, |
| AndroidArtifacts.ArtifactScope.PROJECT, |
| AndroidArtifacts.ArtifactType.FEATURE_DEX, |
| mapOf(MODULE_PATH to variantScope.globalScope.project.path) |
| ) |
| task.javaResFiles = if (variantScope.needsMergedJavaResStream) { |
| variantScope.transformManager |
| .getPipelineOutputAsFileCollection(StreamFilter.RESOURCES) |
| } else { |
| variantScope.globalScope.project.layout.files( |
| artifacts.getFinalProduct<RegularFile>(InternalArtifactType.MERGED_JAVA_RES) |
| ) |
| } |
| task.nativeLibsFiles = getNativeLibsFiles(variantScope, packageCustomClassDependencies) |
| |
| task.abiFilters = variantScope.variantConfiguration.supportedAbis |
| } |
| } |
| } |
| |
| 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 { |
| if (entryPath.startsWith("classes")) { |
| return if (entryPath == "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( |
| scope: VariantScope, |
| packageCustomClassDependencies: Boolean |
| ): FileCollection { |
| val nativeLibs = scope.globalScope.project.files() |
| if (scope.type.isForTesting) { |
| return nativeLibs.from(scope.artifacts.getFinalProduct<Directory>(MERGED_NATIVE_LIBS)) |
| } |
| nativeLibs.from(scope.artifacts.getFinalProduct<Directory>(STRIPPED_NATIVE_LIBS)) |
| if (packageCustomClassDependencies) { |
| nativeLibs.from( |
| scope.transformManager.getPipelineOutputAsFileCollection(StreamFilter.NATIVE_LIBS) |
| ) |
| } |
| return nativeLibs |
| } |