Skip to content

Commit b23314d

Browse files
authored
Migrate to AGP 9.2.1 + Gradle 9.4.1 (new DSL, built-in Kotlin) (#61)
* Migrate to AGP 9.2.1 + Gradle 9.4.1 (new DSL, built-in Kotlin) - buildSrc: Android Gradle Plugin 8.13.2 -> 9.2.1 (bundles KGP 2.3.10, matching the pinned Kotlin version). Gradle wrapper 8.14.5 -> 9.4.1. - app: drop the kotlin-android plugin; AGP 9 provides built-in Kotlin (android.builtInKotlin defaults to true), so applying it would clash. - Helpers.kt: adopt AGP 9's new DSL. Remove the legacy variant API (AbstractAppExtension / applicationVariants / BaseVariantOutputImpl) used for in-place APK output renaming, which no longer exists under android.newDsl=true. - Replace APK renaming with RenameApkTask, wired via androidComponents.onVariants + artifacts.use(...).toListenTo(APK). It copies each ABI split into build/outputs/renamed_apks/<variant>/ as NekoBox-<version>[-<abi>].apk and preserves output-metadata.json. Public APIs only (no AGP internals, no Gradle impldep). - CI workflows: point the arm64-v8a APK find/upload at renamed_apks. - Fix minor Gradle 9 deprecations (tasks.register, jniLibs.directories, rootProject.layout.buildDirectory). KSP stays at 2.3.9 (latest; the 2.3 line dropped the -kspRev suffix and is compatible with KGP 2.3.10). Verified locally: app:assembleOssDebug builds and renameApksForOssDebug runs as part of assemble. * Address CodeRabbit review: stricter task input + CI APK guard - RenameApkTask.baseName: @internal -> @input so a version bump correctly invalidates the task and re-copies APKs with the new name (avoids stale output). Verified renameApksForOssDebug still runs automatically during a clean app:assembleOssDebug (it listens on SingleArtifact.APK). - CI workflows: fail loudly if no arm64-v8a APK is found under renamed_apks instead of silently proceeding with an empty path. * Address review bots: metadata, shell quoting, trailing newline - RenameApkTask: drop builtArtifacts.save(outputDir). The metadata it writes references the original un-renamed APK paths (outputs/apk/...), not the NekoBox-* copies, so it would mislead any BuiltArtifactsLoader consumer (Greptile P1). Nothing downstream consumes it, so omit it. - CI workflows (all four): pipe find through 'head -n 1' and quote dirname "$APK" to avoid multi-match / word-splitting corruption of GITHUB_ENV (CodeRabbit + Greptile P2). - Helpers.kt: add trailing newline (Greptile P2). Re-verified app:assembleOssDebug builds and renameApksForOssDebug emits the four NekoBox-<ver>-<abi>.apk files.
1 parent 4c7bdcf commit b23314d

10 files changed

Lines changed: 173 additions & 57 deletions

File tree

.github/workflows/build.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,12 @@ jobs:
133133
echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > release.keystore
134134
fi
135135
KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS }}" ./gradlew app:assembleOssRelease
136-
APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk')
137-
APK=$(dirname $APK)
136+
APK=$(find app/build/outputs/renamed_apks -name '*arm64-v8a*.apk' | head -n 1)
137+
if [ -z "$APK" ]; then
138+
echo "Error: no arm64-v8a APK found under app/build/outputs/renamed_apks" >&2
139+
exit 1
140+
fi
141+
APK=$(dirname "$APK")
138142
echo "APK=$APK" >> $GITHUB_ENV
139143
- name: Upload Artifacts
140144
uses: namespace-actions/upload-artifact@v1

.github/workflows/ci.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,12 @@ jobs:
139139
export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}"
140140
./run init action gradle
141141
./gradlew app:assembleOssDebug
142-
APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk')
143-
APK=$(dirname $APK)
142+
APK=$(find app/build/outputs/renamed_apks -name '*arm64-v8a*.apk' | head -n 1)
143+
if [ -z "$APK" ]; then
144+
echo "Error: no arm64-v8a APK found under app/build/outputs/renamed_apks" >&2
145+
exit 1
146+
fi
147+
APK=$(dirname "$APK")
144148
echo "APK=$APK" >> $GITHUB_ENV
145149
- name: Upload Artifacts
146150
uses: namespace-actions/upload-artifact@v1

.github/workflows/preview.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,12 @@ jobs:
125125
export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}"
126126
./run init action gradle
127127
./gradlew app:assemblePreviewRelease
128-
APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk')
129-
APK=$(dirname $APK)
128+
APK=$(find app/build/outputs/renamed_apks -name '*arm64-v8a*.apk' | head -n 1)
129+
if [ -z "$APK" ]; then
130+
echo "Error: no arm64-v8a APK found under app/build/outputs/renamed_apks" >&2
131+
exit 1
132+
fi
133+
APK=$(dirname "$APK")
130134
echo "APK=$APK" >> $GITHUB_ENV
131135
- uses: namespace-actions/upload-artifact@v1
132136
with:

.github/workflows/release.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,12 @@ jobs:
135135
echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > release.keystore
136136
fi
137137
KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS }}" ./gradlew app:assembleOssRelease
138-
APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk')
139-
APK=$(dirname $APK)
138+
APK=$(find app/build/outputs/renamed_apks -name '*arm64-v8a*.apk' | head -n 1)
139+
if [ -z "$APK" ]; then
140+
echo "Error: no arm64-v8a APK found under app/build/outputs/renamed_apks" >&2
141+
exit 1
142+
fi
143+
APK=$(dirname "$APK")
140144
echo "APK=$APK" >> $GITHUB_ENV
141145
- uses: namespace-actions/upload-artifact@v1
142146
with:

app/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
plugins {
44
id("com.android.application")
5-
id("kotlin-android")
5+
// kotlin-android is no longer applied: AGP 9.0+ provides built-in Kotlin support
6+
// (android.builtInKotlin defaults to true), so the org.jetbrains.kotlin.android plugin
7+
// would clash ("extension already registered with name 'kotlin'").
68
id("com.google.devtools.ksp")
79
id("kotlin-parcelize")
810
}

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ allprojects {
44
}
55

66
tasks.register<Delete>("clean") {
7-
delete(rootProject.buildDir)
7+
delete(rootProject.layout.buildDirectory)
88
}
99

1010
plugins {

buildSrc/build.gradle.kts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ apply(from = "../repositories.gradle.kts")
77

88
dependencies {
99
// Gradle Plugins
10-
implementation("com.android.tools.build:gradle:8.13.2")
10+
// AGP 9.2.1 has a runtime dependency on KGP and bundles Kotlin Gradle Plugin 2.3.10,
11+
// which matches the version we were pinning explicitly. Keep KGP pinned for clarity so
12+
// the buildSrc classpath is unambiguous.
13+
implementation("com.android.tools.build:gradle:9.2.1")
1114
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10")
1215
}

buildSrc/src/main/kotlin/Helpers.kt

Lines changed: 41 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import com.android.build.api.artifact.SingleArtifact
12
import com.android.build.api.dsl.ApplicationExtension
2-
import com.android.build.gradle.AbstractAppExtension
3-
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
3+
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
44
import org.gradle.api.JavaVersion
55
import org.gradle.api.Project
66
import org.gradle.kotlin.dsl.getByName
7+
import org.gradle.kotlin.dsl.register
78
import org.gradle.kotlin.dsl.withType
89
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
910
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
@@ -12,6 +13,8 @@ import java.util.Properties
1213
import kotlin.system.exitProcess
1314

1415
private val Project.android get() = extensions.getByName<ApplicationExtension>("android")
16+
private val Project.androidComponents
17+
get() = extensions.getByName<ApplicationAndroidComponentsExtension>("androidComponents")
1518

1619
private lateinit var metadata: Properties
1720
private lateinit var localProperties: Properties
@@ -82,28 +85,22 @@ fun Project.setupCommon() {
8285
)
8386
)
8487
}
85-
(this as? AbstractAppExtension)?.apply {
86-
buildTypes {
87-
getByName("release") {
88-
isShrinkResources = true
89-
if (System.getenv("nkmr_minify") == "0") {
90-
isShrinkResources = false
91-
isMinifyEnabled = false
92-
}
93-
}
94-
getByName("debug") {
95-
applicationIdSuffix = "debug"
96-
debuggable(true)
97-
jniDebuggable(true)
88+
// Under AGP 9's new DSL the build types below are configured directly on
89+
// ApplicationExtension (no AbstractAppExtension cast needed). APK output renaming is no
90+
// longer done here via the removed applicationVariants/BaseVariantOutputImpl API; it is
91+
// handled by the RenameApkTask wired in setupApp() through androidComponents.onVariants.
92+
buildTypes {
93+
getByName("release") {
94+
isShrinkResources = true
95+
if (System.getenv("nkmr_minify") == "0") {
96+
isShrinkResources = false
97+
isMinifyEnabled = false
9898
}
9999
}
100-
applicationVariants.forEach { variant ->
101-
variant.outputs.forEach {
102-
it as BaseVariantOutputImpl
103-
it.outputFileName = it.outputFileName.replace(
104-
"app", "${project.name}-" + variant.versionName
105-
).replace("-release", "").replace("-oss", "")
106-
}
100+
getByName("debug") {
101+
applicationIdSuffix = "debug"
102+
isDebuggable = true
103+
isJniDebuggable = true
107104
}
108105
}
109106
}
@@ -173,8 +170,6 @@ fun Project.setupApp() {
173170
setupAppCommon()
174171

175172
android.apply {
176-
this as AbstractAppExtension
177-
178173
buildTypes {
179174
getByName("release") {
180175
proguardFiles(
@@ -208,31 +203,33 @@ fun Project.setupApp() {
208203
}
209204
}
210205

211-
applicationVariants.all {
212-
outputs.all {
213-
this as BaseVariantOutputImpl
214-
val isPreview = outputFileName.contains("-preview")
215-
outputFileName = if (isPreview) {
216-
outputFileName.replace(
217-
project.name,
218-
"NekoBox-" + requireMetadata().getProperty("PRE_VERSION_NAME")
219-
).replace("-preview", "")
220-
} else {
221-
outputFileName.replace(project.name, "NekoBox-$versionName")
222-
.replace("-release", "")
223-
.replace("-oss", "")
224-
}
225-
}
226-
}
227-
228206
for (abi in listOf("Arm64", "Arm", "X64", "X86")) {
229-
tasks.create("assemble" + abi + "FdroidRelease") {
207+
tasks.register("assemble" + abi + "FdroidRelease") {
230208
dependsOn("assembleFdroidRelease")
231209
}
232210
}
233211

234212
sourceSets.getByName("main").apply {
235-
jniLibs.srcDir("executableSo")
213+
jniLibs.directories.add("executableSo")
236214
}
237215
}
238-
}
216+
217+
// APK output renaming. The legacy applicationVariants/BaseVariantOutputImpl API was removed in
218+
// AGP 9 (new DSL). Instead we react to artifact creation and copy the produced APKs into
219+
// build/outputs/renamed_apks/<variant> with friendly NekoBox-<version>[-<abi>].apk names.
220+
// The preview flavor uses PRE_VERSION_NAME to match the previous behaviour.
221+
val previewVersionName = requireMetadata().getProperty("PRE_VERSION_NAME")
222+
androidComponents.onVariants { variant ->
223+
val isPreview = variant.flavorName == "preview" ||
224+
variant.productFlavors.any { (_, flavor) -> flavor == "preview" }
225+
val version = if (isPreview) previewVersionName else verName
226+
val renameTask = tasks.register<RenameApkTask>("renameApksFor${variant.name.replaceFirstChar { it.uppercase() }}") {
227+
output.set(layout.buildDirectory.dir("outputs/renamed_apks/${variant.name}"))
228+
builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
229+
baseName.set("NekoBox-$version")
230+
}
231+
variant.artifacts.use(renameTask)
232+
.wiredWith { it.input }
233+
.toListenTo(SingleArtifact.APK)
234+
}
235+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import com.android.build.api.variant.BuiltArtifactsLoader
2+
import org.gradle.api.DefaultTask
3+
import org.gradle.api.file.DirectoryProperty
4+
import org.gradle.api.provider.Property
5+
import org.gradle.api.tasks.Input
6+
import org.gradle.api.tasks.InputDirectory
7+
import org.gradle.api.tasks.Internal
8+
import org.gradle.api.tasks.OutputDirectory
9+
import org.gradle.api.tasks.PathSensitive
10+
import org.gradle.api.tasks.PathSensitivity
11+
import org.gradle.api.tasks.TaskAction
12+
import java.io.File
13+
import java.nio.file.Files
14+
import java.nio.file.StandardCopyOption
15+
16+
/**
17+
* Copies the APKs produced for a single variant into a dedicated `renamed_apks/<variant>`
18+
* directory, giving them friendly `NekoBox-<versionName>[-<abi>].apk` filenames.
19+
*
20+
* AGP 9 (new DSL) removed the legacy `applicationVariants` / `BaseVariantOutputImpl` API that
21+
* previously let us rewrite `outputFileName` in place. The supported replacement is to react to
22+
* artifact creation via `androidComponents.onVariants { ... }` and wire a task that *copies*
23+
* the produced APKs, using a [BuiltArtifactsLoader] to enumerate every split (one APK per ABI)
24+
* along with its metadata. The original APKs in `outputs/apk/...` are left untouched.
25+
*
26+
* Only public AGP/Gradle APIs are used here (no `com.android.build.gradle.internal.*` and no
27+
* `org.gradle.internal.impldep.*`).
28+
*/
29+
abstract class RenameApkTask : DefaultTask() {
30+
31+
/** The directory of APKs produced by AGP for this variant (wired automatically by AGP). */
32+
@get:InputDirectory
33+
@get:PathSensitive(PathSensitivity.RELATIVE)
34+
abstract val input: DirectoryProperty
35+
36+
/** Destination directory: `build/outputs/renamed_apks/<variant>`. */
37+
@get:OutputDirectory
38+
abstract val output: DirectoryProperty
39+
40+
/** Loader that enumerates the built APKs (one per ABI split) and their metadata. */
41+
@get:Internal
42+
abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>
43+
44+
/**
45+
* Friendly base name to use instead of the default project/module name, e.g.
46+
* `NekoBox-1.4.2-mod-12` or `NekoBox-pre-1.4.2-20260214-1` for the preview flavor.
47+
* Declared as @Input so a version bump invalidates the task and the APKs are re-copied
48+
* with the new name rather than leaving stale files in the output directory.
49+
*/
50+
@get:Input
51+
abstract val baseName: Property<String>
52+
53+
@TaskAction
54+
fun taskAction() {
55+
val outputDir = output.get()
56+
val outputFile = outputDir.asFile
57+
58+
// This task does not run incrementally; start from a clean output directory.
59+
outputFile.deleteRecursively()
60+
outputFile.mkdirs()
61+
62+
val builtArtifacts = builtArtifactsLoader.get().load(input.get())
63+
?: throw RuntimeException("Cannot load APKs from ${input.get().asFile}")
64+
65+
val base = baseName.get()
66+
67+
builtArtifacts.elements.forEach { artifact ->
68+
val source = File(artifact.outputFile)
69+
// Preserve the ABI substring from the original split filename (e.g. "arm64-v8a")
70+
// so downstream tooling (CI globs `*arm64-v8a*.apk`) keeps matching.
71+
val abi = ABIS.firstOrNull { source.name.contains(it) }
72+
73+
val targetName = buildString {
74+
append(base)
75+
if (abi != null) append("-").append(abi)
76+
append(".apk")
77+
}
78+
79+
Files.copy(
80+
source.toPath(),
81+
outputDir.file(targetName).asFile.toPath(),
82+
StandardCopyOption.REPLACE_EXISTING,
83+
)
84+
}
85+
86+
// NOTE: we intentionally do NOT call builtArtifacts.save(outputDir). That would write an
87+
// output-metadata.json whose `outputFile` entries still point at the original (un-renamed)
88+
// APK paths in outputs/apk/<variant>/, not at the NekoBox-* copies next to it - which would
89+
// mislead any BuiltArtifactsLoader consumer back to the originals. Nothing downstream
90+
// consumes this metadata (CI just globs the *.apk files), so we omit it rather than emit a
91+
// misleading file.
92+
}
93+
94+
private companion object {
95+
// Order matters: check the more specific names before substrings would collide.
96+
val ABIS = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
97+
}
98+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
2-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip
3-
distributionSha256Sum=6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854
2+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
3+
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
44
distributionPath=wrapper/dists
55
zipStorePath=wrapper/dists
66
zipStoreBase=GRADLE_USER_HOME

0 commit comments

Comments
 (0)