From 66eb1b49a10aad4031cb7f427bd2a556326022f4 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 3 Oct 2026 23:00:19 +0200 Subject: [PATCH] ci(android): keep the cached libmpv and Rust target trees Gradle deletes the declared outputs of a task it has no history for, so on every fresh checkout buildLibmpvAndroid threw away the libmpv tree just restored from the cache and rebuilt it from source (~14 min per job). The task no longer declares outputs; the script's STAMP decides. rust-cache treats target/android/ as a profile directory and deletes its release tree before saving, so both cargo-ndk ABIs were rebuilt from scratch on every run (~6 min per job). It now caches only the registry and helpers; the target directory goes through actions/cache, keyed on toolchain and lockfile and restoring from the last lockfile. --- .github/workflows/android.yml | 62 +++++++++++++++++++++++++++++------ android/app/build.gradle.kts | 12 +++---- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 001d42d..e29202f 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -107,19 +107,34 @@ jobs: "build-tools;${ANDROID_BUILD_TOOLS}" "ndk;${ANDROID_NDK}" - - uses: dtolnay/rust-toolchain@stable + - id: toolchain + uses: dtolnay/rust-toolchain@stable with: toolchain: '1.96' targets: aarch64-linux-android,x86_64-linux-android - # One cache for both jobs: the signed build compiles the same crates for - # the same targets. Saved on failure too, so a run that breaks late does - # not throw away everything it compiled before the break. + # The registry and the installed helpers only. rust-cache reads + # `target/android/` as a profile directory and deletes the + # `release` tree under it before saving, so with its target caching on + # every run rebuilt both ABIs from scratch. - uses: Swatinem/rust-cache@v2.9.1 with: - shared-key: android + shared-key: android-registry + cache-targets: false cache-on-failure: true + # The target directory as is: the host build the UniFFI bindings come + # from and both cargo-ndk ABIs. One entry for both jobs, since the signed + # build compiles the same crates for the same targets. A new lockfile + # starts from the last one's tree, so a version bump rebuilds only the + # workspace crates. + - id: target + uses: actions/cache/restore@v6 + with: + path: target + key: rust-target-android-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('Cargo.lock') }} + restore-keys: rust-target-android-${{ steps.toolchain.outputs.cachekey }}- + # libmpv and its dependency tree are the slowest part of the build, and # depend only on the pinned revision and the build script, which stamps # its output with both and skips the build when the stamp matches. Built @@ -167,6 +182,12 @@ jobs: - name: Android checks and artifacts run: bash scripts/build-android.sh all + - if: steps.target.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + with: + path: target + key: ${{ steps.target.outputs.cache-primary-key }} + signed-release: name: Signed release if: startsWith(github.ref, 'refs/tags/v') || inputs.signed_release @@ -209,19 +230,34 @@ jobs: "build-tools;${ANDROID_BUILD_TOOLS}" "ndk;${ANDROID_NDK}" - - uses: dtolnay/rust-toolchain@stable + - id: toolchain + uses: dtolnay/rust-toolchain@stable with: toolchain: '1.96' targets: aarch64-linux-android,x86_64-linux-android - # One cache for both jobs: the signed build compiles the same crates for - # the same targets. Saved on failure too, so a run that breaks late does - # not throw away everything it compiled before the break. + # The registry and the installed helpers only. rust-cache reads + # `target/android/` as a profile directory and deletes the + # `release` tree under it before saving, so with its target caching on + # every run rebuilt both ABIs from scratch. - uses: Swatinem/rust-cache@v2.9.1 with: - shared-key: android + shared-key: android-registry + cache-targets: false cache-on-failure: true + # The target directory as is: the host build the UniFFI bindings come + # from and both cargo-ndk ABIs. One entry for both jobs, since the signed + # build compiles the same crates for the same targets. A new lockfile + # starts from the last one's tree, so a version bump rebuilds only the + # workspace crates. + - id: target + uses: actions/cache/restore@v6 + with: + path: target + key: rust-target-android-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('Cargo.lock') }} + restore-keys: rust-target-android-${{ steps.toolchain.outputs.cachekey }}- + # libmpv and its dependency tree are the slowest part of the build, and # depend only on the pinned revision and the build script, which stamps # its output with both and skips the build when the stamp matches. Built @@ -266,6 +302,12 @@ jobs: - name: Build signed APK and AAB run: bash scripts/build-android.sh signed + - if: steps.target.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + with: + path: target + key: ${{ steps.target.outputs.cache-primary-key }} + # The APK bundles a GPL libmpv, so its corresponding source ships next to # it. The AAB is for Play only and stays out of the GitHub release. - name: Stage release files diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1e10979..dfe2e49 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -180,20 +180,16 @@ val uniffiSourceDir = layout.buildDirectory.dir("generated/source/uniffi") val hostRustLibrary = repositoryRoot.resolve( "target/debug/${System.mapLibraryName("pstr_android")}", ) -val stagedMpvDir = layout.buildDirectory.dir("generated/mpv") val buildLibmpvAndroid by tasks.registering(Exec::class) { group = "native" description = "Builds the pinned GPL libmpv and stages both Android ABIs." workingDir(repositoryRoot) commandLine("bash", repositoryRoot.resolve("scripts/build-libmpv-android.sh").absolutePath) - inputs.file(repositoryRoot.resolve("scripts/build-libmpv-android.sh")) - outputs.files( - stagedMpvDir.map { it.file("jniLibs/arm64-v8a/libmpv.so") }, - stagedMpvDir.map { it.file("jniLibs/x86_64/libmpv.so") }, - stagedMpvDir.map { it.file("include/mpv/client.h") }, - stagedMpvDir.map { it.file("REVISION") }, - ) + // No declared outputs: the script's STAMP is the up-to-date check. Gradle + // deletes the declared outputs of a task it has no history for, which on a + // fresh CI checkout threw away the tree just restored from the cache and + // rebuilt libmpv from source on every run. } val buildRustHost by tasks.registering(Exec::class) {