XuqmGroup-RNSDK/packages/update/android/xuqm-bundles.gradle

339 行
12 KiB
Groovy

import javax.inject.Inject
import groovy.json.JsonSlurper
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
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.Internal
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.TaskAction
import org.gradle.process.ExecOperations
import org.gradle.work.DisableCachingByDefault
/**
* Generates every configured RN plugin bundle before Android packages assets.
*
* Apply once from the host application module:
* apply from: file("../../node_modules/@xuqm/rn-update/android/xuqm-bundles.gradle")
*/
@DisableCachingByDefault(because = "Metro maintains its own transform cache")
abstract class GenerateXuqmBundlesTask extends DefaultTask {
@Internal
abstract DirectoryProperty getProjectRootDirectory()
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getCliFile()
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getConfigFile()
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getPackageFile()
/**
* RN/ bundle native baseline
* Gradle out-of-date
*/
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
abstract ConfigurableFileCollection getSourceFiles()
@Input
abstract Property<String> getNodeExecutable()
@Input
abstract Property<String> getAppVersion()
@Input
abstract Property<String> getBuildId()
@Input
abstract Property<String> getBugCollectMode()
@OutputDirectory
abstract DirectoryProperty getAssetsOutputDirectory()
@OutputDirectory
abstract DirectoryProperty getResourcesOutputDirectory()
@Inject
abstract ExecOperations getExecOperations()
@TaskAction
void generate() {
def embeddedBundles = assetsOutputDirectory.get().dir("rn-bundles").asFile
def resources = resourcesOutputDirectory.get().asFile
execOperations.exec {
workingDir(projectRootDirectory.get().asFile)
environment("XUQM_BUILD_ID", buildId.get())
environment("XUQM_BUGCOLLECT_MODE", bugCollectMode.get())
commandLine(
nodeExecutable.get(),
cliFile.get().asFile.absolutePath,
"embed",
"android",
"--output",
embeddedBundles.absolutePath,
"--app-version",
appVersion.get(),
)
}.assertNormalExitValue()
// Android 内嵌 Bundle 通过 AssetManager 读取,但 require() 图片必须同时进入
// aapt2 的 drawable 资源表。只把 drawable-* 放进 assets 会导致图片静默为空。
project.delete(resources)
embeddedBundles.listFiles()?.findAll {
it.isDirectory() && it.name.startsWith("drawable-")
}?.each { drawableDirectory ->
project.copy {
from(drawableDirectory)
into(new File(resources, drawableDirectory.name))
}
}
}
}
/**
* Debug 使 Metro Bundle
* Android SDK assets JS
*/
@DisableCachingByDefault(because = "Signed config is a small immutable build input")
abstract class CopyXuqmConfigTask extends DefaultTask {
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
abstract RegularFileProperty getSignedConfigFile()
@OutputDirectory
abstract DirectoryProperty getAssetsOutputDirectory()
@TaskAction
void copyConfig() {
def output = assetsOutputDirectory.get().asFile
project.delete(output)
project.copy {
from(signedConfigFile.get().asFile)
into(new File(output, "config"))
rename { "config.xuqmconfig" }
}
}
}
/**
* RN 宿 Android SDK Manifest
*
* RN 使宿 Android Gradle
* Android SDK RN 宿
*/
abstract class GenerateXuqmHostManifestTask extends DefaultTask {
@Input
abstract Property<String> getBuildId()
@Input
abstract Property<Boolean> getBugCollectBuildEnabled()
@Input
abstract Property<Boolean> getBugCollectAutomaticEnabled()
@OutputFile
abstract RegularFileProperty getOutputFile()
@TaskAction
void generate() {
def manifest = outputFile.get().asFile
manifest.parentFile.mkdirs()
manifest.setText(
"""<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<meta-data android:name="com.xuqm.common_plugin_applied" android:value="true" />
<meta-data android:name="com.xuqm.build_id" android:value="${buildId.get()}" />
<meta-data android:name="com.xuqm.bugcollect_build_enabled" android:value="${bugCollectBuildEnabled.get()}" />
<meta-data android:name="com.xuqm.bugcollect_automatic_enabled" android:value="${bugCollectAutomaticEnabled.get()}" />
</application>
</manifest>
""",
"UTF-8",
)
}
}
if (!plugins.hasPlugin("com.android.application")) {
throw new GradleException("xuqm-bundles.gradle must be applied to an Android application module")
}
def xuqmProjectRoot = rootProject.projectDir.parentFile
def xuqmHostPackage = new JsonSlurper().parse(new File(xuqmProjectRoot, "package.json"))
def xuqmLocalDependencyDirectories = (xuqmHostPackage.dependencies ?: [:])
.findAll { _, requested -> requested instanceof String && requested.startsWith("file:") }
.collect { _, requested -> new File(xuqmProjectRoot, requested.substring("file:".length())).canonicalFile }
def xuqmUseMetro = providers.gradleProperty("USE_METRO")
.orElse(providers.environmentVariable("USE_METRO"))
.map { it.toBoolean() }
.orElse(false)
def xuqmReleaseVersionCode = providers.gradleProperty("XUQM_VERSION_CODE")
.orElse(providers.environmentVariable("XUQM_VERSION_CODE"))
if (xuqmReleaseVersionCode.isPresent()) {
def parsedVersionCode = Integer.parseInt(xuqmReleaseVersionCode.get())
if (parsedVersionCode <= 0) {
throw new GradleException("XUQM_VERSION_CODE must be a positive integer")
}
android.defaultConfig.versionCode = parsedVersionCode
}
def xuqmBuildId = providers.gradleProperty("XUQM_BUILD_ID")
.orElse(providers.environmentVariable("XUQM_BUILD_ID"))
.orElse("development")
def xuqmBugCollectMode = providers.gradleProperty("XUQM_BUGCOLLECT_MODE")
.orElse(providers.environmentVariable("XUQM_BUGCOLLECT_MODE"))
.orElse("platform")
if (!["platform", "disabled"].contains(xuqmBugCollectMode.get())) {
throw new GradleException("XUQM_BUGCOLLECT_MODE must be platform or disabled")
}
def xuqmSignedConfigDirectory = new File(xuqmProjectRoot, "src/assets/config")
def xuqmSignedConfigCandidates = xuqmSignedConfigDirectory.isDirectory()
? (xuqmSignedConfigDirectory.listFiles()?.findAll {
it.isFile() && it.name.toLowerCase(Locale.ROOT).endsWith(".xuqmconfig")
} ?: []).sort { left, right -> left.name <=> right.name }
: []
if (xuqmSignedConfigCandidates.size() > 1) {
throw new GradleException(
"src/assets/config must contain exactly one .xuqmconfig file; found ${xuqmSignedConfigCandidates.size()}"
)
}
def prepareXuqmBundles = tasks.register(
"prepareXuqmEmbeddedBundles",
GenerateXuqmBundlesTask,
) {
group = "xuqm"
description = "Builds and embeds all configured Xuqm RN plugin bundles."
projectRootDirectory.set(xuqmProjectRoot)
cliFile.set(new File(xuqmProjectRoot, "node_modules/@xuqm/rn-update/scripts/xuqm-rn.mjs"))
configFile.set(new File(xuqmProjectRoot, "xuqm.modules.json"))
packageFile.set(new File(xuqmProjectRoot, "package.json"))
sourceFiles.from(project.fileTree(xuqmProjectRoot) {
include("src/**")
include("assets/**")
include("android/**")
include("index.*")
include("babel.config.*")
include("metro.config.*")
include("react-native.config.*")
include("tsconfig*.json")
include("pnpm-lock.yaml")
include("yarn.lock")
exclude("android/**/build/**")
exclude("android/**/.cxx/**")
exclude("android/**/.gradle/**")
exclude("android/app/src/**/generated/**")
})
sourceFiles.from(project.fileTree(new File(
xuqmProjectRoot,
"node_modules/@xuqm/rn-update/scripts",
)) {
include("*.mjs")
})
// registry 依赖由不可变版本号进入 baseline;file: 依赖在版本不变时也可能修改
// 源码,必须同时成为 Gradle 输入和 native baseline 内容。
// JS/TS 源码决定 bundle 内容,android/ios 决定 native baseline,缺一不可。
xuqmLocalDependencyDirectories.each { dependencyDirectory ->
sourceFiles.from(project.fileTree(dependencyDirectory) {
include("src/**")
include("metro/**")
include("android/**")
include("ios/**")
include("*.podspec")
include("package.json")
exclude("**/build/**")
exclude("**/.cxx/**")
exclude("**/.gradle/**")
exclude("**/generated/**")
})
}
nodeExecutable.set(System.getenv("NODE_BINARY") ?: "node")
appVersion.set(providers.provider {
def version = android.defaultConfig.versionName
if (version == null || version.toString().trim().isEmpty()) {
throw new GradleException("Android defaultConfig.versionName is required")
}
version.toString()
})
buildId.set(xuqmBuildId)
bugCollectMode.set(xuqmBugCollectMode)
assetsOutputDirectory.set(layout.buildDirectory.dir("generated/xuqm/assets"))
resourcesOutputDirectory.set(layout.buildDirectory.dir("generated/xuqm/res"))
}
def prepareXuqmConfig = tasks.register(
"prepareXuqmConfigAssets",
CopyXuqmConfigTask,
) {
group = "xuqm"
description = "Copies the single signed Xuqm config into Android assets."
signedConfigFile.set(
xuqmSignedConfigCandidates
? xuqmSignedConfigCandidates.first()
: new File(xuqmSignedConfigDirectory, "config.xuqmconfig")
)
assetsOutputDirectory.set(layout.buildDirectory.dir("generated/xuqm/config-assets"))
}
androidComponents.onVariants(androidComponents.selector().all()) { variant ->
def variantName = variant.name
def variantSuffix = variantName.substring(0, 1).toUpperCase(Locale.ROOT) +
variantName.substring(1)
def bugCollectEnabled = xuqmBugCollectMode.map { it != "disabled" }
def prepareXuqmHostManifest = tasks.register(
"prepareXuqmHostManifest${variantSuffix}",
GenerateXuqmHostManifestTask,
) {
group = "xuqm"
description = "Generates Xuqm initialization metadata for ${variantName}."
buildId.set(xuqmBuildId)
bugCollectBuildEnabled.set(bugCollectEnabled)
bugCollectAutomaticEnabled.set(
bugCollectEnabled.map { enabled -> enabled && variant.buildType == "release" },
)
outputFile.set(
layout.buildDirectory.file(
"generated/xuqm/${variantName}/AndroidManifest.xml",
),
)
}
variant.sources.manifests.addGeneratedManifestFile(
prepareXuqmHostManifest,
{ task -> task.outputFile },
)
variant.sources.assets?.addGeneratedSourceDirectory(
prepareXuqmConfig,
{ task -> task.assetsOutputDirectory },
)
if (variant.buildType == "release" || !xuqmUseMetro.get()) {
variant.sources.assets?.addGeneratedSourceDirectory(
prepareXuqmBundles,
{ task -> task.assetsOutputDirectory },
)
variant.sources.res?.addGeneratedSourceDirectory(
prepareXuqmBundles,
{ task -> task.resourcesOutputDirectory },
)
}
}
tasks.configureEach { task ->
if (task.name ==~ /^createBundle.*ReleaseJsAndAssets$/) {
// Xuqm hosts load startup/common/app/plugin bundles explicitly. A second
// monolithic index.android.bundle is both redundant and unsafe.
task.enabled = false
}
}