6.7.0 - Alpha6 - 模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件

This commit is contained in:
SuperMonster003
2025-09-24 16:31:06 +08:00
parent 038f4a5ad9
commit 4778794e0e
31 changed files with 1893 additions and 1235 deletions

View File

@@ -36,7 +36,8 @@
"崩溃报告页面支持双指缩放调整字体大小并添加常用功能按钮",
"应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_",
"Gradle 构建脚本提升 7z 格式文件的解压效率",
"使用版本目录 (Version Catalogs) 集中管理 Gradle 依赖和插件版本"
"使用版本目录 (Version Catalogs) 集中管理 Gradle 依赖和插件版本",
"模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件"
],
"dependency": [
"本地化 Root Shell 版本 1.6",

View File

@@ -3,18 +3,19 @@
import com.android.build.gradle.internal.api.ApplicationVariantImpl
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.CRC32
import java.util.Locale.getDefault
import kotlin.text.RegexOption.IGNORE_CASE
val globalApplicationId = "org.autojs.autojs6"
plugins {
id("org.autojs.build.utils")
id("org.autojs.build.versions")
id("org.autojs.build.signs")
id("com.android.application")
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.android") /* kotlin("android") */
}
val sign = Sign("$rootDir/sign.properties")
val versions = Versions("$rootDir/version.properties")
val globalApplicationId = "org.autojs.autojs6"
val flavorDimension = "channel"
val flavorNameApp = "app"
@@ -22,18 +23,13 @@ val flavorNameInrt = "inrt"
val buildTypeDebug = "debug"
val buildTypeRelease = "release"
val buildActionAssemble = "assemble"
val templateName = "template"
val taskNames = gradle.startParameter.taskNames
val isAppAssembleTaskRequested = taskNames.any { it.contains(Regex("^(:?$flavorNameApp:)?$buildActionAssemble", IGNORE_CASE)) }
val isInrtAssembleTaskRequested = taskNames.any { it.contains(Regex("^(:?$flavorNameApp:)?$buildActionAssemble$flavorNameInrt", IGNORE_CASE)) }
val isInrtTaskRequested = taskNames.any { it.contains(flavorNameInrt, true) }
plugins {
id("com.android.application")
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.android") /* kotlin("android") */
}
utils.registerTemplateApkCopy(project)
dependencies /* Unclassified */ {
// Compose
@@ -52,67 +48,66 @@ dependencies /* Unclassified */ {
}
// LeakCanary
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
debugImplementation(libs.leakcanary)
// Android supports
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.multidex:multidex:2.0.1")
implementation(libs.cardview)
implementation(libs.multidex)
// Material Components
implementation("com.google.android.material:material:1.12.0")
implementation(libs.material)
// SwipeRefreshLayout
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation(libs.swiperefreshlayout)
// ConstraintLayout
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation(libs.constraintlayout)
// FlexboxLayout
implementation("com.google.android.flexbox:flexbox:3.0.0")
implementation(libs.flexbox)
// Common Markdown
implementation("com.github.atlassian:commonmark-java:commonmark-parent-0.9.0")
implementation(libs.commonmark)
// Flexmark Java HTML to Markdown Extensible Converter
implementation("com.vladsch.flexmark:flexmark-html2md-converter:0.64.8")
implementation(libs.flexmark.html2md)
// Licenses Dialog
implementation("de.psdev.licensesdialog:licensesdialog:2.2.0")
implementation(libs.licensesdialog)
// Apache Commons
implementation("org.apache.commons:commons-lang3:3.18.0")
implementation(libs.commons.lang3)
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.12.0")
implementation("com.squareup.retrofit2:converter-gson:2.12.0")
implementation("com.squareup.retrofit2:adapter-rxjava2:2.12.0")
implementation("com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2")
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.retrofit.adapter.rxjava2)
implementation(libs.retrofit2.kotlin.coroutines.adapter)
// Glide
implementation("com.github.bumptech.glide:glide:4.16.0")
ksp("com.github.bumptech.glide:ksp:4.16.0")
implementation(libs.glide)
ksp(libs.glide.ksp)
// Joda Time
implementation("joda-time:joda-time:2.14.0")
implementation(libs.joda.time)
// Flurry
implementation("com.flurry.android:analytics:14.4.0")
// Analytics
implementation(libs.analytics)
// Bugly
implementation(project(":libs:com.tencent.bugly.crashreport-4.0.4"))
// OkHttp
// implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.12")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation(libs.okhttp)
// Webkit
implementation("androidx.webkit:webkit:1.14.0")
implementation(libs.webkit)
// Gson
implementation("com.google.code.gson:gson:2.13.1")
implementation(libs.gson)
// Zip4j
implementation("net.lingala.zip4j:zip4j:2.11.5")
implementation(libs.zip4j)
// Log4j
// FIXME by SuperMonster003 on Aug 14, 2024.
@@ -135,30 +130,30 @@ dependencies /* Unclassified */ {
// ! - CVE-2019-17571, 评分: 9.8
// ! 但 log4j 第二版本要求安卓 API 级别不低于 26,
// ! 与最低 API 级别为 24 的当前项目无法兼容.
implementation("log4j:log4j:1.2.17")
implementation(libs.log4j)
// Android Logging Log4j
implementation("de.mindpipe.android:android-logging-log4j:1.0.3")
implementation(libs.android.logging.log4j)
// Preference
implementation("androidx.preference:preference-ktx:1.2.1")
implementation(libs.preference.ktx)
// RootShell
// implementation("com.github.Stericson:RootShell:1.6")
implementation(project(":libs:root-shell-1.6"))
// JDeferred
implementation("org.jdeferred:jdeferred-android-aar:1.2.6")
implementation(libs.jdeferred)
// Rx
implementation("io.reactivex.rxjava2:rxjava:2.2.21")
implementation("io.reactivex.rxjava2:rxandroid:2.1.1@aar")
implementation(libs.rxjava)
implementation(libs.rxandroid)
// Device Names
implementation("com.jaredrummler:android-device-names:2.1.1")
implementation(libs.android.device.names)
// Version Compare
implementation("io.github.g00fy2:versioncompare:1.5.0")
implementation(libs.versioncompare)
// Terminal Emulator
implementation(project(":libs:jackpal.androidterm.libtermexec-1.0"))
@@ -209,18 +204,18 @@ dependencies /* Unclassified */ {
implementation(files("$rootDir/libs/javamail-android/mail.jar"))
// Shizuku
implementation("dev.rikka.shizuku:api:13.1.5")
implementation("dev.rikka.shizuku:provider:13.1.5")
implementation(libs.shizuku.api)
implementation(libs.shizuku.provider)
// ARSCLib
implementation("io.github.reandroid:ARSCLib:1.3.5")
implementation(libs.arsclib)
// Toaster
implementation("com.github.getActivity:Toaster:12.6")
implementation("com.github.getActivity:EasyWindow:10.3")
implementation(libs.toaster)
implementation(libs.easywindow)
// Pinyin4j
implementation("com.belerweb:pinyin4j:2.5.1")
implementation(libs.pinyin4j)
// Jieba Analysis (zh-CN: 结巴分词)
// implementation("com.huaban:jieba-analysis:1.0.2")
@@ -230,9 +225,9 @@ dependencies /* Unclassified */ {
implementation(files("$rootDir/libs/tiny-sign-0.9.jar"))
// Room
implementation("androidx.room:room-runtime:2.7.2")
implementation("androidx.room:room-ktx:2.7.2")
ksp("androidx.room:room-compiler:2.7.2")
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
// ApkSig
// implementation("com.android.tools.build:apksig:8.7.3")
@@ -240,21 +235,21 @@ dependencies /* Unclassified */ {
// ApkSigner
implementation(project(":modules:apk-signer"))
// Spongy Castle
implementation("com.madgag.spongycastle:prov:1.58.0.0")
// Prov
implementation(libs.prov)
// MQTT
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0")
implementation("org.eclipse.paho:org.eclipse.paho.android.service:1.1.1")
implementation(libs.paho.client.mqttv3)
implementation(libs.paho.android.service)
// Jsoup
implementation("org.jsoup:jsoup:1.20.1")
implementation(libs.jsoup)
// Material Date Time Picker
implementation(project(":modules:material-date-time-picker"))
// ICU4J
implementation("com.ibm.icu:icu4j:77.1")
implementation(libs.icu4j)
}
dependencies /* MIME */ {
@@ -270,22 +265,22 @@ dependencies /* MIME */ {
}
dependencies /* Test */ {
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("org.junit.jupiter:junit-jupiter:5.13.0")
testImplementation(libs.junit)
androidTestImplementation(libs.test.runner)
androidTestImplementation(libs.junit.jupiter)
}
dependencies /* Annotations */ {
// Android Annotations
implementation("org.androidannotations:androidannotations-api:4.8.0")
implementation("androidx.annotation:annotation:1.9.1")
ksp("org.androidannotations:androidannotations:4.8.0")
implementation(libs.androidannotations.api)
implementation(libs.annotation)
ksp(libs.androidannotations)
// JCIP Annotations
implementation("net.jcip:jcip-annotations:1.0")
implementation(libs.jcip.annotations)
// EventBus
implementation("org.greenrobot:eventbus:3.3.1")
implementation(libs.eventbus)
}
dependencies /* AppCompat */ {
@@ -295,7 +290,7 @@ dependencies /* AppCompat */ {
// ! zh-CN:
// ! 查看 Appcompat 库的发行版本,
// ! 可访问 https://developer.android.com/jetpack/androidx/releases/appcompat.
implementation("androidx.appcompat:appcompat:1.7.1")
implementation(libs.appcompat)
// AppCompat for legacy views (such as JsTextViewLegacy)
implementation(project(":libs:androidx.appcompat-1.0.2")) {
@@ -319,7 +314,7 @@ dependencies /* Material Dialogs */ {
// # implementation("com.afollestad.material-dialogs:commons", cfg)
// # }
implementation(project(":modules:material-dialogs"))
implementation("me.zhanghai.android.materialprogressbar:library:1.6.1")
implementation(libs.materialprogressbar)
}
dependencies /* Layout */ {
@@ -328,7 +323,7 @@ dependencies /* Layout */ {
implementation(project(":libs:expandable-layout-1.6.0"))
// Expandable RecyclerView
implementation("com.bignerdranch.android:expandablerecyclerview:3.0.0-RC1")
implementation(libs.expandablerecyclerview)
// Flexible Divider
// implementation("com.yqritc:recyclerview-flexibledivider:1.4.0")
@@ -337,13 +332,13 @@ dependencies /* Layout */ {
dependencies /* View */ {
// RoundedImageView
implementation("com.makeramen:roundedimageview:2.3.0")
implementation(libs.roundedimageview)
// CircleImageView
implementation("de.hdodenhof:circleimageview:3.1.0")
implementation(libs.circleimageview)
// Animated SVG
implementation("com.jaredrummler:animated-svg-view:1.0.6")
implementation(libs.animated.svg.view)
}
dependencies /* GitHub API */ {
@@ -353,7 +348,7 @@ dependencies /* GitHub API */ {
because("Compatibility for Android API Level < 26 (Android 8.0) [O]")
version {
strictly("2.8.0")
because("Exception on newer versions: 'NoClassDefFoundError: org.apache.commons.io.IOUtils'")
because("Exception on newer versions: 'NoClassDefFoundError: org.apache.commons.io.IObuildUtils'")
}
}
@@ -372,16 +367,16 @@ dependencies /* GitHub API */ {
dependencies /* MLKit */ {
// OCR
implementation("com.google.mlkit:text-recognition-chinese:16.0.1")
implementation(libs.text.recognition.chinese)
// Barcode
implementation("com.google.mlkit:barcode-scanning:17.3.0")
implementation(libs.barcode.scanning)
}
dependencies /* OpenCC */ {
// OpenCC
// implementation("com.github.qichuan:android-opencc:1.2.0")
implementation("com.github.brooklet:android-opencc:1.2.2")
implementation(libs.opencc)
}
dependencies /* Auto.js Extensions */ {
@@ -402,8 +397,8 @@ dependencies /* Auto.js Extensions */ {
// # implementation(project(":libs:Auto.js-ApkBuilder-1.0.3"))
// Extracted from com.github.hyb1996:MutableTheme:1.0.0
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("com.github.ozodrukh:CircularReveal:2.0.1")
implementation(libs.recyclerview)
implementation(libs.circularreveal)
// @Legacy com.jrummyapps:colorpicker:2.1.7
// @Integrated by SuperMonster003 on Mar 25, 2025.
// # implementation("com.jaredrummler:colorpicker:1.1.0")
@@ -457,7 +452,7 @@ android {
multiDexEnabled = true
buildConfigField("String", "VERSION_DATE", "\"${Utils.getDateString("MMM d, yyyy", "GMT+08:00")}\"")
buildConfigField("String", "VERSION_DATE", "\"${utils.getDateString("MMM d, yyyy", "GMT+08:00")}\"")
buildConfigField("String", "VSCODE_EXT_REQUIRED_VERSION", "\"${versions.vscodeExtRequiredVersion}\"")
buildConfigField("boolean", "is${flavorNameInrt.uppercaseFirstChar()}", "false")
@@ -520,47 +515,6 @@ android {
// ! https://github.com/kkevsekk1/AutoX/blob/a6d482189291b460c3be60970b74c5321d26e457/inrt/build.gradle.kts#L93
// noinspection ChromeOsAbiSupport
ndk.abiFilters += ""
gradle.taskGraph.whenReady(object : Action<TaskExecutionGraph> {
override fun execute(taskGraph: TaskExecutionGraph) {
val taskName = "$buildActionAssemble${flavorNameInrt.uppercaseFirstChar()}${buildTypeRelease.uppercaseFirstChar()}"
project.getTasksByName(taskName, true)
.firstOrNull()
?.doLast {
copy {
val src = "build/outputs/apk/$flavorNameInrt/$buildTypeRelease"
// @Reference to LZX284 (https://github.com/LZX284) by SuperMonster003 on Nov 16, 2023.
val dst = "src/main/assets-$flavorNameApp"
val ext = Utils.FILE_EXTENSION_APK
if (!file(src).isDirectory) {
return@copy
}
from(src); into(dst)
val verName = versionName?.replace(Regex("\\s"), "-")?.lowercase()
/* e.g. inrt-v6.4.0-beta-universal.apk */
val srcFileName = "$flavorNameInrt-v$verName-universal.$ext".also {
if (!file(File(src, it)).exists()) {
throw GradleException("Source file \"${file(File(src, it))}\" doesn't exist")
}
}
val dstFileName = "$templateName.$ext"
val isOverridden = file(File(dst, dstFileName)).exists()
include(srcFileName)
rename(srcFileName, dstFileName)
println("Source: ${file(File(src, srcFileName))}")
println("Destination: ${file(File(dst, dstFileName))}${if (isOverridden) " [overridden]" else ""}")
}
}
?: println("$taskName doesn't exist in project ${project.name}")
}
})
}
androidResources {
@@ -676,12 +630,12 @@ android {
}
signingConfigs {
if (sign.isValid) {
if (signs.isValid) {
create(buildTypeRelease) {
storeFile = sign.properties["storeFile"]?.let { file(it as String) }
keyPassword = sign.properties["keyPassword"] as String
keyAlias = sign.properties["keyAlias"] as String
storePassword = sign.properties["storePassword"] as String
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
}
}
}
@@ -691,7 +645,7 @@ android {
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro",
)
val niceSigningConfig = takeIf { sign.isValid }?.let {
val niceSigningConfig = takeIf { signs.isValid }?.let {
signingConfigs.getByName(buildTypeRelease)
}
debug {
@@ -709,10 +663,12 @@ android {
buildFeatures {
aidl = true
viewBinding = true
// @Hint by SuperMonster003 on Aug 14, 2023.
// ! Substitution of "android.defaults.buildfeatures.buildconfig=true"
// ! zh-CN: "android.defaults.buildfeatures.buildconfig=true" 的替代方案
buildConfig = true
// @Archived by SuperMonster003 on Sep 23, 2024.
// ! Jetpack Compose
// # compose = true
@@ -750,7 +706,14 @@ android {
}
outputs.map { it as BaseVariantOutputImpl }.forEach {
it.outputFileName = Utils.getOutputFileName(this@all as ApplicationVariantImpl, it)
it.outputFileName = run {
val variant = this@all as ApplicationVariantImpl
val autojs = variant.applicationId.replace("^.+\\.(.+)$".toRegex(), "$1") // e.g. autojs6
val version = variant.versionName.replace("\\s".toRegex(), "-") // e.g. 6.1.0
val architecture = it.getFilter("ABI") ?: "universal"
val extension = utils.FILE_EXTENSION_APK
"$autojs-v$version-$architecture.$extension".lowercase(getDefault())
}
}
}
@@ -792,7 +755,7 @@ tasks {
listOf(flavorNameApp, flavorNameInrt).forEach { flavorName ->
val src = "$flavorName/$buildTypeRelease"
val dst = "${src}s"
val ext = Utils.FILE_EXTENSION_APK
val ext = utils.FILE_EXTENSION_APK
if (!file(src).isDirectory) {
return@forEach
@@ -801,7 +764,7 @@ tasks {
from(src); into(dst); include("*.$ext")
rename { name ->
Utils.digestCRC32(file("${src}/$name")).let { digest ->
utils.digestCRC32(file("${src}/$name")).let { digest ->
name.replace(Regex("^(.+?)(\\.$ext)$"), "$1-$digest$2")
}
}
@@ -814,231 +777,3 @@ tasks {
extra {
versions.handleIfNeeded(project, flavorNameApp, listOf(buildTypeDebug, buildTypeRelease))
}
gradle.beforeProject {
extensions.extraProperties["compileSdk"] = versions.sdkVersionCompile
extensions.extraProperties["minSdk"] = versions.sdkVersionMin
extensions.extraProperties["targetSdk"] = versions.sdkVersionTarget
}
class Sign(filePath: String) {
var isValid = false
private set
val properties = Properties().also { props ->
File(filePath).takeIf { it.exists() }?.let {
props.load(FileInputStream(it))
isValid = props.isNotEmpty()
}
}
}
class Versions(filePath: String) {
private val properties = Properties()
private val file = File(filePath).apply {
if (!canRead()) {
throw FileNotFoundException("Cannot read file '$filePath'")
}
properties.load(FileInputStream(this))
}
val sdkVersionMin = properties["MIN_SDK_VERSION"].let { it as String }.toInt()
val sdkVersionTarget = properties["TARGET_SDK_VERSION"].let { it as String }.toInt()
val sdkVersionTargetInrt = properties["TARGET_SDK_VERSION_INRT"].let { it as String }.toInt()
val sdkVersionCompile = properties["COMPILE_SDK_VERSION"].let { it as String }.toInt()
val appVersionName = properties["VERSION_NAME"] as String
val appVersionCode = properties["VERSION_BUILD"].let { it as String }.toInt()
val vscodeExtRequiredVersion = properties["VSCODE_EXT_REQUIRED_VERSION"] as String
private val currentVersionInt = JavaVersion.current().majorVersion.toInt()
private val javaVersionMinSupported: Int = properties["JAVA_VERSION_MIN_SUPPORTED"]
.let { it as String }.toInt()
.also {
if (currentVersionInt < it) {
throw GradleException(
"Current Gradle JDK version ${JavaVersion.current()} does not meet " +
"the minimum requirement which $it is needed."
)
}
}
private val javaVersionMinSuggested: Int = properties["JAVA_VERSION_MIN_SUGGESTED"].let { it as String }.toInt()
private val javaVersionMaxSupported: Int = properties["JAVA_VERSION_MAX_SUPPORTED"].let { it as String }.toInt()
private val javaVersionRaw = properties["JAVA_VERSION"] as String
private var javaVersionInfoSuffix = ""
val javaVersion: JavaVersion by lazy {
val javaVersionInt = determineJavaVersion()
gradle.beforeProject {
extensions.extraProperties["javaVersion"] = javaVersionInt
}
JavaVersion.toVersion(javaVersionInt)
}
private fun determineJavaVersion(): Int {
if (gradle.extra.has("javaVersionOverriddenByUser")) {
(gradle.extra.get("javaVersionOverriddenByUser") as? Int)?.let {
javaVersionInfoSuffix += " [user-specified]"
return it
}
}
var versionInt = javaVersionRaw.toInt()
var isJvmCoercive = false
while (versionInt > javaVersionMinSupported) {
if (JvmTarget.values().any { it.name.contains(Regex("_$versionInt$")) }) {
break
}
versionInt -= 1
isJvmCoercive = true
}
if (isJvmCoercive) {
javaVersionInfoSuffix += " [coercive-jvm-downgraded]"
}
if (versionInt > currentVersionInt) {
versionInt = currentVersionInt
javaVersionInfoSuffix += " [consistent-downgraded]"
}
if (gradle.extra.has("javaVersionCoercedByGradle")) {
(gradle.extra["javaVersionCoercedByGradle"] as? Int)?.let {
if (versionInt > it) {
versionInt = it
javaVersionInfoSuffix += " [coercive-gradle-downgraded]"
}
}
}
return versionInt
}
private var isBuildNumberAutoIncremented = false
private val minBuildTimeGap = Utils.hours2Millis(0.75)
private val isBuildGapEnough
get() = properties["BUILD_TIME"]?.let {
Date().time - (it as String).toLong() > minBuildTimeGap
} == true
init {
if (currentVersionInt < javaVersionMinSuggested) {
logger.error(
"It is recommended to upgrade current Gradle JDK version ${JavaVersion.current()} to $javaVersionMinSuggested or higher${
if (javaVersionMaxSupported > 0) " (but not higher than $javaVersionMaxSupported)" else ""
}."
)
}
if (currentVersionInt > javaVersionMaxSupported) {
logger.error(
"It is recommended to downgrade current Gradle JDK version $currentVersionInt " +
"to $javaVersionMaxSupported${if (javaVersionMaxSupported > javaVersionMinSuggested) " or lower (but not lower than $javaVersionMinSuggested)" else ""}, " +
"as Gradle may be not compatible with JDK $currentVersionInt for now."
)
}
}
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${if (gradle.extra.has("isHideConsoleInfoHintSuffix") && gradle.extra.get("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() {
if (isBuildGapEnough) {
val isBuildAppRelease = gradle.startParameter.taskNames.any {
it.contains(Regex("^(:?$flavorNameApp:)?$buildActionAssemble($flavorNameApp|$flavorNameInrt)$buildTypeRelease", IGNORE_CASE))
}
if (!isBuildAppRelease) {
properties["VERSION_BUILD"] = "${appVersionCode + 1}"
isBuildNumberAutoIncremented = true
}
}
properties["BUILD_TIME"] = "${Date().time}"
properties.store(file.writer(), null)
}
}
object Utils {
const val FILE_EXTENSION_APK = "apk"
fun hours2Millis(hour: Double) = hour * 3.6e6
fun getDateString(format: String, zone: String): String {
// e.g. May 23, 2011
return SimpleDateFormat(format).apply { timeZone = TimeZone.getTimeZone(zone) }.format(Date())
}
fun getOutputFileName(variant: ApplicationVariantImpl, output: BaseVariantOutputImpl): String {
val autojs = variant.applicationId.replace("^.+\\.(.+)$".toRegex(), "$1") // e.g. autojs6
val version = variant.versionName.replace("\\s".toRegex(), "-") // e.g. 6.1.0
val architecture = output.getFilter("ABI") ?: "universal"
val extension = FILE_EXTENSION_APK
return "$autojs-v$version-$architecture.$extension".lowercase(Locale.getDefault())
}
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)
}
}
private fun capitalize(s: String) = "${s[0].uppercase(Locale.getDefault())}${s.substring(1)}"
}

View File

@@ -10,13 +10,13 @@ allprojects {
repositories {
mavenCentral()
google()
gradlePluginPortal()
maven("https://jitpack.io")
maven("https://maven.aliyun.com/repository/central")
maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/gradle-plugin")
maven("https://maven.aliyun.com/repository/jcenter")
maven("https://maven.aliyun.com/repository/public")
gradlePluginPortal()
}
}

3
buildSrc/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/build
/.gradle
/.kotlin

102
buildSrc/build.gradle.kts Normal file
View File

@@ -0,0 +1,102 @@
@file:Suppress("ObjectLiteralToLambda")
import java.util.*
plugins {
`kotlin-dsl` /* kotlin("jvm") */
`java-gradle-plugin`
}
repositories {
mavenCentral()
}
dependencies {
implementation(libs.apache.commons.compress)
implementation(libs.tukaani.xz)
implementation(gradleApi())
}
gradlePlugin {
plugins {
create("buildUtils", object : Action<PluginDeclaration> {
override fun execute(t: PluginDeclaration) {
t.id = "org.autojs.build.utils"
t.implementationClass = "org.autojs.build.UtilsPlugin"
t.displayName = "AutoJs6 Build Utils Plugin"
t.description = "Provides utilities for downloading, extracting archives, and version helpers."
}
})
create("buildVersions", object : Action<PluginDeclaration> {
override fun execute(t: PluginDeclaration) {
t.id = "org.autojs.build.versions"
t.implementationClass = "org.autojs.build.VersionsPlugin"
t.displayName = "AutoJs6 Versions Plugin"
t.description = "Provides version helpers."
}
})
create("buildSigns", object : Action<PluginDeclaration> {
override fun execute(t: PluginDeclaration) {
t.id = "org.autojs.build.signs"
t.implementationClass = "org.autojs.build.SignsPlugin"
t.displayName = "AutoJs6 Signs Plugin"
t.description = "Provides signing helpers."
}
})
create("buildProperties", object : Action<PluginDeclaration> {
override fun execute(t: PluginDeclaration) {
t.id = "org.autojs.build.properties"
t.implementationClass = "org.autojs.build.PropertiesPlugin"
t.displayName = "AutoJs6 Properties Plugin"
t.description = "Provides properties helpers."
}
})
}
}
run determineBuildSrcJdk@{
val propsFile: File = rootDir.parentFile.resolve("version.properties")
val minSupported: Int = Properties().let { props ->
require(propsFile.isFile) {
"version.properties not found in root directory"
}
propsFile.inputStream().use { props.load(it) }
val minSupportedVersion = props.getProperty("JAVA_VERSION_MIN_SUPPORTED")
require(minSupportedVersion != null) {
"version.properties does not contain \"JAVA_VERSION_MIN_SUPPORTED\""
}
minSupportedVersion.toInt()
}
val current = JavaVersion.current().majorVersion.toIntOrNull() ?: minSupported
fun tryAdjustByKotlinJvmTarget(sourceVersion: Int): Int {
return runCatching {
val cls = Class.forName("org.jetbrains.kotlin.gradle.dsl.JvmTarget")
val enumConstants = cls.enumConstants ?: return sourceVersion
var tmpVersion = sourceVersion
while (tmpVersion > minSupported) {
val wanted = "JVM_$tmpVersion"
if (enumConstants.any { it?.toString().equals(wanted, ignoreCase = true) }) {
return tmpVersion
}
tmpVersion -= 1
}
return@runCatching sourceVersion
}.getOrDefault(sourceVersion)
}
tryAdjustByKotlinJvmTarget(maxOf(current, minSupported)).also {
println("Toolchain: selected [$it] / current [$current] / min [$minSupported]")
}
}.let { jdk ->
kotlin {
jvmToolchain(jdk)
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(jdk))
}
}

View File

@@ -0,0 +1,19 @@
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
google()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

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,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))
}
}

View File

@@ -1,23 +1,24 @@
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
apply from: '../utils.build.gradle'
plugins {
id 'org.autojs.build.utils'
id 'org.autojs.build.properties'
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
ext {
projectName = "Image Quantization"
}
def versions = Utils.newVersions(project)
def versionMap = [
"libimagequant": "2.17.0",
"libpng" : "1.6.49",
"MIN_SDK" : versions["MIN_SDK"] as Integer,
"COMPILE_SDK" : versions["COMPILE_SDK"] as Integer,
"TARGET_SDK" : versions["TARGET_SDK"] as Integer,
"MIN_SDK" : props["MIN_SDK"] as Integer,
"COMPILE_SDK" : props["COMPILE_SDK"] as Integer,
"TARGET_SDK" : props["TARGET_SDK"] as Integer,
"NDK" : versions["IMAGE_QUANT/NDK"],
"CMAKE" : versions["IMAGE_QUANT/CMAKE"],
"NDK" : props["IMAGE_QUANT/NDK"],
"CMAKE" : props["IMAGE_QUANT/CMAKE"],
]
def nameMap = [
@@ -26,6 +27,13 @@ def nameMap = [
"CMAKE" : "Cmake",
]
utils.configureLibraryLifecycleHooks(project, nameMap.PROJECT, [
"libimagequant",
"libpng",
"NDK",
"CMAKE",
].collect { "${nameMap[it] ?: it}: ${versionMap[it]}" }, Collections.emptyList(), null, [".cxx"])
android {
namespace = "org.pngquant"
@@ -75,20 +83,5 @@ android {
version versionMap.CMAKE
}
}
}
clean.doFirst {
delete project.layout.buildDirectory
['.cxx'].forEach { delete file(it) }
}
gradle.taskGraph.whenReady { taskGraph ->
if (taskGraph.allTasks.any { it.name == "clean" }) return
Utils.newFormatted("Version information for ${nameMap.PROJECT} Library", [
"libimagequant",
"libpng",
"NDK",
"CMAKE",
].collect { "${nameMap[it] ?: it}: ${versionMap[it]}" }).print()
}

View File

@@ -6,25 +6,24 @@
* Modified by SuperMonster003 as of Sep 4, 2023.
*/
apply {
plugin 'com.android.library'
plugin 'org.jetbrains.kotlin.android'
from '../utils.build.gradle'
plugins {
id 'org.autojs.build.utils'
id 'org.autojs.build.properties'
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
ext {
projectName = "Paddle OCR"
}
def versions = Utils.newVersions(project)
def versionMap = [
"MIN_SDK" : versions["MIN_SDK"] as Integer,
"COMPILE_SDK": versions["COMPILE_SDK"] as Integer,
"TARGET_SDK" : versions["TARGET_SDK"] as Integer,
"NDK" : versions["PADDLE_OCR/NDK"],
"CMAKE" : versions["PADDLE_OCR/CMAKE"],
"OPENCV" : versions["PADDLE_OCR/OPENCV"],
"MIN_SDK" : props["MIN_SDK"] as Integer,
"COMPILE_SDK": props["COMPILE_SDK"] as Integer,
"TARGET_SDK" : props["TARGET_SDK"] as Integer,
"NDK" : props["PADDLE_OCR/NDK"],
"CMAKE" : props["PADDLE_OCR/CMAKE"],
"OPENCV" : props["PADDLE_OCR/OPENCV"],
]
def nameMap = [
@@ -43,12 +42,18 @@ def libsToDeploy = [
// ! Download the archive for source code of OpenCV (defaults to 4.2.0).
// ! Not matching the version in Auto.js (e.g., 4.8.0) will cause conflicts.
// ! Adjust the corresponding content in version.properties as needed.
Utils.newLibDeployer(project, nameMap.OPENCV, "https://github.com/opencv/opencv/releases/download" +
utils.newLibDeployer(project, nameMap.OPENCV, "https://github.com/opencv/opencv/releases/download" +
"/${versionMap.OPENCV}/opencv-${versionMap.OPENCV}-android-sdk.zip")
.setSourceDir("/OpenCV-android-sdk/sdk/native/")
.setDestDir("/src/sdk/native/")
]
utils.configureLibraryLifecycleHooks(project, nameMap.PROJECT, [
"OPENCV",
"NDK",
"CMAKE",
].collect { "${nameMap[it]}: ${versionMap[it]}" }, libsToDeploy, "isCleanupPaddleOcr", [".cxx"])
android {
namespace = "com.baidu.paddle.lite.ocr"
@@ -148,25 +153,3 @@ dependencies {
implementation libs.preference.ktx
}
clean.doFirst {
delete project.layout.buildDirectory
['.cxx'].forEach { delete file(it) }
if (gradle.ext["isCleanupPaddleOcr"] as Boolean) {
libsToDeploy.forEach { it.clean() }
} else {
println("The library files of ${project.ext["projectName"]} won't be cleaned up due to the configuration")
}
}
gradle.taskGraph.whenReady { taskGraph ->
if (taskGraph.allTasks.every { it.name == "clean" }) return
Utils.newFormatted("Version information for ${nameMap.PROJECT} library", [
"OPENCV",
"NDK",
"CMAKE",
].collect { "${nameMap[it]}: ${versionMap[it]}" }).print()
libsToDeploy.forEach { it.deploy() }
}

View File

@@ -10,10 +10,11 @@ cmake_minimum_required(VERSION 3.4.1)
# CMake builds them for you. Gradle automatically packages shared libraries with
# your APK.
set(PaddleLite_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../PaddleLite")
get_filename_component(MODULE_DIR "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE)
set(PaddleLite_DIR "${MODULE_DIR}/PaddleLite")
include_directories(${PaddleLite_DIR}/cxx/include)
set(OpenCV_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../src/sdk/native/jni")
set(OpenCV_DIR "${MODULE_DIR}/src/sdk/native/jni")
message(STATUS "opencv dir: ${OpenCV_DIR}")
find_package(OpenCV REQUIRED)
message(STATUS "OpenCV libraries: ${OpenCV_LIBS}")

View File

@@ -4,26 +4,27 @@
* Created by SuperMonster003 on Sep 19, 2024.
*/
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'kotlin-parcelize'
apply from: '../utils.build.gradle'
plugins {
id 'org.autojs.build.utils'
id 'org.autojs.build.properties'
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'kotlin-parcelize'
}
ext["projectName"] = "Rapid OCR"
def versions = Utils.newVersions(project)
def versionMap = [
"OFFICIAL_NAME" : "1.3.0", /* From original build.gradle file. */
"MIN_SDK" : versions["MIN_SDK"] as Integer,
"COMPILE_SDK" : versions["COMPILE_SDK"] as Integer,
"TARGET_SDK" : versions["TARGET_SDK"] as Integer,
"NDK" : versions["RAPID_OCR/NDK"],
"CMAKE" : versions["RAPID_OCR/CMAKE"],
"OPENCV_MOBILE" : versions["RAPID_OCR/OPENCV_MOBILE"],
"OPENCV_MOBILE_LABEL": versions["RAPID_OCR/OPENCV_MOBILE_LABEL"],
"ONNX" : versions["RAPID_OCR/ONNX"],
"ONNX_RUNTIME" : versions["RAPID_OCR/ONNX_RUNTIME"],
"MIN_SDK" : props["MIN_SDK"] as Integer,
"COMPILE_SDK" : props["COMPILE_SDK"] as Integer,
"TARGET_SDK" : props["TARGET_SDK"] as Integer,
"NDK" : props["RAPID_OCR/NDK"],
"CMAKE" : props["RAPID_OCR/CMAKE"],
"OPENCV_MOBILE" : props["RAPID_OCR/OPENCV_MOBILE"],
"OPENCV_MOBILE_LABEL": props["RAPID_OCR/OPENCV_MOBILE_LABEL"],
"ONNX" : props["RAPID_OCR/ONNX"],
"ONNX_RUNTIME" : props["RAPID_OCR/ONNX_RUNTIME"],
]
def nameMap = [
@@ -37,20 +38,29 @@ def nameMap = [
]
def libsToDeploy = [
Utils.newLibDeployer(project, nameMap.OPENCV_MOBILE, "https://github.com/nihui/opencv-mobile/releases/download" +
utils.newLibDeployer(project, nameMap.OPENCV_MOBILE, "https://github.com/nihui/opencv-mobile/releases/download" +
"/v${versionMap.OPENCV_MOBILE_LABEL}/opencv-mobile-${versionMap.OPENCV_MOBILE}-android.zip")
.setSourceDir("/opencv-mobile-${versionMap.OPENCV_MOBILE}-android/sdk/native/")
.setDestDir("/src/sdk/native/"),
Utils.newLibDeployer(project, nameMap.ONNX, "https://github.com/RapidAI/RapidOcrOnnx/releases/download" +
utils.newLibDeployer(project, nameMap.ONNX, "https://github.com/RapidAI/RapidOcrOnnx/releases/download" +
"/${versionMap.ONNX}/Project_RapidOcrOnnx-${versionMap.ONNX}.7z")
.setSourceDir("/Project_RapidOcrOnnx-${versionMap.ONNX}/models/")
.setDestDir("/src/main/assets/models/"),
Utils.newLibDeployer(project, nameMap.ONNX_RUNTIME, "https://github.com/RapidAI/OnnxruntimeBuilder/releases/download" +
utils.newLibDeployer(project, nameMap.ONNX_RUNTIME, "https://github.com/RapidAI/OnnxruntimeBuilder/releases/download" +
"/${versionMap.ONNX_RUNTIME}/onnxruntime-${versionMap.ONNX_RUNTIME}-android-shared.7z")
.setSourceDir("/onnxruntime-shared/")
.setDestDir("/src/main/onnxruntime-shared/"),
]
utils.configureLibraryLifecycleHooks(project, nameMap.PROJECT, [
"OPENCV_MOBILE",
"OPENCV_MOBILE_LABEL",
"ONNX",
"ONNX_RUNTIME",
"NDK",
"CMAKE",
].collect { "${nameMap[it]}: ${versionMap[it]}" }, libsToDeploy, "isCleanupRapidOcr", [".cxx"])
android {
namespace = "com.benjaminwan.ocrlibrary"
@@ -113,29 +123,7 @@ dependencies {
androidTestImplementation libs.test.ext.junit
androidTestImplementation libs.test.espresso.core
}
clean.doFirst {
delete project.layout.buildDirectory
['.cxx'].forEach { delete file(it) }
if (gradle.ext["isCleanupRapidOcr"] as Boolean) {
libsToDeploy.forEach { it.clean() }
} else {
println("The library files of ${project.ext["projectName"]} won't be cleaned up due to the configuration")
}
}
gradle.taskGraph.whenReady { taskGraph ->
if (taskGraph.allTasks.every { it.name == "clean" }) return
Utils.newFormatted("Version information for ${nameMap.PROJECT} library", [
"OPENCV_MOBILE",
"OPENCV_MOBILE_LABEL",
"ONNX",
"ONNX_RUNTIME",
"NDK",
"CMAKE",
].collect { "${nameMap[it]}: ${versionMap[it]}" }).print()
libsToDeploy.forEach { it.deploy() }
implementation libs.core.ktx
implementation libs.appcompat
}

View File

@@ -4,7 +4,8 @@ project(RapidOcr)
# OnnxRuntime
include(${CMAKE_CURRENT_SOURCE_DIR}/../onnxruntime-shared/OnnxRuntimeWrapper.cmake)
get_filename_component(MODULE_DIR "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE)
include(${MODULE_DIR}/src/main/onnxruntime-shared/OnnxRuntimeWrapper.cmake)
find_package(OnnxRuntime REQUIRED)
if (OnnxRuntime_FOUND)
message(STATUS "OnnxRuntime_LIBS: ${OnnxRuntime_LIBS}")
@@ -15,7 +16,7 @@ endif (OnnxRuntime_FOUND)
## opencv 库
set(OpenCV_DIR "${CMAKE_SOURCE_DIR}/../../sdk/native/jni")
set(OpenCV_DIR "${MODULE_DIR}/src/sdk/native/jni")
find_package(OpenCV REQUIRED)
if (OpenCV_FOUND)
message(STATUS "OpenCV_LIBS: ${OpenCV_LIBS}")

View File

@@ -1,679 +0,0 @@
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry
import org.apache.commons.compress.archivers.sevenz.SevenZFile
import java.nio.ByteBuffer
import java.nio.channels.FileChannel
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.zip.ZipFile
ext.Utils = Utils
class Utils {
static LibDeployer newLibDeployer(Project project, String name, String downloadUrl) {
return new LibDeployer(project, name, downloadUrl)
}
static Formatted newFormatted(String title, Collection<String> contents = [], String subtitle = null) {
return new Formatted(title, contents, subtitle)
}
static Versions newVersions(Project project) {
return new Versions("$project.rootDir/version.properties")
}
private static String generateProgressBar(double progress, int length = 30) {
int filledLength = (int) (length * progress / 100.0)
String filled = '#' * filledLength
String empty = '-' * (length - filledLength)
filled + empty
}
private static String formatFileSize(long size) {
if (size < 1024) {
String.format("%d B", size)
} else if (size < (1024 * 1024)) {
String.format("%.2f KB", size / 1024.0)
} else if (size < (1024 * 1024 * 1024)) {
String.format("%.2f MB", size / (1024.0 * 1024))
} else {
String.format("%.2f GB", size / (1024.0 * 1024 * 1024))
}
}
private static ExtractedFile extractFileFromUrl(url, standardize = false) {
def fileNameWithExtension = url.tokenize("/").last()
String fileName
String extensionName
if (fileNameWithExtension.contains('.')) {
def lastIndexOfDot = fileNameWithExtension.lastIndexOf('.')
fileName = fileNameWithExtension.take(lastIndexOfDot)
extensionName = fileNameWithExtension.substring(lastIndexOfDot + 1)
} else {
fileName = fileNameWithExtension
extensionName = ""
}
if (standardize) {
fileName = fileName.toLowerCase().replaceAll(/\s+/, '').replaceAll(/[^a-z0-9.]/, '-')
}
return new ExtractedFile(fileName, extensionName)
}
private static class LibDeployer {
String name
String downloadUrl = null
Project project
String sourceDir = File.separator
File sourceFile = project.file(File.separator)
String destDir = File.separator
File destFile = project.file(File.separator)
File cacheFile
File cacheRootFile
String cacheFileName
String cacheFileExtensionName
File getSkipFile() {
project.file(new File(destFile, "${cacheFileName}.skip"))
}
File getTempOutFile() {
project.file(new File(destFile, "temp-extracted"))
}
LibDeployer(Project project, String name, String downloadUrl) {
this.project = project
this.name = name
this.downloadUrl = downloadUrl
def extractedFile = extractFileFromUrl(downloadUrl, true)
this.cacheRootFile = project.file("cache")
cacheRootFile.mkdirs()
this.cacheFileName = "${extractedFile.fileName}-[${generateShortMd5String(downloadUrl).toLowerCase()}]"
this.cacheFileExtensionName = extractedFile.extensionName
this.cacheFile = project.file(new File(cacheRootFile, "$cacheFileName.$cacheFileExtensionName"))
}
LibDeployer setSourceDir(String sourceDir) {
this.sourceDir = sourceDir
this.sourceFile = project.file(sourceDir)
return this
}
LibDeployer setDestDir(String destDir) {
// @Hint by SuperMonster003 on Dec 2, 2024.
// ! On some platforms (such as Temurin),
// ! using project.file might not correctly retrieve the absolute path of the current project.
// ! For example, using "/foo/bar",
// ! Expected path: "/.../AutoJs6/libs/.../src/sdk/native",
// ! Actual path: "/src/sdk/native",
// ! In this case, the leading path separator should be removed, using "foo/bar" instead of "/foo/bar".
// !
// ! zh-CN:
// !
// ! 某些平台 (如 Temurin) 使用 project.file 可能无法正确获取当前项目的绝对路径.
// ! 以 "/foo/bar" 为例,
// ! 预期路径: "/.../AutoJs6/libs/.../src/sdk/native",
// ! 实际路径: "/src/sdk/native",
// ! 这种情况下需要去除路径分隔符前缀, 即使用 "foo/bar" 而非 "/foo/bar".
def destFile = destDir.startsWith(File.separator)
? project.file(destDir.substring(1))
: project.file(destDir)
destFile.mkdirs()
this.destDir = destDir
this.destFile = destFile
return this
}
void deploy() {
if (tempOutFile.exists()) {
project.delete tempOutFile
}
def checkResult = checkCacheAndSkipFiles()
def (shouldDownload, shouldExtract) = [checkResult.shouldDownload, checkResult.shouldExtract]
if (shouldDownload) {
printDownloadInfo()
downloadWithRetry()
shouldExtract = true
}
if (shouldExtract) {
printExtractInfo()
try {
extractCacheFile()
} catch (Exception e) {
println("\n") // two line breaks
println("Cache file was deleted as there is an error during extraction")
println("Cache file: ${cacheFile.absolutePath}")
cacheFile.delete()
if (e.message != null) {
println("Error message: $e.message")
}
println()
throw e
}
generateMd5File(cacheFile)
}
}
void clean() {
project.delete skipFile
project.delete tempOutFile
deleteDestAccordingToSrc()
deleteCacheAccordingToMd5()
}
private LinkedHashMap<String, Boolean> checkCacheAndSkipFiles() {
def shouldDownload = true
def shouldExtract = true
if (skipFile.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
def md5File = new 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: shouldDownload, shouldExtract: shouldExtract]
}
private static boolean validateMd5File(File cacheFile) {
def md5File = new File(cacheFile.parentFile, cacheFile.name + ".md5")
if (!md5File.exists()) {
return false
}
def expectedMd5 = md5File.text.trim().toUpperCase()
def actualMd5 = generateMd5String(cacheFile).toUpperCase()
return expectedMd5 == actualMd5
}
private static void generateMd5File(File file) {
def md5File = new File(file.parentFile, file.name + ".md5")
println("Generating MD5...")
def generatedMd5 = generateMd5String(file)
md5File.text = generatedMd5
println("MD5 generated: $generatedMd5")
println()
}
private static String generateMd5String(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
FileChannel fileChannel = fis.getChannel()
MessageDigest md = MessageDigest.getInstance("MD5")
ByteBuffer buffer = ByteBuffer.allocate(4096)
while (fileChannel.read(buffer) > 0) {
buffer.flip()
md.update(buffer)
buffer.clear()
}
return new BigInteger(1, md.digest()).toString(16).padLeft(32, '0')
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not found", e)
}
}
private static String generateShortMd5String(String s) {
MessageDigest md = MessageDigest.getInstance("MD5")
md.update(s.bytes)
return new BigInteger(1, md.digest()).toString(32)
}
private void printDownloadInfo() {
def title = "Download \"$name\" archive file for \"${project.ext["projectName"]}\" Gradle project"
def srcInfo = "Source: $downloadUrl"
def destInfo = "Destination: $cacheFile"
def hintInfo = [
"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.",
]
def maxLength = [
[title, srcInfo, destInfo].max { it.length() },
hintInfo.max { it.length() }
].max { it.length() }.length()
def infoList = [
"=".repeat(maxLength),
title,
"-".repeat(maxLength),
srcInfo,
destInfo,
"-".repeat(maxLength),
hintInfo.join("\n"),
"=".repeat(maxLength),
"",
]
infoList.forEach { println(it) }
}
private void printExtractInfo() {
def title = "Extract the archive file for \"${project.ext["projectName"]}\" Gradle project"
def srcInfo = "Source: $cacheFile"
def destInfo = "Destination: $destFile"
def hintInfo = []
def maxLength = [
[title, srcInfo, destInfo].max { it.length() },
hintInfo.max { it.length() }
].max { it != null ? it.length() : 0 }.length()
def infoList = [
"=".repeat(maxLength),
title,
"-".repeat(maxLength),
srcInfo,
destInfo,
hintInfo.isEmpty() ? null : "-".repeat(maxLength),
hintInfo.isEmpty() ? null : hintInfo.join("\n"),
"=".repeat(maxLength),
"",
]
infoList.forEach { if (it != null) println(it) }
}
private void downloadWithRetry(int maxRetries = 3, int retryDelay = 2000) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
download()
return
} catch (IOException e) {
println("Attempt $attempt of $maxRetries failed: ${e.message}")
if (attempt < maxRetries) {
println("Retrying after ${retryDelay / 1000} seconds...")
sleep(retryDelay)
} else {
throw new GradleException("Download failed after $maxRetries attempts", e)
}
}
}
}
private void extractCacheFile() {
switch (cacheFileExtensionName) {
case 'zip':
handleZip()
break
case '7z':
handleSevenZip()
break
default:
throw new GradleException("Unknown archive file type: $cacheFileExtensionName")
}
println("All files extracted into [ $destFile ]")
if (!skipFile.exists()) {
skipFile.parentFile.mkdirs()
skipFile.createNewFile()
println("File \"${skipFile.name}\" created")
}
println()
}
private void deleteDestAccordingToSrc() {
// TODO by SuperMonster003 on Oct 22, 2024.
// ! Use the cache file as a reference file.
// ! If the cache file does not exist, attempt to re-download it.
// ! According to the sourceDir, obtain a single-level file directory tree
// ! from the compressed file, and delete the corresponding files
// ! under the destDir directory based on this.
// ! If the processed destDir is empty, delete it as well.
// ! zh-CN:
// ! 将缓存文件作为参考文件, 缓存文件不存在时, 尝试重新下载.
// ! 根据 sourceDir, 从压缩文件中获取单一层级的文件目录树, 由此删除 destDir 目录下的对应文件.
// ! 处理后的 destDir 若为空, 则一并删除.
/* Pending code... */
// @Caution by SuperMonster003 on Oct 22, 2024.
// ! This operation is NOT safe.
// ! zh-CN: 此操作存在安全隐患.
project.delete destFile.absolutePath
def tmp = destFile.parentFile
while (tmp != project.projectDir) {
if (tmp.listFiles().toList().isEmpty()) {
println("Delete empty directory: $tmp.absolutePath")
project.delete tmp
}
tmp = tmp.parentFile
}
}
private deleteCacheAccordingToMd5() {
cacheRootFile.eachFile { file ->
if (!file.name.endsWith('.md5')) {
def md5File = new File(file.parentFile, file.name + ".md5")
if (!md5File.exists() || !validateMd5File(file)) {
project.delete(file)
println("Delete cache file: ${file.absolutePath}")
if (md5File.exists()) {
project.delete(md5File)
println("Delete MD5 file: ${md5File.absolutePath}")
}
println()
}
}
}
}
private void download() {
final int MAX_RETRIES = 3
int attempt = 0
boolean success = false
boolean shouldPrintProgress = project.ext["platform"]["shouldPrintProgress"] == true
while (attempt < MAX_RETRIES && !success) {
try {
attempt++
cacheFile.parentFile.mkdirs()
def urlConn = new URI(downloadUrl).toURL().openConnection()
urlConn.connectTimeout = 120_000
urlConn.readTimeout = 90_000
long fileSize = urlConn.contentLengthLong
urlConn.getInputStream().withCloseable { inputStream ->
cacheFile.withOutputStream { outputStream ->
byte[] buffer = new byte[8192]
long downloadedSize = 0
int bytesRead
if (shouldPrintProgress && fileSize <= 0) {
println "\rDownloading..."
}
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead)
downloadedSize += bytesRead
if (shouldPrintProgress && fileSize > 0) {
try {
double progress = (downloadedSize * 100.0 / fileSize)
String progressBar = generateProgressBar(progress)
print String.format("\rDownloading... [ %s ] %.2f%%", progressBar, progress)
System.out.flush()
} catch (Exception ignored) {
/* Ignored. */
}
}
}
}
}
String downloadPath = cacheFile.absolutePath
if (fileSize > 0) {
String formattedFileSize = formatFileSize(fileSize)
print String.format("\rDownload complete [ %s | %s ]\n", downloadPath, formattedFileSize)
} else {
print String.format("\rDownload complete [ %s ]\n", downloadPath)
}
System.out.flush()
println()
success = true
} catch (SocketTimeoutException ignored) {
println String.format("Attempt %d/%d failed: Connection timed out. Retrying...", attempt, MAX_RETRIES)
} catch (IOException e) {
println String.format("Attempt %d/%d failed: %s. Retrying...", attempt, MAX_RETRIES, e.message)
}
if (!success && attempt >= MAX_RETRIES) {
println "Download failed after $MAX_RETRIES attempts."
}
}
}
private void handleZip() {
def shouldPrintProgress = project.ext["platform"]["shouldPrintProgress"] == true
def zipFileForTotalSize = new ZipFile(cacheFile)
def zipEntriesForTotalSize = zipFileForTotalSize.entries()
def entries = []
def totalExtractedSize = 0L
def sourceDirPath = new File(sourceDir).path
if (sourceDirPath.startsWith(File.separator)) sourceDirPath = sourceDirPath.substring(1)
while (zipEntriesForTotalSize.hasMoreElements()) {
def entry = zipEntriesForTotalSize.nextElement()
def entryName = new File(entry.name).path
if (entryName.startsWith(sourceDirPath)) {
entries.add(entry)
totalExtractedSize += entry.size
}
}
zipFileForTotalSize.close()
def zipFile = new ZipFile(cacheFile)
def zipEntries = zipFile.entries()
int totalEntries = entries.size()
int processedEntries = 0
while (zipEntries.hasMoreElements()) {
def entry = zipEntries.nextElement()
def entryName = new File(entry.name).path
if (entryName.startsWith(sourceDirPath)) {
File outFile = project.file(new File(tempOutFile, entryName.substring(sourceDirPath.length())))
if (entry.isDirectory()) {
outFile.mkdirs()
} else {
outFile.parentFile.mkdirs()
zipFile.getInputStream(entry).withCloseable { entryInputStream ->
outFile.withOutputStream { outputStream ->
byte[] buffer = new byte[8192]
int bytesRead
while ((bytesRead = entryInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
}
}
if (shouldPrintProgress) {
double progress = (processedEntries * 100.0 / totalEntries)
String progressBar = generateProgressBar(progress)
print String.format("\rExtracting... [ %s ] %.2f%%", progressBar, progress)
System.out.flush()
}
processedEntries++
}
}
String formattedTotalExtractedSize = formatFileSize(totalExtractedSize)
print String.format("\rExtraction complete [ %s | %s ]\n", destFile.absolutePath, formattedTotalExtractedSize)
System.out.flush()
println()
zipFile.close()
project.copy {
from tempOutFile
into destFile
}
project.delete tempOutFile
}
private void handleSevenZip() {
boolean shouldPrintProgress = project.ext["platform"]["shouldPrintProgress"] == true
String sourceDirPath = new File(sourceDir).path
if (sourceDirPath.startsWith(File.separator)) sourceDirPath = sourceDirPath.substring(1)
SevenZFile sevenZFile = new SevenZFile.Builder().setFile(cacheFile).get()
List<SevenZArchiveEntry> allEntries = sevenZFile.getEntries()
List<SevenZArchiveEntry> targetEntries = allEntries.findAll { it.name.startsWith(sourceDirPath) }
int entriesCount = targetEntries.size()
long totalExtractedSize = 0L
for (SevenZArchiveEntry entry : targetEntries) {
totalExtractedSize += entry.size
}
int processedEntries = 0
byte[] buffer = new byte[64 * 1024]
for (SevenZArchiveEntry entry : targetEntries) {
File outFile = project.file(new File(tempOutFile, new File(entry.name).path.substring(sourceDirPath.length())))
if (entry.isDirectory()) {
outFile.mkdirs()
} else {
outFile.parentFile.mkdirs()
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outFile))) {
sevenZFile.getInputStream(entry).withCloseable { entryInputStream ->
int bytesRead
while ((bytesRead = entryInputStream.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead)
}
}
}
}
if (shouldPrintProgress) {
double progress = (processedEntries * 100.0 / entriesCount)
String progressBar = generateProgressBar(progress)
print String.format("\rExtracting... [ %s ] %.2f%%", progressBar, progress)
System.out.flush()
}
processedEntries++
}
String formattedUncompressedSize = formatFileSize(totalExtractedSize)
print String.format("\rExtraction complete [ %s | %s ]\n", destFile.absolutePath, formattedUncompressedSize)
System.out.flush()
sevenZFile.close()
project.copy {
from tempOutFile
into destFile
}
project.delete tempOutFile
}
}
private static class Versions {
private Properties properties
private static String PREFIX_PUBLIC = "PUBLIC"
private static String SUFFIX_VERSION = "VERSION"
Versions(String filePath) {
File file = new File(filePath)
if (!file.canRead()) {
throw FileNotFoundException("Cannot read file '$filePath'")
}
properties = new Properties()
properties.load(new FileInputStream(file))
}
String get(String propertyName) {
if (propertyName.contains("/")) {
return get(propertyName.split("/").toList())
}
var value = properties["${propertyName}_$SUFFIX_VERSION"] as String
return value == PREFIX_PUBLIC
? properties["${PREFIX_PUBLIC}_${propertyName}_$SUFFIX_VERSION"] as String
: value
}
String get(List<String> propertyInfo) {
def (lib, body) = propertyInfo
var value = properties["${lib}_${body}_$SUFFIX_VERSION"] as String
return value == PREFIX_PUBLIC
? properties["${PREFIX_PUBLIC}_${body}_$SUFFIX_VERSION"] as String
: value
}
}
private static class Formatted {
private String title
private Collection<String> contents
private String subtitle
private List<String> formattedOutput
Formatted(String title, Collection<String> contents = [], String subtitle = null) {
this.title = title
this.contents = contents
this.subtitle = subtitle
formattedOutput = {
def elements = []
if (subtitle != null) elements.add(subtitle)
elements.addAll(contents)
def maxLength = ([title, subtitle] + contents).findAll().collect { it.length() }.max()
def result = [
'=' * maxLength,
title,
subtitle,
contents.isEmpty() ? null : '-' * maxLength
].findAll() + contents + ['=' * maxLength, '']
return result
}()
}
void print(boolean contentsMatters = false) {
formattedOutput.each {
if (!contentsMatters || !contents.isEmpty()) {
println(it)
}
}
}
void throwException() {
throw new Exception(formattedOutput.join('\n'))
}
}
private static class ExtractedFile {
String fileName
String extensionName
ExtractedFile(String fileName, String extensionName) {
this.fileName = fileName
this.extensionName = extensionName
}
}
}

View File

@@ -1,15 +1,16 @@
plugins {
id 'org.autojs.build.versions'
id 'com.android.library'
id 'kotlin-android'
}
android {
namespace 'net.dongliu.apk.parser'
compileSdk = project.ext.compileSdk
compileSdk = versions.sdkVersionCompile
defaultConfig {
minSdk = project.ext.minSdk
targetSdk = project.ext.targetSdk
minSdk = versions.sdkVersionMin
targetSdk = versions.sdkVersionTarget
versionName '6'

View File

@@ -1,15 +1,16 @@
plugins {
id 'org.autojs.build.versions'
id 'com.android.library'
id 'kotlin-android'
}
android {
namespace = 'com.mcal.apksigner'
compileSdk = project.ext.compileSdk
compileSdk = versions.sdkVersionCompile
defaultConfig {
minSdk = project.ext.minSdk
targetSdk = project.ext.targetSdk
minSdk = versions.sdkVersionMin
targetSdk = versions.sdkVersionTarget
versionName '1.1-template'
versionCode 11

View File

@@ -1,4 +1,5 @@
plugins {
id 'org.autojs.build.versions'
id 'com.android.library'
id 'kotlin-android'
}
@@ -6,13 +7,13 @@ plugins {
android {
namespace = 'com.jaredrummler.android.colorpicker'
compileSdk = project.ext.compileSdk
compileSdk = versions.sdkVersionCompile
resourcePrefix "cpv_"
defaultConfig {
minSdk = project.ext.minSdk
targetSdk = project.ext.targetSdk
minSdk = versions.sdkVersionMin
targetSdk = versions.sdkVersionTarget
versionName '1.1.0'
}

View File

@@ -1,15 +1,16 @@
plugins {
id 'org.autojs.build.versions'
id 'com.android.library'
id 'kotlin-android'
}
android {
namespace = 'com.huaban.jieba'
compileSdk = project.ext.compileSdk
compileSdk = versions.sdkVersionCompile
defaultConfig {
minSdk = project.ext.minSdk
targetSdk = project.ext.targetSdk
minSdk = versions.sdkVersionMin
targetSdk = versions.sdkVersionTarget
group = 'com.huaban'
versionName = '1.0.3-SNAPSHOT (Optimized for AutoJs6)'

View File

@@ -1,4 +1,5 @@
plugins {
id 'org.autojs.build.versions'
id 'com.android.library'
id 'kotlin-android'
}
@@ -6,11 +7,11 @@ plugins {
android {
namespace = 'com.wdullaer.materialdatetimepicker'
compileSdk = project.ext.compileSdk
compileSdk = versions.sdkVersionCompile
defaultConfig {
minSdk = project.ext.minSdk
targetSdk = project.ext.targetSdk
minSdk = versions.sdkVersionMin
targetSdk = versions.sdkVersionTarget
versionName '4.2.3'
versionCode 54

View File

@@ -1,4 +1,5 @@
plugins {
id 'org.autojs.build.versions'
id 'com.android.library'
id 'kotlin-android'
}
@@ -6,11 +7,11 @@ plugins {
android {
namespace = 'com.afollestad.materialdialogs'
compileSdk = project.ext.compileSdk
compileSdk = versions.sdkVersionCompile
defaultConfig {
minSdk = project.ext.minSdk
targetSdk = project.ext.targetSdk
minSdk = versions.sdkVersionMin
targetSdk = versions.sdkVersionTarget
versionName '0.9.6.0'
versionCode 179

View File

@@ -61,19 +61,16 @@ pluginManagement {
val overriddenKspVersion: String? = null
val versionProperties = java.util.Properties().apply {
load(java.io.FileInputStream("$rootDir/version.properties"))
rootDir.resolve("version.properties").inputStream().use { load(it) }
}
// @AnchorBegin EMBEDDED_KOTLIN_LIST
// @Script /.utils/scrape-and-inject-embedded-kotlin-list.mjs
// @Signature Pair<Gradle Version, Embedded Compatible Kotlin Version>
// @Reference gradle-<version>-src.zip\gradle\dependency-management\kotlin-version.properties
// @AnchorBegin GRADLE_KOTLIN_COMPATIBILITY_LIST
// @Script /.utils/scrape-and-inject-gradle-kotlin-compatibility-list.mjs
// @Signature Pair<Gradle Version, Max Supported Kotlin Version>
// @Reference https://docs.gradle.org/current/userguide/compatibility.html#kotlin
// @Updated by SuperMonster003 on Aug 9, 2025.
val embeddedKotlin = listOf(
"9.0" to "2.2.0",
"8.14" to "2.1.10", /* Unofficial. */
"8.13" to "2.1.10", /* Unofficial. */
// @Updated by SuperMonster003 on Sep 18, 2025.
val gradleKotlinCompatibility = listOf(
"9.0.0" to "2.2.0",
"8.12" to "2.0.21",
"8.11" to "2.0.20",
"8.10" to "1.9.24",
@@ -84,15 +81,16 @@ pluginManagement {
"8.3" to "1.9.0",
"8.2" to "1.8.20",
)
// @AnchorEnd EMBEDDED_KOTLIN_LIST
// @AnchorEnd GRADLE_KOTLIN_COMPATIBILITY_LIST
// @AnchorBegin JAVA_GRADLE_COMPATIBILITY_LIST
// @Script /.utils/scrape-java-gradle-compatibility-map.mjs
// @Signature Pair<Supported Java Version, Min Required Gradle Version>
// @Reference https://docs.gradle.org/current/userguide/compatibility.html#java_runtime
// @Updated by SuperMonster003 on May 12, 2025.
// @Updated by SuperMonster003 on Sep 18, 2025.
val javaGradleCompatibility = listOf(
25 to "9.0", /* Unofficial. */
26 to "N/A",
25 to "9.1",
24 to "8.14",
23 to "8.10",
22 to "8.8",
@@ -128,9 +126,9 @@ pluginManagement {
// @AnchorBegin ANDROID_GRADLE_PLUGIN_RELEASES_LIST
// @Script /.utils/scrape-and-inject-agp-releases.mjs
// @Reference https://developer.android.com/reference/tools/gradle-api
// @Updated by SuperMonster003 on Sep 12, 2025.
// @Updated by SuperMonster003 on Sep 23, 2025.
val agpReleases = listOf(
"9.0.0-alpha05",
"9.0.0-alpha06",
"8.13.0",
"8.12.3",
"8.11.2",
@@ -215,13 +213,11 @@ pluginManagement {
val utils = object {
private val SUFFIX_PRIORITY: Map<String, Int> = mapOf(
// 早期/快照
"canary" to 1, "nightly" to 1, "snapshot" to 1, "dev" to 1,
"pre-alpha" to 2, "prealpha" to 2, "preview" to 2, "eap" to 2, "milestone" to 2,
"alpha" to 3,
"beta" to 4,
"rc" to 5,
// 稳定/正式
"" to 10, "stable" to 10, "ga" to 10, "final" to 10, "release" to 10, "lts" to 10
)
@@ -269,8 +265,7 @@ pluginManagement {
}
fun toVersionParts(version: String): Pair<List<Int>, Pair<String, Int>> {
// 支持: 1.2.3-rc1 / 1.2.3 RC 1 / 1.2.3-Alpha / 1.2.3.m2 / 1.2.3_preview-2
// 以第一个空白/加号/连字符分隔数字部分与后缀部分
// e.g. "1.2.3-rc1" / "1.2.3 RC 1" / "1.2.3-Alpha" / "1.2.3.m2" / "1.2.3_preview-2".
val split = version.split(Regex("[\\s+\\-]"), limit = 2)
val numberStr = split[0]
val numberParts = numberStr.split('.').map {
@@ -280,8 +275,6 @@ pluginManagement {
val suffixStr = split.getOrNull(1)?.trim().orEmpty()
if (suffixStr.isEmpty()) return numberParts to ("" to 0)
// 更宽松的匹配: 名称 + 可选分隔符 + 可选数字; 或 空名称 + 数字 (极少见)
// 分隔符允许: 空格 . _ -
val regex = Regex("([A-Za-z]+)[\\s._-]*(\\d*)|([A-Za-z]*)[\\s._-]*(\\d+)", RegexOption.IGNORE_CASE)
val m = regex.matchEntire(suffixStr) ?: return numberParts to ("" to 0)
@@ -362,8 +355,9 @@ pluginManagement {
// @AnchorBegin ANDROID_STUDIO_CODENAME_VERSION_MAP
// @Script /.utils/scrape-and-inject-android-studio-codename_maps.mjs
// @Reference https://developer.android.com/studio/archive?hl=en
// @Updated by SuperMonster003 on Sep 5, 2025.
// @Updated by SuperMonster003 on Sep 23, 2025.
codenameVersionMap = mapOf(
"2025.2" to "O",
"2025.1" to "N",
"2024.3" to "M",
"2024.2" to "L",
@@ -387,8 +381,9 @@ pluginManagement {
// @AnchorBegin ANDROID_STUDIO_CODENAME_MAP
// @Script /.utils/scrape-and-inject-android-studio-codename_maps.mjs
// @Reference https://developer.android.com/studio/archive?hl=en
// @Updated by SuperMonster003 on Apr 11, 2025.
// @Updated by SuperMonster003 on Sep 23, 2025.
codenameMap = mapOf(
"O" to "Otter", /* Born on Sep 22, 2025. */
"N" to "Narwhal", /* Born on Mar 19, 2025. */
"M" to "Meerkat", /* Born on Nov 12, 2024. */
"L" to "Ladybug", /* Born on Jul 15, 2024. */
@@ -427,9 +422,10 @@ pluginManagement {
val intelliJIdea = object : Platform(
name = "IntelliJIdea", vendor = "Jetbrains",
// @Reference AGP Upgrade Assistant integrated within JetBrains IntelliJ IDEA.
// @Updated by SuperMonster003 on Aug 10, 2025. (Manual)
// @Updated by SuperMonster003 on Aug 20, 2025. (Manual)
agpVersionMap = mapOf(
"2025.2" to "8.11.1",
"2025.2.2" to "8.12.0",
"2025.2.1" to "8.11.1",
"2025.1" to "8.10.1",
"2024.3" to "8.7.3",
"2024.2" to "8.5.2",
@@ -510,6 +506,8 @@ pluginManagement {
val libs = listOf(
Classpath(id = "com.android.tools.build:gradle", version = overriddenAgpVersion ?: "auto:agp"),
Classpath(id = "org.jetbrains.kotlin:kotlin-gradle-plugin", version = overriddenKotlinVersion ?: "auto:kotlin"),
Classpath(id = "org.apache.commons:commons-compress", version = "toml:commons-compress"),
Classpath(id = "org.tukaani:xz", version = "toml:xz"),
Plugin(id = "com.google.devtools.ksp", version = overriddenKspVersion ?: "auto:ksp"),
)
@@ -585,12 +583,6 @@ pluginManagement {
"1.8.22" to "1.0.11", /* Jun 9, 2023. */
"1.8.21" to "1.0.11", /* Apr 27, 2023. */
"1.8.20" to "1.0.11", /* Apr 18, 2023. */
"1.8.20-RC2" to "1.0.9", /* Mar 24, 2023. */
"1.8.20-RC" to "1.0.9", /* Mar 9, 2023. */
"1.8.20-Beta" to "1.0.9", /* Feb 9, 2023. */
"1.8.10" to "1.0.9", /* Feb 3, 2023. */
"1.8.0" to "1.0.9", /* Jan 26, 2023. */
"1.8.0-RC2" to "1.0.8", /* Dec 21, 2022. */
)
// @AnchorEnd KSP_VERSION_MAP
@@ -672,6 +664,7 @@ pluginManagement {
val specified = "specified"
val auto = "auto"
val toml = "toml"
val fallbackSuffix = " [$fallback]"
val upgradedSuffix = " [$upgraded]"
@@ -679,6 +672,7 @@ pluginManagement {
val nearestLowerMatchedSuffix = " [$nearestLowerMatched]"
val autoSpecifiedSuffix = " [$auto-$specified]"
val userSpecifiedSuffix = " [user-$specified]"
val tomlSpecifiedSuffix = " [toml-$specified]"
}
@@ -748,23 +742,23 @@ pluginManagement {
override fun refinedBestMatchingValue(bestMatchingValue: String?): String? {
val currentGradleVersion = gradle.gradleVersion.toGradleVersion()
val embeddedMin = embeddedKotlin
val kotlinMin = gradleKotlinCompatibility
.filter { (gradleMin, _) -> currentGradleVersion >= gradleMin.toGradleVersion() }
.maxByOrNull { it.first.toGradleVersion() }
?.second
return when {
embeddedMin == null -> bestMatchingValue
kotlinMin == null -> bestMatchingValue
bestMatchingValue == null -> {
bestMatchingOperationHintSuffix += identifier.autoSpecifiedSuffix
embeddedMin
kotlinMin
}
bestMatchingValue.toGradleVersion() < embeddedMin.toGradleVersion() -> {
bestMatchingValue.toGradleVersion() < kotlinMin.toGradleVersion() -> {
bestMatchingOperationHintSuffix += identifier.upgradedSuffix
embeddedMin
kotlinMin
}
bestMatchingValue.toGradleVersion() > embeddedMin.toGradleVersion() -> {
bestMatchingValue.toGradleVersion() > kotlinMin.toGradleVersion() -> {
bestMatchingOperationHintSuffix += identifier.downgradedSuffix
embeddedMin
kotlinMin
}
else -> bestMatchingValue
}
@@ -803,11 +797,34 @@ pluginManagement {
},
)
private val toml: Map<String, String> = run {
val lines = file("gradle/libs.versions.toml").also {
require(it.isFile) { "File $it doesn't exist" }
}.readLines()
val versions = mutableMapOf<String, String>()
var inVersions = false
val keyValuePattern = Regex("""^\s*([A-Za-z0-9._-]+)\s*=\s*"(.*?)"\s*(#.*)?$""")
for (raw in lines) {
val line = raw.trim()
if (line.isEmpty() || line.startsWith("#")) continue
if (line.startsWith("[")) {
inVersions = line == "[versions]"
continue
}
if (!inVersions) continue
keyValuePattern.find(line)?.let { m ->
val (_, key, value) = m.groupValues
versions[key] = value
}
}
return@run versions
}
val classpath = config.libs.filterIsInstance<Classpath>().map { lib ->
var suffix = ""
val version: String = when {
lib.version.startsWith("${identifier.auto}:") -> {
val mapType = lib.version.substring("${identifier.auto}:".length)
val mapType = lib.version.removePrefix("${identifier.auto}:")
val ver = when (mapType) {
"agp" -> versions.agp
"kotlin" -> versions.kotlin
@@ -819,6 +836,11 @@ pluginManagement {
suffix += identifier.fallbackSuffix
} ?: consts.DEFAULT_VERSION
}
lib.version.startsWith("${identifier.toml}:") -> {
toml.getValue(lib.version.removePrefix("${identifier.toml}:")).also {
suffix += identifier.tomlSpecifiedSuffix
}
}
else -> lib.version.also {
suffix += identifier.userSpecifiedSuffix
}
@@ -836,11 +858,16 @@ pluginManagement {
var suffix = ""
val version: String = when {
lib.version.startsWith("${identifier.auto}:") -> {
val automator = pluginVersionAutomatorMap[lib.version.substring("${identifier.auto}:".length)]!!
val automator = pluginVersionAutomatorMap.getValue(lib.version.removePrefix("${identifier.auto}:"))
val result = automator.invoke(config.kspVersionMap)
result["suffix"]?.let { s -> suffix += s }
result["version"] ?: throw Exception("Unknown version for plugin ${lib.id}")
}
lib.version.startsWith("${identifier.toml}:") -> {
toml.getValue(lib.version.removePrefix("${identifier.toml}:")).also {
suffix += identifier.tomlSpecifiedSuffix
}
}
else -> {
suffix += identifier.userSpecifiedSuffix
lib.version
@@ -923,18 +950,20 @@ pluginManagement {
}
repositories {
gradlePluginPortal()
mavenCentral()
google()
}
buildscript {
repositories {
mavenCentral()
google()
}
dependencies /* Android/Kotlin Gradle Plugin. */ {
dependencies {
notations.classpath.forEach { classpath(it) }
}
dependencies /* Apache Compress for utils.build.gradle module. */ {
classpath("org.apache.commons:commons-compress:1.28.0")
classpath("org.tukaani:xz:1.10")
}
}
plugins {
@@ -951,28 +980,10 @@ pluginManagement {
}
gradle.extra.apply {
set("platform", platform)
set("isCleanupPaddleOcr", config.isCleanupPaddleOcr)
set("isCleanupRapidOcr", config.isCleanupRapidOcr)
set("isHideConsoleInfoHintSuffix", config.isHideConsoleInfoHintSuffix)
}
gradle.beforeProject {
extensions.extraProperties["kotlinVersion"] = notations.classpath.find {
it.contains("kotlin-gradle-plugin")
}?.substringAfterLast(":") ?: config.fallbackKotlinVersion
extensions.extraProperties["platform"] = platform
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("./gradle/libs.versions.toml"))
}
}
}

View File

@@ -3,7 +3,6 @@ BUILD_TIME=1758543658764
COMPILE_SDK_VERSION=35
IMAGE_QUANT_CMAKE_VERSION=3.22.1
IMAGE_QUANT_NDK_VERSION=26.1.10909125
JAVA_VERSION=24
JAVA_VERSION_MAX_SUPPORTED=24
JAVA_VERSION_MIN_SUGGESTED=21
JAVA_VERSION_MIN_SUPPORTED=17