From 4778794e0e54ccdc5aa3a6d43c4185b1bbb0b55a Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Wed, 24 Sep 2025 16:31:06 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha6=20-=20=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=8C=96=20Gradle=20=E8=84=9A=E6=9C=AC,=20=E5=B0=86=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E6=9E=84=E5=BB=BA=E9=80=BB=E8=BE=91=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=20buildSrc=20=E5=B9=B6=E6=8A=BD=E8=B1=A1=E4=B8=BA?= =?UTF-8?q?=E7=BA=A6=E5=AE=9A=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_zh-Hans.json | 3 +- app/build.gradle.kts | 459 +++--------- build.gradle.kts | 4 +- buildSrc/.gitignore | 3 + buildSrc/build.gradle.kts | 102 +++ buildSrc/settings.gradle.kts | 19 + .../org/autojs/build/BuildProperties.kt | 76 ++ .../main/kotlin/org/autojs/build/Formatted.kt | 33 + .../kotlin/org/autojs/build/LibDeployer.kt | 437 +++++++++++ .../org/autojs/build/PropertiesPlugin.kt | 46 ++ .../org/autojs/build/SevenZExtractor.kt | 141 ++++ .../src/main/kotlin/org/autojs/build/Signs.kt | 19 + .../kotlin/org/autojs/build/SignsPlugin.kt | 45 ++ .../src/main/kotlin/org/autojs/build/Utils.kt | 408 +++++++++++ .../kotlin/org/autojs/build/UtilsPlugin.kt | 40 ++ .../main/kotlin/org/autojs/build/Versions.kt | 210 ++++++ .../kotlin/org/autojs/build/VersionsPlugin.kt | 40 ++ libs/imagequant/build.gradle | 45 +- libs/paddleocr/build.gradle | 53 +- libs/paddleocr/src/main/cpp/CMakeLists.txt | 5 +- libs/rapidocr/build.gradle | 74 +- libs/rapidocr/src/main/cpp/CMakeLists.txt | 5 +- libs/utils.build.gradle | 679 ------------------ modules/apk-parser/build.gradle | 7 +- modules/apk-signer/build.gradle | 7 +- modules/color-picker/build.gradle | 7 +- modules/jieba-analysis/build.gradle | 7 +- .../material-date-time-picker/build.gradle | 7 +- modules/material-dialogs/build.gradle | 7 +- settings.gradle.kts | 139 ++-- version.properties | 1 - 31 files changed, 1893 insertions(+), 1235 deletions(-) create mode 100644 buildSrc/.gitignore create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/settings.gradle.kts create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/BuildProperties.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/Formatted.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/LibDeployer.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/PropertiesPlugin.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/SevenZExtractor.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/Signs.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/SignsPlugin.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/Utils.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/UtilsPlugin.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/Versions.kt create mode 100644 buildSrc/src/main/kotlin/org/autojs/build/VersionsPlugin.kt delete mode 100644 libs/utils.build.gradle diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 6e38946d..d585f090 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -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", diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 790430fb..9bf4fbe8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { - 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) { - project.gradle.taskGraph.whenReady(object : Action { - 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)}" - -} diff --git a/build.gradle.kts b/build.gradle.kts index de5dacc2..15b2f77b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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() } } @@ -25,4 +25,4 @@ tasks { // @Legacy delete(rootProject.buildDir) delete(rootProject.layout.buildDirectory) } -} \ No newline at end of file +} diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 00000000..c3239a4b --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1,3 @@ +/build +/.gradle +/.kotlin \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..fcdef6b4 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -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 { + 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 { + 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 { + 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 { + 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)) + } + +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..9fed38fa --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,19 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + google() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/BuildProperties.kt b/buildSrc/src/main/kotlin/org/autojs/build/BuildProperties.kt new file mode 100644 index 00000000..70a90d78 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/BuildProperties.kt @@ -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 { + 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) + } + + } + +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/Formatted.kt b/buildSrc/src/main/kotlin/org/autojs/build/Formatted.kt new file mode 100644 index 00000000..886db5a0 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/Formatted.kt @@ -0,0 +1,33 @@ +package org.autojs.build + +class Formatted( + private val title: CharSequence, + private val contents: Collection = emptyList(), + private val subtitle: CharSequence? = null +) { + private val formattedOutput: List = 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")) + } +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/LibDeployer.kt b/buildSrc/src/main/kotlin/org/autojs/build/LibDeployer.kt new file mode 100644 index 00000000..cf463468 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/LibDeployer.kt @@ -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 { + 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() + 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 + } + +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/PropertiesPlugin.kt b/buildSrc/src/main/kotlin/org/autojs/build/PropertiesPlugin.kt new file mode 100644 index 00000000..0b047de5 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/PropertiesPlugin.kt @@ -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` 中应用此插件:
+ * + * ```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 { + override fun apply(project: Project) { + project.extensions.add("props", Utils.newProperties(project)) + } +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/SevenZExtractor.kt b/buildSrc/src/main/kotlin/org/autojs/build/SevenZExtractor.kt new file mode 100644 index 00000000..c223f227 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/SevenZExtractor.kt @@ -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 = 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() + } + +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/Signs.kt b/buildSrc/src/main/kotlin/org/autojs/build/Signs.kt new file mode 100644 index 00000000..22c67d5b --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/Signs.kt @@ -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() + } + } + +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/org/autojs/build/SignsPlugin.kt b/buildSrc/src/main/kotlin/org/autojs/build/SignsPlugin.kt new file mode 100644 index 00000000..be582f53 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/SignsPlugin.kt @@ -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` 中应用此插件:
+ * + * ```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 { + override fun apply(project: Project) { + project.extensions.add("signs", Utils.newSigns(project)) + } +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/Utils.kt b/buildSrc/src/main/kotlin/org/autojs/build/Utils.kt new file mode 100644 index 00000000..8d76fb5d --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/Utils.kt @@ -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 = 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 = emptyList(), + libsToDeploy: List = emptyList(), + cleanupFlagKey: String? = null, + extraFilesToDeleteOnClean: List = 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 { + 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(key) + if (cleanupEnabled && onlyClean.get()) { + libsToDeploy.forEach { it.clean() } + } else { + val projectName = project.extensions.extraProperties.getOrNull("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-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 { + 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 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 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 { + 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 { + override fun execute(task: Task) { + if (!isKotlinCompileTask(task, project)) return + + project.logInfo("[JvmConv] '${task.path}' class='${task.javaClass.name}'") + + // 优先尝试 Kotlin 2.x: compilerOptions.jvmTarget(Property) + 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) + } + } + +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/UtilsPlugin.kt b/buildSrc/src/main/kotlin/org/autojs/build/UtilsPlugin.kt new file mode 100644 index 00000000..1b180fae --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/UtilsPlugin.kt @@ -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` 中应用此插件:
+ * + * ```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 { + override fun apply(project: Project) { + // project.extensions.extraProperties.set("utils", Utils) + project.extensions.add("utils", Utils) + } +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/Versions.kt b/buildSrc/src/main/kotlin/org/autojs/build/Versions.kt new file mode 100644 index 00000000..f4ec4f77 --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/Versions.kt @@ -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() + var javaVersionInfoSuffix by Delegates.notNull() + + 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(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 { + var javaVersionInfoSuffix = "" + + gradle.extra.getOrNull("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("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) = 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("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) { + project.gradle.taskGraph.whenReady(object : Action { + 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" + } + +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/VersionsPlugin.kt b/buildSrc/src/main/kotlin/org/autojs/build/VersionsPlugin.kt new file mode 100644 index 00000000..155195ec --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/VersionsPlugin.kt @@ -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` 中应用此插件:
+ * + * ```kts + * plugins { + * id("org.autojs.build.versions") + * } + * + * versions.appVersionName + * versions.appVersionCode + * versions.sdkVersionCompile + * versions.sdkVersionMin + * versions.sdkVersionTarget + * versions.sdkVersionTargetInrt + * versions.vscodeExtRequiredVersion + * ``` + */ +class VersionsPlugin : Plugin { + override fun apply(project: Project) { + project.extensions.add("versions", Utils.newVersions(project)) + } +} diff --git a/libs/imagequant/build.gradle b/libs/imagequant/build.gradle index dfdf821f..a854dc98 100644 --- a/libs/imagequant/build.gradle +++ b/libs/imagequant/build.gradle @@ -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() -} \ No newline at end of file diff --git a/libs/paddleocr/build.gradle b/libs/paddleocr/build.gradle index 3de9920c..41f297bb 100644 --- a/libs/paddleocr/build.gradle +++ b/libs/paddleocr/build.gradle @@ -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() } -} \ No newline at end of file diff --git a/libs/paddleocr/src/main/cpp/CMakeLists.txt b/libs/paddleocr/src/main/cpp/CMakeLists.txt index 771031da..8d26f7f2 100644 --- a/libs/paddleocr/src/main/cpp/CMakeLists.txt +++ b/libs/paddleocr/src/main/cpp/CMakeLists.txt @@ -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}") diff --git a/libs/rapidocr/build.gradle b/libs/rapidocr/build.gradle index 77462005..eb0aef46 100644 --- a/libs/rapidocr/build.gradle +++ b/libs/rapidocr/build.gradle @@ -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 + implementation libs.core.ktx + implementation libs.appcompat + } - -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() } -} \ No newline at end of file diff --git a/libs/rapidocr/src/main/cpp/CMakeLists.txt b/libs/rapidocr/src/main/cpp/CMakeLists.txt index d79d1ca3..5df11f32 100644 --- a/libs/rapidocr/src/main/cpp/CMakeLists.txt +++ b/libs/rapidocr/src/main/cpp/CMakeLists.txt @@ -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}") diff --git a/libs/utils.build.gradle b/libs/utils.build.gradle deleted file mode 100644 index da19c1c7..00000000 --- a/libs/utils.build.gradle +++ /dev/null @@ -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 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 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 allEntries = sevenZFile.getEntries() - - List 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 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 contents - private String subtitle - private List formattedOutput - - Formatted(String title, Collection 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 - } - } - -} \ No newline at end of file diff --git a/modules/apk-parser/build.gradle b/modules/apk-parser/build.gradle index 62898a60..7233ba6f 100644 --- a/modules/apk-parser/build.gradle +++ b/modules/apk-parser/build.gradle @@ -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' diff --git a/modules/apk-signer/build.gradle b/modules/apk-signer/build.gradle index a223b175..f1966431 100644 --- a/modules/apk-signer/build.gradle +++ b/modules/apk-signer/build.gradle @@ -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 diff --git a/modules/color-picker/build.gradle b/modules/color-picker/build.gradle index 750fc295..293c07a9 100644 --- a/modules/color-picker/build.gradle +++ b/modules/color-picker/build.gradle @@ -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' } diff --git a/modules/jieba-analysis/build.gradle b/modules/jieba-analysis/build.gradle index fea60eea..7f47d244 100644 --- a/modules/jieba-analysis/build.gradle +++ b/modules/jieba-analysis/build.gradle @@ -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)' diff --git a/modules/material-date-time-picker/build.gradle b/modules/material-date-time-picker/build.gradle index b0ebe091..6a6e91e9 100644 --- a/modules/material-date-time-picker/build.gradle +++ b/modules/material-date-time-picker/build.gradle @@ -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 diff --git a/modules/material-dialogs/build.gradle b/modules/material-dialogs/build.gradle index 35180ece..f9dc4785 100644 --- a/modules/material-dialogs/build.gradle +++ b/modules/material-dialogs/build.gradle @@ -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 diff --git a/settings.gradle.kts b/settings.gradle.kts index fb1014f2..ee97e16f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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 - // @Reference gradle--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 // @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 // @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 = 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, Pair> { - // 支持: 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 = run { + val lines = file("gradle/libs.versions.toml").also { + require(it.isFile) { "File $it doesn't exist" } + }.readLines() + val versions = mutableMapOf() + 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().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")) - } - } -} \ No newline at end of file diff --git a/version.properties b/version.properties index 0fd0914c..07186e42 100644 --- a/version.properties +++ b/version.properties @@ -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