| /* |
| * Copyright (C) 2020 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.LibraryVariantPropertiesImpl |
| import com.android.build.gradle.internal.core.Abi |
| import com.android.build.gradle.internal.cxx.json.AndroidBuildGradleJsons |
| import com.android.build.gradle.internal.cxx.json.NativeLibraryValueMini |
| import com.android.build.gradle.internal.cxx.logging.errorln |
| import com.android.build.gradle.internal.cxx.logging.infoln |
| import com.android.build.gradle.internal.cxx.logging.warnln |
| import com.android.build.gradle.internal.cxx.model.CxxAbiModel |
| import com.android.build.gradle.internal.cxx.model.CxxVariantModel |
| import com.android.build.gradle.internal.cxx.model.DetermineUsedStlResult |
| import com.android.build.gradle.internal.cxx.model.determineUsedStl |
| import com.android.build.gradle.internal.cxx.model.jsonFile |
| import com.android.build.gradle.internal.scope.InternalArtifactType |
| import com.android.build.gradle.internal.tasks.factory.VariantTaskCreationAction |
| import com.android.sdklib.AndroidVersion |
| import com.google.gson.GsonBuilder |
| import com.google.gson.annotations.SerializedName |
| import org.gradle.api.file.DirectoryProperty |
| import org.gradle.api.provider.Provider |
| import org.gradle.api.tasks.Input |
| import org.gradle.api.tasks.InputDirectory |
| import org.gradle.api.tasks.InputFile |
| import org.gradle.api.tasks.Nested |
| 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.io.StringWriter |
| import kotlin.math.max |
| |
| // TODO: Automatic importing of CMake/ndk-build modules? |
| // CMake doesn't appear to expose the data we need from CMake server (though that could be a |
| // missing request or an outdated server version). It would make sense to try using installed |
| // modules for this. |
| // ndk-build doesn't currently install headers. |
| |
| // The following metadata classes are copied from |
| // https://github.com/google/prefab/tree/master/api/src/main/kotlin/com/google/prefab/api. |
| // |
| // The metadata is stable for a given schema version, but if we need to update to a newer schema |
| // version we'll need to update these. |
| // |
| // TODO: Depend on com.google.prefab:api. |
| // We can't currently do this because we can't use kotlinx.serialization. |
| |
| /** |
| * The v1 prefab.json schema. |
| * |
| * @property[name] The name of the module. |
| * @property[schemaVersion] The version of the schema. Must be 1. |
| * @property[dependencies] A list of other packages required by this package. |
| * @property[version] The package version. For compatibility with CMake, this |
| * *must* be formatted as major[.minor[.patch[.tweak]]] with all components |
| * being numeric, even if that does not match the package's native version. |
| */ |
| data class PackageMetadataV1( |
| val name: String, |
| @SerializedName("schema_version") |
| val schemaVersion: Int, |
| val dependencies: List<String>, |
| val version: String? = null |
| ) |
| |
| /** |
| * The module metadata that is overridable per-platform in module.json. |
| * |
| * @property[exportLibraries] The list of libraries other than the library |
| * described by this module that must be linked by users of this module. |
| * @property[libraryName] The name (without the file extension) of the library |
| * file. The file extension is automatically detected based on the contents of |
| * the directory. |
| */ |
| data class PlatformSpecificModuleMetadataV1( |
| @SerializedName("export_libraries") |
| val exportLibraries: List<String>? = null, |
| @SerializedName("library_name") |
| val libraryName: String? = null |
| ) |
| |
| /** |
| * The v1 module.json schema. |
| * |
| * @property[exportLibraries] The list of libraries other than the library |
| * described by this module that must be linked by users of this module. |
| * @property[libraryName] The name (without the file extension) of the library |
| * file. The file extension is automatically detected based on the contents of |
| * the directory. |
| * @property[android] Android specific values that override the main values if |
| * present. |
| */ |
| data class ModuleMetadataV1( |
| @SerializedName("export_libraries") |
| val exportLibraries: List<String>, |
| @SerializedName("library_name") |
| val libraryName: String? = null, |
| // Allowing per-platform overrides before we support more than a single |
| // platform might seem like overkill, but this makes it easier to maintain |
| // compatibility for old packages when new platforms are added. If we added |
| // this in schema version 2, there would be no way to reliably migrate v1 |
| // packages because we wouldn't know whether the exported libraries were |
| // Android specific or not. |
| val android: PlatformSpecificModuleMetadataV1 = |
| PlatformSpecificModuleMetadataV1( |
| null, |
| null |
| ) |
| ) |
| |
| /** |
| * The Android abi.json schema. |
| * |
| * @property[abi] The ABI name of the described library. These names match the tag field for |
| * [com.android.build.gradle.internal.core.Abi]. |
| * @property[api] The minimum OS version supported by the library. i.e. the |
| * library's `minSdkVersion`. |
| * @property[ndk] The major version of the NDK that this library was built with. |
| * @property[stl] The STL that this library was built with. |
| */ |
| data class AndroidAbiMetadata( |
| val abi: String, |
| val api: Int, |
| val ndk: Int, |
| val stl: String |
| ) |
| |
| private class JsonSerializer { |
| private val gson = GsonBuilder().setPrettyPrinting().create() |
| |
| fun toJson(obj: Any): String = |
| StringWriter().also { writer -> gson.toJson(obj, writer) }.toString() |
| } |
| |
| data class PrefabModuleTaskData( |
| @get:Input |
| val name: String, |
| @get:Optional |
| @get:InputDirectory |
| @get:PathSensitive(PathSensitivity.ABSOLUTE) |
| val headers: File?, |
| @get:Optional |
| @get:Input |
| val libraryName: String? |
| ) |
| |
| data class PrefabAbiData( |
| @get:Input |
| val abi: Abi, |
| @get:Input |
| val minSdkVersion: Int, |
| @get:Input |
| val ndkMajorVersion: Int, |
| @get:Input |
| val stl: String, |
| @get:InputFile |
| @get:PathSensitive(PathSensitivity.ABSOLUTE) |
| val nativeBuildJson: Provider<File> |
| ) |
| |
| abstract class PrefabPackageTask : NonIncrementalTask() { |
| @get:OutputDirectory |
| abstract val outputDirectory: DirectoryProperty |
| |
| @get:Input |
| lateinit var packageName: String |
| private set |
| |
| @get:Input |
| lateinit var packageVersion: String |
| private set |
| |
| @get:Nested |
| lateinit var abiData: List<PrefabAbiData> |
| private set |
| |
| @get:Nested |
| lateinit var modules: List<PrefabModuleTaskData> |
| private set |
| |
| override fun doTaskAction() { |
| val installDir = outputDirectory.get().asFile.apply { mkdirs() } |
| createPackageMetadata(installDir) |
| for (module in modules) { |
| createModule(module, installDir) |
| } |
| } |
| |
| private fun createPackageMetadata(installDir: File) { |
| installDir.resolve("prefab.json").writeText( |
| JsonSerializer().toJson( |
| PackageMetadataV1( |
| name = packageName, |
| schemaVersion = 1, |
| version = packageVersion.takeIf { it != "unspecified" }, |
| dependencies = emptyList() |
| ) |
| ) |
| ) |
| } |
| |
| private fun createModule(module: PrefabModuleTaskData, packageDir: File) { |
| val installDir = packageDir.resolve("modules/${module.name}").apply { mkdirs() } |
| createModuleMetadata(module, installDir) |
| installHeaders(module, installDir) |
| installLibs(module, installDir) |
| } |
| |
| private fun createModuleMetadata(module: PrefabModuleTaskData, installDir: File) { |
| installDir.resolve("module.json").writeText( |
| JsonSerializer().toJson( |
| ModuleMetadataV1( |
| exportLibraries = emptyList(), |
| libraryName = module.libraryName |
| ) |
| ) |
| ) |
| } |
| |
| private fun installHeaders(module: PrefabModuleTaskData, installDir: File) { |
| val includeDir = installDir.resolve("include").apply { deleteRecursively() } |
| module.headers?.let { |
| it.copyRecursively(includeDir) |
| infoln("Installing $it to $includeDir") |
| } |
| } |
| |
| private fun findLibraryForAbi(moduleName: String, abi: PrefabAbiData): NativeLibraryValueMini { |
| val config = |
| AndroidBuildGradleJsons.getNativeBuildMiniConfig(abi.nativeBuildJson.get(), null) |
| |
| // The libraries are keyed by $name-$config-$abi. For example, the debug, arm64 variant |
| // of gtestjni would be gtestjni-Debug-arm64-v8a. The config here is the CMake build |
| // variant of the library, not the name of the gradle build variant. Fortunately the |
| // JSON file here only exposes the variant we're building, so we don't need to determine |
| // what CMake build variant is used here. We also want to copy the libraries for every |
| // ABI that's supported by this library, so no need to filter by ABI. |
| val matchingLibs = config.libraries.filterKeys { it.startsWith("$moduleName-") }.values |
| if (matchingLibs.isEmpty()) { |
| errorln("No libraries found for $moduleName") |
| } |
| return matchingLibs.single() |
| } |
| |
| private fun installLibs(module: PrefabModuleTaskData, installDir: File) { |
| val libsDir = installDir.resolve("libs").apply { deleteRecursively() } |
| for (abiData in abiData) { |
| val srcLibrary = findLibraryForAbi(module.name, abiData).output |
| if (srcLibrary != null) { |
| val libDir = libsDir.resolve("android.${abiData.abi.tag}") |
| val dest = libDir.resolve(srcLibrary.name) |
| infoln("Installing $srcLibrary to $dest") |
| srcLibrary.copyTo(dest) |
| createLibraryMetadata(libDir, abiData) |
| } else { |
| warnln("${module.name}} has no library output") |
| } |
| } |
| } |
| |
| private fun createLibraryMetadata(libDir: File, abi: PrefabAbiData) { |
| libDir.resolve("abi.json").writeText( |
| JsonSerializer().toJson( |
| AndroidAbiMetadata( |
| abi = abi.abi.tag, |
| api = abi.minSdkVersion, |
| ndk = abi.ndkMajorVersion, |
| stl = abi.stl |
| ) |
| ) |
| ) |
| } |
| |
| class CreationAction( |
| private val modules: List<PrefabModuleTaskData>, |
| private val variantModel: CxxVariantModel, |
| private val abiModels: Collection<CxxAbiModel>, |
| componentProperties: LibraryVariantPropertiesImpl |
| ) : VariantTaskCreationAction<PrefabPackageTask, LibraryVariantPropertiesImpl>( |
| componentProperties |
| ) { |
| override val name: String |
| get() = computeTaskName("prefab", "Package") |
| |
| override val type: Class<PrefabPackageTask> |
| get() = PrefabPackageTask::class.java |
| |
| override fun handleProvider(taskProvider: TaskProvider<PrefabPackageTask>) { |
| super.handleProvider(taskProvider) |
| |
| creationConfig.artifacts.setInitialProvider( |
| taskProvider, |
| PrefabPackageTask::outputDirectory |
| ).withName("prefab").on(InternalArtifactType.PREFAB_PACKAGE) |
| } |
| |
| override fun configure(task: PrefabPackageTask) { |
| super.configure(task) |
| task.description = "Creates a Prefab package for inclusion in an AAR" |
| |
| val project = creationConfig.globalScope.project |
| task.packageName = project.name |
| task.packageVersion = project.version.toString() |
| task.modules = modules |
| |
| val stl = when (val result = variantModel.determineUsedStl()) { |
| is DetermineUsedStlResult.Success -> result.stl |
| is DetermineUsedStlResult.Failure -> { |
| errorln(result.error) |
| return |
| } |
| } |
| |
| task.abiData = abiModels.map { |
| // Pull up the app's minSdkVersion to be within the bounds for the ABI and NDK. |
| val ndkVersion = it.variant.module.ndkVersion.major |
| val metaPlatforms = it.variant.module.ndkMetaPlatforms |
| val minVersionForAbi = when { |
| it.abi.supports64Bits() -> AndroidVersion.SUPPORTS_64_BIT.apiLevel |
| else -> 1 |
| } |
| val minVersionForNdk = when { |
| // Newer NDKs expose the minimum supported version via meta/platforms.json |
| metaPlatforms != null -> metaPlatforms.min |
| // Older NDKs did not expose this, but the information is in the change logs |
| ndkVersion < 12 -> 1 |
| ndkVersion < 15 -> 9 |
| ndkVersion < 18 -> 14 |
| else -> 16 |
| } |
| PrefabAbiData( |
| it.abi, |
| max(it.abiPlatformVersion, max(minVersionForAbi, minVersionForNdk)), |
| ndkVersion, |
| stl.argumentName, |
| project.provider { it.jsonFile } |
| ) |
| } |
| } |
| } |
| } |