246 行
8.7 KiB
Groovy
246 行
8.7 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.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()
|
|
|
|
@InputFile
|
|
@PathSensitive(PathSensitivity.RELATIVE)
|
|
abstract RegularFileProperty getSignedConfigFile()
|
|
|
|
/**
|
|
* 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()
|
|
project.copy {
|
|
from(signedConfigFile.get().asFile)
|
|
into(assetsOutputDirectory.get().dir("config"))
|
|
rename { "config.xuqmconfig" }
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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"))
|
|
signedConfigFile.set(
|
|
xuqmSignedConfigCandidates
|
|
? xuqmSignedConfigCandidates.first()
|
|
: new File(xuqmSignedConfigDirectory, "config.xuqmconfig")
|
|
)
|
|
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"))
|
|
}
|
|
|
|
androidComponents.onVariants(androidComponents.selector().all()) { variant ->
|
|
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
|
|
}
|
|
}
|