blob: 42683755730d20655cfa182652fb69df52d2c763 [file] [log] [blame]
/*
* 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 }
)
}
}
}
}