6.7.0 - Alpha6 - 模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
import com.android.build.gradle.internal.api.ApplicationVariantImpl
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.zip.CRC32
|
||||
import java.util.Locale.getDefault
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
|
||||
val globalApplicationId = "org.autojs.autojs6"
|
||||
plugins {
|
||||
id("org.autojs.build.utils")
|
||||
id("org.autojs.build.versions")
|
||||
id("org.autojs.build.signs")
|
||||
id("com.android.application")
|
||||
id("com.google.devtools.ksp")
|
||||
id("org.jetbrains.kotlin.android") /* kotlin("android") */
|
||||
}
|
||||
|
||||
val sign = Sign("$rootDir/sign.properties")
|
||||
val versions = Versions("$rootDir/version.properties")
|
||||
val globalApplicationId = "org.autojs.autojs6"
|
||||
|
||||
val flavorDimension = "channel"
|
||||
val flavorNameApp = "app"
|
||||
@@ -22,18 +23,13 @@ val flavorNameInrt = "inrt"
|
||||
val buildTypeDebug = "debug"
|
||||
val buildTypeRelease = "release"
|
||||
val buildActionAssemble = "assemble"
|
||||
val templateName = "template"
|
||||
|
||||
val taskNames = gradle.startParameter.taskNames
|
||||
val isAppAssembleTaskRequested = taskNames.any { it.contains(Regex("^(:?$flavorNameApp:)?$buildActionAssemble", IGNORE_CASE)) }
|
||||
val isInrtAssembleTaskRequested = taskNames.any { it.contains(Regex("^(:?$flavorNameApp:)?$buildActionAssemble$flavorNameInrt", IGNORE_CASE)) }
|
||||
val isInrtTaskRequested = taskNames.any { it.contains(flavorNameInrt, true) }
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("com.google.devtools.ksp")
|
||||
id("org.jetbrains.kotlin.android") /* kotlin("android") */
|
||||
}
|
||||
utils.registerTemplateApkCopy(project)
|
||||
|
||||
dependencies /* Unclassified */ {
|
||||
// Compose
|
||||
@@ -52,67 +48,66 @@ dependencies /* Unclassified */ {
|
||||
}
|
||||
|
||||
// LeakCanary
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
|
||||
debugImplementation(libs.leakcanary)
|
||||
|
||||
// Android supports
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.multidex:multidex:2.0.1")
|
||||
implementation(libs.cardview)
|
||||
implementation(libs.multidex)
|
||||
|
||||
// Material Components
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation(libs.material)
|
||||
|
||||
// SwipeRefreshLayout
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation(libs.swiperefreshlayout)
|
||||
|
||||
// ConstraintLayout
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
implementation(libs.constraintlayout)
|
||||
|
||||
// FlexboxLayout
|
||||
implementation("com.google.android.flexbox:flexbox:3.0.0")
|
||||
implementation(libs.flexbox)
|
||||
|
||||
// Common Markdown
|
||||
implementation("com.github.atlassian:commonmark-java:commonmark-parent-0.9.0")
|
||||
implementation(libs.commonmark)
|
||||
|
||||
// Flexmark Java HTML to Markdown Extensible Converter
|
||||
implementation("com.vladsch.flexmark:flexmark-html2md-converter:0.64.8")
|
||||
implementation(libs.flexmark.html2md)
|
||||
|
||||
// Licenses Dialog
|
||||
implementation("de.psdev.licensesdialog:licensesdialog:2.2.0")
|
||||
implementation(libs.licensesdialog)
|
||||
|
||||
// Apache Commons
|
||||
implementation("org.apache.commons:commons-lang3:3.18.0")
|
||||
implementation(libs.commons.lang3)
|
||||
|
||||
// Retrofit
|
||||
implementation("com.squareup.retrofit2:retrofit:2.12.0")
|
||||
implementation("com.squareup.retrofit2:converter-gson:2.12.0")
|
||||
implementation("com.squareup.retrofit2:adapter-rxjava2:2.12.0")
|
||||
implementation("com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2")
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.converter.gson)
|
||||
implementation(libs.retrofit.adapter.rxjava2)
|
||||
implementation(libs.retrofit2.kotlin.coroutines.adapter)
|
||||
|
||||
// Glide
|
||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
ksp("com.github.bumptech.glide:ksp:4.16.0")
|
||||
implementation(libs.glide)
|
||||
ksp(libs.glide.ksp)
|
||||
|
||||
// Joda Time
|
||||
implementation("joda-time:joda-time:2.14.0")
|
||||
implementation(libs.joda.time)
|
||||
|
||||
// Flurry
|
||||
implementation("com.flurry.android:analytics:14.4.0")
|
||||
// Analytics
|
||||
implementation(libs.analytics)
|
||||
|
||||
// Bugly
|
||||
implementation(project(":libs:com.tencent.bugly.crashreport-4.0.4"))
|
||||
|
||||
// OkHttp
|
||||
// implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.12")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
implementation(libs.okhttp)
|
||||
|
||||
// Webkit
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation(libs.webkit)
|
||||
|
||||
// Gson
|
||||
implementation("com.google.code.gson:gson:2.13.1")
|
||||
implementation(libs.gson)
|
||||
|
||||
// Zip4j
|
||||
implementation("net.lingala.zip4j:zip4j:2.11.5")
|
||||
implementation(libs.zip4j)
|
||||
|
||||
// Log4j
|
||||
// FIXME by SuperMonster003 on Aug 14, 2024.
|
||||
@@ -135,30 +130,30 @@ dependencies /* Unclassified */ {
|
||||
// ! - CVE-2019-17571, 评分: 9.8
|
||||
// ! 但 log4j 第二版本要求安卓 API 级别不低于 26,
|
||||
// ! 与最低 API 级别为 24 的当前项目无法兼容.
|
||||
implementation("log4j:log4j:1.2.17")
|
||||
implementation(libs.log4j)
|
||||
|
||||
// Android Logging Log4j
|
||||
implementation("de.mindpipe.android:android-logging-log4j:1.0.3")
|
||||
implementation(libs.android.logging.log4j)
|
||||
|
||||
// Preference
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation(libs.preference.ktx)
|
||||
|
||||
// RootShell
|
||||
// implementation("com.github.Stericson:RootShell:1.6")
|
||||
implementation(project(":libs:root-shell-1.6"))
|
||||
|
||||
// JDeferred
|
||||
implementation("org.jdeferred:jdeferred-android-aar:1.2.6")
|
||||
implementation(libs.jdeferred)
|
||||
|
||||
// Rx
|
||||
implementation("io.reactivex.rxjava2:rxjava:2.2.21")
|
||||
implementation("io.reactivex.rxjava2:rxandroid:2.1.1@aar")
|
||||
implementation(libs.rxjava)
|
||||
implementation(libs.rxandroid)
|
||||
|
||||
// Device Names
|
||||
implementation("com.jaredrummler:android-device-names:2.1.1")
|
||||
implementation(libs.android.device.names)
|
||||
|
||||
// Version Compare
|
||||
implementation("io.github.g00fy2:versioncompare:1.5.0")
|
||||
implementation(libs.versioncompare)
|
||||
|
||||
// Terminal Emulator
|
||||
implementation(project(":libs:jackpal.androidterm.libtermexec-1.0"))
|
||||
@@ -209,18 +204,18 @@ dependencies /* Unclassified */ {
|
||||
implementation(files("$rootDir/libs/javamail-android/mail.jar"))
|
||||
|
||||
// Shizuku
|
||||
implementation("dev.rikka.shizuku:api:13.1.5")
|
||||
implementation("dev.rikka.shizuku:provider:13.1.5")
|
||||
implementation(libs.shizuku.api)
|
||||
implementation(libs.shizuku.provider)
|
||||
|
||||
// ARSCLib
|
||||
implementation("io.github.reandroid:ARSCLib:1.3.5")
|
||||
implementation(libs.arsclib)
|
||||
|
||||
// Toaster
|
||||
implementation("com.github.getActivity:Toaster:12.6")
|
||||
implementation("com.github.getActivity:EasyWindow:10.3")
|
||||
implementation(libs.toaster)
|
||||
implementation(libs.easywindow)
|
||||
|
||||
// Pinyin4j
|
||||
implementation("com.belerweb:pinyin4j:2.5.1")
|
||||
implementation(libs.pinyin4j)
|
||||
|
||||
// Jieba Analysis (zh-CN: 结巴分词)
|
||||
// implementation("com.huaban:jieba-analysis:1.0.2")
|
||||
@@ -230,9 +225,9 @@ dependencies /* Unclassified */ {
|
||||
implementation(files("$rootDir/libs/tiny-sign-0.9.jar"))
|
||||
|
||||
// Room
|
||||
implementation("androidx.room:room-runtime:2.7.2")
|
||||
implementation("androidx.room:room-ktx:2.7.2")
|
||||
ksp("androidx.room:room-compiler:2.7.2")
|
||||
implementation(libs.room.runtime)
|
||||
implementation(libs.room.ktx)
|
||||
ksp(libs.room.compiler)
|
||||
|
||||
// ApkSig
|
||||
// implementation("com.android.tools.build:apksig:8.7.3")
|
||||
@@ -240,21 +235,21 @@ dependencies /* Unclassified */ {
|
||||
// ApkSigner
|
||||
implementation(project(":modules:apk-signer"))
|
||||
|
||||
// Spongy Castle
|
||||
implementation("com.madgag.spongycastle:prov:1.58.0.0")
|
||||
// Prov
|
||||
implementation(libs.prov)
|
||||
|
||||
// MQTT
|
||||
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0")
|
||||
implementation("org.eclipse.paho:org.eclipse.paho.android.service:1.1.1")
|
||||
implementation(libs.paho.client.mqttv3)
|
||||
implementation(libs.paho.android.service)
|
||||
|
||||
// Jsoup
|
||||
implementation("org.jsoup:jsoup:1.20.1")
|
||||
implementation(libs.jsoup)
|
||||
|
||||
// Material Date Time Picker
|
||||
implementation(project(":modules:material-date-time-picker"))
|
||||
|
||||
// ICU4J
|
||||
implementation("com.ibm.icu:icu4j:77.1")
|
||||
implementation(libs.icu4j)
|
||||
}
|
||||
|
||||
dependencies /* MIME */ {
|
||||
@@ -270,22 +265,22 @@ dependencies /* MIME */ {
|
||||
}
|
||||
|
||||
dependencies /* Test */ {
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test:runner:1.6.2")
|
||||
androidTestImplementation("org.junit.jupiter:junit-jupiter:5.13.0")
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.test.runner)
|
||||
androidTestImplementation(libs.junit.jupiter)
|
||||
}
|
||||
|
||||
dependencies /* Annotations */ {
|
||||
// Android Annotations
|
||||
implementation("org.androidannotations:androidannotations-api:4.8.0")
|
||||
implementation("androidx.annotation:annotation:1.9.1")
|
||||
ksp("org.androidannotations:androidannotations:4.8.0")
|
||||
implementation(libs.androidannotations.api)
|
||||
implementation(libs.annotation)
|
||||
ksp(libs.androidannotations)
|
||||
|
||||
// JCIP Annotations
|
||||
implementation("net.jcip:jcip-annotations:1.0")
|
||||
implementation(libs.jcip.annotations)
|
||||
|
||||
// EventBus
|
||||
implementation("org.greenrobot:eventbus:3.3.1")
|
||||
implementation(libs.eventbus)
|
||||
}
|
||||
|
||||
dependencies /* AppCompat */ {
|
||||
@@ -295,7 +290,7 @@ dependencies /* AppCompat */ {
|
||||
// ! zh-CN:
|
||||
// ! 查看 Appcompat 库的发行版本,
|
||||
// ! 可访问 https://developer.android.com/jetpack/androidx/releases/appcompat.
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation(libs.appcompat)
|
||||
|
||||
// AppCompat for legacy views (such as JsTextViewLegacy)
|
||||
implementation(project(":libs:androidx.appcompat-1.0.2")) {
|
||||
@@ -319,7 +314,7 @@ dependencies /* Material Dialogs */ {
|
||||
// # implementation("com.afollestad.material-dialogs:commons", cfg)
|
||||
// # }
|
||||
implementation(project(":modules:material-dialogs"))
|
||||
implementation("me.zhanghai.android.materialprogressbar:library:1.6.1")
|
||||
implementation(libs.materialprogressbar)
|
||||
}
|
||||
|
||||
dependencies /* Layout */ {
|
||||
@@ -328,7 +323,7 @@ dependencies /* Layout */ {
|
||||
implementation(project(":libs:expandable-layout-1.6.0"))
|
||||
|
||||
// Expandable RecyclerView
|
||||
implementation("com.bignerdranch.android:expandablerecyclerview:3.0.0-RC1")
|
||||
implementation(libs.expandablerecyclerview)
|
||||
|
||||
// Flexible Divider
|
||||
// implementation("com.yqritc:recyclerview-flexibledivider:1.4.0")
|
||||
@@ -337,13 +332,13 @@ dependencies /* Layout */ {
|
||||
|
||||
dependencies /* View */ {
|
||||
// RoundedImageView
|
||||
implementation("com.makeramen:roundedimageview:2.3.0")
|
||||
implementation(libs.roundedimageview)
|
||||
|
||||
// CircleImageView
|
||||
implementation("de.hdodenhof:circleimageview:3.1.0")
|
||||
implementation(libs.circleimageview)
|
||||
|
||||
// Animated SVG
|
||||
implementation("com.jaredrummler:animated-svg-view:1.0.6")
|
||||
implementation(libs.animated.svg.view)
|
||||
}
|
||||
|
||||
dependencies /* GitHub API */ {
|
||||
@@ -353,7 +348,7 @@ dependencies /* GitHub API */ {
|
||||
because("Compatibility for Android API Level < 26 (Android 8.0) [O]")
|
||||
version {
|
||||
strictly("2.8.0")
|
||||
because("Exception on newer versions: 'NoClassDefFoundError: org.apache.commons.io.IOUtils'")
|
||||
because("Exception on newer versions: 'NoClassDefFoundError: org.apache.commons.io.IObuildUtils'")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,16 +367,16 @@ dependencies /* GitHub API */ {
|
||||
|
||||
dependencies /* MLKit */ {
|
||||
// OCR
|
||||
implementation("com.google.mlkit:text-recognition-chinese:16.0.1")
|
||||
implementation(libs.text.recognition.chinese)
|
||||
|
||||
// Barcode
|
||||
implementation("com.google.mlkit:barcode-scanning:17.3.0")
|
||||
implementation(libs.barcode.scanning)
|
||||
}
|
||||
|
||||
dependencies /* OpenCC */ {
|
||||
// OpenCC
|
||||
// implementation("com.github.qichuan:android-opencc:1.2.0")
|
||||
implementation("com.github.brooklet:android-opencc:1.2.2")
|
||||
implementation(libs.opencc)
|
||||
}
|
||||
|
||||
dependencies /* Auto.js Extensions */ {
|
||||
@@ -402,8 +397,8 @@ dependencies /* Auto.js Extensions */ {
|
||||
// # implementation(project(":libs:Auto.js-ApkBuilder-1.0.3"))
|
||||
|
||||
// Extracted from com.github.hyb1996:MutableTheme:1.0.0
|
||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
||||
implementation("com.github.ozodrukh:CircularReveal:2.0.1")
|
||||
implementation(libs.recyclerview)
|
||||
implementation(libs.circularreveal)
|
||||
// @Legacy com.jrummyapps:colorpicker:2.1.7
|
||||
// @Integrated by SuperMonster003 on Mar 25, 2025.
|
||||
// # implementation("com.jaredrummler:colorpicker:1.1.0")
|
||||
@@ -457,7 +452,7 @@ android {
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
buildConfigField("String", "VERSION_DATE", "\"${Utils.getDateString("MMM d, yyyy", "GMT+08:00")}\"")
|
||||
buildConfigField("String", "VERSION_DATE", "\"${utils.getDateString("MMM d, yyyy", "GMT+08:00")}\"")
|
||||
buildConfigField("String", "VSCODE_EXT_REQUIRED_VERSION", "\"${versions.vscodeExtRequiredVersion}\"")
|
||||
buildConfigField("boolean", "is${flavorNameInrt.uppercaseFirstChar()}", "false")
|
||||
|
||||
@@ -520,47 +515,6 @@ android {
|
||||
// ! https://github.com/kkevsekk1/AutoX/blob/a6d482189291b460c3be60970b74c5321d26e457/inrt/build.gradle.kts#L93
|
||||
// noinspection ChromeOsAbiSupport
|
||||
ndk.abiFilters += ""
|
||||
|
||||
gradle.taskGraph.whenReady(object : Action<TaskExecutionGraph> {
|
||||
override fun execute(taskGraph: TaskExecutionGraph) {
|
||||
val taskName = "$buildActionAssemble${flavorNameInrt.uppercaseFirstChar()}${buildTypeRelease.uppercaseFirstChar()}"
|
||||
project.getTasksByName(taskName, true)
|
||||
.firstOrNull()
|
||||
?.doLast {
|
||||
copy {
|
||||
val src = "build/outputs/apk/$flavorNameInrt/$buildTypeRelease"
|
||||
|
||||
// @Reference to LZX284 (https://github.com/LZX284) by SuperMonster003 on Nov 16, 2023.
|
||||
val dst = "src/main/assets-$flavorNameApp"
|
||||
|
||||
val ext = Utils.FILE_EXTENSION_APK
|
||||
|
||||
if (!file(src).isDirectory) {
|
||||
return@copy
|
||||
}
|
||||
|
||||
from(src); into(dst)
|
||||
|
||||
val verName = versionName?.replace(Regex("\\s"), "-")?.lowercase()
|
||||
|
||||
/* e.g. inrt-v6.4.0-beta-universal.apk */
|
||||
val srcFileName = "$flavorNameInrt-v$verName-universal.$ext".also {
|
||||
if (!file(File(src, it)).exists()) {
|
||||
throw GradleException("Source file \"${file(File(src, it))}\" doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
val dstFileName = "$templateName.$ext"
|
||||
val isOverridden = file(File(dst, dstFileName)).exists()
|
||||
include(srcFileName)
|
||||
rename(srcFileName, dstFileName)
|
||||
println("Source: ${file(File(src, srcFileName))}")
|
||||
println("Destination: ${file(File(dst, dstFileName))}${if (isOverridden) " [overridden]" else ""}")
|
||||
}
|
||||
}
|
||||
?: println("$taskName doesn't exist in project ${project.name}")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
androidResources {
|
||||
@@ -676,12 +630,12 @@ android {
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (sign.isValid) {
|
||||
if (signs.isValid) {
|
||||
create(buildTypeRelease) {
|
||||
storeFile = sign.properties["storeFile"]?.let { file(it as String) }
|
||||
keyPassword = sign.properties["keyPassword"] as String
|
||||
keyAlias = sign.properties["keyAlias"] as String
|
||||
storePassword = sign.properties["storePassword"] as String
|
||||
storeFile = signs.properties["storeFile"]?.let { file(it as String) }
|
||||
keyPassword = signs.properties["keyPassword"] as String
|
||||
keyAlias = signs.properties["keyAlias"] as String
|
||||
storePassword = signs.properties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -691,7 +645,7 @@ android {
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
val niceSigningConfig = takeIf { sign.isValid }?.let {
|
||||
val niceSigningConfig = takeIf { signs.isValid }?.let {
|
||||
signingConfigs.getByName(buildTypeRelease)
|
||||
}
|
||||
debug {
|
||||
@@ -709,10 +663,12 @@ android {
|
||||
buildFeatures {
|
||||
aidl = true
|
||||
viewBinding = true
|
||||
|
||||
// @Hint by SuperMonster003 on Aug 14, 2023.
|
||||
// ! Substitution of "android.defaults.buildfeatures.buildconfig=true"
|
||||
// ! zh-CN: "android.defaults.buildfeatures.buildconfig=true" 的替代方案
|
||||
buildConfig = true
|
||||
|
||||
// @Archived by SuperMonster003 on Sep 23, 2024.
|
||||
// ! Jetpack Compose
|
||||
// # compose = true
|
||||
@@ -750,7 +706,14 @@ android {
|
||||
}
|
||||
|
||||
outputs.map { it as BaseVariantOutputImpl }.forEach {
|
||||
it.outputFileName = Utils.getOutputFileName(this@all as ApplicationVariantImpl, it)
|
||||
it.outputFileName = run {
|
||||
val variant = this@all as ApplicationVariantImpl
|
||||
val autojs = variant.applicationId.replace("^.+\\.(.+)$".toRegex(), "$1") // e.g. autojs6
|
||||
val version = variant.versionName.replace("\\s".toRegex(), "-") // e.g. 6.1.0
|
||||
val architecture = it.getFilter("ABI") ?: "universal"
|
||||
val extension = utils.FILE_EXTENSION_APK
|
||||
"$autojs-v$version-$architecture.$extension".lowercase(getDefault())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,7 +755,7 @@ tasks {
|
||||
listOf(flavorNameApp, flavorNameInrt).forEach { flavorName ->
|
||||
val src = "$flavorName/$buildTypeRelease"
|
||||
val dst = "${src}s"
|
||||
val ext = Utils.FILE_EXTENSION_APK
|
||||
val ext = utils.FILE_EXTENSION_APK
|
||||
|
||||
if (!file(src).isDirectory) {
|
||||
return@forEach
|
||||
@@ -801,7 +764,7 @@ tasks {
|
||||
from(src); into(dst); include("*.$ext")
|
||||
|
||||
rename { name ->
|
||||
Utils.digestCRC32(file("${src}/$name")).let { digest ->
|
||||
utils.digestCRC32(file("${src}/$name")).let { digest ->
|
||||
name.replace(Regex("^(.+?)(\\.$ext)$"), "$1-$digest$2")
|
||||
}
|
||||
}
|
||||
@@ -814,231 +777,3 @@ tasks {
|
||||
extra {
|
||||
versions.handleIfNeeded(project, flavorNameApp, listOf(buildTypeDebug, buildTypeRelease))
|
||||
}
|
||||
|
||||
gradle.beforeProject {
|
||||
extensions.extraProperties["compileSdk"] = versions.sdkVersionCompile
|
||||
extensions.extraProperties["minSdk"] = versions.sdkVersionMin
|
||||
extensions.extraProperties["targetSdk"] = versions.sdkVersionTarget
|
||||
}
|
||||
|
||||
class Sign(filePath: String) {
|
||||
|
||||
var isValid = false
|
||||
private set
|
||||
|
||||
val properties = Properties().also { props ->
|
||||
File(filePath).takeIf { it.exists() }?.let {
|
||||
props.load(FileInputStream(it))
|
||||
isValid = props.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Versions(filePath: String) {
|
||||
|
||||
private val properties = Properties()
|
||||
private val file = File(filePath).apply {
|
||||
if (!canRead()) {
|
||||
throw FileNotFoundException("Cannot read file '$filePath'")
|
||||
}
|
||||
properties.load(FileInputStream(this))
|
||||
}
|
||||
|
||||
val sdkVersionMin = properties["MIN_SDK_VERSION"].let { it as String }.toInt()
|
||||
val sdkVersionTarget = properties["TARGET_SDK_VERSION"].let { it as String }.toInt()
|
||||
val sdkVersionTargetInrt = properties["TARGET_SDK_VERSION_INRT"].let { it as String }.toInt()
|
||||
val sdkVersionCompile = properties["COMPILE_SDK_VERSION"].let { it as String }.toInt()
|
||||
val appVersionName = properties["VERSION_NAME"] as String
|
||||
val appVersionCode = properties["VERSION_BUILD"].let { it as String }.toInt()
|
||||
val vscodeExtRequiredVersion = properties["VSCODE_EXT_REQUIRED_VERSION"] as String
|
||||
|
||||
private val currentVersionInt = JavaVersion.current().majorVersion.toInt()
|
||||
|
||||
private val javaVersionMinSupported: Int = properties["JAVA_VERSION_MIN_SUPPORTED"]
|
||||
.let { it as String }.toInt()
|
||||
.also {
|
||||
if (currentVersionInt < it) {
|
||||
throw GradleException(
|
||||
"Current Gradle JDK version ${JavaVersion.current()} does not meet " +
|
||||
"the minimum requirement which $it is needed."
|
||||
)
|
||||
}
|
||||
}
|
||||
private val javaVersionMinSuggested: Int = properties["JAVA_VERSION_MIN_SUGGESTED"].let { it as String }.toInt()
|
||||
private val javaVersionMaxSupported: Int = properties["JAVA_VERSION_MAX_SUPPORTED"].let { it as String }.toInt()
|
||||
private val javaVersionRaw = properties["JAVA_VERSION"] as String
|
||||
private var javaVersionInfoSuffix = ""
|
||||
|
||||
val javaVersion: JavaVersion by lazy {
|
||||
val javaVersionInt = determineJavaVersion()
|
||||
gradle.beforeProject {
|
||||
extensions.extraProperties["javaVersion"] = javaVersionInt
|
||||
}
|
||||
JavaVersion.toVersion(javaVersionInt)
|
||||
}
|
||||
|
||||
private fun determineJavaVersion(): Int {
|
||||
if (gradle.extra.has("javaVersionOverriddenByUser")) {
|
||||
(gradle.extra.get("javaVersionOverriddenByUser") as? Int)?.let {
|
||||
javaVersionInfoSuffix += " [user-specified]"
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
var versionInt = javaVersionRaw.toInt()
|
||||
var isJvmCoercive = false
|
||||
|
||||
while (versionInt > javaVersionMinSupported) {
|
||||
if (JvmTarget.values().any { it.name.contains(Regex("_$versionInt$")) }) {
|
||||
break
|
||||
}
|
||||
versionInt -= 1
|
||||
isJvmCoercive = true
|
||||
}
|
||||
|
||||
if (isJvmCoercive) {
|
||||
javaVersionInfoSuffix += " [coercive-jvm-downgraded]"
|
||||
}
|
||||
|
||||
if (versionInt > currentVersionInt) {
|
||||
versionInt = currentVersionInt
|
||||
javaVersionInfoSuffix += " [consistent-downgraded]"
|
||||
}
|
||||
|
||||
if (gradle.extra.has("javaVersionCoercedByGradle")) {
|
||||
(gradle.extra["javaVersionCoercedByGradle"] as? Int)?.let {
|
||||
if (versionInt > it) {
|
||||
versionInt = it
|
||||
javaVersionInfoSuffix += " [coercive-gradle-downgraded]"
|
||||
}
|
||||
}
|
||||
}
|
||||
return versionInt
|
||||
}
|
||||
|
||||
private var isBuildNumberAutoIncremented = false
|
||||
private val minBuildTimeGap = Utils.hours2Millis(0.75)
|
||||
|
||||
private val isBuildGapEnough
|
||||
get() = properties["BUILD_TIME"]?.let {
|
||||
Date().time - (it as String).toLong() > minBuildTimeGap
|
||||
} == true
|
||||
|
||||
init {
|
||||
if (currentVersionInt < javaVersionMinSuggested) {
|
||||
logger.error(
|
||||
"It is recommended to upgrade current Gradle JDK version ${JavaVersion.current()} to $javaVersionMinSuggested or higher${
|
||||
if (javaVersionMaxSupported > 0) " (but not higher than $javaVersionMaxSupported)" else ""
|
||||
}."
|
||||
)
|
||||
}
|
||||
if (currentVersionInt > javaVersionMaxSupported) {
|
||||
logger.error(
|
||||
"It is recommended to downgrade current Gradle JDK version $currentVersionInt " +
|
||||
"to $javaVersionMaxSupported${if (javaVersionMaxSupported > javaVersionMinSuggested) " or lower (but not lower than $javaVersionMinSuggested)" else ""}, " +
|
||||
"as Gradle may be not compatible with JDK $currentVersionInt for now."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showInfo() {
|
||||
val title = "Version information for AutoJs6 app library"
|
||||
|
||||
val infoVerName = "Version name: $appVersionName"
|
||||
val infoVerCode = "Version code: ${if (isBuildNumberAutoIncremented) "${appVersionCode + 1} [auto-incremented]" else appVersionCode}"
|
||||
val infoVerSdk = "SDK versions: min [$sdkVersionMin] / target [$sdkVersionTarget] / compile [$sdkVersionCompile]"
|
||||
val infoVerJava = "Java version: $javaVersion${if (gradle.extra.has("isHideConsoleInfoHintSuffix") && gradle.extra.get("isHideConsoleInfoHintSuffix") == true) "" else javaVersionInfoSuffix}"
|
||||
|
||||
val maxLength = arrayOf(title, infoVerName, infoVerCode, infoVerSdk, infoVerJava).maxOf { it.length }
|
||||
|
||||
arrayOf(
|
||||
"=".repeat(maxLength),
|
||||
title,
|
||||
"-".repeat(maxLength),
|
||||
infoVerName,
|
||||
infoVerCode,
|
||||
infoVerSdk,
|
||||
infoVerJava,
|
||||
"=".repeat(maxLength),
|
||||
"",
|
||||
).forEach { println(it) }
|
||||
}
|
||||
|
||||
fun handleIfNeeded(project: Project, flavorName: String, targetBuildType: List<String>) {
|
||||
project.gradle.taskGraph.whenReady(object : Action<TaskExecutionGraph> {
|
||||
override fun execute(taskGraph: TaskExecutionGraph) {
|
||||
for (buildType in targetBuildType) {
|
||||
if (taskGraph.hasTask(Utils.getAssembleFullTaskName(project.name, flavorName, buildType))) {
|
||||
return appendToTask(project, flavorName, buildType)
|
||||
}
|
||||
}
|
||||
return showInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun appendToTask(project: Project, flavorName: String, buildType: String) {
|
||||
project.tasks.getByName(Utils.getAssembleTaskName(flavorName, buildType)).doLast {
|
||||
updateProperties()
|
||||
println()
|
||||
showInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProperties() {
|
||||
if (isBuildGapEnough) {
|
||||
val isBuildAppRelease = gradle.startParameter.taskNames.any {
|
||||
it.contains(Regex("^(:?$flavorNameApp:)?$buildActionAssemble($flavorNameApp|$flavorNameInrt)$buildTypeRelease", IGNORE_CASE))
|
||||
}
|
||||
if (!isBuildAppRelease) {
|
||||
properties["VERSION_BUILD"] = "${appVersionCode + 1}"
|
||||
isBuildNumberAutoIncremented = true
|
||||
}
|
||||
}
|
||||
properties["BUILD_TIME"] = "${Date().time}"
|
||||
properties.store(file.writer(), null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Utils {
|
||||
|
||||
const val FILE_EXTENSION_APK = "apk"
|
||||
|
||||
fun hours2Millis(hour: Double) = hour * 3.6e6
|
||||
|
||||
fun getDateString(format: String, zone: String): String {
|
||||
// e.g. May 23, 2011
|
||||
return SimpleDateFormat(format).apply { timeZone = TimeZone.getTimeZone(zone) }.format(Date())
|
||||
}
|
||||
|
||||
fun getOutputFileName(variant: ApplicationVariantImpl, output: BaseVariantOutputImpl): String {
|
||||
val autojs = variant.applicationId.replace("^.+\\.(.+)$".toRegex(), "$1") // e.g. autojs6
|
||||
val version = variant.versionName.replace("\\s".toRegex(), "-") // e.g. 6.1.0
|
||||
val architecture = output.getFilter("ABI") ?: "universal"
|
||||
val extension = FILE_EXTENSION_APK
|
||||
|
||||
return "$autojs-v$version-$architecture.$extension".lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
fun getAssembleTaskName(flavorName: String, buildType: String) = "assemble${capitalize(flavorName)}${capitalize(buildType)}"
|
||||
|
||||
fun getAssembleFullTaskName(projectName: String, flavorName: String, buildType: String) = ":$projectName:${getAssembleTaskName(flavorName, buildType)}"
|
||||
|
||||
fun digestCRC32(file: File): String {
|
||||
val fis = FileInputStream(file)
|
||||
val buffer = ByteArray(4096)
|
||||
var read: Int
|
||||
|
||||
return CRC32().let { o ->
|
||||
while (fis.read(buffer).also { read = it } > 0) {
|
||||
o.update(buffer, 0, read)
|
||||
}
|
||||
String.format("%08x", o.value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun capitalize(s: String) = "${s[0].uppercase(Locale.getDefault())}${s.substring(1)}"
|
||||
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
gradlePluginPortal()
|
||||
maven("https://jitpack.io")
|
||||
maven("https://maven.aliyun.com/repository/central")
|
||||
maven("https://maven.aliyun.com/repository/google")
|
||||
maven("https://maven.aliyun.com/repository/gradle-plugin")
|
||||
maven("https://maven.aliyun.com/repository/jcenter")
|
||||
maven("https://maven.aliyun.com/repository/public")
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
buildSrc/.gitignore
vendored
Normal file
3
buildSrc/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/build
|
||||
/.gradle
|
||||
/.kotlin
|
||||
102
buildSrc/build.gradle.kts
Normal file
102
buildSrc/build.gradle.kts
Normal file
@@ -0,0 +1,102 @@
|
||||
@file:Suppress("ObjectLiteralToLambda")
|
||||
|
||||
import java.util.*
|
||||
|
||||
plugins {
|
||||
`kotlin-dsl` /* kotlin("jvm") */
|
||||
`java-gradle-plugin`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.apache.commons.compress)
|
||||
implementation(libs.tukaani.xz)
|
||||
|
||||
implementation(gradleApi())
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
create("buildUtils", object : Action<PluginDeclaration> {
|
||||
override fun execute(t: PluginDeclaration) {
|
||||
t.id = "org.autojs.build.utils"
|
||||
t.implementationClass = "org.autojs.build.UtilsPlugin"
|
||||
t.displayName = "AutoJs6 Build Utils Plugin"
|
||||
t.description = "Provides utilities for downloading, extracting archives, and version helpers."
|
||||
}
|
||||
})
|
||||
create("buildVersions", object : Action<PluginDeclaration> {
|
||||
override fun execute(t: PluginDeclaration) {
|
||||
t.id = "org.autojs.build.versions"
|
||||
t.implementationClass = "org.autojs.build.VersionsPlugin"
|
||||
t.displayName = "AutoJs6 Versions Plugin"
|
||||
t.description = "Provides version helpers."
|
||||
}
|
||||
})
|
||||
create("buildSigns", object : Action<PluginDeclaration> {
|
||||
override fun execute(t: PluginDeclaration) {
|
||||
t.id = "org.autojs.build.signs"
|
||||
t.implementationClass = "org.autojs.build.SignsPlugin"
|
||||
t.displayName = "AutoJs6 Signs Plugin"
|
||||
t.description = "Provides signing helpers."
|
||||
}
|
||||
})
|
||||
create("buildProperties", object : Action<PluginDeclaration> {
|
||||
override fun execute(t: PluginDeclaration) {
|
||||
t.id = "org.autojs.build.properties"
|
||||
t.implementationClass = "org.autojs.build.PropertiesPlugin"
|
||||
t.displayName = "AutoJs6 Properties Plugin"
|
||||
t.description = "Provides properties helpers."
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
run determineBuildSrcJdk@{
|
||||
val propsFile: File = rootDir.parentFile.resolve("version.properties")
|
||||
val minSupported: Int = Properties().let { props ->
|
||||
require(propsFile.isFile) {
|
||||
"version.properties not found in root directory"
|
||||
}
|
||||
propsFile.inputStream().use { props.load(it) }
|
||||
val minSupportedVersion = props.getProperty("JAVA_VERSION_MIN_SUPPORTED")
|
||||
require(minSupportedVersion != null) {
|
||||
"version.properties does not contain \"JAVA_VERSION_MIN_SUPPORTED\""
|
||||
}
|
||||
minSupportedVersion.toInt()
|
||||
}
|
||||
val current = JavaVersion.current().majorVersion.toIntOrNull() ?: minSupported
|
||||
|
||||
fun tryAdjustByKotlinJvmTarget(sourceVersion: Int): Int {
|
||||
return runCatching {
|
||||
val cls = Class.forName("org.jetbrains.kotlin.gradle.dsl.JvmTarget")
|
||||
val enumConstants = cls.enumConstants ?: return sourceVersion
|
||||
var tmpVersion = sourceVersion
|
||||
while (tmpVersion > minSupported) {
|
||||
val wanted = "JVM_$tmpVersion"
|
||||
if (enumConstants.any { it?.toString().equals(wanted, ignoreCase = true) }) {
|
||||
return tmpVersion
|
||||
}
|
||||
tmpVersion -= 1
|
||||
}
|
||||
return@runCatching sourceVersion
|
||||
}.getOrDefault(sourceVersion)
|
||||
}
|
||||
|
||||
tryAdjustByKotlinJvmTarget(maxOf(current, minSupported)).also {
|
||||
println("Toolchain: selected [$it] / current [$current] / min [$minSupported]")
|
||||
}
|
||||
}.let { jdk ->
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(jdk)
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain.languageVersion.set(JavaLanguageVersion.of(jdk))
|
||||
}
|
||||
|
||||
}
|
||||
19
buildSrc/settings.gradle.kts
Normal file
19
buildSrc/settings.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
76
buildSrc/src/main/kotlin/org/autojs/build/BuildProperties.kt
Normal file
76
buildSrc/src/main/kotlin/org/autojs/build/BuildProperties.kt
Normal file
@@ -0,0 +1,76 @@
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.Project
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
|
||||
class BuildProperties private constructor(
|
||||
private val file: File,
|
||||
private val props: Properties,
|
||||
) {
|
||||
|
||||
val path: String get() = file.absolutePath
|
||||
|
||||
operator fun get(propertyName: String): String = when {
|
||||
propertyName.contains("/") -> {
|
||||
get(propertyName.split("/"))
|
||||
}
|
||||
else -> requireValue(propertyName)
|
||||
}
|
||||
|
||||
operator fun get(propertyInfo: List<String>): String {
|
||||
val (lib, body) = propertyInfo
|
||||
return requireValue(lib, body)
|
||||
}
|
||||
|
||||
private fun requireString(key: String, alternate: String): String {
|
||||
return props.getProperty(key)
|
||||
?: props.getProperty(alternate)
|
||||
?: throw IllegalStateException("Property '$key' is missing in '${file.absolutePath}'")
|
||||
}
|
||||
|
||||
fun requireString(key: String): String {
|
||||
return props.getProperty(key)
|
||||
?: throw IllegalStateException("Property '$key' is missing in '${file.absolutePath}'")
|
||||
}
|
||||
|
||||
fun requireInt(key: String): Int = requireString(key).toInt()
|
||||
|
||||
fun getIntOrNull(key: String): Int? = props.getProperty(key)?.toIntOrNull()
|
||||
|
||||
private fun requireValue(name: String): String {
|
||||
return when (val value = requireString("${name}_VERSION", name)) {
|
||||
"PUBLIC" -> requireString("PUBLIC_${name}_VERSION", "PUBLIC_${name}")
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireValue(lib: String, body: String): String {
|
||||
return when (val value = requireString("${lib}_${body}_VERSION", "${lib}_${body}")) {
|
||||
"PUBLIC" -> requireString("PUBLIC_${body}_VERSION", "PUBLIC_${body}")
|
||||
else -> value
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun loadFrom(project: Project): BuildProperties {
|
||||
return loadFrom("${project.rootDir}/version.properties")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadFrom(filePath: String): BuildProperties {
|
||||
val f = File(filePath)
|
||||
if (!f.canRead()) {
|
||||
throw FileNotFoundException("Cannot read file '$filePath'")
|
||||
}
|
||||
val p = Properties()
|
||||
f.inputStream().use { p.load(it) }
|
||||
return BuildProperties(f, p)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
33
buildSrc/src/main/kotlin/org/autojs/build/Formatted.kt
Normal file
33
buildSrc/src/main/kotlin/org/autojs/build/Formatted.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
package org.autojs.build
|
||||
|
||||
class Formatted(
|
||||
private val title: CharSequence,
|
||||
private val contents: Collection<CharSequence> = emptyList(),
|
||||
private val subtitle: CharSequence? = null
|
||||
) {
|
||||
private val formattedOutput: List<CharSequence> = run {
|
||||
val maxLength = (listOfNotNull(title, subtitle) + contents).maxOf { it.length }
|
||||
buildList {
|
||||
add("=".repeat(maxLength))
|
||||
add(title)
|
||||
subtitle?.let { add(it) }
|
||||
if (contents.isNotEmpty()) add("-".repeat(maxLength))
|
||||
addAll(contents)
|
||||
add("=".repeat(maxLength))
|
||||
add("")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun print(contentsMatters: Boolean = false) {
|
||||
formattedOutput.forEach {
|
||||
if (!contentsMatters || contents.isNotEmpty()) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun throwException() {
|
||||
throw Exception(formattedOutput.joinToString("\n"))
|
||||
}
|
||||
}
|
||||
437
buildSrc/src/main/kotlin/org/autojs/build/LibDeployer.kt
Normal file
437
buildSrc/src/main/kotlin/org/autojs/build/LibDeployer.kt
Normal file
@@ -0,0 +1,437 @@
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.extra
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.math.BigInteger
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.URI
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.math.max
|
||||
|
||||
class LibDeployer(
|
||||
private val project: Project,
|
||||
private val name: String,
|
||||
private val downloadUrl: String,
|
||||
) {
|
||||
private var sourceDir: String = File.separator
|
||||
private var sourceFile: File = project.file(File.separator)
|
||||
|
||||
private var destDir: String = File.separator
|
||||
private var destFile: File = project.file(File.separator)
|
||||
|
||||
private val cacheRootFile: File = project.file("cache").apply { mkdirs() }
|
||||
private val cacheFileName: String
|
||||
private val cacheFileExtensionName: String
|
||||
private val cacheFile: File
|
||||
|
||||
private val shouldPrintProgress: Boolean
|
||||
get() = project.gradle.extra["platform"]?.let {
|
||||
it::class.java.getMethod("getShouldPrintProgress")
|
||||
.apply { isAccessible = true }
|
||||
.invoke(it)
|
||||
} == true
|
||||
|
||||
init {
|
||||
val extracted = extractFileFromUrl(downloadUrl)
|
||||
cacheFileName = "${extracted.fileName}-[${generateShortMd5String(downloadUrl).lowercase()}]"
|
||||
cacheFileExtensionName = extracted.extensionName
|
||||
cacheFile = project.file(File(cacheRootFile, "$cacheFileName.$cacheFileExtensionName"))
|
||||
}
|
||||
|
||||
private fun getSkipFile(): File = project.file(File(destFile, "$cacheFileName.skip"))
|
||||
private fun getTempOutFile(): File = project.file(File(destFile, "temp-extracted"))
|
||||
|
||||
fun setSourceDir(sourceDir: String): LibDeployer {
|
||||
this.sourceDir = sourceDir
|
||||
this.sourceFile = project.file(sourceDir)
|
||||
return this
|
||||
}
|
||||
|
||||
fun setDestDir(destDir: String): LibDeployer {
|
||||
val dest = if (destDir.startsWith(File.separator)) {
|
||||
project.file(destDir.substring(1))
|
||||
} else {
|
||||
project.file(destDir)
|
||||
}
|
||||
dest.mkdirs()
|
||||
this.destDir = destDir
|
||||
this.destFile = dest
|
||||
return this
|
||||
}
|
||||
|
||||
fun deploy() {
|
||||
val tempOutFile = getTempOutFile()
|
||||
if (tempOutFile.exists()) {
|
||||
project.delete(tempOutFile)
|
||||
}
|
||||
|
||||
val (shouldDownload, shouldExtract) = checkCacheAndSkipFiles()
|
||||
|
||||
var needExtract = shouldExtract
|
||||
if (shouldDownload) {
|
||||
printDownloadInfo()
|
||||
downloadWithRetry()
|
||||
needExtract = true
|
||||
}
|
||||
|
||||
if (needExtract) {
|
||||
printExtractInfo()
|
||||
try {
|
||||
extractCacheFile()
|
||||
} catch (e: Exception) {
|
||||
println()
|
||||
println("Cache file was deleted as there is an error during extraction")
|
||||
println("Cache file: ${cacheFile.absolutePath}")
|
||||
cacheFile.delete()
|
||||
e.message?.let { println("Error message: $it") }
|
||||
println()
|
||||
throw e
|
||||
}
|
||||
generateMd5File(cacheFile)
|
||||
}
|
||||
}
|
||||
|
||||
fun clean() {
|
||||
project.delete(getSkipFile())
|
||||
project.delete(getTempOutFile())
|
||||
deleteDestAccordingToSrc()
|
||||
deleteCacheAccordingToMd5()
|
||||
}
|
||||
|
||||
private data class ExtractedFile(val fileName: String, val extensionName: String)
|
||||
|
||||
private fun extractFileFromUrl(url: String): ExtractedFile {
|
||||
val fileNameWithExtension = url.substringAfterLast('/')
|
||||
val dot = fileNameWithExtension.lastIndexOf('.')
|
||||
val fileNameRaw = if (dot >= 0) fileNameWithExtension.take(dot) else fileNameWithExtension
|
||||
val fileName = fileNameRaw.lowercase()
|
||||
.replace(Regex("\\s+"), "")
|
||||
.replace(Regex("[^a-z0-9.]"), "-")
|
||||
val extension = if (dot >= 0) fileNameWithExtension.substring(dot + 1) else ""
|
||||
return ExtractedFile(fileName, extension)
|
||||
}
|
||||
|
||||
private fun checkCacheAndSkipFiles(): Pair<Boolean, Boolean> {
|
||||
var shouldDownload = true
|
||||
var shouldExtract = true
|
||||
|
||||
val skip = getSkipFile()
|
||||
if (skip.exists()) {
|
||||
println("No need to download or extract \"$name\" archive file as the \"skip file\" exists")
|
||||
shouldDownload = false
|
||||
shouldExtract = false
|
||||
println()
|
||||
} else if (cacheFile.exists()) {
|
||||
if (validateMd5File(cacheFile)) {
|
||||
println("No need to download \"$name\" archive file as the cache file exists and is valid")
|
||||
shouldDownload = false
|
||||
println("Cache file of \"$name\" needs to be extracted as the \"skip file\" doesn't exist")
|
||||
} else {
|
||||
println("Cache file of \"$name\" was deleted as it is invalid")
|
||||
println("Cache file: $cacheFile")
|
||||
project.delete(cacheFile)
|
||||
val md5File = File(cacheFile.parentFile, cacheFile.name + ".md5")
|
||||
if (md5File.exists()) {
|
||||
println("MD5 file of \"$name\" was deleted as it is unreliable")
|
||||
println("MD5 file: $md5File")
|
||||
project.delete(md5File)
|
||||
}
|
||||
}
|
||||
println()
|
||||
}
|
||||
return shouldDownload to shouldExtract
|
||||
}
|
||||
|
||||
private fun validateMd5File(file: File): Boolean {
|
||||
val md5File = File(file.parentFile, file.name + ".md5")
|
||||
if (!md5File.exists()) return false
|
||||
val expected = md5File.readText().trim().uppercase()
|
||||
val actual = generateMd5String(file).uppercase()
|
||||
return expected == actual
|
||||
}
|
||||
|
||||
private fun generateMd5File(file: File) {
|
||||
val md5File = File(file.parentFile, file.name + ".md5")
|
||||
println("Generating MD5...")
|
||||
val md5 = generateMd5String(file)
|
||||
md5File.writeText(md5)
|
||||
println("MD5 generated: $md5")
|
||||
println()
|
||||
}
|
||||
|
||||
private fun generateMd5String(file: File): String {
|
||||
FileInputStream(file).use { fis ->
|
||||
val channel: FileChannel = fis.channel
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
val buffer = ByteBuffer.allocate(4096)
|
||||
while (channel.read(buffer) > 0) {
|
||||
buffer.flip()
|
||||
md.update(buffer)
|
||||
buffer.clear()
|
||||
}
|
||||
return BigInteger(1, md.digest()).toString(16).padStart(32, '0')
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateShortMd5String(s: String): String {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
md.update(s.toByteArray())
|
||||
return BigInteger(1, md.digest()).toString(32)
|
||||
}
|
||||
|
||||
private fun printDownloadInfo() {
|
||||
val title = "Download \"$name\" archive file for \"${project.extensions.extraProperties["projectName"]}\" Gradle project"
|
||||
val srcInfo = "Source: $downloadUrl"
|
||||
val destInfo = "Destination: $cacheFile"
|
||||
val hintInfo = listOf(
|
||||
"If the download gets stuck and won't finish,",
|
||||
"try downloading the source file with tools like IDM (Internet Download Manager),",
|
||||
"then renaming it into the destination path above."
|
||||
)
|
||||
|
||||
val maxLength = listOf(title, srcInfo, destInfo, *hintInfo.toTypedArray()).maxOf { it.length }
|
||||
listOf(
|
||||
"=".repeat(maxLength),
|
||||
title,
|
||||
"-".repeat(maxLength),
|
||||
srcInfo,
|
||||
destInfo,
|
||||
"-".repeat(maxLength),
|
||||
hintInfo.joinToString("\n"),
|
||||
"=".repeat(maxLength),
|
||||
""
|
||||
).forEach { println(it) }
|
||||
}
|
||||
|
||||
private fun printExtractInfo() {
|
||||
val title = "Extract the archive file for \"${project.extensions.extraProperties["projectName"]}\" Gradle project"
|
||||
val srcInfo = "Source: $cacheFile"
|
||||
val destInfo = "Destination: $destFile"
|
||||
val items = listOf(title, srcInfo, destInfo)
|
||||
val maxLength = items.maxOf { it.length }
|
||||
listOf(
|
||||
"=".repeat(maxLength),
|
||||
title,
|
||||
"-".repeat(maxLength),
|
||||
srcInfo,
|
||||
destInfo,
|
||||
"=".repeat(maxLength),
|
||||
""
|
||||
).forEach { println(it) }
|
||||
}
|
||||
|
||||
private fun downloadWithRetry(maxRetries: Int = 3, retryDelayMs: Long = 2000) {
|
||||
var attempt = 0
|
||||
var success = false
|
||||
while (attempt < maxRetries && !success) {
|
||||
try {
|
||||
attempt++
|
||||
download()
|
||||
success = true
|
||||
} catch (_: SocketTimeoutException) {
|
||||
println("Attempt $attempt/$maxRetries failed: Connection timed out. Retrying...")
|
||||
} catch (e: IOException) {
|
||||
println("Attempt $attempt/$maxRetries failed: ${e.message}. Retrying...")
|
||||
}
|
||||
if (!success) {
|
||||
if (attempt < maxRetries) {
|
||||
Thread.sleep(retryDelayMs)
|
||||
} else {
|
||||
println("Download failed after $maxRetries attempts.")
|
||||
throw GradleException("Download failed after $maxRetries attempts", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun download() {
|
||||
cacheFile.parentFile.mkdirs()
|
||||
val urlConn = URI(downloadUrl).toURL().openConnection().apply {
|
||||
connectTimeout = 120_000
|
||||
readTimeout = 90_000
|
||||
}
|
||||
val fileSize = urlConn.contentLengthLong
|
||||
urlConn.getInputStream().use { input ->
|
||||
FileOutputStream(cacheFile).use { output ->
|
||||
val buffer = ByteArray(8192)
|
||||
var downloaded = 0L
|
||||
var read: Int
|
||||
if (shouldPrintProgress && fileSize <= 0) {
|
||||
println("\rDownloading...")
|
||||
}
|
||||
while (true) {
|
||||
read = input.read(buffer)
|
||||
if (read == -1) break
|
||||
output.write(buffer, 0, read)
|
||||
downloaded += read
|
||||
if (shouldPrintProgress && fileSize > 0) {
|
||||
val progress = downloaded * 100.0 / fileSize
|
||||
val bar = generateProgressBar(progress)
|
||||
print(String.format(Locale.getDefault(), "\rDownloading... [ %s ] %.2f%%", bar, progress))
|
||||
System.out.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val path = cacheFile.absolutePath
|
||||
if (fileSize > 0) {
|
||||
val formattedSize = formatFileSize(fileSize)
|
||||
print(String.format("\rDownload complete [ %s | %s ]\n", path, formattedSize))
|
||||
} else {
|
||||
print(String.format("\rDownload complete [ %s ]\n", path))
|
||||
}
|
||||
System.out.flush()
|
||||
println()
|
||||
}
|
||||
|
||||
private fun extractCacheFile() {
|
||||
when (cacheFileExtensionName.lowercase()) {
|
||||
"zip" -> handleZip()
|
||||
"7z" -> handleSevenZip()
|
||||
else -> throw GradleException("Unknown archive file type: $cacheFileExtensionName")
|
||||
}
|
||||
println("All files extracted into [ $destFile ]")
|
||||
val skip = getSkipFile()
|
||||
if (!skip.exists()) {
|
||||
skip.parentFile.mkdirs()
|
||||
skip.createNewFile()
|
||||
println("File \"${skip.name}\" created")
|
||||
}
|
||||
println()
|
||||
}
|
||||
|
||||
private fun handleZip() {
|
||||
val sourceDirPath = File(sourceDir).path.let { p ->
|
||||
if (p.startsWith(File.separator)) p.substring(1) else p
|
||||
}
|
||||
val tempOut = getTempOutFile()
|
||||
val zipForTotal = ZipFile(cacheFile)
|
||||
val entriesAll = zipForTotal.entries()
|
||||
val entries = mutableListOf<ZipEntry>()
|
||||
var totalExtractedSize = 0L
|
||||
while (entriesAll.hasMoreElements()) {
|
||||
val entry = entriesAll.nextElement()
|
||||
val entryName = File(entry.name).path
|
||||
if (entryName.startsWith(sourceDirPath)) {
|
||||
entries += entry
|
||||
if (!entry.isDirectory) totalExtractedSize += entry.size
|
||||
}
|
||||
}
|
||||
zipForTotal.close()
|
||||
|
||||
val zip = ZipFile(cacheFile)
|
||||
val zipEntries = zip.entries()
|
||||
var processed = 0
|
||||
val totalEntries = entries.size
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
val entry = zipEntries.nextElement()
|
||||
val entryName = File(entry.name).path
|
||||
if (!entryName.startsWith(sourceDirPath)) continue
|
||||
val outFile = project.file(File(tempOut, entryName.substring(sourceDirPath.length)))
|
||||
if (entry.isDirectory) {
|
||||
outFile.mkdirs()
|
||||
} else {
|
||||
outFile.parentFile.mkdirs()
|
||||
zip.getInputStream(entry).use { input ->
|
||||
FileOutputStream(outFile).use { output ->
|
||||
val buffer = ByteArray(8192)
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read == -1) break
|
||||
output.write(buffer, 0, read)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldPrintProgress) {
|
||||
val progress = processed * 100.0 / max(1, totalEntries)
|
||||
val bar = generateProgressBar(progress)
|
||||
print(String.format(Locale.getDefault(), "\rExtracting... [ %s ] %.2f%%", bar, progress))
|
||||
System.out.flush()
|
||||
}
|
||||
processed++
|
||||
}
|
||||
val formatted = formatFileSize(totalExtractedSize)
|
||||
print(String.format("\rExtraction complete [ %s | %s ]\n", destFile.absolutePath, formatted))
|
||||
System.out.flush()
|
||||
println()
|
||||
zip.close()
|
||||
project.copy {
|
||||
from(tempOut)
|
||||
into(destFile)
|
||||
}
|
||||
project.delete(tempOut)
|
||||
}
|
||||
|
||||
private fun handleSevenZip() {
|
||||
val tempOut = getTempOutFile()
|
||||
val totalBytes = SevenZExtractor.extractDirectoryFrom7z(
|
||||
cacheFile, sourceDir, tempOut, shouldPrintProgress
|
||||
)
|
||||
val formatted = formatFileSize(max(totalBytes, 0L))
|
||||
print(String.format("\rExtraction complete [ %s | %s ]\n", destFile.absolutePath, formatted))
|
||||
System.out.flush()
|
||||
project.copy {
|
||||
from(tempOut)
|
||||
into(destFile)
|
||||
}
|
||||
project.delete(tempOut)
|
||||
}
|
||||
|
||||
private fun deleteDestAccordingToSrc() {
|
||||
project.delete(destFile.absolutePath)
|
||||
var tmp: File? = destFile.parentFile
|
||||
while (tmp != null && tmp != project.projectDir) {
|
||||
val list = tmp.listFiles()?.toList().orEmpty()
|
||||
if (list.isEmpty()) {
|
||||
println("Delete empty directory: ${tmp.absolutePath}")
|
||||
project.delete(tmp)
|
||||
}
|
||||
tmp = tmp.parentFile
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteCacheAccordingToMd5() {
|
||||
cacheRootFile.listFiles()?.forEach { f ->
|
||||
if (!f.name.endsWith(".md5")) {
|
||||
val md5 = File(f.parentFile, f.name + ".md5")
|
||||
if (!md5.exists() || !validateMd5File(f)) {
|
||||
project.delete(f)
|
||||
println("Delete cache file: ${f.absolutePath}")
|
||||
if (md5.exists()) {
|
||||
project.delete(md5)
|
||||
println("Delete MD5 file: ${md5.absolutePath}")
|
||||
}
|
||||
println()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateProgressBar(progress: Double, length: Int = 30): String {
|
||||
val filledLen = (length * progress / 100.0).toInt().coerceIn(0, length)
|
||||
val filled = "#".repeat(filledLen)
|
||||
val empty = "-".repeat(length - filledLen)
|
||||
return filled + empty
|
||||
}
|
||||
|
||||
private fun formatFileSize(size: Long): String = when {
|
||||
// @formatter:off
|
||||
size < 1024L -> String.format(Locale.getDefault(), "%d B", size)
|
||||
size < 1024L * 1024 -> String.format(Locale.getDefault(), "%.2f KB", size / (1024.0))
|
||||
size < 1024L * 1024 * 1024 -> String.format(Locale.getDefault(), "%.2f MB", size / (1024.0 * 1024))
|
||||
else -> String.format(Locale.getDefault(), "%.2f GB", size / (1024.0 * 1024 * 1024))
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
/**
|
||||
* A Gradle plugin that provides properties helpers.
|
||||
*
|
||||
* zh-CN: Gradle properties 辅助工具.
|
||||
*
|
||||
* - `id`: "org.autojs.build.properties"
|
||||
* - `implementationClass`: "org.autojs.build.PropertiesPlugin"
|
||||
* - `displayName`: "AutoJs6 Properties Plugin"
|
||||
* - `description`: "Provides properties helpers."
|
||||
*
|
||||
* Apply this plugin to your Android module's `build.gradle.kts`:
|
||||
*
|
||||
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
|
||||
*
|
||||
* ```kts
|
||||
* plugins {
|
||||
* id("org.autojs.build.properties")
|
||||
* }
|
||||
*
|
||||
* props["MIN_SDK"]
|
||||
* props["COMPILE_SDK"]
|
||||
* props["TARGET_SDK"]
|
||||
*
|
||||
* props["RAPID_OCR/NDK"]
|
||||
* props["RAPID_OCR/CMAKE"]
|
||||
*
|
||||
* props["PADDLE_OCR/NDK"]
|
||||
* props["PADDLE_OCR/CMAKE"]
|
||||
* props["PADDLE_OCR/OPENCV"]
|
||||
*
|
||||
* props["IMAGE_QUANT/NDK"]
|
||||
* props["IMAGE_QUANT/CMAKE"]
|
||||
* ```
|
||||
*/
|
||||
class PropertiesPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.extensions.add("props", Utils.newProperties(project))
|
||||
}
|
||||
}
|
||||
141
buildSrc/src/main/kotlin/org/autojs/build/SevenZExtractor.kt
Normal file
141
buildSrc/src/main/kotlin/org/autojs/build/SevenZExtractor.kt
Normal file
@@ -0,0 +1,141 @@
|
||||
package org.autojs.build
|
||||
|
||||
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry
|
||||
import org.apache.commons.compress.archivers.sevenz.SevenZFile
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
object SevenZExtractor {
|
||||
|
||||
@JvmStatic
|
||||
fun extractDirectoryFrom7z(
|
||||
archive: File,
|
||||
sourceDir: String,
|
||||
outDir: File,
|
||||
shouldPrintProgress: Boolean = true
|
||||
): Long {
|
||||
require(archive.isFile) { "7z archive not found: ${archive.absolutePath}" }
|
||||
if (!outDir.exists()) outDir.mkdirs()
|
||||
|
||||
val sourceDirPath = normalizePrefix(sourceDir)
|
||||
var sevenZFile: SevenZFile? = null
|
||||
|
||||
var totalBytes: Long
|
||||
var writtenBytes = 0L
|
||||
val buffer = ByteArray(64 * 1024)
|
||||
|
||||
try {
|
||||
sevenZFile = SevenZFile.Builder().setFile(archive).get()
|
||||
val allEntries: Iterable<SevenZArchiveEntry> = sevenZFile.entries
|
||||
|
||||
val targetEntries = allEntries.filter { e ->
|
||||
val entryPath = e.name.replace('\\', '/')
|
||||
entryPath.startsWith(sourceDirPath) ||
|
||||
entryPath.startsWith(trimLeadingSlash(sourceDirPath))
|
||||
}
|
||||
|
||||
totalBytes = targetEntries
|
||||
.filter { !it.isDirectory && it.size >= 0 && it.hasStream() }
|
||||
.sumOf { it.size }
|
||||
|
||||
val entriesCount = targetEntries.size
|
||||
var processed = 0
|
||||
|
||||
for (entry in targetEntries) {
|
||||
val entryPath = entry.name.replace('\\', '/')
|
||||
var relative = when {
|
||||
entryPath.startsWith(sourceDirPath) ->
|
||||
entryPath.substring(sourceDirPath.length)
|
||||
entryPath.startsWith(trimLeadingSlash(sourceDirPath)) ->
|
||||
entryPath.substring(trimLeadingSlash(sourceDirPath).length)
|
||||
else -> {
|
||||
processed++
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading separator to avoid being treated as absolute path.
|
||||
// zh-CN: // 去掉前导分隔符, 避免被当作绝对路径.
|
||||
while (relative.startsWith("/") || relative.startsWith("\\")) {
|
||||
relative = relative.substring(1)
|
||||
}
|
||||
if (relative.isEmpty()) {
|
||||
processed++
|
||||
continue
|
||||
}
|
||||
|
||||
val outFile = File(outDir, relative)
|
||||
if (entry.isDirectory) {
|
||||
outFile.mkdirs()
|
||||
} else {
|
||||
if (!entry.hasStream()) {
|
||||
processed++
|
||||
continue
|
||||
}
|
||||
outFile.parentFile?.mkdirs()
|
||||
var ins: InputStream? = null
|
||||
var bos: BufferedOutputStream? = null
|
||||
try {
|
||||
ins = sevenZFile.getInputStream(entry)
|
||||
bos = BufferedOutputStream(FileOutputStream(outFile))
|
||||
while (true) {
|
||||
val read = ins.read(buffer)
|
||||
if (read == -1) break
|
||||
bos.write(buffer, 0, read)
|
||||
if (shouldPrintProgress && totalBytes > 0) {
|
||||
writtenBytes += read
|
||||
printProgress(writtenBytes, totalBytes)
|
||||
}
|
||||
}
|
||||
bos.flush()
|
||||
if (entry.size >= 0 && outFile.length() != entry.size) {
|
||||
throw IllegalStateException(
|
||||
"Extracted file size mismatch for: ${entry.name}, expected=${entry.size}, actual=${outFile.length()}"
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
try { bos?.close() } catch (_: Throwable) {}
|
||||
try { ins?.close() } catch (_: Throwable) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPrintProgress && totalBytes == 0L) {
|
||||
val pct = processed * 100.0 / entriesCount
|
||||
printProgress(pct)
|
||||
}
|
||||
processed++
|
||||
}
|
||||
return totalBytes
|
||||
} finally {
|
||||
try { sevenZFile?.close() } catch (_: Throwable) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizePrefix(path: String): String {
|
||||
var p = File(path).path.replace('\\', '/')
|
||||
if (p.startsWith("/")) p = p.substring(1)
|
||||
if (!p.endsWith("/")) p += "/"
|
||||
return p
|
||||
}
|
||||
|
||||
private fun trimLeadingSlash(s: String): String =
|
||||
if (s.startsWith("/")) s.substring(1) else s
|
||||
|
||||
private fun printProgress(written: Long, total: Long) {
|
||||
val pct = if (total > 0) written * 100.0 / total else 0.0
|
||||
printProgress(pct)
|
||||
}
|
||||
|
||||
private fun printProgress(percent: Double) {
|
||||
val width = 30
|
||||
val filled = ((percent / 100.0) * width).toInt().coerceIn(0, width)
|
||||
val bar = buildString {
|
||||
append("[").append("#".repeat(filled)).append("-".repeat(width - filled)).append("]")
|
||||
}
|
||||
print("\rExtracting... $bar ${"%.2f".format(percent)}%")
|
||||
System.out.flush()
|
||||
}
|
||||
|
||||
}
|
||||
19
buildSrc/src/main/kotlin/org/autojs/build/Signs.kt
Normal file
19
buildSrc/src/main/kotlin/org/autojs/build/Signs.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.Project
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class Signs @JvmOverloads constructor(project: Project, filePath: String = "${project.rootDir}/sign.properties") {
|
||||
|
||||
var isValid = false
|
||||
private set
|
||||
|
||||
val properties = Properties().also { props ->
|
||||
File(filePath).takeIf { it.exists() }?.let { file ->
|
||||
file.inputStream().use { props.load(it) }
|
||||
isValid = props.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
45
buildSrc/src/main/kotlin/org/autojs/build/SignsPlugin.kt
Normal file
45
buildSrc/src/main/kotlin/org/autojs/build/SignsPlugin.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
/**
|
||||
* A Gradle plugin that provides signing functionality.
|
||||
*
|
||||
* zh-CN: Gradle 签名功能插件.
|
||||
*
|
||||
* - `id`: "org.autojs.build.signs"
|
||||
* - `implementationClass`: "org.autojs.build.SignsPlugin"
|
||||
* - `displayName`: "AutoJs6 Signs Plugin"
|
||||
* - `description`: "Provides signing helpers."
|
||||
*
|
||||
* Apply this plugin to your Android module's `build.gradle.kts`:
|
||||
*
|
||||
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
|
||||
*
|
||||
* ```kts
|
||||
* plugins {
|
||||
* id("org.autojs.build.signs")
|
||||
* }
|
||||
*
|
||||
* android {
|
||||
* signingConfigs {
|
||||
* if (signs.isValid) {
|
||||
* create(buildTypeRelease) {
|
||||
* storeFile = signs.properties["storeFile"]?.let { file(it as String) }
|
||||
* keyPassword = signs.properties["keyPassword"] as String
|
||||
* keyAlias = signs.properties["keyAlias"] as String
|
||||
* storePassword = signs.properties["storePassword"] as String
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class SignsPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.extensions.add("signs", Utils.newSigns(project))
|
||||
}
|
||||
}
|
||||
408
buildSrc/src/main/kotlin/org/autojs/build/Utils.kt
Normal file
408
buildSrc/src/main/kotlin/org/autojs/build/Utils.kt
Normal file
@@ -0,0 +1,408 @@
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.file.CopySpec
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||
import org.gradle.jvm.toolchain.JavaToolchainService
|
||||
import org.gradle.kotlin.dsl.extra
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.zip.CRC32
|
||||
|
||||
object Utils {
|
||||
|
||||
const val FILE_EXTENSION_APK = "apk"
|
||||
|
||||
private object LOGGER {
|
||||
const val LEVEL_INFO = 1
|
||||
const val LEVEL_WARN = 2
|
||||
const val LEVEL_ERROR = 3
|
||||
}
|
||||
|
||||
private var CURRENT_LOGGER_LEVEL = LOGGER.LEVEL_ERROR
|
||||
|
||||
fun newLibDeployer(project: Project, name: String, downloadUrl: String) = LibDeployer(project, name, downloadUrl)
|
||||
|
||||
@JvmOverloads
|
||||
fun newFormatted(title: String, contents: Collection<String> = emptyList(), subtitle: String? = null) = Formatted(title, contents, subtitle)
|
||||
|
||||
fun newVersions(project: Project) = Versions(project)
|
||||
|
||||
fun newSigns(project: Project) = Signs(project)
|
||||
|
||||
fun newProperties(project: Project) = BuildProperties.loadFrom(project)
|
||||
|
||||
fun hours2Millis(hour: Double) = hour * 3.6e6
|
||||
|
||||
fun getDateString(format: String, zone: String): String {
|
||||
// e.g. May 23, 2011
|
||||
return SimpleDateFormat(format, Locale.getDefault()).apply {
|
||||
timeZone = TimeZone.getTimeZone(zone)
|
||||
}.format(Date())
|
||||
}
|
||||
|
||||
fun getAssembleTaskName(flavorName: String, buildType: String) = "assemble${capitalize(flavorName)}${capitalize(buildType)}"
|
||||
|
||||
fun getAssembleFullTaskName(projectName: String, flavorName: String, buildType: String) = ":$projectName:${getAssembleTaskName(flavorName, buildType)}"
|
||||
|
||||
fun digestCRC32(file: File): String {
|
||||
val fis = FileInputStream(file)
|
||||
val buffer = ByteArray(4096)
|
||||
var read: Int
|
||||
|
||||
return CRC32().let { o ->
|
||||
while (fis.read(buffer).also { read = it } > 0) {
|
||||
o.update(buffer, 0, read)
|
||||
}
|
||||
String.format("%08x", o.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一 "版本信息打印 + 部署 + 清理" 生命周期钩子.
|
||||
*
|
||||
* @param project 当前模块 Project
|
||||
* @param projectDisplayName 用于打印的项目展示名 (默认用模块名)
|
||||
* @param versionLines 版本信息行 (如 ["OpenCV: 4.2.0", "NDK: 21.1.6352462"])
|
||||
* @param libsToDeploy 需要部署/清理的 LibDeployer 列表
|
||||
* @param cleanupFlagKey gradle.ext 的布尔开关键 (如 "isCleanupPaddleOcr"), null 表示不参与清理逻辑
|
||||
* @param extraFilesToDeleteOnClean clean 时额外需删除的相对路径
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun configureLibraryLifecycleHooks(
|
||||
project: Project,
|
||||
projectDisplayName: String = project.name,
|
||||
versionLines: List<String> = emptyList(),
|
||||
libsToDeploy: List<LibDeployer> = emptyList(),
|
||||
cleanupFlagKey: String? = null,
|
||||
extraFilesToDeleteOnClean: List<String> = listOf(".cxx"),
|
||||
) {
|
||||
val gradle = project.gradle
|
||||
val onlyClean = AtomicBoolean(false)
|
||||
|
||||
// 单一监听器: 既判断 "是否纯 clean", 也负责非 clean 流程的打印与部署
|
||||
gradle.taskGraph.addTaskExecutionGraphListener { graph ->
|
||||
val all = graph.allTasks
|
||||
val isOnlyClean = all.isNotEmpty() && all.all { it.name.contains("clean", ignoreCase = true) }
|
||||
onlyClean.set(isOnlyClean)
|
||||
|
||||
if (!isOnlyClean) {
|
||||
if (versionLines.isNotEmpty()) {
|
||||
newFormatted("Version information for $projectDisplayName library", versionLines).print()
|
||||
}
|
||||
if (libsToDeploy.isNotEmpty()) {
|
||||
libsToDeploy.forEach { it.deploy() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clean 钩子 (显式 Java SAM, 避免 Kotlin/Groovy 重载歧义)
|
||||
@Suppress("ObjectLiteralToLambda")
|
||||
project.tasks.named("clean").configure(object : Action<Task> {
|
||||
override fun execute(cleanTask: Task) {
|
||||
cleanTask.doFirst {
|
||||
project.delete(project.layout.buildDirectory)
|
||||
extraFilesToDeleteOnClean.forEach { rel ->
|
||||
project.delete(project.file(rel))
|
||||
}
|
||||
|
||||
// 未提供开关键则直接跳过清理逻辑
|
||||
val key = cleanupFlagKey ?: return@doFirst
|
||||
val cleanupEnabled = gradle.extra.require<Boolean>(key)
|
||||
if (cleanupEnabled && onlyClean.get()) {
|
||||
libsToDeploy.forEach { it.clean() }
|
||||
} else {
|
||||
val projectName = project.extensions.extraProperties.getOrNull<String>("projectName") ?: projectDisplayName
|
||||
println("The library files of $projectName won't be cleaned up due to the configuration")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册模板 APK 拷贝: 在指定 assemble 任务完成后, 将 universal APK 拷贝为 assets 模板.
|
||||
*
|
||||
* @param project 当前模块 Project
|
||||
* @param taskName 构建任务名 (如 "assembleInrtRelease")
|
||||
* @param srcDir APK 输出目录 (如 "build/outputs/apk/inrt/release")
|
||||
* @param destDir 目标目录 (如 "src/main/assets-app")
|
||||
* @param templateApkName 模板文件名 (如 "template.apk")
|
||||
* @param universalNameFn 从版本名得出源 APK 名的函数 (默认 inrt-v<ver>-universal.apk)
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun registerTemplateApkCopy(
|
||||
project: Project,
|
||||
taskName: String = "assembleInrtRelease",
|
||||
srcDir: String = "build/outputs/apk/inrt/release",
|
||||
destDir: String = "src/main/assets-app",
|
||||
templateApkName: String = "template.$FILE_EXTENSION_APK",
|
||||
universalNameFn: (String) -> String = { ver -> "inrt-v${ver.replace(Regex("\\s"), "-").lowercase()}-universal.$FILE_EXTENSION_APK" },
|
||||
) {
|
||||
val versions = newVersions(project)
|
||||
val versionName = versions.appVersionName
|
||||
|
||||
// 待所有项目评估完成后再定位并配置任务, 避免早期查找不到任务
|
||||
project.gradle.projectsEvaluated {
|
||||
val assembleTask = project.tasks.findByName(taskName)
|
||||
if (assembleTask == null) {
|
||||
println("$taskName doesn't exist in project ${project.name}")
|
||||
return@projectsEvaluated
|
||||
}
|
||||
assembleTask.doLast {
|
||||
@Suppress("ObjectLiteralToLambda")
|
||||
project.copy(object : Action<CopySpec> {
|
||||
override fun execute(spec: CopySpec) {
|
||||
val srcFileName = universalNameFn(versionName)
|
||||
val srcFile = project.file(File(srcDir, srcFileName))
|
||||
require(srcFile.exists()) {
|
||||
"Source file \"$srcFile\" doesn't exist"
|
||||
}
|
||||
|
||||
spec.from(srcDir)
|
||||
spec.into(destDir)
|
||||
spec.include(srcFileName)
|
||||
spec.rename(srcFileName, templateApkName)
|
||||
|
||||
val dstFile = project.file(File(destDir, templateApkName))
|
||||
val overridden = dstFile.exists()
|
||||
newFormatted(
|
||||
"Copy template APK into assets", listOf(
|
||||
"Source: $srcFile",
|
||||
"Destination: $dstFile${if (overridden) " [overridden]" else ""}"
|
||||
)
|
||||
).print()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> org.gradle.api.plugins.ExtraPropertiesExtension.getOrNull(key: String): T? {
|
||||
if (!has(key)) return null
|
||||
val result = get(key)
|
||||
require(result is T?) {
|
||||
"The type of $key is ${result?.javaClass?.name}, but ${T::class.java.name} is required"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
inline fun <reified T> org.gradle.api.plugins.ExtraPropertiesExtension.require(key: String): T {
|
||||
require(has(key)) {
|
||||
"The key $key is not found in extra properties"
|
||||
}
|
||||
val result = get(key)
|
||||
require(result is T) {
|
||||
"The type of $key is ${result?.javaClass?.name}, but ${T::class.java.name} is required"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun capitalize(s: String) = "${s[0].uppercase(Locale.getDefault())}${s.substring(1)}"
|
||||
|
||||
/**
|
||||
* 统一为 Android 模块配置 Java/Kotlin 的目标版本:
|
||||
* - Android.compileOptions.sourceCompatibility/targetCompatibility = versions.javaVersion
|
||||
* - Kotlin 编译任务的 jvmTarget = versions.javaVersion 对应级别
|
||||
*
|
||||
* 注意:
|
||||
* - Android 扩展在 projectsEvaluated 后稳定可得
|
||||
* - Kotlin 任务用 tasks.configureEach 动态配置, 任务何时创建都能命中
|
||||
*/
|
||||
@JvmStatic
|
||||
fun configureJvmForAndroidModule(project: Project) {
|
||||
val versions = newVersions(project)
|
||||
project.logInfo("[JvmConv] Enter configureJvmForAndroidModule for module='${project.path}', javaVersion='${versions.javaVersion}', javaVersionString='${versions.javaVersionString}'")
|
||||
|
||||
val installer = {
|
||||
project.logInfo("[JvmConv] Detected Android plugin in module='${project.path}', installing configuration")
|
||||
|
||||
// A) Java: 使用 Toolchain (模块级 + 任务级), 不要设置 --release
|
||||
configureJavaToolchainLanguageLevel(project, versions.javaVersionInt)
|
||||
configureJavaToolchainForAllJavaCompile(project, versions.javaVersionInt)
|
||||
|
||||
// B) Kotlin: 继续懒配置设置 jvmTarget (你之前已验证成功)
|
||||
configureKotlinJvmTargetLazily(project, versions)
|
||||
|
||||
project.logInfo("[JvmConv] Installed Java toolchain (module+tasks) and Kotlin jvmTarget for '${project.path}'")
|
||||
}
|
||||
|
||||
project.plugins.withId("com.android.application") { installer() }
|
||||
project.plugins.withId("com.android.library") { installer() }
|
||||
}
|
||||
|
||||
// 模块级 Toolchain: 让 AGP/Gradle 知道本模块应使用的 JDK 语言级别
|
||||
private fun configureJavaToolchainLanguageLevel(project: Project, target: Int) {
|
||||
val javaExt = project.extensions.findByType(JavaPluginExtension::class.java)
|
||||
if (javaExt == null) {
|
||||
project.logWarn("[JvmConv] JavaPluginExtension not found in '${project.path}', skip module-level toolchain")
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
javaExt.toolchain.languageVersion.set(JavaLanguageVersion.of(target))
|
||||
}.onSuccess {
|
||||
project.logInfo("[JvmConv] Module-level toolchain languageVersion set to $target for '${project.path}'")
|
||||
}.onFailure {
|
||||
project.logError("[JvmConv] Set module-level toolchain languageVersion failed on '${project.path}': ${it.message}", it)
|
||||
}
|
||||
}
|
||||
|
||||
// 任务级 Toolchain: 对所有 JavaCompile 指定 javaCompiler, 且不要设置 --release (AGP 禁止)
|
||||
private fun configureJavaToolchainForAllJavaCompile(project: Project, target: Int) {
|
||||
val toolchains = runCatching {
|
||||
project.extensions.getByType(JavaToolchainService::class.java)
|
||||
}.onFailure {
|
||||
project.logError("[JvmConv] JavaToolchainService not available on '${project.path}': ${it.message}", it)
|
||||
}.getOrNull() ?: return
|
||||
|
||||
val langVersion = JavaLanguageVersion.of(target)
|
||||
project.tasks.withType(JavaCompile::class.java).configureEach(object : Action<JavaCompile> {
|
||||
override fun execute(t: JavaCompile) {
|
||||
runCatching {
|
||||
val compilerProvider = toolchains.compilerFor { languageVersion.set(langVersion) }
|
||||
t.javaCompiler.set(compilerProvider)
|
||||
project.logInfo("[JvmConv] JavaCompile '${t.path}' uses toolchain JDK $target (no --release)")
|
||||
}.onFailure {
|
||||
project.logError("[JvmConv] '${t.path}' set javaCompiler(toolchain) failed: ${it.message}", it)
|
||||
}
|
||||
|
||||
// 不要设置 t.options.release, AGP 会报错阻止
|
||||
// 也不强制改写 sourceCompatibility/targetCompatibility, 交给 AGP + toolchain 统一管理
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 为 KotlinCompile 任务设置 jvmTarget: 使用 configureEach, 任务实现时自动应用; 兼容新旧 API
|
||||
private fun configureKotlinJvmTargetLazily(project: Project, versions: Versions) {
|
||||
val desiredStr = versions.javaVersionString // 例如 "22"
|
||||
val desiredEnumName = "JVM_${desiredStr}" // 例如 "JVM_22"
|
||||
project.logInfo("[JvmConv] Will configure Kotlin jvmTarget lazily to '$desiredEnumName' in '${project.path}'")
|
||||
|
||||
project.tasks.configureEach(object : Action<Task> {
|
||||
override fun execute(task: Task) {
|
||||
if (!isKotlinCompileTask(task, project)) return
|
||||
|
||||
project.logInfo("[JvmConv] <KotlinTask> '${task.path}' class='${task.javaClass.name}'")
|
||||
|
||||
// 优先尝试 Kotlin 2.x: compilerOptions.jvmTarget(Property<JvmTarget>)
|
||||
val compilerOptions = runCatching {
|
||||
task.javaClass.methods.firstOrNull { it.name == "getCompilerOptions" && it.parameterTypes.isEmpty() }
|
||||
?.also { project.logInfo("[JvmConv] '${task.path}' found method: ${it.toGenericString()}") }
|
||||
?.invoke(task)
|
||||
}.onFailure {
|
||||
project.logError("[JvmConv] '${task.path}' getCompilerOptions() failed", it)
|
||||
}.getOrNull()
|
||||
|
||||
if (compilerOptions != null) {
|
||||
project.logInfo("[JvmConv] '${task.path}' compilerOptions class='${compilerOptions.javaClass.name}'")
|
||||
val jvmTargetProp = runCatching {
|
||||
compilerOptions.javaClass.methods.firstOrNull { it.name == "getJvmTarget" && it.parameterTypes.isEmpty() }
|
||||
?.also { project.logInfo("[JvmConv] '${task.path}' found method: ${it.toGenericString()}") }
|
||||
?.invoke(compilerOptions)
|
||||
}.onFailure {
|
||||
project.logError("[JvmConv] '${task.path}' compilerOptions.getJvmTarget() failed", it)
|
||||
}.getOrNull()
|
||||
|
||||
if (jvmTargetProp != null) {
|
||||
project.logInfo("[JvmConv] '${task.path}' jvmTarget property class='${jvmTargetProp.javaClass.name}'")
|
||||
val propSet = jvmTargetProp.javaClass.methods.firstOrNull {
|
||||
it.name == "set" && it.parameterTypes.size == 1
|
||||
}
|
||||
project.logInfo("[JvmConv] '${task.path}' Property.set method: ${propSet?.toGenericString() ?: "NOT_FOUND"}")
|
||||
|
||||
val jvmTargetEnum = runCatching {
|
||||
val enumClass = Class.forName("org.jetbrains.kotlin.gradle.dsl.JvmTarget")
|
||||
enumClass.enumConstants?.firstOrNull { it.toString().equals(desiredEnumName, ignoreCase = true) }
|
||||
?.also { project.logInfo("[JvmConv] '${task.path}' resolved enum '$desiredEnumName' = $it") }
|
||||
}.onFailure {
|
||||
project.logError("[JvmConv] '${task.path}' resolve enum '$desiredEnumName' failed", it)
|
||||
}.getOrNull()
|
||||
|
||||
if (propSet != null && jvmTargetEnum != null) {
|
||||
runCatching { propSet.invoke(jvmTargetProp, jvmTargetEnum) }
|
||||
.onSuccess {
|
||||
project.logInfo("[JvmConv] '${task.path}' jvmTarget set via compilerOptions to '$desiredEnumName'")
|
||||
return
|
||||
}
|
||||
.onFailure {
|
||||
project.logWarn("[JvmConv] '${task.path}' jvmTarget set via compilerOptions failed, will try legacy API.\ne: $it")
|
||||
}
|
||||
} else {
|
||||
project.logWarn("[JvmConv] '${task.path}' compilerOptions path unavailable (propSet=$propSet, enum=$jvmTargetEnum), trying legacy kotlinOptions")
|
||||
}
|
||||
} else {
|
||||
project.logWarn("[JvmConv] '${task.path}' compilerOptions.getJvmTarget() returned null, trying legacy kotlinOptions")
|
||||
}
|
||||
} else {
|
||||
project.logWarn("[JvmConv] '${task.path}' compilerOptions not found, trying legacy kotlinOptions")
|
||||
}
|
||||
|
||||
// 兼容旧 API: kotlinOptions.setJvmTarget(String)
|
||||
val kotlinOptions = runCatching {
|
||||
task.javaClass.methods.firstOrNull { it.name == "getKotlinOptions" && it.parameterTypes.isEmpty() }
|
||||
?.also { project.logInfo("[JvmConv] '${task.path}' found method: ${it.toGenericString()}") }
|
||||
?.invoke(task)
|
||||
}.onFailure {
|
||||
project.logError("[JvmConv] '${task.path}' getKotlinOptions() failed", it)
|
||||
}.getOrNull()
|
||||
|
||||
if (kotlinOptions != null) {
|
||||
project.logInfo("[JvmConv] '${task.path}' kotlinOptions class='${kotlinOptions.javaClass.name}'")
|
||||
val setJvmTarget = kotlinOptions.javaClass.methods.firstOrNull {
|
||||
it.name == "setJvmTarget" && it.parameterTypes.size == 1 && it.parameterTypes[0] == String::class.java
|
||||
}
|
||||
project.logInfo("[JvmConv] '${task.path}' kotlinOptions.setJvmTarget method: ${setJvmTarget?.toGenericString() ?: "NOT_FOUND"}")
|
||||
|
||||
runCatching { setJvmTarget?.invoke(kotlinOptions, desiredStr) }
|
||||
.onSuccess { project.logInfo("[JvmConv] '${task.path}' jvmTarget set via kotlinOptions to '$desiredStr'") }
|
||||
.onFailure { project.logError("[JvmConv] '${task.path}' jvmTarget set via kotlinOptions failed", it) }
|
||||
} else {
|
||||
project.logWarn("[JvmConv] '${task.path}' kotlinOptions not found; jvmTarget not configured")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun isKotlinCompileTask(task: Task, project: Project): Boolean {
|
||||
val name = task.name.lowercase(Locale.getDefault())
|
||||
val clsName = task.javaClass.name
|
||||
val hasCompilerOptions = task.javaClass.methods.any { it.name == "getCompilerOptions" && it.parameterTypes.isEmpty() }
|
||||
val hasKotlinOptions = task.javaClass.methods.any { it.name == "getKotlinOptions" && it.parameterTypes.isEmpty() }
|
||||
val matched = when {
|
||||
name.contains("kotlin") && name.contains("compile") -> true
|
||||
clsName.contains("Kotlin", ignoreCase = true) && clsName.contains("Compile", ignoreCase = true) -> true
|
||||
hasCompilerOptions || hasKotlinOptions -> true
|
||||
else -> false
|
||||
}
|
||||
if (matched) {
|
||||
project.logInfo("[JvmConv] Task matched as KotlinCompile: path='${task.path}', class='$clsName'")
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
private fun Project.logInfo(msg: String) {
|
||||
if (CURRENT_LOGGER_LEVEL <= LOGGER.LEVEL_INFO) {
|
||||
logger.lifecycle(msg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Project.logWarn(msg: String) {
|
||||
if (CURRENT_LOGGER_LEVEL <= LOGGER.LEVEL_WARN) {
|
||||
logger.warn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Project.logError(msg: String, t: Throwable? = null) {
|
||||
if (CURRENT_LOGGER_LEVEL <= LOGGER.LEVEL_ERROR) {
|
||||
if (t != null) logger.error(msg, t) else logger.error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
40
buildSrc/src/main/kotlin/org/autojs/build/UtilsPlugin.kt
Normal file
40
buildSrc/src/main/kotlin/org/autojs/build/UtilsPlugin.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
/**
|
||||
* A Gradle plugin that provides basic utilities.
|
||||
*
|
||||
* zh-CN: Gradle 基础工具插件.
|
||||
*
|
||||
* - `id`: "org.autojs.build.utils"
|
||||
* - `implementationClass`: "org.autojs.build.UtilsPlugin"
|
||||
* - `displayName`: "AutoJs6 Build Utils Plugin"
|
||||
* - `description`: "Provides utilities for downloading, extracting archives, and version helpers."
|
||||
*
|
||||
* Apply this plugin to your Android module's `build.gradle.kts`:
|
||||
*
|
||||
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
|
||||
*
|
||||
* ```kts
|
||||
* plugins {
|
||||
* id("org.autojs.build.utils")
|
||||
* }
|
||||
*
|
||||
* utils.digestCRC32(file("some/file.zip"))
|
||||
* utils.getDateString("MMM d, yyyy", "GMT+08:00")
|
||||
* utils.hours2Millis(0.75)
|
||||
* utils.compareVersionStrings("6.3.2 beta", "6.3.2 alpha4) > 0
|
||||
*
|
||||
* utils.registerTemplateApkCopy(project)
|
||||
* ```
|
||||
*/
|
||||
class UtilsPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
// project.extensions.extraProperties.set("utils", Utils)
|
||||
project.extensions.add("utils", Utils)
|
||||
}
|
||||
}
|
||||
210
buildSrc/src/main/kotlin/org/autojs/build/Versions.kt
Normal file
210
buildSrc/src/main/kotlin/org/autojs/build/Versions.kt
Normal file
@@ -0,0 +1,210 @@
|
||||
package org.autojs.build
|
||||
|
||||
import org.autojs.build.Utils.getOrNull
|
||||
import org.gradle.api.Action
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.execution.TaskExecutionGraph
|
||||
import org.gradle.kotlin.dsl.extra
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.*
|
||||
import kotlin.properties.Delegates
|
||||
import kotlin.text.RegexOption.IGNORE_CASE
|
||||
|
||||
class Versions @JvmOverloads constructor(
|
||||
private val project: Project,
|
||||
filePath: String = "${project.rootDir}/version.properties",
|
||||
) {
|
||||
private val gradle = project.gradle
|
||||
private val logger = project.logger
|
||||
|
||||
private val currentVersionInt = JavaVersion.current().majorVersion.toInt()
|
||||
|
||||
private val bp = BuildProperties.loadFrom(filePath)
|
||||
|
||||
private val javaVersionMinSuggested: Int = bp.requireInt("JAVA_VERSION_MIN_SUGGESTED")
|
||||
private val javaVersionMaxSupported: Int = bp.requireInt("JAVA_VERSION_MAX_SUPPORTED")
|
||||
|
||||
private var isBuildNumberAutoIncremented = false
|
||||
private val minBuildTimeGap = Utils.hours2Millis(0.75)
|
||||
|
||||
private val isBuildGapEnough
|
||||
get() = Date().time - bp["BUILD_TIME"].toLong() > minBuildTimeGap
|
||||
|
||||
val sdkVersionMin = bp.requireInt("MIN_SDK_VERSION")
|
||||
val sdkVersionTarget = bp.requireInt("TARGET_SDK_VERSION")
|
||||
val sdkVersionTargetInrt = bp.requireInt("TARGET_SDK_VERSION_INRT")
|
||||
val sdkVersionCompile = bp.requireInt("COMPILE_SDK_VERSION")
|
||||
val appVersionName = bp.requireString("VERSION_NAME")
|
||||
val appVersionCode = bp.requireInt("VERSION_BUILD")
|
||||
val vscodeExtRequiredVersion = bp.requireString("VSCODE_EXT_REQUIRED_VERSION")
|
||||
|
||||
private val javaVersionMinSupported: Int = bp.requireInt("JAVA_VERSION_MIN_SUPPORTED")
|
||||
|
||||
var javaVersionInt by Delegates.notNull<Int>()
|
||||
var javaVersionInfoSuffix by Delegates.notNull<String>()
|
||||
|
||||
val javaVersion: JavaVersion
|
||||
get() = JavaVersion.toVersion(javaVersionInt)
|
||||
|
||||
val javaVersionString: String
|
||||
get() = javaVersion.toString()
|
||||
|
||||
init {
|
||||
validateCurrentVersion()
|
||||
determineJavaVersion().also { (javaVersionInt, javaVersionInfoSuffix) ->
|
||||
this.javaVersionInt = javaVersionInt
|
||||
this.javaVersionInfoSuffix = javaVersionInfoSuffix
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateCurrentVersion() {
|
||||
if (gradle.extra.getOrNull<Boolean>(VALIDATED_EXTRA_KEY) == true) {
|
||||
return
|
||||
}
|
||||
if (currentVersionInt < javaVersionMinSupported) {
|
||||
throw GradleException("Current Gradle JDK version [$currentVersionInt] does not meet the minimum requirement which [$javaVersionMinSupported] is needed.")
|
||||
}
|
||||
if (currentVersionInt < javaVersionMinSuggested) {
|
||||
val suffix = if (javaVersionMaxSupported > 0) " (but not higher than [$javaVersionMaxSupported])" else ""
|
||||
logger.error("It is recommended to upgrade current Gradle JDK version [$currentVersionInt] to [$javaVersionMinSuggested] or higher$suffix.")
|
||||
}
|
||||
if (currentVersionInt > javaVersionMaxSupported) {
|
||||
val suffix = if (javaVersionMaxSupported > javaVersionMinSuggested) " or lower (but not lower than [$javaVersionMinSuggested])" else ""
|
||||
logger.error("It is recommended to downgrade current Gradle JDK version [$currentVersionInt] to [$javaVersionMaxSupported]$suffix, as Gradle may be not compatible with JDK [$currentVersionInt] for now.")
|
||||
}
|
||||
gradle.extra.set(VALIDATED_EXTRA_KEY, true)
|
||||
}
|
||||
|
||||
private fun determineJavaVersion(): Pair<Int, String> {
|
||||
var javaVersionInfoSuffix = ""
|
||||
|
||||
gradle.extra.getOrNull<Int>("javaVersionOverriddenByUser")?.let {
|
||||
javaVersionInfoSuffix += " [user-specified]"
|
||||
return it to javaVersionInfoSuffix
|
||||
}
|
||||
|
||||
var versionInt = JavaVersion.current().majorVersion.toInt()
|
||||
|
||||
run tryAdjustJavaVersionByKotlinJvmTarget@{
|
||||
var isJvmCoercive = false
|
||||
|
||||
while (versionInt > javaVersionMinSupported) {
|
||||
if (isJvmTargetAvailable(versionInt)) {
|
||||
break
|
||||
}
|
||||
versionInt -= 1
|
||||
isJvmCoercive = true
|
||||
}
|
||||
|
||||
if (isJvmCoercive) {
|
||||
javaVersionInfoSuffix += " [coercive-jvm-downgraded]"
|
||||
}
|
||||
}
|
||||
|
||||
gradle.extra.getOrNull<Int>("javaVersionCoercedByGradle")?.let {
|
||||
if (versionInt > it) {
|
||||
versionInt = it
|
||||
javaVersionInfoSuffix += " [coercive-gradle-downgraded]"
|
||||
}
|
||||
}
|
||||
return versionInt to javaVersionInfoSuffix
|
||||
}
|
||||
|
||||
private fun isJvmTargetAvailable(target: Int): Boolean {
|
||||
try {
|
||||
// Weak dependency with Kotlin plugin: Use reflection to check if JvmTarget is available;
|
||||
// if reflection fails, treat it as available (to avoid incorrect downgrading).
|
||||
// zh-CN: 与 Kotlin 插件弱依赖: 反射判断 JvmTarget 是否可用; 反射失败则视为可用 (避免误降级).
|
||||
val cls = Class.forName("org.jetbrains.kotlin.gradle.dsl.JvmTarget")
|
||||
cls.enumConstants?.let { values ->
|
||||
return values.any { it?.toString().equals("JVM_$target", true) }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.error("Failed to check JVM target availability: $e")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
operator fun get(propertyName: String) = bp[propertyName]
|
||||
|
||||
operator fun get(propertyInfo: List<String>) = bp[propertyInfo]
|
||||
|
||||
fun showInfo() {
|
||||
val title = "Version information for AutoJs6 app library"
|
||||
|
||||
val infoVerName = "Version name: $appVersionName"
|
||||
val infoVerCode = "Version code: ${if (isBuildNumberAutoIncremented) "${appVersionCode + 1} [auto-incremented]" else appVersionCode}"
|
||||
val infoVerSdk = "SDK versions: min [$sdkVersionMin] / target [$sdkVersionTarget] / compile [$sdkVersionCompile]"
|
||||
val infoVerJava = "Java version: $javaVersion${
|
||||
when {
|
||||
gradle.extra.getOrNull<Boolean>("isHideConsoleInfoHintSuffix") == true -> ""
|
||||
else -> javaVersionInfoSuffix
|
||||
}
|
||||
}"
|
||||
|
||||
val maxLength = arrayOf(title, infoVerName, infoVerCode, infoVerSdk, infoVerJava).maxOf { it.length }
|
||||
|
||||
arrayOf(
|
||||
"=".repeat(maxLength),
|
||||
title,
|
||||
"-".repeat(maxLength),
|
||||
infoVerName,
|
||||
infoVerCode,
|
||||
infoVerSdk,
|
||||
infoVerJava,
|
||||
"=".repeat(maxLength),
|
||||
"",
|
||||
).forEach { println(it) }
|
||||
}
|
||||
|
||||
fun handleIfNeeded(project: Project, flavorName: String, targetBuildType: List<String>) {
|
||||
project.gradle.taskGraph.whenReady(object : Action<TaskExecutionGraph> {
|
||||
override fun execute(taskGraph: TaskExecutionGraph) {
|
||||
for (buildType in targetBuildType) {
|
||||
if (taskGraph.hasTask(Utils.getAssembleFullTaskName(project.name, flavorName, buildType))) {
|
||||
return appendToTask(project, flavorName, buildType)
|
||||
}
|
||||
}
|
||||
return showInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun appendToTask(project: Project, flavorName: String, buildType: String) {
|
||||
project.tasks.getByName(Utils.getAssembleTaskName(flavorName, buildType)).doLast {
|
||||
updateProperties()
|
||||
println()
|
||||
showInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateProperties() {
|
||||
val propsPath = bp.path
|
||||
val props = Properties().apply {
|
||||
FileInputStream(propsPath).use { load(it) }
|
||||
}
|
||||
|
||||
if (isBuildGapEnough) {
|
||||
val isBuildAppRelease = gradle.startParameter.taskNames.any {
|
||||
it.contains(Regex("^(:?app:)?assemble(app|inrt)release", IGNORE_CASE))
|
||||
}
|
||||
if (!isBuildAppRelease) {
|
||||
props["VERSION_BUILD"] = "${appVersionCode + 1}"
|
||||
isBuildNumberAutoIncremented = true
|
||||
}
|
||||
}
|
||||
props["BUILD_TIME"] = "${Date().time}"
|
||||
|
||||
FileOutputStream(propsPath).use { out ->
|
||||
props.store(out, null)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VALIDATED_EXTRA_KEY = "org.autojs.build.Versions.currentValidated"
|
||||
}
|
||||
|
||||
}
|
||||
40
buildSrc/src/main/kotlin/org/autojs/build/VersionsPlugin.kt
Normal file
40
buildSrc/src/main/kotlin/org/autojs/build/VersionsPlugin.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
/**
|
||||
* A Gradle plugin that provides version management functionality.
|
||||
*
|
||||
* zh-CN: Gradle 版本管理插件.
|
||||
*
|
||||
* - `id`: "org.autojs.build.versions"
|
||||
* - `implementationClass`: "org.autojs.build.VersionsPlugin"
|
||||
* - `displayName`: "AutoJs6 Versions Plugin"
|
||||
* - `description`: "Provides version helpers."
|
||||
*
|
||||
* Apply this plugin to your Android module's `build.gradle.kts`:
|
||||
*
|
||||
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
|
||||
*
|
||||
* ```kts
|
||||
* plugins {
|
||||
* id("org.autojs.build.versions")
|
||||
* }
|
||||
*
|
||||
* versions.appVersionName
|
||||
* versions.appVersionCode
|
||||
* versions.sdkVersionCompile
|
||||
* versions.sdkVersionMin
|
||||
* versions.sdkVersionTarget
|
||||
* versions.sdkVersionTargetInrt
|
||||
* versions.vscodeExtRequiredVersion
|
||||
* ```
|
||||
*/
|
||||
class VersionsPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
project.extensions.add("versions", Utils.newVersions(project))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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}")
|
||||
|
||||
@@ -4,26 +4,27 @@
|
||||
* Created by SuperMonster003 on Sep 19, 2024.
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply from: '../utils.build.gradle'
|
||||
plugins {
|
||||
id 'org.autojs.build.utils'
|
||||
id 'org.autojs.build.properties'
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-parcelize'
|
||||
}
|
||||
|
||||
ext["projectName"] = "Rapid OCR"
|
||||
|
||||
def versions = Utils.newVersions(project)
|
||||
|
||||
def versionMap = [
|
||||
"OFFICIAL_NAME" : "1.3.0", /* From original build.gradle file. */
|
||||
"MIN_SDK" : versions["MIN_SDK"] as Integer,
|
||||
"COMPILE_SDK" : versions["COMPILE_SDK"] as Integer,
|
||||
"TARGET_SDK" : versions["TARGET_SDK"] as Integer,
|
||||
"NDK" : versions["RAPID_OCR/NDK"],
|
||||
"CMAKE" : versions["RAPID_OCR/CMAKE"],
|
||||
"OPENCV_MOBILE" : versions["RAPID_OCR/OPENCV_MOBILE"],
|
||||
"OPENCV_MOBILE_LABEL": versions["RAPID_OCR/OPENCV_MOBILE_LABEL"],
|
||||
"ONNX" : versions["RAPID_OCR/ONNX"],
|
||||
"ONNX_RUNTIME" : versions["RAPID_OCR/ONNX_RUNTIME"],
|
||||
"MIN_SDK" : props["MIN_SDK"] as Integer,
|
||||
"COMPILE_SDK" : props["COMPILE_SDK"] as Integer,
|
||||
"TARGET_SDK" : props["TARGET_SDK"] as Integer,
|
||||
"NDK" : props["RAPID_OCR/NDK"],
|
||||
"CMAKE" : props["RAPID_OCR/CMAKE"],
|
||||
"OPENCV_MOBILE" : props["RAPID_OCR/OPENCV_MOBILE"],
|
||||
"OPENCV_MOBILE_LABEL": props["RAPID_OCR/OPENCV_MOBILE_LABEL"],
|
||||
"ONNX" : props["RAPID_OCR/ONNX"],
|
||||
"ONNX_RUNTIME" : props["RAPID_OCR/ONNX_RUNTIME"],
|
||||
]
|
||||
|
||||
def nameMap = [
|
||||
@@ -37,20 +38,29 @@ def nameMap = [
|
||||
]
|
||||
|
||||
def libsToDeploy = [
|
||||
Utils.newLibDeployer(project, nameMap.OPENCV_MOBILE, "https://github.com/nihui/opencv-mobile/releases/download" +
|
||||
utils.newLibDeployer(project, nameMap.OPENCV_MOBILE, "https://github.com/nihui/opencv-mobile/releases/download" +
|
||||
"/v${versionMap.OPENCV_MOBILE_LABEL}/opencv-mobile-${versionMap.OPENCV_MOBILE}-android.zip")
|
||||
.setSourceDir("/opencv-mobile-${versionMap.OPENCV_MOBILE}-android/sdk/native/")
|
||||
.setDestDir("/src/sdk/native/"),
|
||||
Utils.newLibDeployer(project, nameMap.ONNX, "https://github.com/RapidAI/RapidOcrOnnx/releases/download" +
|
||||
utils.newLibDeployer(project, nameMap.ONNX, "https://github.com/RapidAI/RapidOcrOnnx/releases/download" +
|
||||
"/${versionMap.ONNX}/Project_RapidOcrOnnx-${versionMap.ONNX}.7z")
|
||||
.setSourceDir("/Project_RapidOcrOnnx-${versionMap.ONNX}/models/")
|
||||
.setDestDir("/src/main/assets/models/"),
|
||||
Utils.newLibDeployer(project, nameMap.ONNX_RUNTIME, "https://github.com/RapidAI/OnnxruntimeBuilder/releases/download" +
|
||||
utils.newLibDeployer(project, nameMap.ONNX_RUNTIME, "https://github.com/RapidAI/OnnxruntimeBuilder/releases/download" +
|
||||
"/${versionMap.ONNX_RUNTIME}/onnxruntime-${versionMap.ONNX_RUNTIME}-android-shared.7z")
|
||||
.setSourceDir("/onnxruntime-shared/")
|
||||
.setDestDir("/src/main/onnxruntime-shared/"),
|
||||
]
|
||||
|
||||
utils.configureLibraryLifecycleHooks(project, nameMap.PROJECT, [
|
||||
"OPENCV_MOBILE",
|
||||
"OPENCV_MOBILE_LABEL",
|
||||
"ONNX",
|
||||
"ONNX_RUNTIME",
|
||||
"NDK",
|
||||
"CMAKE",
|
||||
].collect { "${nameMap[it]}: ${versionMap[it]}" }, libsToDeploy, "isCleanupRapidOcr", [".cxx"])
|
||||
|
||||
android {
|
||||
|
||||
namespace = "com.benjaminwan.ocrlibrary"
|
||||
@@ -113,29 +123,7 @@ dependencies {
|
||||
androidTestImplementation libs.test.ext.junit
|
||||
androidTestImplementation libs.test.espresso.core
|
||||
|
||||
}
|
||||
|
||||
clean.doFirst {
|
||||
delete project.layout.buildDirectory
|
||||
['.cxx'].forEach { delete file(it) }
|
||||
if (gradle.ext["isCleanupRapidOcr"] as Boolean) {
|
||||
libsToDeploy.forEach { it.clean() }
|
||||
} else {
|
||||
println("The library files of ${project.ext["projectName"]} won't be cleaned up due to the configuration")
|
||||
}
|
||||
}
|
||||
|
||||
gradle.taskGraph.whenReady { taskGraph ->
|
||||
if (taskGraph.allTasks.every { it.name == "clean" }) return
|
||||
|
||||
Utils.newFormatted("Version information for ${nameMap.PROJECT} library", [
|
||||
"OPENCV_MOBILE",
|
||||
"OPENCV_MOBILE_LABEL",
|
||||
"ONNX",
|
||||
"ONNX_RUNTIME",
|
||||
"NDK",
|
||||
"CMAKE",
|
||||
].collect { "${nameMap[it]}: ${versionMap[it]}" }).print()
|
||||
|
||||
libsToDeploy.forEach { it.deploy() }
|
||||
implementation libs.core.ktx
|
||||
implementation libs.appcompat
|
||||
|
||||
}
|
||||
@@ -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}")
|
||||
|
||||
@@ -1,679 +0,0 @@
|
||||
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry
|
||||
import org.apache.commons.compress.archivers.sevenz.SevenZFile
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.security.MessageDigest
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
ext.Utils = Utils
|
||||
|
||||
class Utils {
|
||||
|
||||
static LibDeployer newLibDeployer(Project project, String name, String downloadUrl) {
|
||||
return new LibDeployer(project, name, downloadUrl)
|
||||
}
|
||||
|
||||
static Formatted newFormatted(String title, Collection<String> contents = [], String subtitle = null) {
|
||||
return new Formatted(title, contents, subtitle)
|
||||
}
|
||||
|
||||
static Versions newVersions(Project project) {
|
||||
return new Versions("$project.rootDir/version.properties")
|
||||
}
|
||||
|
||||
private static String generateProgressBar(double progress, int length = 30) {
|
||||
int filledLength = (int) (length * progress / 100.0)
|
||||
String filled = '#' * filledLength
|
||||
String empty = '-' * (length - filledLength)
|
||||
filled + empty
|
||||
}
|
||||
|
||||
private static String formatFileSize(long size) {
|
||||
if (size < 1024) {
|
||||
String.format("%d B", size)
|
||||
} else if (size < (1024 * 1024)) {
|
||||
String.format("%.2f KB", size / 1024.0)
|
||||
} else if (size < (1024 * 1024 * 1024)) {
|
||||
String.format("%.2f MB", size / (1024.0 * 1024))
|
||||
} else {
|
||||
String.format("%.2f GB", size / (1024.0 * 1024 * 1024))
|
||||
}
|
||||
}
|
||||
|
||||
private static ExtractedFile extractFileFromUrl(url, standardize = false) {
|
||||
def fileNameWithExtension = url.tokenize("/").last()
|
||||
|
||||
String fileName
|
||||
String extensionName
|
||||
|
||||
if (fileNameWithExtension.contains('.')) {
|
||||
def lastIndexOfDot = fileNameWithExtension.lastIndexOf('.')
|
||||
fileName = fileNameWithExtension.take(lastIndexOfDot)
|
||||
extensionName = fileNameWithExtension.substring(lastIndexOfDot + 1)
|
||||
} else {
|
||||
fileName = fileNameWithExtension
|
||||
extensionName = ""
|
||||
}
|
||||
|
||||
if (standardize) {
|
||||
fileName = fileName.toLowerCase().replaceAll(/\s+/, '').replaceAll(/[^a-z0-9.]/, '-')
|
||||
}
|
||||
|
||||
return new ExtractedFile(fileName, extensionName)
|
||||
}
|
||||
|
||||
private static class LibDeployer {
|
||||
|
||||
String name
|
||||
String downloadUrl = null
|
||||
Project project
|
||||
|
||||
String sourceDir = File.separator
|
||||
File sourceFile = project.file(File.separator)
|
||||
|
||||
String destDir = File.separator
|
||||
File destFile = project.file(File.separator)
|
||||
|
||||
File cacheFile
|
||||
File cacheRootFile
|
||||
String cacheFileName
|
||||
String cacheFileExtensionName
|
||||
|
||||
File getSkipFile() {
|
||||
project.file(new File(destFile, "${cacheFileName}.skip"))
|
||||
}
|
||||
|
||||
File getTempOutFile() {
|
||||
project.file(new File(destFile, "temp-extracted"))
|
||||
}
|
||||
|
||||
LibDeployer(Project project, String name, String downloadUrl) {
|
||||
this.project = project
|
||||
this.name = name
|
||||
this.downloadUrl = downloadUrl
|
||||
|
||||
def extractedFile = extractFileFromUrl(downloadUrl, true)
|
||||
|
||||
this.cacheRootFile = project.file("cache")
|
||||
cacheRootFile.mkdirs()
|
||||
this.cacheFileName = "${extractedFile.fileName}-[${generateShortMd5String(downloadUrl).toLowerCase()}]"
|
||||
this.cacheFileExtensionName = extractedFile.extensionName
|
||||
this.cacheFile = project.file(new File(cacheRootFile, "$cacheFileName.$cacheFileExtensionName"))
|
||||
}
|
||||
|
||||
LibDeployer setSourceDir(String sourceDir) {
|
||||
this.sourceDir = sourceDir
|
||||
this.sourceFile = project.file(sourceDir)
|
||||
return this
|
||||
}
|
||||
|
||||
LibDeployer setDestDir(String destDir) {
|
||||
|
||||
// @Hint by SuperMonster003 on Dec 2, 2024.
|
||||
// ! On some platforms (such as Temurin),
|
||||
// ! using project.file might not correctly retrieve the absolute path of the current project.
|
||||
// ! For example, using "/foo/bar",
|
||||
// ! Expected path: "/.../AutoJs6/libs/.../src/sdk/native",
|
||||
// ! Actual path: "/src/sdk/native",
|
||||
// ! In this case, the leading path separator should be removed, using "foo/bar" instead of "/foo/bar".
|
||||
// !
|
||||
// ! zh-CN:
|
||||
// !
|
||||
// ! 某些平台 (如 Temurin) 使用 project.file 可能无法正确获取当前项目的绝对路径.
|
||||
// ! 以 "/foo/bar" 为例,
|
||||
// ! 预期路径: "/.../AutoJs6/libs/.../src/sdk/native",
|
||||
// ! 实际路径: "/src/sdk/native",
|
||||
// ! 这种情况下需要去除路径分隔符前缀, 即使用 "foo/bar" 而非 "/foo/bar".
|
||||
def destFile = destDir.startsWith(File.separator)
|
||||
? project.file(destDir.substring(1))
|
||||
: project.file(destDir)
|
||||
|
||||
destFile.mkdirs()
|
||||
this.destDir = destDir
|
||||
this.destFile = destFile
|
||||
return this
|
||||
}
|
||||
|
||||
void deploy() {
|
||||
if (tempOutFile.exists()) {
|
||||
project.delete tempOutFile
|
||||
}
|
||||
|
||||
def checkResult = checkCacheAndSkipFiles()
|
||||
def (shouldDownload, shouldExtract) = [checkResult.shouldDownload, checkResult.shouldExtract]
|
||||
|
||||
if (shouldDownload) {
|
||||
printDownloadInfo()
|
||||
downloadWithRetry()
|
||||
shouldExtract = true
|
||||
}
|
||||
|
||||
if (shouldExtract) {
|
||||
printExtractInfo()
|
||||
try {
|
||||
extractCacheFile()
|
||||
} catch (Exception e) {
|
||||
println("\n") // two line breaks
|
||||
println("Cache file was deleted as there is an error during extraction")
|
||||
println("Cache file: ${cacheFile.absolutePath}")
|
||||
cacheFile.delete()
|
||||
if (e.message != null) {
|
||||
println("Error message: $e.message")
|
||||
}
|
||||
println()
|
||||
throw e
|
||||
}
|
||||
generateMd5File(cacheFile)
|
||||
}
|
||||
}
|
||||
|
||||
void clean() {
|
||||
project.delete skipFile
|
||||
project.delete tempOutFile
|
||||
deleteDestAccordingToSrc()
|
||||
deleteCacheAccordingToMd5()
|
||||
}
|
||||
|
||||
private LinkedHashMap<String, Boolean> checkCacheAndSkipFiles() {
|
||||
def shouldDownload = true
|
||||
def shouldExtract = true
|
||||
|
||||
if (skipFile.exists()) {
|
||||
println("No need to download or extract \"$name\" archive file as the \"skip file\" exists")
|
||||
shouldDownload = false
|
||||
shouldExtract = false
|
||||
println()
|
||||
} else if (cacheFile.exists()) {
|
||||
if (validateMd5File(cacheFile)) {
|
||||
println("No need to download \"$name\" archive file as the cache file exists and is valid")
|
||||
shouldDownload = false
|
||||
println("Cache file of \"$name\" needs to be extracted as the \"skip file\" doesn't exist")
|
||||
} else {
|
||||
println("Cache file of \"$name\" was deleted as it is invalid")
|
||||
println("Cache file: $cacheFile")
|
||||
project.delete cacheFile
|
||||
def md5File = new File(cacheFile.parentFile, cacheFile.name + ".md5")
|
||||
if (md5File.exists()) {
|
||||
println("MD5 file of \"$name\" was deleted as it is unreliable")
|
||||
println("MD5 file: $md5File")
|
||||
project.delete md5File
|
||||
}
|
||||
}
|
||||
println()
|
||||
}
|
||||
return [shouldDownload: shouldDownload, shouldExtract: shouldExtract]
|
||||
}
|
||||
|
||||
private static boolean validateMd5File(File cacheFile) {
|
||||
def md5File = new File(cacheFile.parentFile, cacheFile.name + ".md5")
|
||||
if (!md5File.exists()) {
|
||||
return false
|
||||
}
|
||||
def expectedMd5 = md5File.text.trim().toUpperCase()
|
||||
def actualMd5 = generateMd5String(cacheFile).toUpperCase()
|
||||
return expectedMd5 == actualMd5
|
||||
}
|
||||
|
||||
private static void generateMd5File(File file) {
|
||||
def md5File = new File(file.parentFile, file.name + ".md5")
|
||||
|
||||
println("Generating MD5...")
|
||||
def generatedMd5 = generateMd5String(file)
|
||||
md5File.text = generatedMd5
|
||||
println("MD5 generated: $generatedMd5")
|
||||
|
||||
println()
|
||||
}
|
||||
|
||||
private static String generateMd5String(File file) {
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
FileChannel fileChannel = fis.getChannel()
|
||||
MessageDigest md = MessageDigest.getInstance("MD5")
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4096)
|
||||
|
||||
while (fileChannel.read(buffer) > 0) {
|
||||
buffer.flip()
|
||||
md.update(buffer)
|
||||
buffer.clear()
|
||||
}
|
||||
|
||||
return new BigInteger(1, md.digest()).toString(16).padLeft(32, '0')
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("MD5 algorithm not found", e)
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateShortMd5String(String s) {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5")
|
||||
md.update(s.bytes)
|
||||
return new BigInteger(1, md.digest()).toString(32)
|
||||
}
|
||||
|
||||
private void printDownloadInfo() {
|
||||
def title = "Download \"$name\" archive file for \"${project.ext["projectName"]}\" Gradle project"
|
||||
def srcInfo = "Source: $downloadUrl"
|
||||
def destInfo = "Destination: $cacheFile"
|
||||
def hintInfo = [
|
||||
"If the download gets stuck and won't finish,",
|
||||
"try downloading the source file with tools like IDM (Internet Download Manager),",
|
||||
"then renaming it into the destination path above.",
|
||||
]
|
||||
|
||||
def maxLength = [
|
||||
[title, srcInfo, destInfo].max { it.length() },
|
||||
hintInfo.max { it.length() }
|
||||
].max { it.length() }.length()
|
||||
|
||||
def infoList = [
|
||||
"=".repeat(maxLength),
|
||||
title,
|
||||
"-".repeat(maxLength),
|
||||
srcInfo,
|
||||
destInfo,
|
||||
"-".repeat(maxLength),
|
||||
hintInfo.join("\n"),
|
||||
"=".repeat(maxLength),
|
||||
"",
|
||||
]
|
||||
infoList.forEach { println(it) }
|
||||
}
|
||||
|
||||
private void printExtractInfo() {
|
||||
def title = "Extract the archive file for \"${project.ext["projectName"]}\" Gradle project"
|
||||
def srcInfo = "Source: $cacheFile"
|
||||
def destInfo = "Destination: $destFile"
|
||||
def hintInfo = []
|
||||
|
||||
def maxLength = [
|
||||
[title, srcInfo, destInfo].max { it.length() },
|
||||
hintInfo.max { it.length() }
|
||||
].max { it != null ? it.length() : 0 }.length()
|
||||
|
||||
def infoList = [
|
||||
"=".repeat(maxLength),
|
||||
title,
|
||||
"-".repeat(maxLength),
|
||||
srcInfo,
|
||||
destInfo,
|
||||
hintInfo.isEmpty() ? null : "-".repeat(maxLength),
|
||||
hintInfo.isEmpty() ? null : hintInfo.join("\n"),
|
||||
"=".repeat(maxLength),
|
||||
"",
|
||||
]
|
||||
infoList.forEach { if (it != null) println(it) }
|
||||
}
|
||||
|
||||
private void downloadWithRetry(int maxRetries = 3, int retryDelay = 2000) {
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
download()
|
||||
return
|
||||
} catch (IOException e) {
|
||||
println("Attempt $attempt of $maxRetries failed: ${e.message}")
|
||||
if (attempt < maxRetries) {
|
||||
println("Retrying after ${retryDelay / 1000} seconds...")
|
||||
sleep(retryDelay)
|
||||
} else {
|
||||
throw new GradleException("Download failed after $maxRetries attempts", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractCacheFile() {
|
||||
switch (cacheFileExtensionName) {
|
||||
case 'zip':
|
||||
handleZip()
|
||||
break
|
||||
case '7z':
|
||||
handleSevenZip()
|
||||
break
|
||||
default:
|
||||
throw new GradleException("Unknown archive file type: $cacheFileExtensionName")
|
||||
}
|
||||
println("All files extracted into [ $destFile ]")
|
||||
if (!skipFile.exists()) {
|
||||
skipFile.parentFile.mkdirs()
|
||||
skipFile.createNewFile()
|
||||
println("File \"${skipFile.name}\" created")
|
||||
}
|
||||
println()
|
||||
}
|
||||
|
||||
private void deleteDestAccordingToSrc() {
|
||||
// TODO by SuperMonster003 on Oct 22, 2024.
|
||||
// ! Use the cache file as a reference file.
|
||||
// ! If the cache file does not exist, attempt to re-download it.
|
||||
// ! According to the sourceDir, obtain a single-level file directory tree
|
||||
// ! from the compressed file, and delete the corresponding files
|
||||
// ! under the destDir directory based on this.
|
||||
// ! If the processed destDir is empty, delete it as well.
|
||||
// ! zh-CN:
|
||||
// ! 将缓存文件作为参考文件, 缓存文件不存在时, 尝试重新下载.
|
||||
// ! 根据 sourceDir, 从压缩文件中获取单一层级的文件目录树, 由此删除 destDir 目录下的对应文件.
|
||||
// ! 处理后的 destDir 若为空, 则一并删除.
|
||||
|
||||
/* Pending code... */
|
||||
|
||||
// @Caution by SuperMonster003 on Oct 22, 2024.
|
||||
// ! This operation is NOT safe.
|
||||
// ! zh-CN: 此操作存在安全隐患.
|
||||
project.delete destFile.absolutePath
|
||||
|
||||
def tmp = destFile.parentFile
|
||||
while (tmp != project.projectDir) {
|
||||
if (tmp.listFiles().toList().isEmpty()) {
|
||||
println("Delete empty directory: $tmp.absolutePath")
|
||||
project.delete tmp
|
||||
}
|
||||
tmp = tmp.parentFile
|
||||
}
|
||||
}
|
||||
|
||||
private deleteCacheAccordingToMd5() {
|
||||
cacheRootFile.eachFile { file ->
|
||||
if (!file.name.endsWith('.md5')) {
|
||||
def md5File = new File(file.parentFile, file.name + ".md5")
|
||||
if (!md5File.exists() || !validateMd5File(file)) {
|
||||
project.delete(file)
|
||||
println("Delete cache file: ${file.absolutePath}")
|
||||
if (md5File.exists()) {
|
||||
project.delete(md5File)
|
||||
println("Delete MD5 file: ${md5File.absolutePath}")
|
||||
}
|
||||
println()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void download() {
|
||||
final int MAX_RETRIES = 3
|
||||
int attempt = 0
|
||||
boolean success = false
|
||||
boolean shouldPrintProgress = project.ext["platform"]["shouldPrintProgress"] == true
|
||||
|
||||
while (attempt < MAX_RETRIES && !success) {
|
||||
try {
|
||||
attempt++
|
||||
|
||||
cacheFile.parentFile.mkdirs()
|
||||
|
||||
def urlConn = new URI(downloadUrl).toURL().openConnection()
|
||||
urlConn.connectTimeout = 120_000
|
||||
urlConn.readTimeout = 90_000
|
||||
long fileSize = urlConn.contentLengthLong
|
||||
|
||||
urlConn.getInputStream().withCloseable { inputStream ->
|
||||
cacheFile.withOutputStream { outputStream ->
|
||||
byte[] buffer = new byte[8192]
|
||||
long downloadedSize = 0
|
||||
int bytesRead
|
||||
|
||||
if (shouldPrintProgress && fileSize <= 0) {
|
||||
println "\rDownloading..."
|
||||
}
|
||||
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
downloadedSize += bytesRead
|
||||
if (shouldPrintProgress && fileSize > 0) {
|
||||
try {
|
||||
double progress = (downloadedSize * 100.0 / fileSize)
|
||||
String progressBar = generateProgressBar(progress)
|
||||
print String.format("\rDownloading... [ %s ] %.2f%%", progressBar, progress)
|
||||
System.out.flush()
|
||||
} catch (Exception ignored) {
|
||||
/* Ignored. */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String downloadPath = cacheFile.absolutePath
|
||||
if (fileSize > 0) {
|
||||
String formattedFileSize = formatFileSize(fileSize)
|
||||
print String.format("\rDownload complete [ %s | %s ]\n", downloadPath, formattedFileSize)
|
||||
} else {
|
||||
print String.format("\rDownload complete [ %s ]\n", downloadPath)
|
||||
}
|
||||
System.out.flush()
|
||||
println()
|
||||
|
||||
success = true
|
||||
|
||||
} catch (SocketTimeoutException ignored) {
|
||||
println String.format("Attempt %d/%d failed: Connection timed out. Retrying...", attempt, MAX_RETRIES)
|
||||
} catch (IOException e) {
|
||||
println String.format("Attempt %d/%d failed: %s. Retrying...", attempt, MAX_RETRIES, e.message)
|
||||
}
|
||||
|
||||
if (!success && attempt >= MAX_RETRIES) {
|
||||
println "Download failed after $MAX_RETRIES attempts."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleZip() {
|
||||
def shouldPrintProgress = project.ext["platform"]["shouldPrintProgress"] == true
|
||||
|
||||
def zipFileForTotalSize = new ZipFile(cacheFile)
|
||||
def zipEntriesForTotalSize = zipFileForTotalSize.entries()
|
||||
def entries = []
|
||||
def totalExtractedSize = 0L
|
||||
|
||||
def sourceDirPath = new File(sourceDir).path
|
||||
if (sourceDirPath.startsWith(File.separator)) sourceDirPath = sourceDirPath.substring(1)
|
||||
|
||||
while (zipEntriesForTotalSize.hasMoreElements()) {
|
||||
def entry = zipEntriesForTotalSize.nextElement()
|
||||
def entryName = new File(entry.name).path
|
||||
if (entryName.startsWith(sourceDirPath)) {
|
||||
entries.add(entry)
|
||||
totalExtractedSize += entry.size
|
||||
}
|
||||
}
|
||||
|
||||
zipFileForTotalSize.close()
|
||||
|
||||
def zipFile = new ZipFile(cacheFile)
|
||||
def zipEntries = zipFile.entries()
|
||||
|
||||
int totalEntries = entries.size()
|
||||
int processedEntries = 0
|
||||
|
||||
while (zipEntries.hasMoreElements()) {
|
||||
def entry = zipEntries.nextElement()
|
||||
def entryName = new File(entry.name).path
|
||||
if (entryName.startsWith(sourceDirPath)) {
|
||||
File outFile = project.file(new File(tempOutFile, entryName.substring(sourceDirPath.length())))
|
||||
if (entry.isDirectory()) {
|
||||
outFile.mkdirs()
|
||||
} else {
|
||||
outFile.parentFile.mkdirs()
|
||||
zipFile.getInputStream(entry).withCloseable { entryInputStream ->
|
||||
outFile.withOutputStream { outputStream ->
|
||||
byte[] buffer = new byte[8192]
|
||||
int bytesRead
|
||||
while ((bytesRead = entryInputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPrintProgress) {
|
||||
double progress = (processedEntries * 100.0 / totalEntries)
|
||||
String progressBar = generateProgressBar(progress)
|
||||
print String.format("\rExtracting... [ %s ] %.2f%%", progressBar, progress)
|
||||
System.out.flush()
|
||||
}
|
||||
processedEntries++
|
||||
}
|
||||
}
|
||||
|
||||
String formattedTotalExtractedSize = formatFileSize(totalExtractedSize)
|
||||
print String.format("\rExtraction complete [ %s | %s ]\n", destFile.absolutePath, formattedTotalExtractedSize)
|
||||
System.out.flush()
|
||||
println()
|
||||
|
||||
zipFile.close()
|
||||
|
||||
project.copy {
|
||||
from tempOutFile
|
||||
into destFile
|
||||
}
|
||||
project.delete tempOutFile
|
||||
}
|
||||
|
||||
private void handleSevenZip() {
|
||||
boolean shouldPrintProgress = project.ext["platform"]["shouldPrintProgress"] == true
|
||||
|
||||
String sourceDirPath = new File(sourceDir).path
|
||||
if (sourceDirPath.startsWith(File.separator)) sourceDirPath = sourceDirPath.substring(1)
|
||||
|
||||
SevenZFile sevenZFile = new SevenZFile.Builder().setFile(cacheFile).get()
|
||||
List<SevenZArchiveEntry> allEntries = sevenZFile.getEntries()
|
||||
|
||||
List<SevenZArchiveEntry> targetEntries = allEntries.findAll { it.name.startsWith(sourceDirPath) }
|
||||
|
||||
int entriesCount = targetEntries.size()
|
||||
long totalExtractedSize = 0L
|
||||
for (SevenZArchiveEntry entry : targetEntries) {
|
||||
totalExtractedSize += entry.size
|
||||
}
|
||||
|
||||
int processedEntries = 0
|
||||
byte[] buffer = new byte[64 * 1024]
|
||||
|
||||
for (SevenZArchiveEntry entry : targetEntries) {
|
||||
File outFile = project.file(new File(tempOutFile, new File(entry.name).path.substring(sourceDirPath.length())))
|
||||
if (entry.isDirectory()) {
|
||||
outFile.mkdirs()
|
||||
} else {
|
||||
outFile.parentFile.mkdirs()
|
||||
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outFile))) {
|
||||
sevenZFile.getInputStream(entry).withCloseable { entryInputStream ->
|
||||
int bytesRead
|
||||
while ((bytesRead = entryInputStream.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, bytesRead)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldPrintProgress) {
|
||||
double progress = (processedEntries * 100.0 / entriesCount)
|
||||
String progressBar = generateProgressBar(progress)
|
||||
print String.format("\rExtracting... [ %s ] %.2f%%", progressBar, progress)
|
||||
System.out.flush()
|
||||
}
|
||||
processedEntries++
|
||||
}
|
||||
|
||||
String formattedUncompressedSize = formatFileSize(totalExtractedSize)
|
||||
print String.format("\rExtraction complete [ %s | %s ]\n", destFile.absolutePath, formattedUncompressedSize)
|
||||
System.out.flush()
|
||||
|
||||
sevenZFile.close()
|
||||
|
||||
project.copy {
|
||||
from tempOutFile
|
||||
into destFile
|
||||
}
|
||||
project.delete tempOutFile
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Versions {
|
||||
|
||||
private Properties properties
|
||||
|
||||
private static String PREFIX_PUBLIC = "PUBLIC"
|
||||
private static String SUFFIX_VERSION = "VERSION"
|
||||
|
||||
Versions(String filePath) {
|
||||
File file = new File(filePath)
|
||||
if (!file.canRead()) {
|
||||
throw FileNotFoundException("Cannot read file '$filePath'")
|
||||
}
|
||||
properties = new Properties()
|
||||
properties.load(new FileInputStream(file))
|
||||
}
|
||||
|
||||
String get(String propertyName) {
|
||||
if (propertyName.contains("/")) {
|
||||
return get(propertyName.split("/").toList())
|
||||
}
|
||||
var value = properties["${propertyName}_$SUFFIX_VERSION"] as String
|
||||
return value == PREFIX_PUBLIC
|
||||
? properties["${PREFIX_PUBLIC}_${propertyName}_$SUFFIX_VERSION"] as String
|
||||
: value
|
||||
}
|
||||
|
||||
String get(List<String> propertyInfo) {
|
||||
def (lib, body) = propertyInfo
|
||||
var value = properties["${lib}_${body}_$SUFFIX_VERSION"] as String
|
||||
return value == PREFIX_PUBLIC
|
||||
? properties["${PREFIX_PUBLIC}_${body}_$SUFFIX_VERSION"] as String
|
||||
: value
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Formatted {
|
||||
|
||||
private String title
|
||||
private Collection<String> contents
|
||||
private String subtitle
|
||||
private List<String> formattedOutput
|
||||
|
||||
Formatted(String title, Collection<String> contents = [], String subtitle = null) {
|
||||
this.title = title
|
||||
this.contents = contents
|
||||
this.subtitle = subtitle
|
||||
|
||||
formattedOutput = {
|
||||
def elements = []
|
||||
if (subtitle != null) elements.add(subtitle)
|
||||
elements.addAll(contents)
|
||||
def maxLength = ([title, subtitle] + contents).findAll().collect { it.length() }.max()
|
||||
def result = [
|
||||
'=' * maxLength,
|
||||
title,
|
||||
subtitle,
|
||||
contents.isEmpty() ? null : '-' * maxLength
|
||||
].findAll() + contents + ['=' * maxLength, '']
|
||||
return result
|
||||
}()
|
||||
}
|
||||
|
||||
void print(boolean contentsMatters = false) {
|
||||
formattedOutput.each {
|
||||
if (!contentsMatters || !contents.isEmpty()) {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void throwException() {
|
||||
throw new Exception(formattedOutput.join('\n'))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ExtractedFile {
|
||||
String fileName
|
||||
String extensionName
|
||||
|
||||
ExtractedFile(String fileName, String extensionName) {
|
||||
this.fileName = fileName
|
||||
this.extensionName = extensionName
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -61,19 +61,16 @@ pluginManagement {
|
||||
val overriddenKspVersion: String? = null
|
||||
|
||||
val versionProperties = java.util.Properties().apply {
|
||||
load(java.io.FileInputStream("$rootDir/version.properties"))
|
||||
rootDir.resolve("version.properties").inputStream().use { load(it) }
|
||||
}
|
||||
|
||||
// @AnchorBegin EMBEDDED_KOTLIN_LIST
|
||||
// @Script /.utils/scrape-and-inject-embedded-kotlin-list.mjs
|
||||
// @Signature Pair<Gradle Version, Embedded Compatible Kotlin Version>
|
||||
// @Reference gradle-<version>-src.zip\gradle\dependency-management\kotlin-version.properties
|
||||
// @AnchorBegin GRADLE_KOTLIN_COMPATIBILITY_LIST
|
||||
// @Script /.utils/scrape-and-inject-gradle-kotlin-compatibility-list.mjs
|
||||
// @Signature Pair<Gradle Version, Max Supported Kotlin Version>
|
||||
// @Reference https://docs.gradle.org/current/userguide/compatibility.html#kotlin
|
||||
// @Updated by SuperMonster003 on Aug 9, 2025.
|
||||
val embeddedKotlin = listOf(
|
||||
"9.0" to "2.2.0",
|
||||
"8.14" to "2.1.10", /* Unofficial. */
|
||||
"8.13" to "2.1.10", /* Unofficial. */
|
||||
// @Updated by SuperMonster003 on Sep 18, 2025.
|
||||
val gradleKotlinCompatibility = listOf(
|
||||
"9.0.0" to "2.2.0",
|
||||
"8.12" to "2.0.21",
|
||||
"8.11" to "2.0.20",
|
||||
"8.10" to "1.9.24",
|
||||
@@ -84,15 +81,16 @@ pluginManagement {
|
||||
"8.3" to "1.9.0",
|
||||
"8.2" to "1.8.20",
|
||||
)
|
||||
// @AnchorEnd EMBEDDED_KOTLIN_LIST
|
||||
// @AnchorEnd GRADLE_KOTLIN_COMPATIBILITY_LIST
|
||||
|
||||
// @AnchorBegin JAVA_GRADLE_COMPATIBILITY_LIST
|
||||
// @Script /.utils/scrape-java-gradle-compatibility-map.mjs
|
||||
// @Signature Pair<Supported Java Version, Min Required Gradle Version>
|
||||
// @Reference https://docs.gradle.org/current/userguide/compatibility.html#java_runtime
|
||||
// @Updated by SuperMonster003 on May 12, 2025.
|
||||
// @Updated by SuperMonster003 on Sep 18, 2025.
|
||||
val javaGradleCompatibility = listOf(
|
||||
25 to "9.0", /* Unofficial. */
|
||||
26 to "N/A",
|
||||
25 to "9.1",
|
||||
24 to "8.14",
|
||||
23 to "8.10",
|
||||
22 to "8.8",
|
||||
@@ -128,9 +126,9 @@ pluginManagement {
|
||||
// @AnchorBegin ANDROID_GRADLE_PLUGIN_RELEASES_LIST
|
||||
// @Script /.utils/scrape-and-inject-agp-releases.mjs
|
||||
// @Reference https://developer.android.com/reference/tools/gradle-api
|
||||
// @Updated by SuperMonster003 on Sep 12, 2025.
|
||||
// @Updated by SuperMonster003 on Sep 23, 2025.
|
||||
val agpReleases = listOf(
|
||||
"9.0.0-alpha05",
|
||||
"9.0.0-alpha06",
|
||||
"8.13.0",
|
||||
"8.12.3",
|
||||
"8.11.2",
|
||||
@@ -215,13 +213,11 @@ pluginManagement {
|
||||
val utils = object {
|
||||
|
||||
private val SUFFIX_PRIORITY: Map<String, Int> = mapOf(
|
||||
// 早期/快照
|
||||
"canary" to 1, "nightly" to 1, "snapshot" to 1, "dev" to 1,
|
||||
"pre-alpha" to 2, "prealpha" to 2, "preview" to 2, "eap" to 2, "milestone" to 2,
|
||||
"alpha" to 3,
|
||||
"beta" to 4,
|
||||
"rc" to 5,
|
||||
// 稳定/正式
|
||||
"" to 10, "stable" to 10, "ga" to 10, "final" to 10, "release" to 10, "lts" to 10
|
||||
)
|
||||
|
||||
@@ -269,8 +265,7 @@ pluginManagement {
|
||||
}
|
||||
|
||||
fun toVersionParts(version: String): Pair<List<Int>, Pair<String, Int>> {
|
||||
// 支持: 1.2.3-rc1 / 1.2.3 RC 1 / 1.2.3-Alpha / 1.2.3.m2 / 1.2.3_preview-2 等
|
||||
// 以第一个空白/加号/连字符分隔数字部分与后缀部分
|
||||
// e.g. "1.2.3-rc1" / "1.2.3 RC 1" / "1.2.3-Alpha" / "1.2.3.m2" / "1.2.3_preview-2".
|
||||
val split = version.split(Regex("[\\s+\\-]"), limit = 2)
|
||||
val numberStr = split[0]
|
||||
val numberParts = numberStr.split('.').map {
|
||||
@@ -280,8 +275,6 @@ pluginManagement {
|
||||
val suffixStr = split.getOrNull(1)?.trim().orEmpty()
|
||||
if (suffixStr.isEmpty()) return numberParts to ("" to 0)
|
||||
|
||||
// 更宽松的匹配: 名称 + 可选分隔符 + 可选数字; 或 空名称 + 数字 (极少见)
|
||||
// 分隔符允许: 空格 . _ -
|
||||
val regex = Regex("([A-Za-z]+)[\\s._-]*(\\d*)|([A-Za-z]*)[\\s._-]*(\\d+)", RegexOption.IGNORE_CASE)
|
||||
val m = regex.matchEntire(suffixStr) ?: return numberParts to ("" to 0)
|
||||
|
||||
@@ -362,8 +355,9 @@ pluginManagement {
|
||||
// @AnchorBegin ANDROID_STUDIO_CODENAME_VERSION_MAP
|
||||
// @Script /.utils/scrape-and-inject-android-studio-codename_maps.mjs
|
||||
// @Reference https://developer.android.com/studio/archive?hl=en
|
||||
// @Updated by SuperMonster003 on Sep 5, 2025.
|
||||
// @Updated by SuperMonster003 on Sep 23, 2025.
|
||||
codenameVersionMap = mapOf(
|
||||
"2025.2" to "O",
|
||||
"2025.1" to "N",
|
||||
"2024.3" to "M",
|
||||
"2024.2" to "L",
|
||||
@@ -387,8 +381,9 @@ pluginManagement {
|
||||
// @AnchorBegin ANDROID_STUDIO_CODENAME_MAP
|
||||
// @Script /.utils/scrape-and-inject-android-studio-codename_maps.mjs
|
||||
// @Reference https://developer.android.com/studio/archive?hl=en
|
||||
// @Updated by SuperMonster003 on Apr 11, 2025.
|
||||
// @Updated by SuperMonster003 on Sep 23, 2025.
|
||||
codenameMap = mapOf(
|
||||
"O" to "Otter", /* Born on Sep 22, 2025. */
|
||||
"N" to "Narwhal", /* Born on Mar 19, 2025. */
|
||||
"M" to "Meerkat", /* Born on Nov 12, 2024. */
|
||||
"L" to "Ladybug", /* Born on Jul 15, 2024. */
|
||||
@@ -427,9 +422,10 @@ pluginManagement {
|
||||
val intelliJIdea = object : Platform(
|
||||
name = "IntelliJIdea", vendor = "Jetbrains",
|
||||
// @Reference AGP Upgrade Assistant integrated within JetBrains IntelliJ IDEA.
|
||||
// @Updated by SuperMonster003 on Aug 10, 2025. (Manual)
|
||||
// @Updated by SuperMonster003 on Aug 20, 2025. (Manual)
|
||||
agpVersionMap = mapOf(
|
||||
"2025.2" to "8.11.1",
|
||||
"2025.2.2" to "8.12.0",
|
||||
"2025.2.1" to "8.11.1",
|
||||
"2025.1" to "8.10.1",
|
||||
"2024.3" to "8.7.3",
|
||||
"2024.2" to "8.5.2",
|
||||
@@ -510,6 +506,8 @@ pluginManagement {
|
||||
val libs = listOf(
|
||||
Classpath(id = "com.android.tools.build:gradle", version = overriddenAgpVersion ?: "auto:agp"),
|
||||
Classpath(id = "org.jetbrains.kotlin:kotlin-gradle-plugin", version = overriddenKotlinVersion ?: "auto:kotlin"),
|
||||
Classpath(id = "org.apache.commons:commons-compress", version = "toml:commons-compress"),
|
||||
Classpath(id = "org.tukaani:xz", version = "toml:xz"),
|
||||
Plugin(id = "com.google.devtools.ksp", version = overriddenKspVersion ?: "auto:ksp"),
|
||||
)
|
||||
|
||||
@@ -585,12 +583,6 @@ pluginManagement {
|
||||
"1.8.22" to "1.0.11", /* Jun 9, 2023. */
|
||||
"1.8.21" to "1.0.11", /* Apr 27, 2023. */
|
||||
"1.8.20" to "1.0.11", /* Apr 18, 2023. */
|
||||
"1.8.20-RC2" to "1.0.9", /* Mar 24, 2023. */
|
||||
"1.8.20-RC" to "1.0.9", /* Mar 9, 2023. */
|
||||
"1.8.20-Beta" to "1.0.9", /* Feb 9, 2023. */
|
||||
"1.8.10" to "1.0.9", /* Feb 3, 2023. */
|
||||
"1.8.0" to "1.0.9", /* Jan 26, 2023. */
|
||||
"1.8.0-RC2" to "1.0.8", /* Dec 21, 2022. */
|
||||
)
|
||||
// @AnchorEnd KSP_VERSION_MAP
|
||||
|
||||
@@ -672,6 +664,7 @@ pluginManagement {
|
||||
|
||||
val specified = "specified"
|
||||
val auto = "auto"
|
||||
val toml = "toml"
|
||||
|
||||
val fallbackSuffix = " [$fallback]"
|
||||
val upgradedSuffix = " [$upgraded]"
|
||||
@@ -679,6 +672,7 @@ pluginManagement {
|
||||
val nearestLowerMatchedSuffix = " [$nearestLowerMatched]"
|
||||
val autoSpecifiedSuffix = " [$auto-$specified]"
|
||||
val userSpecifiedSuffix = " [user-$specified]"
|
||||
val tomlSpecifiedSuffix = " [toml-$specified]"
|
||||
|
||||
}
|
||||
|
||||
@@ -748,23 +742,23 @@ pluginManagement {
|
||||
|
||||
override fun refinedBestMatchingValue(bestMatchingValue: String?): String? {
|
||||
val currentGradleVersion = gradle.gradleVersion.toGradleVersion()
|
||||
val embeddedMin = embeddedKotlin
|
||||
val kotlinMin = gradleKotlinCompatibility
|
||||
.filter { (gradleMin, _) -> currentGradleVersion >= gradleMin.toGradleVersion() }
|
||||
.maxByOrNull { it.first.toGradleVersion() }
|
||||
?.second
|
||||
return when {
|
||||
embeddedMin == null -> bestMatchingValue
|
||||
kotlinMin == null -> bestMatchingValue
|
||||
bestMatchingValue == null -> {
|
||||
bestMatchingOperationHintSuffix += identifier.autoSpecifiedSuffix
|
||||
embeddedMin
|
||||
kotlinMin
|
||||
}
|
||||
bestMatchingValue.toGradleVersion() < embeddedMin.toGradleVersion() -> {
|
||||
bestMatchingValue.toGradleVersion() < kotlinMin.toGradleVersion() -> {
|
||||
bestMatchingOperationHintSuffix += identifier.upgradedSuffix
|
||||
embeddedMin
|
||||
kotlinMin
|
||||
}
|
||||
bestMatchingValue.toGradleVersion() > embeddedMin.toGradleVersion() -> {
|
||||
bestMatchingValue.toGradleVersion() > kotlinMin.toGradleVersion() -> {
|
||||
bestMatchingOperationHintSuffix += identifier.downgradedSuffix
|
||||
embeddedMin
|
||||
kotlinMin
|
||||
}
|
||||
else -> bestMatchingValue
|
||||
}
|
||||
@@ -803,11 +797,34 @@ pluginManagement {
|
||||
},
|
||||
)
|
||||
|
||||
private val toml: Map<String, String> = run {
|
||||
val lines = file("gradle/libs.versions.toml").also {
|
||||
require(it.isFile) { "File $it doesn't exist" }
|
||||
}.readLines()
|
||||
val versions = mutableMapOf<String, String>()
|
||||
var inVersions = false
|
||||
val keyValuePattern = Regex("""^\s*([A-Za-z0-9._-]+)\s*=\s*"(.*?)"\s*(#.*)?$""")
|
||||
for (raw in lines) {
|
||||
val line = raw.trim()
|
||||
if (line.isEmpty() || line.startsWith("#")) continue
|
||||
if (line.startsWith("[")) {
|
||||
inVersions = line == "[versions]"
|
||||
continue
|
||||
}
|
||||
if (!inVersions) continue
|
||||
keyValuePattern.find(line)?.let { m ->
|
||||
val (_, key, value) = m.groupValues
|
||||
versions[key] = value
|
||||
}
|
||||
}
|
||||
return@run versions
|
||||
}
|
||||
|
||||
val classpath = config.libs.filterIsInstance<Classpath>().map { lib ->
|
||||
var suffix = ""
|
||||
val version: String = when {
|
||||
lib.version.startsWith("${identifier.auto}:") -> {
|
||||
val mapType = lib.version.substring("${identifier.auto}:".length)
|
||||
val mapType = lib.version.removePrefix("${identifier.auto}:")
|
||||
val ver = when (mapType) {
|
||||
"agp" -> versions.agp
|
||||
"kotlin" -> versions.kotlin
|
||||
@@ -819,6 +836,11 @@ pluginManagement {
|
||||
suffix += identifier.fallbackSuffix
|
||||
} ?: consts.DEFAULT_VERSION
|
||||
}
|
||||
lib.version.startsWith("${identifier.toml}:") -> {
|
||||
toml.getValue(lib.version.removePrefix("${identifier.toml}:")).also {
|
||||
suffix += identifier.tomlSpecifiedSuffix
|
||||
}
|
||||
}
|
||||
else -> lib.version.also {
|
||||
suffix += identifier.userSpecifiedSuffix
|
||||
}
|
||||
@@ -836,11 +858,16 @@ pluginManagement {
|
||||
var suffix = ""
|
||||
val version: String = when {
|
||||
lib.version.startsWith("${identifier.auto}:") -> {
|
||||
val automator = pluginVersionAutomatorMap[lib.version.substring("${identifier.auto}:".length)]!!
|
||||
val automator = pluginVersionAutomatorMap.getValue(lib.version.removePrefix("${identifier.auto}:"))
|
||||
val result = automator.invoke(config.kspVersionMap)
|
||||
result["suffix"]?.let { s -> suffix += s }
|
||||
result["version"] ?: throw Exception("Unknown version for plugin ${lib.id}")
|
||||
}
|
||||
lib.version.startsWith("${identifier.toml}:") -> {
|
||||
toml.getValue(lib.version.removePrefix("${identifier.toml}:")).also {
|
||||
suffix += identifier.tomlSpecifiedSuffix
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
suffix += identifier.userSpecifiedSuffix
|
||||
lib.version
|
||||
@@ -923,18 +950,20 @@ pluginManagement {
|
||||
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
dependencies /* Android/Kotlin Gradle Plugin. */ {
|
||||
dependencies {
|
||||
notations.classpath.forEach { classpath(it) }
|
||||
}
|
||||
dependencies /* Apache Compress for utils.build.gradle module. */ {
|
||||
classpath("org.apache.commons:commons-compress:1.28.0")
|
||||
classpath("org.tukaani:xz:1.10")
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
@@ -951,28 +980,10 @@ pluginManagement {
|
||||
}
|
||||
|
||||
gradle.extra.apply {
|
||||
set("platform", platform)
|
||||
set("isCleanupPaddleOcr", config.isCleanupPaddleOcr)
|
||||
set("isCleanupRapidOcr", config.isCleanupRapidOcr)
|
||||
set("isHideConsoleInfoHintSuffix", config.isHideConsoleInfoHintSuffix)
|
||||
}
|
||||
|
||||
gradle.beforeProject {
|
||||
extensions.extraProperties["kotlinVersion"] = notations.classpath.find {
|
||||
it.contains("kotlin-gradle-plugin")
|
||||
}?.substringAfterLast(":") ?: config.fallbackKotlinVersion
|
||||
extensions.extraProperties["platform"] = platform
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("./gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user