diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9629caa3..f67ac2c99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -133,8 +133,12 @@ jobs: echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > release.keystore fi KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS }}" ./gradlew app:assembleOssRelease - APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') - APK=$(dirname $APK) + APK=$(find app/build/outputs/renamed_apks -name '*arm64-v8a*.apk' | head -n 1) + if [ -z "$APK" ]; then + echo "Error: no arm64-v8a APK found under app/build/outputs/renamed_apks" >&2 + exit 1 + fi + APK=$(dirname "$APK") echo "APK=$APK" >> $GITHUB_ENV - name: Upload Artifacts uses: namespace-actions/upload-artifact@v1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10079a7f5..97d3281c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,8 +139,12 @@ jobs: export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}" ./run init action gradle ./gradlew app:assembleOssDebug - APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') - APK=$(dirname $APK) + APK=$(find app/build/outputs/renamed_apks -name '*arm64-v8a*.apk' | head -n 1) + if [ -z "$APK" ]; then + echo "Error: no arm64-v8a APK found under app/build/outputs/renamed_apks" >&2 + exit 1 + fi + APK=$(dirname "$APK") echo "APK=$APK" >> $GITHUB_ENV - name: Upload Artifacts uses: namespace-actions/upload-artifact@v1 diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 332113162..b3c09d1bd 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -125,8 +125,12 @@ jobs: export LOCAL_PROPERTIES="${{ secrets.LOCAL_PROPERTIES }}" ./run init action gradle ./gradlew app:assemblePreviewRelease - APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') - APK=$(dirname $APK) + APK=$(find app/build/outputs/renamed_apks -name '*arm64-v8a*.apk' | head -n 1) + if [ -z "$APK" ]; then + echo "Error: no arm64-v8a APK found under app/build/outputs/renamed_apks" >&2 + exit 1 + fi + APK=$(dirname "$APK") echo "APK=$APK" >> $GITHUB_ENV - uses: namespace-actions/upload-artifact@v1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b4c344a4..b127454cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -135,8 +135,12 @@ jobs: echo "${{ secrets.KEYSTORE_B64 }}" | base64 -d > release.keystore fi KEYSTORE_PASS="${{ secrets.KEYSTORE_PASS }}" ALIAS_NAME="${{ secrets.ALIAS_NAME }}" ALIAS_PASS="${{ secrets.ALIAS_PASS }}" ./gradlew app:assembleOssRelease - APK=$(find app/build/outputs/apk -name '*arm64-v8a*.apk') - APK=$(dirname $APK) + APK=$(find app/build/outputs/renamed_apks -name '*arm64-v8a*.apk' | head -n 1) + if [ -z "$APK" ]; then + echo "Error: no arm64-v8a APK found under app/build/outputs/renamed_apks" >&2 + exit 1 + fi + APK=$(dirname "$APK") echo "APK=$APK" >> $GITHUB_ENV - uses: namespace-actions/upload-artifact@v1 with: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 21a8ccd25..b96e18833 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,9 @@ plugins { id("com.android.application") - id("kotlin-android") + // kotlin-android is no longer applied: AGP 9.0+ provides built-in Kotlin support + // (android.builtInKotlin defaults to true), so the org.jetbrains.kotlin.android plugin + // would clash ("extension already registered with name 'kotlin'"). id("com.google.devtools.ksp") id("kotlin-parcelize") } diff --git a/build.gradle.kts b/build.gradle.kts index 42ccd3663..600379f61 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ allprojects { } tasks.register("clean") { - delete(rootProject.buildDir) + delete(rootProject.layout.buildDirectory) } plugins { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 271df8d0b..1abdfc6a2 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -7,6 +7,9 @@ apply(from = "../repositories.gradle.kts") dependencies { // Gradle Plugins - implementation("com.android.tools.build:gradle:8.13.2") + // AGP 9.2.1 has a runtime dependency on KGP and bundles Kotlin Gradle Plugin 2.3.10, + // which matches the version we were pinning explicitly. Keep KGP pinned for clarity so + // the buildSrc classpath is unambiguous. + implementation("com.android.tools.build:gradle:9.2.1") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.3.10") } diff --git a/buildSrc/src/main/kotlin/Helpers.kt b/buildSrc/src/main/kotlin/Helpers.kt index 6435cb038..248cfd2c4 100644 --- a/buildSrc/src/main/kotlin/Helpers.kt +++ b/buildSrc/src/main/kotlin/Helpers.kt @@ -1,9 +1,10 @@ +import com.android.build.api.artifact.SingleArtifact import com.android.build.api.dsl.ApplicationExtension -import com.android.build.gradle.AbstractAppExtension -import com.android.build.gradle.internal.api.BaseVariantOutputImpl +import com.android.build.api.variant.ApplicationAndroidComponentsExtension import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.kotlin.dsl.getByName +import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -12,6 +13,8 @@ import java.util.Properties import kotlin.system.exitProcess private val Project.android get() = extensions.getByName("android") +private val Project.androidComponents + get() = extensions.getByName("androidComponents") private lateinit var metadata: Properties private lateinit var localProperties: Properties @@ -82,28 +85,22 @@ fun Project.setupCommon() { ) ) } - (this as? AbstractAppExtension)?.apply { - buildTypes { - getByName("release") { - isShrinkResources = true - if (System.getenv("nkmr_minify") == "0") { - isShrinkResources = false - isMinifyEnabled = false - } - } - getByName("debug") { - applicationIdSuffix = "debug" - debuggable(true) - jniDebuggable(true) + // Under AGP 9's new DSL the build types below are configured directly on + // ApplicationExtension (no AbstractAppExtension cast needed). APK output renaming is no + // longer done here via the removed applicationVariants/BaseVariantOutputImpl API; it is + // handled by the RenameApkTask wired in setupApp() through androidComponents.onVariants. + buildTypes { + getByName("release") { + isShrinkResources = true + if (System.getenv("nkmr_minify") == "0") { + isShrinkResources = false + isMinifyEnabled = false } } - applicationVariants.forEach { variant -> - variant.outputs.forEach { - it as BaseVariantOutputImpl - it.outputFileName = it.outputFileName.replace( - "app", "${project.name}-" + variant.versionName - ).replace("-release", "").replace("-oss", "") - } + getByName("debug") { + applicationIdSuffix = "debug" + isDebuggable = true + isJniDebuggable = true } } } @@ -173,8 +170,6 @@ fun Project.setupApp() { setupAppCommon() android.apply { - this as AbstractAppExtension - buildTypes { getByName("release") { proguardFiles( @@ -208,31 +203,33 @@ fun Project.setupApp() { } } - applicationVariants.all { - outputs.all { - this as BaseVariantOutputImpl - val isPreview = outputFileName.contains("-preview") - outputFileName = if (isPreview) { - outputFileName.replace( - project.name, - "NekoBox-" + requireMetadata().getProperty("PRE_VERSION_NAME") - ).replace("-preview", "") - } else { - outputFileName.replace(project.name, "NekoBox-$versionName") - .replace("-release", "") - .replace("-oss", "") - } - } - } - for (abi in listOf("Arm64", "Arm", "X64", "X86")) { - tasks.create("assemble" + abi + "FdroidRelease") { + tasks.register("assemble" + abi + "FdroidRelease") { dependsOn("assembleFdroidRelease") } } sourceSets.getByName("main").apply { - jniLibs.srcDir("executableSo") + jniLibs.directories.add("executableSo") } } -} \ No newline at end of file + + // APK output renaming. The legacy applicationVariants/BaseVariantOutputImpl API was removed in + // AGP 9 (new DSL). Instead we react to artifact creation and copy the produced APKs into + // build/outputs/renamed_apks/ with friendly NekoBox-[-].apk names. + // The preview flavor uses PRE_VERSION_NAME to match the previous behaviour. + val previewVersionName = requireMetadata().getProperty("PRE_VERSION_NAME") + androidComponents.onVariants { variant -> + val isPreview = variant.flavorName == "preview" || + variant.productFlavors.any { (_, flavor) -> flavor == "preview" } + val version = if (isPreview) previewVersionName else verName + val renameTask = tasks.register("renameApksFor${variant.name.replaceFirstChar { it.uppercase() }}") { + output.set(layout.buildDirectory.dir("outputs/renamed_apks/${variant.name}")) + builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader()) + baseName.set("NekoBox-$version") + } + variant.artifacts.use(renameTask) + .wiredWith { it.input } + .toListenTo(SingleArtifact.APK) + } +} diff --git a/buildSrc/src/main/kotlin/RenameApkTask.kt b/buildSrc/src/main/kotlin/RenameApkTask.kt new file mode 100644 index 000000000..32717c270 --- /dev/null +++ b/buildSrc/src/main/kotlin/RenameApkTask.kt @@ -0,0 +1,98 @@ +import com.android.build.api.variant.BuiltArtifactsLoader +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +/** + * Copies the APKs produced for a single variant into a dedicated `renamed_apks/` + * directory, giving them friendly `NekoBox-[-].apk` filenames. + * + * AGP 9 (new DSL) removed the legacy `applicationVariants` / `BaseVariantOutputImpl` API that + * previously let us rewrite `outputFileName` in place. The supported replacement is to react to + * artifact creation via `androidComponents.onVariants { ... }` and wire a task that *copies* + * the produced APKs, using a [BuiltArtifactsLoader] to enumerate every split (one APK per ABI) + * along with its metadata. The original APKs in `outputs/apk/...` are left untouched. + * + * Only public AGP/Gradle APIs are used here (no `com.android.build.gradle.internal.*` and no + * `org.gradle.internal.impldep.*`). + */ +abstract class RenameApkTask : DefaultTask() { + + /** The directory of APKs produced by AGP for this variant (wired automatically by AGP). */ + @get:InputDirectory + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val input: DirectoryProperty + + /** Destination directory: `build/outputs/renamed_apks/`. */ + @get:OutputDirectory + abstract val output: DirectoryProperty + + /** Loader that enumerates the built APKs (one per ABI split) and their metadata. */ + @get:Internal + abstract val builtArtifactsLoader: Property + + /** + * Friendly base name to use instead of the default project/module name, e.g. + * `NekoBox-1.4.2-mod-12` or `NekoBox-pre-1.4.2-20260214-1` for the preview flavor. + * Declared as @Input so a version bump invalidates the task and the APKs are re-copied + * with the new name rather than leaving stale files in the output directory. + */ + @get:Input + abstract val baseName: Property + + @TaskAction + fun taskAction() { + val outputDir = output.get() + val outputFile = outputDir.asFile + + // This task does not run incrementally; start from a clean output directory. + outputFile.deleteRecursively() + outputFile.mkdirs() + + val builtArtifacts = builtArtifactsLoader.get().load(input.get()) + ?: throw RuntimeException("Cannot load APKs from ${input.get().asFile}") + + val base = baseName.get() + + builtArtifacts.elements.forEach { artifact -> + val source = File(artifact.outputFile) + // Preserve the ABI substring from the original split filename (e.g. "arm64-v8a") + // so downstream tooling (CI globs `*arm64-v8a*.apk`) keeps matching. + val abi = ABIS.firstOrNull { source.name.contains(it) } + + val targetName = buildString { + append(base) + if (abi != null) append("-").append(abi) + append(".apk") + } + + Files.copy( + source.toPath(), + outputDir.file(targetName).asFile.toPath(), + StandardCopyOption.REPLACE_EXISTING, + ) + } + + // NOTE: we intentionally do NOT call builtArtifacts.save(outputDir). That would write an + // output-metadata.json whose `outputFile` entries still point at the original (un-renamed) + // APK paths in outputs/apk//, not at the NekoBox-* copies next to it - which would + // mislead any BuiltArtifactsLoader consumer back to the originals. Nothing downstream + // consumes this metadata (CI just globs the *.apk files), so we omit it rather than emit a + // misleading file. + } + + private companion object { + // Order matters: check the more specific names before substrings would collide. + val ABIS = listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86") + } +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b5614690f..39fb6bce6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip -distributionSha256Sum=6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME