6.7.0 - Alpha6 - Gradle 脚本由 buildSrc 迁移至 build-logic 并启用类型安全访问器

This commit is contained in:
SuperMonster003
2025-10-06 12:56:22 +08:00
parent 4235ffda82
commit aa603aac44
61 changed files with 165 additions and 180 deletions

View File

@@ -0,0 +1,76 @@
package org.autojs.build
import org.gradle.api.Project
import java.io.File
import java.io.FileNotFoundException
import java.util.*
class BuildProperties private constructor(
private val file: File,
private val props: Properties,
) {
val path: String get() = file.absolutePath
operator fun get(propertyName: String): String = when {
propertyName.contains("/") -> {
get(propertyName.split("/"))
}
else -> requireValue(propertyName)
}
operator fun get(propertyInfo: List<String>): String {
val (lib, body) = propertyInfo
return requireValue(lib, body)
}
private fun requireString(key: String, alternate: String): String {
return props.getProperty(key)
?: props.getProperty(alternate)
?: throw IllegalStateException("Property '$key' is missing in '${file.absolutePath}'")
}
fun requireString(key: String): String {
return props.getProperty(key)
?: throw IllegalStateException("Property '$key' is missing in '${file.absolutePath}'")
}
fun requireInt(key: String): Int = requireString(key).toInt()
fun getIntOrNull(key: String): Int? = props.getProperty(key)?.toIntOrNull()
private fun requireValue(name: String): String {
return when (val value = requireString("${name}_VERSION", name)) {
"PUBLIC" -> requireString("PUBLIC_${name}_VERSION", "PUBLIC_${name}")
else -> value
}
}
private fun requireValue(lib: String, body: String): String {
return when (val value = requireString("${lib}_${body}_VERSION", "${lib}_${body}")) {
"PUBLIC" -> requireString("PUBLIC_${body}_VERSION", "PUBLIC_${body}")
else -> value
}
}
companion object {
@JvmStatic
fun loadFrom(project: Project): BuildProperties {
return loadFrom("${project.rootDir}/version.properties")
}
@JvmStatic
fun loadFrom(filePath: String): BuildProperties {
val f = File(filePath)
if (!f.canRead()) {
throw FileNotFoundException("Cannot read file '$filePath'")
}
val p = Properties()
f.inputStream().use { p.load(it) }
return BuildProperties(f, p)
}
}
}

View File

@@ -0,0 +1,33 @@
package org.autojs.build
class Formatted(
private val title: CharSequence,
private val contents: Collection<CharSequence> = emptyList(),
private val subtitle: CharSequence? = null
) {
private val formattedOutput: List<CharSequence> = run {
val maxLength = (listOfNotNull(title, subtitle) + contents).maxOf { it.length }
buildList {
add("=".repeat(maxLength))
add(title)
subtitle?.let { add(it) }
if (contents.isNotEmpty()) add("-".repeat(maxLength))
addAll(contents)
add("=".repeat(maxLength))
add("")
}
}
@JvmOverloads
fun print(contentsMatters: Boolean = false) {
formattedOutput.forEach {
if (!contentsMatters || contents.isNotEmpty()) {
println(it)
}
}
}
fun throwException() {
throw Exception(formattedOutput.joinToString("\n"))
}
}

View File

@@ -0,0 +1,32 @@
@file:Suppress("unused")
package org.autojs.build
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Convention plugin: Unified Java/Kotlin target version configuration for Android modules.
*
* zh-CN: 约定插件: 为 Android 模块统一配置 Java/Kotlin 目标版本.
*
* - `id`: "org.autojs.build.jvm-convention"
* - `implementationClass`: "org.autojs.build.JvmConventionPlugin"
* - `displayName`: "AutoJs6 JVM Convention Plugin"
* - `description`: "Configures Java/Kotlin targets for Android modules using central Versions."
*
* Apply this plugin to your Android module's `build.gradle.kts`:
*
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
*
* ```kts
* plugins {
* id("org.autojs.build.jvm-convention")
* }
* ```
*/
class JvmConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
Utils.configureJvmForAndroidModule(project)
}
}

View File

@@ -0,0 +1,437 @@
package org.autojs.build
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.kotlin.dsl.extra
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.math.BigInteger
import java.net.SocketTimeoutException
import java.net.URI
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.security.MessageDigest
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import kotlin.math.max
class LibDeployer(
private val project: Project,
private val name: String,
private val downloadUrl: String,
) {
private var sourceDir: String = File.separator
private var sourceFile: File = project.file(File.separator)
private var destDir: String = File.separator
private var destFile: File = project.file(File.separator)
private val cacheRootFile: File = project.file("cache").apply { mkdirs() }
private val cacheFileName: String
private val cacheFileExtensionName: String
private val cacheFile: File
private val shouldPrintProgress: Boolean
get() = project.gradle.extra["platform"]?.let {
it::class.java.getMethod("getShouldPrintProgress")
.apply { isAccessible = true }
.invoke(it)
} == true
init {
val extracted = extractFileFromUrl(downloadUrl)
cacheFileName = "${extracted.fileName}-[${generateShortMd5String(downloadUrl).lowercase()}]"
cacheFileExtensionName = extracted.extensionName
cacheFile = project.file(File(cacheRootFile, "$cacheFileName.$cacheFileExtensionName"))
}
private fun getSkipFile(): File = project.file(File(destFile, "$cacheFileName.skip"))
private fun getTempOutFile(): File = project.file(File(destFile, "temp-extracted"))
fun setSourceDir(sourceDir: String): LibDeployer {
this.sourceDir = sourceDir
this.sourceFile = project.file(sourceDir)
return this
}
fun setDestDir(destDir: String): LibDeployer {
val dest = if (destDir.startsWith(File.separator)) {
project.file(destDir.substring(1))
} else {
project.file(destDir)
}
dest.mkdirs()
this.destDir = destDir
this.destFile = dest
return this
}
fun deploy() {
val tempOutFile = getTempOutFile()
if (tempOutFile.exists()) {
project.delete(tempOutFile)
}
val (shouldDownload, shouldExtract) = checkCacheAndSkipFiles()
var needExtract = shouldExtract
if (shouldDownload) {
printDownloadInfo()
downloadWithRetry()
needExtract = true
}
if (needExtract) {
printExtractInfo()
try {
extractCacheFile()
} catch (e: Exception) {
println()
println("Cache file was deleted as there is an error during extraction")
println("Cache file: ${cacheFile.absolutePath}")
cacheFile.delete()
e.message?.let { println("Error message: $it") }
println()
throw e
}
generateMd5File(cacheFile)
}
}
fun clean() {
project.delete(getSkipFile())
project.delete(getTempOutFile())
deleteDestAccordingToSrc()
deleteCacheAccordingToMd5()
}
private data class ExtractedFile(val fileName: String, val extensionName: String)
private fun extractFileFromUrl(url: String): ExtractedFile {
val fileNameWithExtension = url.substringAfterLast('/')
val dot = fileNameWithExtension.lastIndexOf('.')
val fileNameRaw = if (dot >= 0) fileNameWithExtension.take(dot) else fileNameWithExtension
val fileName = fileNameRaw.lowercase()
.replace(Regex("\\s+"), "")
.replace(Regex("[^a-z0-9.]"), "-")
val extension = if (dot >= 0) fileNameWithExtension.substring(dot + 1) else ""
return ExtractedFile(fileName, extension)
}
private fun checkCacheAndSkipFiles(): Pair<Boolean, Boolean> {
var shouldDownload = true
var shouldExtract = true
val skip = getSkipFile()
if (skip.exists()) {
println("No need to download or extract \"$name\" archive file as the \"skip file\" exists")
shouldDownload = false
shouldExtract = false
println()
} else if (cacheFile.exists()) {
if (validateMd5File(cacheFile)) {
println("No need to download \"$name\" archive file as the cache file exists and is valid")
shouldDownload = false
println("Cache file of \"$name\" needs to be extracted as the \"skip file\" doesn't exist")
} else {
println("Cache file of \"$name\" was deleted as it is invalid")
println("Cache file: $cacheFile")
project.delete(cacheFile)
val md5File = File(cacheFile.parentFile, cacheFile.name + ".md5")
if (md5File.exists()) {
println("MD5 file of \"$name\" was deleted as it is unreliable")
println("MD5 file: $md5File")
project.delete(md5File)
}
}
println()
}
return shouldDownload to shouldExtract
}
private fun validateMd5File(file: File): Boolean {
val md5File = File(file.parentFile, file.name + ".md5")
if (!md5File.exists()) return false
val expected = md5File.readText().trim().uppercase()
val actual = generateMd5String(file).uppercase()
return expected == actual
}
private fun generateMd5File(file: File) {
val md5File = File(file.parentFile, file.name + ".md5")
println("Generating MD5...")
val md5 = generateMd5String(file)
md5File.writeText(md5)
println("MD5 generated: $md5")
println()
}
private fun generateMd5String(file: File): String {
FileInputStream(file).use { fis ->
val channel: FileChannel = fis.channel
val md = MessageDigest.getInstance("MD5")
val buffer = ByteBuffer.allocate(4096)
while (channel.read(buffer) > 0) {
buffer.flip()
md.update(buffer)
buffer.clear()
}
return BigInteger(1, md.digest()).toString(16).padStart(32, '0')
}
}
private fun generateShortMd5String(s: String): String {
val md = MessageDigest.getInstance("MD5")
md.update(s.toByteArray())
return BigInteger(1, md.digest()).toString(32)
}
private fun printDownloadInfo() {
val title = "Download \"$name\" archive file for \"${project.extensions.extraProperties["projectName"]}\" Gradle project"
val srcInfo = "Source: $downloadUrl"
val destInfo = "Destination: $cacheFile"
val hintInfo = listOf(
"If the download gets stuck and won't finish,",
"try downloading the source file with tools like IDM (Internet Download Manager),",
"then renaming it into the destination path above."
)
val maxLength = listOf(title, srcInfo, destInfo, *hintInfo.toTypedArray()).maxOf { it.length }
listOf(
"=".repeat(maxLength),
title,
"-".repeat(maxLength),
srcInfo,
destInfo,
"-".repeat(maxLength),
hintInfo.joinToString("\n"),
"=".repeat(maxLength),
""
).forEach { println(it) }
}
private fun printExtractInfo() {
val title = "Extract the archive file for \"${project.extensions.extraProperties["projectName"]}\" Gradle project"
val srcInfo = "Source: $cacheFile"
val destInfo = "Destination: $destFile"
val items = listOf(title, srcInfo, destInfo)
val maxLength = items.maxOf { it.length }
listOf(
"=".repeat(maxLength),
title,
"-".repeat(maxLength),
srcInfo,
destInfo,
"=".repeat(maxLength),
""
).forEach { println(it) }
}
private fun downloadWithRetry(maxRetries: Int = 3, retryDelayMs: Long = 2000) {
var attempt = 0
var success = false
while (attempt < maxRetries && !success) {
try {
attempt++
download()
success = true
} catch (_: SocketTimeoutException) {
println("Attempt $attempt/$maxRetries failed: Connection timed out. Retrying...")
} catch (e: IOException) {
println("Attempt $attempt/$maxRetries failed: ${e.message}. Retrying...")
}
if (!success) {
if (attempt < maxRetries) {
Thread.sleep(retryDelayMs)
} else {
println("Download failed after $maxRetries attempts.")
throw GradleException("Download failed after $maxRetries attempts", null)
}
}
}
}
private fun download() {
cacheFile.parentFile.mkdirs()
val urlConn = URI(downloadUrl).toURL().openConnection().apply {
connectTimeout = 120_000
readTimeout = 90_000
}
val fileSize = urlConn.contentLengthLong
urlConn.getInputStream().use { input ->
FileOutputStream(cacheFile).use { output ->
val buffer = ByteArray(8192)
var downloaded = 0L
var read: Int
if (shouldPrintProgress && fileSize <= 0) {
println("\rDownloading...")
}
while (true) {
read = input.read(buffer)
if (read == -1) break
output.write(buffer, 0, read)
downloaded += read
if (shouldPrintProgress && fileSize > 0) {
val progress = downloaded * 100.0 / fileSize
val bar = generateProgressBar(progress)
print(String.format(Locale.getDefault(), "\rDownloading... [ %s ] %.2f%%", bar, progress))
System.out.flush()
}
}
}
}
val path = cacheFile.absolutePath
if (fileSize > 0) {
val formattedSize = formatFileSize(fileSize)
print(String.format("\rDownload complete [ %s | %s ]\n", path, formattedSize))
} else {
print(String.format("\rDownload complete [ %s ]\n", path))
}
System.out.flush()
println()
}
private fun extractCacheFile() {
when (cacheFileExtensionName.lowercase()) {
"zip" -> handleZip()
"7z" -> handleSevenZip()
else -> throw GradleException("Unknown archive file type: $cacheFileExtensionName")
}
println("All files extracted into [ $destFile ]")
val skip = getSkipFile()
if (!skip.exists()) {
skip.parentFile.mkdirs()
skip.createNewFile()
println("File \"${skip.name}\" created")
}
println()
}
private fun handleZip() {
val sourceDirPath = File(sourceDir).path.let { p ->
if (p.startsWith(File.separator)) p.substring(1) else p
}
val tempOut = getTempOutFile()
val zipForTotal = ZipFile(cacheFile)
val entriesAll = zipForTotal.entries()
val entries = mutableListOf<ZipEntry>()
var totalExtractedSize = 0L
while (entriesAll.hasMoreElements()) {
val entry = entriesAll.nextElement()
val entryName = File(entry.name).path
if (entryName.startsWith(sourceDirPath)) {
entries += entry
if (!entry.isDirectory) totalExtractedSize += entry.size
}
}
zipForTotal.close()
val zip = ZipFile(cacheFile)
val zipEntries = zip.entries()
var processed = 0
val totalEntries = entries.size
while (zipEntries.hasMoreElements()) {
val entry = zipEntries.nextElement()
val entryName = File(entry.name).path
if (!entryName.startsWith(sourceDirPath)) continue
val outFile = project.file(File(tempOut, entryName.substring(sourceDirPath.length)))
if (entry.isDirectory) {
outFile.mkdirs()
} else {
outFile.parentFile.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(outFile).use { output ->
val buffer = ByteArray(8192)
while (true) {
val read = input.read(buffer)
if (read == -1) break
output.write(buffer, 0, read)
}
}
}
}
if (shouldPrintProgress) {
val progress = processed * 100.0 / max(1, totalEntries)
val bar = generateProgressBar(progress)
print(String.format(Locale.getDefault(), "\rExtracting... [ %s ] %.2f%%", bar, progress))
System.out.flush()
}
processed++
}
val formatted = formatFileSize(totalExtractedSize)
print(String.format("\rExtraction complete [ %s | %s ]\n", destFile.absolutePath, formatted))
System.out.flush()
println()
zip.close()
project.copy {
from(tempOut)
into(destFile)
}
project.delete(tempOut)
}
private fun handleSevenZip() {
val tempOut = getTempOutFile()
val totalBytes = SevenZExtractor.extractDirectoryFrom7z(
cacheFile, sourceDir, tempOut, shouldPrintProgress
)
val formatted = formatFileSize(max(totalBytes, 0L))
print(String.format("\rExtraction complete [ %s | %s ]\n", destFile.absolutePath, formatted))
System.out.flush()
project.copy {
from(tempOut)
into(destFile)
}
project.delete(tempOut)
}
private fun deleteDestAccordingToSrc() {
project.delete(destFile.absolutePath)
var tmp: File? = destFile.parentFile
while (tmp != null && tmp != project.projectDir) {
val list = tmp.listFiles()?.toList().orEmpty()
if (list.isEmpty()) {
println("Delete empty directory: ${tmp.absolutePath}")
project.delete(tmp)
}
tmp = tmp.parentFile
}
}
private fun deleteCacheAccordingToMd5() {
cacheRootFile.listFiles()?.forEach { f ->
if (!f.name.endsWith(".md5")) {
val md5 = File(f.parentFile, f.name + ".md5")
if (!md5.exists() || !validateMd5File(f)) {
project.delete(f)
println("Delete cache file: ${f.absolutePath}")
if (md5.exists()) {
project.delete(md5)
println("Delete MD5 file: ${md5.absolutePath}")
}
println()
}
}
}
}
private fun generateProgressBar(progress: Double, length: Int = 30): String {
val filledLen = (length * progress / 100.0).toInt().coerceIn(0, length)
val filled = "#".repeat(filledLen)
val empty = "-".repeat(length - filledLen)
return filled + empty
}
private fun formatFileSize(size: Long): String = when {
// @formatter:off
size < 1024L -> String.format(Locale.getDefault(), "%d B", size)
size < 1024L * 1024 -> String.format(Locale.getDefault(), "%.2f KB", size / (1024.0))
size < 1024L * 1024 * 1024 -> String.format(Locale.getDefault(), "%.2f MB", size / (1024.0 * 1024))
else -> String.format(Locale.getDefault(), "%.2f GB", size / (1024.0 * 1024 * 1024))
// @formatter:on
}
}

View File

@@ -0,0 +1,46 @@
@file:Suppress("unused")
package org.autojs.build
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* A Gradle plugin that provides properties helpers.
*
* zh-CN: Gradle properties 辅助工具.
*
* - `id`: "org.autojs.build.properties"
* - `implementationClass`: "org.autojs.build.PropertiesPlugin"
* - `displayName`: "AutoJs6 Properties Plugin"
* - `description`: "Provides properties helpers."
*
* Apply this plugin to your Android module's `build.gradle.kts`:
*
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
*
* ```kts
* plugins {
* id("org.autojs.build.properties")
* }
*
* props["MIN_SDK"]
* props["COMPILE_SDK"]
* props["TARGET_SDK"]
*
* props["RAPID_OCR/NDK"]
* props["RAPID_OCR/CMAKE"]
*
* props["PADDLE_OCR/NDK"]
* props["PADDLE_OCR/CMAKE"]
* props["PADDLE_OCR/OPENCV"]
*
* props["IMAGE_QUANT/NDK"]
* props["IMAGE_QUANT/CMAKE"]
* ```
*/
class PropertiesPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.add("props", Utils.newProperties(project))
}
}

View File

@@ -0,0 +1,141 @@
package org.autojs.build
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry
import org.apache.commons.compress.archivers.sevenz.SevenZFile
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
object SevenZExtractor {
@JvmStatic
fun extractDirectoryFrom7z(
archive: File,
sourceDir: String,
outDir: File,
shouldPrintProgress: Boolean = true
): Long {
require(archive.isFile) { "7z archive not found: ${archive.absolutePath}" }
if (!outDir.exists()) outDir.mkdirs()
val sourceDirPath = normalizePrefix(sourceDir)
var sevenZFile: SevenZFile? = null
var totalBytes: Long
var writtenBytes = 0L
val buffer = ByteArray(64 * 1024)
try {
sevenZFile = SevenZFile.Builder().setFile(archive).get()
val allEntries: Iterable<SevenZArchiveEntry> = sevenZFile.entries
val targetEntries = allEntries.filter { e ->
val entryPath = e.name.replace('\\', '/')
entryPath.startsWith(sourceDirPath) ||
entryPath.startsWith(trimLeadingSlash(sourceDirPath))
}
totalBytes = targetEntries
.filter { !it.isDirectory && it.size >= 0 && it.hasStream() }
.sumOf { it.size }
val entriesCount = targetEntries.size
var processed = 0
for (entry in targetEntries) {
val entryPath = entry.name.replace('\\', '/')
var relative = when {
entryPath.startsWith(sourceDirPath) ->
entryPath.substring(sourceDirPath.length)
entryPath.startsWith(trimLeadingSlash(sourceDirPath)) ->
entryPath.substring(trimLeadingSlash(sourceDirPath).length)
else -> {
processed++
continue
}
}
// Remove leading separator to avoid being treated as absolute path.
// zh-CN: 去掉前导分隔符, 避免被当作绝对路径.
while (relative.startsWith("/") || relative.startsWith("\\")) {
relative = relative.substring(1)
}
if (relative.isEmpty()) {
processed++
continue
}
val outFile = File(outDir, relative)
if (entry.isDirectory) {
outFile.mkdirs()
} else {
if (!entry.hasStream()) {
processed++
continue
}
outFile.parentFile?.mkdirs()
var ins: InputStream? = null
var bos: BufferedOutputStream? = null
try {
ins = sevenZFile.getInputStream(entry)
bos = BufferedOutputStream(FileOutputStream(outFile))
while (true) {
val read = ins.read(buffer)
if (read == -1) break
bos.write(buffer, 0, read)
if (shouldPrintProgress && totalBytes > 0) {
writtenBytes += read
printProgress(writtenBytes, totalBytes)
}
}
bos.flush()
if (entry.size >= 0 && outFile.length() != entry.size) {
throw IllegalStateException(
"Extracted file size mismatch for: ${entry.name}, expected=${entry.size}, actual=${outFile.length()}"
)
}
} finally {
try { bos?.close() } catch (_: Throwable) {}
try { ins?.close() } catch (_: Throwable) {}
}
}
if (shouldPrintProgress && totalBytes == 0L) {
val pct = processed * 100.0 / entriesCount
printProgress(pct)
}
processed++
}
return totalBytes
} finally {
try { sevenZFile?.close() } catch (_: Throwable) {}
}
}
private fun normalizePrefix(path: String): String {
var p = File(path).path.replace('\\', '/')
if (p.startsWith("/")) p = p.substring(1)
if (!p.endsWith("/")) p += "/"
return p
}
private fun trimLeadingSlash(s: String): String =
if (s.startsWith("/")) s.substring(1) else s
private fun printProgress(written: Long, total: Long) {
val pct = if (total > 0) written * 100.0 / total else 0.0
printProgress(pct)
}
private fun printProgress(percent: Double) {
val width = 30
val filled = ((percent / 100.0) * width).toInt().coerceIn(0, width)
val bar = buildString {
append("[").append("#".repeat(filled)).append("-".repeat(width - filled)).append("]")
}
print("\rExtracting... $bar ${"%.2f".format(percent)}%")
System.out.flush()
}
}

View File

@@ -0,0 +1,19 @@
package org.autojs.build
import org.gradle.api.Project
import java.io.File
import java.util.*
class Signs @JvmOverloads constructor(project: Project, filePath: String = "${project.rootDir}/sign.properties") {
var isValid = false
private set
val properties = Properties().also { props ->
File(filePath).takeIf { it.exists() }?.let { file ->
file.inputStream().use { props.load(it) }
isValid = props.isNotEmpty()
}
}
}

View File

@@ -0,0 +1,45 @@
@file:Suppress("unused")
package org.autojs.build
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* A Gradle plugin that provides signing functionality.
*
* zh-CN: Gradle 签名功能插件.
*
* - `id`: "org.autojs.build.signs"
* - `implementationClass`: "org.autojs.build.SignsPlugin"
* - `displayName`: "AutoJs6 Signs Plugin"
* - `description`: "Provides signing helpers."
*
* Apply this plugin to your Android module's `build.gradle.kts`:
*
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
*
* ```kts
* plugins {
* id("org.autojs.build.signs")
* }
*
* android {
* signingConfigs {
* if (signs.isValid) {
* create(buildTypeRelease) {
* storeFile = signs.properties["storeFile"]?.let { file(it as String) }
* keyPassword = signs.properties["keyPassword"] as String
* keyAlias = signs.properties["keyAlias"] as String
* storePassword = signs.properties["storePassword"] as String
* }
* }
* }
* }
* ```
*/
class SignsPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.add("signs", Utils.newSigns(project))
}
}

View File

@@ -0,0 +1,408 @@
package org.autojs.build
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.file.CopySpec
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.jvm.toolchain.JavaLanguageVersion
import org.gradle.jvm.toolchain.JavaToolchainService
import org.gradle.kotlin.dsl.extra
import java.io.File
import java.io.FileInputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.zip.CRC32
object Utils {
const val FILE_EXTENSION_APK = "apk"
private object LOGGER {
const val LEVEL_INFO = 1
const val LEVEL_WARN = 2
const val LEVEL_ERROR = 3
}
private var CURRENT_LOGGER_LEVEL = LOGGER.LEVEL_ERROR
fun newLibDeployer(project: Project, name: String, downloadUrl: String) = LibDeployer(project, name, downloadUrl)
@JvmOverloads
fun newFormatted(title: String, contents: Collection<String> = emptyList(), subtitle: String? = null) = Formatted(title, contents, subtitle)
fun newVersions(project: Project) = Versions(project)
fun newSigns(project: Project) = Signs(project)
fun newProperties(project: Project) = BuildProperties.loadFrom(project)
fun hours2Millis(hour: Double) = hour * 3.6e6
fun getDateString(format: String, zone: String): String {
// e.g. May 23, 2011
return SimpleDateFormat(format, Locale.getDefault()).apply {
timeZone = TimeZone.getTimeZone(zone)
}.format(Date())
}
fun getAssembleTaskName(flavorName: String, buildType: String) = "assemble${capitalize(flavorName)}${capitalize(buildType)}"
fun getAssembleFullTaskName(projectName: String, flavorName: String, buildType: String) = ":$projectName:${getAssembleTaskName(flavorName, buildType)}"
fun digestCRC32(file: File): String {
val fis = FileInputStream(file)
val buffer = ByteArray(4096)
var read: Int
return CRC32().let { o ->
while (fis.read(buffer).also { read = it } > 0) {
o.update(buffer, 0, read)
}
String.format("%08x", o.value)
}
}
/**
* 统一 "版本信息打印 + 部署 + 清理" 生命周期钩子.
*
* @param project 当前模块 Project
* @param projectDisplayName 用于打印的项目展示名 (默认用模块名)
* @param versionLines 版本信息行 (如 ["OpenCV: 4.2.0", "NDK: 21.1.6352462"])
* @param libsToDeploy 需要部署/清理的 LibDeployer 列表
* @param cleanupFlagKey gradle.ext 的布尔开关键 (如 "isCleanupPaddleOcr"), null 表示不参与清理逻辑
* @param extraFilesToDeleteOnClean clean 时额外需删除的相对路径
*/
@JvmOverloads
fun configureLibraryLifecycleHooks(
project: Project,
projectDisplayName: String = project.name,
versionLines: List<String> = emptyList(),
libsToDeploy: List<LibDeployer> = emptyList(),
cleanupFlagKey: String? = null,
extraFilesToDeleteOnClean: List<String> = listOf(".cxx"),
) {
val gradle = project.gradle
val onlyClean = AtomicBoolean(false)
// 单一监听器: 既判断 "是否纯 clean", 也负责非 clean 流程的打印与部署
gradle.taskGraph.addTaskExecutionGraphListener { graph ->
val all = graph.allTasks
val isOnlyClean = all.isNotEmpty() && all.all { it.name.contains("clean", ignoreCase = true) }
onlyClean.set(isOnlyClean)
if (!isOnlyClean) {
if (versionLines.isNotEmpty()) {
newFormatted("Version information for $projectDisplayName library", versionLines).print()
}
if (libsToDeploy.isNotEmpty()) {
libsToDeploy.forEach { it.deploy() }
}
}
}
// clean 钩子 (显式 Java SAM, 避免 Kotlin/Groovy 重载歧义)
@Suppress("ObjectLiteralToLambda")
project.tasks.named("clean").configure(object : Action<Task> {
override fun execute(cleanTask: Task) {
cleanTask.doFirst {
project.delete(project.layout.buildDirectory)
extraFilesToDeleteOnClean.forEach { rel ->
project.delete(project.file(rel))
}
// 未提供开关键则直接跳过清理逻辑
val key = cleanupFlagKey ?: return@doFirst
val cleanupEnabled = gradle.extra.require<Boolean>(key)
if (cleanupEnabled && onlyClean.get()) {
libsToDeploy.forEach { it.clean() }
} else {
val projectName = project.extensions.extraProperties.getOrNull<String>("projectName") ?: projectDisplayName
println("The library files of $projectName won't be cleaned up due to the configuration")
}
}
}
})
}
/**
* 注册模板 APK 拷贝: 在指定 assemble 任务完成后, 将 universal APK 拷贝为 assets 模板.
*
* @param project 当前模块 Project
* @param taskName 构建任务名 (如 "assembleInrtRelease")
* @param srcDir APK 输出目录 (如 "build/outputs/apk/inrt/release")
* @param destDir 目标目录 (如 "src/main/assets-app")
* @param templateApkName 模板文件名 (如 "template.apk")
* @param universalNameFn 从版本名得出源 APK 名的函数 (默认 inrt-v<ver>-universal.apk)
*/
@JvmOverloads
fun registerTemplateApkCopy(
project: Project,
taskName: String = "assembleInrtRelease",
srcDir: String = "build/outputs/apk/inrt/release",
destDir: String = "src/main/assets-app",
templateApkName: String = "template.$FILE_EXTENSION_APK",
universalNameFn: (String) -> String = { ver -> "inrt-v${ver.replace(Regex("\\s"), "-").lowercase()}-universal.$FILE_EXTENSION_APK" },
) {
val versions = newVersions(project)
val versionName = versions.appVersionName
// 待所有项目评估完成后再定位并配置任务, 避免早期查找不到任务
project.gradle.projectsEvaluated {
val assembleTask = project.tasks.findByName(taskName)
if (assembleTask == null) {
println("$taskName doesn't exist in project ${project.name}")
return@projectsEvaluated
}
assembleTask.doLast {
@Suppress("ObjectLiteralToLambda")
project.copy(object : Action<CopySpec> {
override fun execute(spec: CopySpec) {
val srcFileName = universalNameFn(versionName)
val srcFile = project.file(File(srcDir, srcFileName))
require(srcFile.exists()) {
"Source file \"$srcFile\" doesn't exist"
}
spec.from(srcDir)
spec.into(destDir)
spec.include(srcFileName)
spec.rename(srcFileName, templateApkName)
val dstFile = project.file(File(destDir, templateApkName))
val overridden = dstFile.exists()
newFormatted(
"Copy template APK into assets", listOf(
"Source: $srcFile",
"Destination: $dstFile${if (overridden) " [overridden]" else ""}"
)
).print()
}
})
}
}
}
inline fun <reified T> org.gradle.api.plugins.ExtraPropertiesExtension.getOrNull(key: String): T? {
if (!has(key)) return null
val result = get(key)
require(result is T?) {
"The type of $key is ${result?.javaClass?.name}, but ${T::class.java.name} is required"
}
return result
}
inline fun <reified T> org.gradle.api.plugins.ExtraPropertiesExtension.require(key: String): T {
require(has(key)) {
"The key $key is not found in extra properties"
}
val result = get(key)
require(result is T) {
"The type of $key is ${result?.javaClass?.name}, but ${T::class.java.name} is required"
}
return result
}
private fun capitalize(s: String) = "${s[0].uppercase(Locale.getDefault())}${s.substring(1)}"
/**
* 统一为 Android 模块配置 Java/Kotlin 的目标版本:
* - Android.compileOptions.sourceCompatibility/targetCompatibility = versions.javaVersion
* - Kotlin 编译任务的 jvmTarget = versions.javaVersion 对应级别
*
* 注意:
* - Android 扩展在 projectsEvaluated 后稳定可得
* - Kotlin 任务用 tasks.configureEach 动态配置, 任务何时创建都能命中
*/
@JvmStatic
fun configureJvmForAndroidModule(project: Project) {
val versions = newVersions(project)
project.logInfo("[JvmConv] Enter configureJvmForAndroidModule for module='${project.path}', javaVersion='${versions.javaVersion}', javaVersionString='${versions.javaVersionString}'")
val installer = {
project.logInfo("[JvmConv] Detected Android plugin in module='${project.path}', installing configuration")
// A) Java: 使用 Toolchain (模块级 + 任务级), 不要设置 --release
configureJavaToolchainLanguageLevel(project, versions.javaVersionInt)
configureJavaToolchainForAllJavaCompile(project, versions.javaVersionInt)
// B) Kotlin: 继续懒配置设置 jvmTarget (你之前已验证成功)
configureKotlinJvmTargetLazily(project, versions)
project.logInfo("[JvmConv] Installed Java toolchain (module+tasks) and Kotlin jvmTarget for '${project.path}'")
}
project.plugins.withId("com.android.application") { installer() }
project.plugins.withId("com.android.library") { installer() }
}
// 模块级 Toolchain: 让 AGP/Gradle 知道本模块应使用的 JDK 语言级别
private fun configureJavaToolchainLanguageLevel(project: Project, target: Int) {
val javaExt = project.extensions.findByType(JavaPluginExtension::class.java)
if (javaExt == null) {
project.logWarn("[JvmConv] JavaPluginExtension not found in '${project.path}', skip module-level toolchain")
return
}
runCatching {
javaExt.toolchain.languageVersion.set(JavaLanguageVersion.of(target))
}.onSuccess {
project.logInfo("[JvmConv] Module-level toolchain languageVersion set to $target for '${project.path}'")
}.onFailure {
project.logError("[JvmConv] Set module-level toolchain languageVersion failed on '${project.path}': ${it.message}", it)
}
}
// 任务级 Toolchain: 对所有 JavaCompile 指定 javaCompiler, 且不要设置 --release (AGP 禁止)
private fun configureJavaToolchainForAllJavaCompile(project: Project, target: Int) {
val toolchains = runCatching {
project.extensions.getByType(JavaToolchainService::class.java)
}.onFailure {
project.logError("[JvmConv] JavaToolchainService not available on '${project.path}': ${it.message}", it)
}.getOrNull() ?: return
val langVersion = JavaLanguageVersion.of(target)
project.tasks.withType(JavaCompile::class.java).configureEach(object : Action<JavaCompile> {
override fun execute(t: JavaCompile) {
runCatching {
val compilerProvider = toolchains.compilerFor { languageVersion.set(langVersion) }
t.javaCompiler.set(compilerProvider)
project.logInfo("[JvmConv] JavaCompile '${t.path}' uses toolchain JDK $target (no --release)")
}.onFailure {
project.logError("[JvmConv] '${t.path}' set javaCompiler(toolchain) failed: ${it.message}", it)
}
// 不要设置 t.options.release, AGP 会报错阻止
// 也不强制改写 sourceCompatibility/targetCompatibility, 交给 AGP + toolchain 统一管理
}
})
}
// 为 KotlinCompile 任务设置 jvmTarget: 使用 configureEach, 任务实现时自动应用; 兼容新旧 API
private fun configureKotlinJvmTargetLazily(project: Project, versions: Versions) {
val desiredStr = versions.javaVersionString // 例如 "22"
val desiredEnumName = "JVM_${desiredStr}" // 例如 "JVM_22"
project.logInfo("[JvmConv] Will configure Kotlin jvmTarget lazily to '$desiredEnumName' in '${project.path}'")
project.tasks.configureEach(object : Action<Task> {
override fun execute(task: Task) {
if (!isKotlinCompileTask(task, project)) return
project.logInfo("[JvmConv] <KotlinTask> '${task.path}' class='${task.javaClass.name}'")
// 优先尝试 Kotlin 2.x: compilerOptions.jvmTarget(Property<JvmTarget>)
val compilerOptions = runCatching {
task.javaClass.methods.firstOrNull { it.name == "getCompilerOptions" && it.parameterTypes.isEmpty() }
?.also { project.logInfo("[JvmConv] '${task.path}' found method: ${it.toGenericString()}") }
?.invoke(task)
}.onFailure {
project.logError("[JvmConv] '${task.path}' getCompilerOptions() failed", it)
}.getOrNull()
if (compilerOptions != null) {
project.logInfo("[JvmConv] '${task.path}' compilerOptions class='${compilerOptions.javaClass.name}'")
val jvmTargetProp = runCatching {
compilerOptions.javaClass.methods.firstOrNull { it.name == "getJvmTarget" && it.parameterTypes.isEmpty() }
?.also { project.logInfo("[JvmConv] '${task.path}' found method: ${it.toGenericString()}") }
?.invoke(compilerOptions)
}.onFailure {
project.logError("[JvmConv] '${task.path}' compilerOptions.getJvmTarget() failed", it)
}.getOrNull()
if (jvmTargetProp != null) {
project.logInfo("[JvmConv] '${task.path}' jvmTarget property class='${jvmTargetProp.javaClass.name}'")
val propSet = jvmTargetProp.javaClass.methods.firstOrNull {
it.name == "set" && it.parameterTypes.size == 1
}
project.logInfo("[JvmConv] '${task.path}' Property.set method: ${propSet?.toGenericString() ?: "NOT_FOUND"}")
val jvmTargetEnum = runCatching {
val enumClass = Class.forName("org.jetbrains.kotlin.gradle.dsl.JvmTarget")
enumClass.enumConstants?.firstOrNull { it.toString().equals(desiredEnumName, ignoreCase = true) }
?.also { project.logInfo("[JvmConv] '${task.path}' resolved enum '$desiredEnumName' = $it") }
}.onFailure {
project.logError("[JvmConv] '${task.path}' resolve enum '$desiredEnumName' failed", it)
}.getOrNull()
if (propSet != null && jvmTargetEnum != null) {
runCatching { propSet.invoke(jvmTargetProp, jvmTargetEnum) }
.onSuccess {
project.logInfo("[JvmConv] '${task.path}' jvmTarget set via compilerOptions to '$desiredEnumName'")
return
}
.onFailure {
project.logWarn("[JvmConv] '${task.path}' jvmTarget set via compilerOptions failed, will try legacy API.\ne: $it")
}
} else {
project.logWarn("[JvmConv] '${task.path}' compilerOptions path unavailable (propSet=$propSet, enum=$jvmTargetEnum), trying legacy kotlinOptions")
}
} else {
project.logWarn("[JvmConv] '${task.path}' compilerOptions.getJvmTarget() returned null, trying legacy kotlinOptions")
}
} else {
project.logWarn("[JvmConv] '${task.path}' compilerOptions not found, trying legacy kotlinOptions")
}
// 兼容旧 API: kotlinOptions.setJvmTarget(String)
val kotlinOptions = runCatching {
task.javaClass.methods.firstOrNull { it.name == "getKotlinOptions" && it.parameterTypes.isEmpty() }
?.also { project.logInfo("[JvmConv] '${task.path}' found method: ${it.toGenericString()}") }
?.invoke(task)
}.onFailure {
project.logError("[JvmConv] '${task.path}' getKotlinOptions() failed", it)
}.getOrNull()
if (kotlinOptions != null) {
project.logInfo("[JvmConv] '${task.path}' kotlinOptions class='${kotlinOptions.javaClass.name}'")
val setJvmTarget = kotlinOptions.javaClass.methods.firstOrNull {
it.name == "setJvmTarget" && it.parameterTypes.size == 1 && it.parameterTypes[0] == String::class.java
}
project.logInfo("[JvmConv] '${task.path}' kotlinOptions.setJvmTarget method: ${setJvmTarget?.toGenericString() ?: "NOT_FOUND"}")
runCatching { setJvmTarget?.invoke(kotlinOptions, desiredStr) }
.onSuccess { project.logInfo("[JvmConv] '${task.path}' jvmTarget set via kotlinOptions to '$desiredStr'") }
.onFailure { project.logError("[JvmConv] '${task.path}' jvmTarget set via kotlinOptions failed", it) }
} else {
project.logWarn("[JvmConv] '${task.path}' kotlinOptions not found; jvmTarget not configured")
}
}
})
}
private fun isKotlinCompileTask(task: Task, project: Project): Boolean {
val name = task.name.lowercase(Locale.getDefault())
val clsName = task.javaClass.name
val hasCompilerOptions = task.javaClass.methods.any { it.name == "getCompilerOptions" && it.parameterTypes.isEmpty() }
val hasKotlinOptions = task.javaClass.methods.any { it.name == "getKotlinOptions" && it.parameterTypes.isEmpty() }
val matched = when {
name.contains("kotlin") && name.contains("compile") -> true
clsName.contains("Kotlin", ignoreCase = true) && clsName.contains("Compile", ignoreCase = true) -> true
hasCompilerOptions || hasKotlinOptions -> true
else -> false
}
if (matched) {
project.logInfo("[JvmConv] Task matched as KotlinCompile: path='${task.path}', class='$clsName'")
}
return matched
}
private fun Project.logInfo(msg: String) {
if (CURRENT_LOGGER_LEVEL <= LOGGER.LEVEL_INFO) {
logger.lifecycle(msg)
}
}
private fun Project.logWarn(msg: String) {
if (CURRENT_LOGGER_LEVEL <= LOGGER.LEVEL_WARN) {
logger.warn(msg)
}
}
private fun Project.logError(msg: String, t: Throwable? = null) {
if (CURRENT_LOGGER_LEVEL <= LOGGER.LEVEL_ERROR) {
if (t != null) logger.error(msg, t) else logger.error(msg)
}
}
}

View File

@@ -0,0 +1,40 @@
@file:Suppress("unused")
package org.autojs.build
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* A Gradle plugin that provides basic utilities.
*
* zh-CN: Gradle 基础工具插件.
*
* - `id`: "org.autojs.build.utils"
* - `implementationClass`: "org.autojs.build.UtilsPlugin"
* - `displayName`: "AutoJs6 Build Utils Plugin"
* - `description`: "Provides utilities for downloading, extracting archives, and version helpers."
*
* Apply this plugin to your Android module's `build.gradle.kts`:
*
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
*
* ```kts
* plugins {
* id("org.autojs.build.utils")
* }
*
* utils.digestCRC32(file("some/file.zip"))
* utils.getDateString("MMM d, yyyy", "GMT+08:00")
* utils.hours2Millis(0.75)
* utils.compareVersionStrings("6.3.2 beta", "6.3.2 alpha4) > 0
*
* utils.registerTemplateApkCopy(project)
* ```
*/
class UtilsPlugin : Plugin<Project> {
override fun apply(project: Project) {
// project.extensions.extraProperties.set("utils", Utils)
project.extensions.add("utils", Utils)
}
}

View File

@@ -0,0 +1,210 @@
package org.autojs.build
import org.autojs.build.Utils.getOrNull
import org.gradle.api.Action
import org.gradle.api.GradleException
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.kotlin.dsl.extra
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.*
import kotlin.properties.Delegates
import kotlin.text.RegexOption.IGNORE_CASE
class Versions @JvmOverloads constructor(
private val project: Project,
filePath: String = "${project.rootDir}/version.properties",
) {
private val gradle = project.gradle
private val logger = project.logger
private val currentVersionInt = JavaVersion.current().majorVersion.toInt()
private val bp = BuildProperties.loadFrom(filePath)
private val javaVersionMinSuggested: Int = bp.requireInt("JAVA_VERSION_MIN_SUGGESTED")
private val javaVersionMaxSupported: Int = bp.requireInt("JAVA_VERSION_MAX_SUPPORTED")
private var isBuildNumberAutoIncremented = false
private val minBuildTimeGap = Utils.hours2Millis(0.75)
private val isBuildGapEnough
get() = Date().time - bp["BUILD_TIME"].toLong() > minBuildTimeGap
val sdkVersionMin = bp.requireInt("MIN_SDK_VERSION")
val sdkVersionTarget = bp.requireInt("TARGET_SDK_VERSION")
val sdkVersionTargetInrt = bp.requireInt("TARGET_SDK_VERSION_INRT")
val sdkVersionCompile = bp.requireInt("COMPILE_SDK_VERSION")
val appVersionName = bp.requireString("VERSION_NAME")
val appVersionCode = bp.requireInt("VERSION_BUILD")
val vscodeExtRequiredVersion = bp.requireString("VSCODE_EXT_REQUIRED_VERSION")
private val javaVersionMinSupported: Int = bp.requireInt("JAVA_VERSION_MIN_SUPPORTED")
var javaVersionInt by Delegates.notNull<Int>()
var javaVersionInfoSuffix by Delegates.notNull<String>()
val javaVersion: JavaVersion
get() = JavaVersion.toVersion(javaVersionInt)
val javaVersionString: String
get() = javaVersion.toString()
init {
validateCurrentVersion()
determineJavaVersion().also { (javaVersionInt, javaVersionInfoSuffix) ->
this.javaVersionInt = javaVersionInt
this.javaVersionInfoSuffix = javaVersionInfoSuffix
}
}
private fun validateCurrentVersion() {
if (gradle.extra.getOrNull<Boolean>(VALIDATED_EXTRA_KEY) == true) {
return
}
if (currentVersionInt < javaVersionMinSupported) {
throw GradleException("Current Gradle JDK version [$currentVersionInt] does not meet the minimum requirement which [$javaVersionMinSupported] is needed.")
}
if (currentVersionInt < javaVersionMinSuggested) {
val suffix = if (javaVersionMaxSupported > 0) " (but not higher than [$javaVersionMaxSupported])" else ""
logger.error("It is recommended to upgrade current Gradle JDK version [$currentVersionInt] to [$javaVersionMinSuggested] or higher$suffix.")
}
if (currentVersionInt > javaVersionMaxSupported) {
val suffix = if (javaVersionMaxSupported > javaVersionMinSuggested) " or lower (but not lower than [$javaVersionMinSuggested])" else ""
logger.error("It is recommended to downgrade current Gradle JDK version [$currentVersionInt] to [$javaVersionMaxSupported]$suffix, as Gradle may be not compatible with JDK [$currentVersionInt] for now.")
}
gradle.extra.set(VALIDATED_EXTRA_KEY, true)
}
private fun determineJavaVersion(): Pair<Int, String> {
var javaVersionInfoSuffix = ""
gradle.extra.getOrNull<Int>("javaVersionOverriddenByUser")?.let {
javaVersionInfoSuffix += " [user-specified]"
return it to javaVersionInfoSuffix
}
var versionInt = JavaVersion.current().majorVersion.toInt()
run tryAdjustJavaVersionByKotlinJvmTarget@{
var isJvmCoercive = false
while (versionInt > javaVersionMinSupported) {
if (isJvmTargetAvailable(versionInt)) {
break
}
versionInt -= 1
isJvmCoercive = true
}
if (isJvmCoercive) {
javaVersionInfoSuffix += " [coercive-jvm-downgraded]"
}
}
gradle.extra.getOrNull<Int>("javaVersionCoercedByGradle")?.let {
if (versionInt > it) {
versionInt = it
javaVersionInfoSuffix += " [coercive-gradle-downgraded]"
}
}
return versionInt to javaVersionInfoSuffix
}
private fun isJvmTargetAvailable(target: Int): Boolean {
try {
// Weak dependency with Kotlin plugin: Use reflection to check if JvmTarget is available;
// if reflection fails, treat it as available (to avoid incorrect downgrading).
// zh-CN: 与 Kotlin 插件弱依赖: 反射判断 JvmTarget 是否可用; 反射失败则视为可用 (避免误降级).
val cls = Class.forName("org.jetbrains.kotlin.gradle.dsl.JvmTarget")
cls.enumConstants?.let { values ->
return values.any { it?.toString().equals("JVM_$target", true) }
}
} catch (e: Throwable) {
logger.error("Failed to check JVM target availability: $e")
}
return true
}
operator fun get(propertyName: String) = bp[propertyName]
operator fun get(propertyInfo: List<String>) = bp[propertyInfo]
fun showInfo() {
val title = "Version information for AutoJs6 app library"
val infoVerName = "Version name: $appVersionName"
val infoVerCode = "Version code: ${if (isBuildNumberAutoIncremented) "${appVersionCode + 1} [auto-incremented]" else appVersionCode}"
val infoVerSdk = "SDK versions: min [$sdkVersionMin] / target [$sdkVersionTarget] / compile [$sdkVersionCompile]"
val infoVerJava = "Java version: $javaVersion${
when {
gradle.extra.getOrNull<Boolean>("isHideConsoleInfoHintSuffix") == true -> ""
else -> javaVersionInfoSuffix
}
}"
val maxLength = arrayOf(title, infoVerName, infoVerCode, infoVerSdk, infoVerJava).maxOf { it.length }
arrayOf(
"=".repeat(maxLength),
title,
"-".repeat(maxLength),
infoVerName,
infoVerCode,
infoVerSdk,
infoVerJava,
"=".repeat(maxLength),
"",
).forEach { println(it) }
}
fun handleIfNeeded(project: Project, flavorName: String, targetBuildType: List<String>) {
project.gradle.taskGraph.whenReady(object : Action<TaskExecutionGraph> {
override fun execute(taskGraph: TaskExecutionGraph) {
for (buildType in targetBuildType) {
if (taskGraph.hasTask(Utils.getAssembleFullTaskName(project.name, flavorName, buildType))) {
return appendToTask(project, flavorName, buildType)
}
}
return showInfo()
}
})
}
private fun appendToTask(project: Project, flavorName: String, buildType: String) {
project.tasks.getByName(Utils.getAssembleTaskName(flavorName, buildType)).doLast {
updateProperties()
println()
showInfo()
}
}
private fun updateProperties() {
val propsPath = bp.path
val props = Properties().apply {
FileInputStream(propsPath).use { load(it) }
}
if (isBuildGapEnough) {
val isBuildAppRelease = gradle.startParameter.taskNames.any {
it.contains(Regex("^(:?app:)?assemble(app|inrt)release", IGNORE_CASE))
}
if (!isBuildAppRelease) {
props["VERSION_BUILD"] = "${appVersionCode + 1}"
isBuildNumberAutoIncremented = true
}
}
props["BUILD_TIME"] = "${Date().time}"
FileOutputStream(propsPath).use { out ->
props.store(out, null)
}
}
companion object {
private const val VALIDATED_EXTRA_KEY = "org.autojs.build.Versions.currentValidated"
}
}

View File

@@ -0,0 +1,40 @@
@file:Suppress("unused")
package org.autojs.build
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* A Gradle plugin that provides version management functionality.
*
* zh-CN: Gradle 版本管理插件.
*
* - `id`: "org.autojs.build.versions"
* - `implementationClass`: "org.autojs.build.VersionsPlugin"
* - `displayName`: "AutoJs6 Versions Plugin"
* - `description`: "Provides version helpers."
*
* Apply this plugin to your Android module's `build.gradle.kts`:
*
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
*
* ```kts
* plugins {
* id("org.autojs.build.versions")
* }
*
* versions.appVersionName
* versions.appVersionCode
* versions.sdkVersionCompile
* versions.sdkVersionMin
* versions.sdkVersionTarget
* versions.sdkVersionTargetInrt
* versions.vscodeExtRequiredVersion
* ```
*/
class VersionsPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.extensions.add("versions", Utils.newVersions(project))
}
}