6.7.0 - Alpha6 - Gradle 脚本由 buildSrc 迁移至 build-logic 并启用类型安全访问器
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
408
build-logic/convention/src/main/kotlin/org/autojs/build/Utils.kt
Normal file
408
build-logic/convention/src/main/kotlin/org/autojs/build/Utils.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user