XuqmGroup-HarmonySDK/xuqm-sdk/scripts/xuqm_rn_release.gradle.kts

263 行
10 KiB
Plaintext

/**
* XuqmGroup Update Service — Harmony RN Bundle Release Task
*
* Copy this file to your Harmony project and apply it:
* apply(from = "xuqm_rn_release.gradle.kts")
*
* Then run:
* ./hvigorw xuqmRnRelease
*
* Config: xuqm.properties in the project root (or module root)
* ---
* xuqm.serverUrl=https://update.dev.xuqinmin.com
* xuqm.appKey=your-app-key
* xuqm.apiToken=your-api-token
* xuqm.moduleId=main
* xuqm.bundleFile=/path/to/index.bundle
* xuqm.resourceDir=/path/to/resources # optional, but recommended
* xuqm.bundleVersion=100
* xuqm.minCommonVersion=1.0.0 # optional
* xuqm.packageName=com.example.app # optional
* xuqm.note=optional note
* ---
*
* The task:
* 1. Reads local RN release metadata from xuqm.properties
* 2. Packages bundle + resources + rn-manifest.json into a zip
* 3. Uploads the zip to update-service so the server can auto-detect metadata
*/
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.io.FileOutputStream
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.util.Properties
import java.util.UUID
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
fun loadXuqmConfig(projectDir: File): Properties {
val props = Properties()
listOf(
File(projectDir, "xuqm.properties"),
File(projectDir.parentFile, "xuqm.properties"),
).firstOrNull { it.exists() }?.inputStream()?.use(props::load)
?: throw GradleException("xuqm.properties not found in module or project root")
return props
}
fun httpGet(url: String, token: String): String {
val client = HttpClient.newHttpClient()
val builder = HttpRequest.newBuilder(URI.create(url))
if (token.isNotBlank()) {
builder.header("Authorization", "Bearer $token")
}
val req = builder.GET().build()
return client.send(req, HttpResponse.BodyHandlers.ofString()).body()
}
fun httpMultipartPost(url: String, token: String, parts: Map<String, Any>): String {
val boundary = UUID.randomUUID().toString()
val baos = java.io.ByteArrayOutputStream()
fun writeln(s: String) = baos.write(("$s\r\n").toByteArray())
for ((name, value) in parts) {
writeln("--$boundary")
when (value) {
is File -> {
writeln("Content-Disposition: form-data; name=\"$name\"; filename=\"${value.name}\"")
writeln("Content-Type: application/octet-stream")
writeln("")
baos.write(value.readBytes())
writeln("")
}
else -> {
writeln("Content-Disposition: form-data; name=\"$name\"")
writeln("")
writeln(value.toString())
}
}
}
writeln("--$boundary--")
val body = baos.toByteArray()
val client = HttpClient.newHttpClient()
val builder = HttpRequest.newBuilder(URI.create(url))
.header("Content-Type", "multipart/form-data; boundary=$boundary")
if (token.isNotBlank()) {
builder.header("Authorization", "Bearer $token")
}
val req = builder.POST(HttpRequest.BodyPublishers.ofByteArray(body)).build()
return client.send(req, HttpResponse.BodyHandlers.ofString()).body()
}
fun parseJson(json: String, key: String): String? =
Regex("\"$key\"\\s*:\\s*\"([^\"]+)\"").find(json)?.groupValues?.get(1)
fun parseJsonInt(json: String, key: String): Int? =
Regex("\"$key\"\\s*:\\s*(\\d+)").find(json)?.groupValues?.get(1)?.toIntOrNull()
fun parseCsv(value: String?): List<String> =
value?.split(",")?.map { it.trim() }?.filter { it.isNotBlank() } ?: emptyList()
fun promptLine(message: String): String? {
val console = System.console() ?: return null
print(message)
return console.readLine()
}
fun escapeJson(value: String?): String {
if (value == null) return ""
return buildString(value.length + 8) {
value.forEach { ch ->
when (ch) {
'\\' -> append("\\\\")
'"' -> append("\\\"")
'\b' -> append("\\b")
'\u000C' -> append("\\f")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> append(ch)
}
}
}
}
fun writeZipEntry(zip: ZipOutputStream, entryName: String, content: ByteArray) {
val entry = ZipEntry(entryName)
zip.putNextEntry(entry)
zip.write(content)
zip.closeEntry()
}
fun addFileToZip(zip: ZipOutputStream, file: File, entryName: String) {
val entry = ZipEntry(entryName)
zip.putNextEntry(entry)
file.inputStream().use { input ->
input.copyTo(zip)
}
zip.closeEntry()
}
fun addDirectoryToZip(zip: ZipOutputStream, dir: File, prefix: String) {
if (!dir.exists()) return
dir.walkTopDown()
.filter { it.isFile }
.forEach { file ->
val relative = file.relativeTo(dir).path.replace(File.separatorChar, '/')
addFileToZip(zip, file, "$prefix/$relative")
}
}
fun packRnReleaseZip(bundleFile: File, resourceDir: File?, manifestJson: String): File {
val zipPath = Files.createTempFile("xuqm-rn-release-", ".zip").toFile()
ZipOutputStream(FileOutputStream(zipPath)).use { zip ->
writeZipEntry(zip, "rn-manifest.json", manifestJson.toByteArray(StandardCharsets.UTF_8))
addFileToZip(zip, bundleFile, "bundle/${bundleFile.name}")
if (resourceDir != null && resourceDir.exists()) {
addDirectoryToZip(zip, resourceDir, "resources")
}
}
return zipPath
}
fun findHighestVersion(listResp: String): Int {
return Regex("\"version\"\\s*:\\s*\"?(\\d+)\"?").findAll(listResp)
.mapNotNull { it.groupValues[1].toIntOrNull() }
.maxOrNull() ?: 0
}
tasks.register("xuqmRnRelease") {
group = "xuqm"
description = "Package RN bundle + resources into zip and upload to XuqmGroup Update Service"
doLast {
val cfg = loadXuqmConfig(projectDir)
val serverUrl = cfg.getProperty("xuqm.serverUrl") ?: throw GradleException("xuqm.serverUrl missing")
val appKey = cfg.getProperty("xuqm.appKey") ?: throw GradleException("xuqm.appKey missing")
val apiToken = cfg.getProperty("xuqm.apiToken") ?: throw GradleException("xuqm.apiToken missing")
val moduleId = cfg.getProperty("xuqm.moduleId") ?: throw GradleException("xuqm.moduleId missing")
val bundleFilePath = cfg.getProperty("xuqm.bundleFile") ?: throw GradleException("xuqm.bundleFile missing")
val bundleVersion = cfg.getProperty("xuqm.bundleVersion")
?.trim()
?.toIntOrNull()
?: throw GradleException("xuqm.bundleVersion missing or invalid")
val resourceDirPath = cfg.getProperty("xuqm.resourceDir", "").trim()
val minCommonVersion = cfg.getProperty("xuqm.minCommonVersion", "").trim()
val packageName = cfg.getProperty("xuqm.packageName", "").trim()
val note = cfg.getProperty("xuqm.note", "").trim()
val bundleFile = File(bundleFilePath)
if (!bundleFile.exists()) {
throw GradleException("Bundle file not found: ${bundleFile.absolutePath}")
}
val resourceDir = if (resourceDirPath.isNotBlank()) File(resourceDirPath) else null
if (resourceDir != null && !resourceDir.exists()) {
throw GradleException("Resource directory not found: ${resourceDir.absolutePath}")
}
val listResp = httpGet("$serverUrl/api/v1/rn/list?appId=$appKey&moduleId=$moduleId&platform=HARMONY", apiToken)
val serverVersion = findHighestVersion(listResp)
println("[xuqm] Server latest bundleVersion: $serverVersion")
var releaseVersion = bundleVersion
if (releaseVersion <= serverVersion && System.console() != null) {
println("[xuqm] Local bundleVersion is not greater than server latest. Please enter corrected release version info.")
val inputVersion = promptLine("Release bundleVersion [$bundleVersion]: ")?.trim().orEmpty()
if (inputVersion.isNotBlank()) {
releaseVersion = inputVersion.toInt()
}
}
if (releaseVersion <= serverVersion) {
throw GradleException(
"Release bundleVersion ($releaseVersion) must be greater than server ($serverVersion). " +
"Bump the local RN release metadata or override intentionally via script config."
)
}
val manifestJson = buildString {
append("{")
append("\"moduleId\":\"").append(escapeJson(moduleId)).append("\",")
append("\"platform\":\"HARMONY\",")
append("\"version\":\"").append(releaseVersion).append("\",")
append("\"bundleVersion\":").append(releaseVersion).append(",")
append("\"minCommonVersion\":\"").append(escapeJson(minCommonVersion)).append("\",")
append("\"packageName\":\"").append(escapeJson(packageName)).append("\",")
append("\"note\":\"").append(escapeJson(note)).append("\"")
append("}")
}
val zipFile = packRnReleaseZip(bundleFile, resourceDir, manifestJson)
println("[xuqm] RN bundle zip: ${zipFile.absolutePath}")
println("[xuqm] bundleVersion=$releaseVersion, moduleId=$moduleId, minCommonVersion=${minCommonVersion.ifBlank { "-" }}, packageName=${packageName.ifBlank { "-" }}")
val parts = mutableMapOf<String, Any>(
"appId" to appKey,
"moduleId" to moduleId,
"platform" to "HARMONY",
"version" to releaseVersion.toString(),
"bundle" to zipFile,
)
if (minCommonVersion.isNotBlank()) parts["minCommonVersion"] = minCommonVersion
if (packageName.isNotBlank()) parts["packageName"] = packageName
if (note.isNotBlank()) parts["note"] = note
println("[xuqm] Uploading RN release zip...")
val uploadResp = httpMultipartPost("$serverUrl/api/v1/rn/upload", apiToken, parts)
val versionId = parseJson(uploadResp, "id")
?: throw GradleException("[xuqm] Upload failed:\n$uploadResp")
println("[xuqm] Uploaded, version ID: $versionId")
println("[xuqm] Release zip contains rn-manifest.json + bundle + resources")
}
}