name: Android on: push: branches: [main] # A `v*` tag reaches this workflow through release.yml, which calls it and # publishes the signed APK. Triggering on the tag here as well would build # the release twice. workflow_call: inputs: signed_release: required: false default: false type: boolean pull_request: paths: - 'android/**' - 'crates/pstr-android/**' - 'crates/pstr-core/**' - 'crates/pstr-meta/**' - 'crates/pstr-stream/**' - 'Cargo.toml' - 'Cargo.lock' - 'scripts/build-android.sh' - 'scripts/build-libmpv-android.sh' - '.github/workflows/android.yml' workflow_dispatch: inputs: signed_release: description: Build artifacts with the configured release key required: false default: false type: boolean permissions: contents: read env: CARGO_TERM_COLOR: always ANDROID_API: '36' ANDROID_BUILD_TOOLS: '37.0.0' ANDROID_NDK: '30.0.16248370' ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/30.0.16248370 jobs: build: name: Validate, debug, and unsigned release runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - name: Enforce release versions and tag ancestry shell: bash run: | workspace_version="$(awk '/^\[workspace.package\]/{found=1; next} found && /^\[/{exit} found && /^version = /{gsub(/[" ]/, "", $3); print $3; exit}' Cargo.toml)" test -n "$workspace_version" if [[ ! "$workspace_version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)([-+][0-9A-Za-z.-]+)?$ ]]; then echo "::error::workspace version is not supported Android semver: $workspace_version" exit 1 fi major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" patch="${BASH_REMATCH[3]}" test "$minor" -le 999 test "$patch" -le 999 expected_code=$((major * 1000000 + minor * 1000 + patch)) test "$expected_code" -ge 1 test "$expected_code" -le 2100000000 echo "EXPECTED_ANDROID_VERSION_NAME=$workspace_version" >> "$GITHUB_ENV" echo "EXPECTED_ANDROID_VERSION_CODE=$expected_code" >> "$GITHUB_ENV" if [[ "$GITHUB_REF" == refs/tags/v* ]]; then tag="${GITHUB_REF_NAME#v}" test "$tag" = "$workspace_version" git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main git merge-base --is-ancestor "$GITHUB_SHA" origin/main fi - uses: actions/setup-java@v5 with: distribution: temurin java-version: '17' # The action's default `packages` includes `tools`, which cmdline-tools # 16.0 no longer offers, so sdkmanager exits 1. The SDK pieces this # build needs are installed explicitly below. - uses: android-actions/setup-android@v3 with: packages: platform-tools log-accepted-android-sdk-licenses: false - name: Install native build prerequisites run: | sudo apt-get update sudo apt-get install --no-install-recommends \ autoconf automake build-essential cmake curl git gperf libdbus-1-dev \ libtool nasm ninja-build pkg-config python3 unzip wget xz-utils yasm # Not apt's meson: noble ships 1.3.2, and the fontconfig that the pinned # mpv-android revision builds requires >= 1.11. pipx install meson==1.12.1 - name: Install Android SDK and NDK run: >- sdkmanager "platforms;android-${ANDROID_API}" "build-tools;${ANDROID_BUILD_TOOLS}" "ndk;${ANDROID_NDK}" - id: toolchain uses: dtolnay/rust-toolchain@stable with: toolchain: '1.96' targets: aarch64-linux-android,x86_64-linux-android # 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-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 # and saved in steps of its own, so a later failure in Gradle does not # discard it the way a post-job cache save would. - id: libmpv uses: actions/cache/restore@v6 with: path: android/app/build/generated/mpv key: libmpv-${{ hashFiles('scripts/build-libmpv-android.sh') }} - name: Build libmpv run: bash scripts/build-libmpv-android.sh - if: steps.libmpv.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: path: android/app/build/generated/mpv key: ${{ steps.libmpv.outputs.cache-primary-key }} - uses: gradle/actions/setup-gradle@v4 with: gradle-version: '8.13' - name: Validate Android version derivation and override shell: bash run: | resolved="$(gradle --no-daemon -q -p android :app:printAndroidVersion)" grep -Fx "versionName=$EXPECTED_ANDROID_VERSION_NAME" <<< "$resolved" grep -Fx "versionCode=$EXPECTED_ANDROID_VERSION_CODE" <<< "$resolved" if [[ "$EXPECTED_ANDROID_VERSION_CODE" -lt 2100000000 ]]; then override_code=$((EXPECTED_ANDROID_VERSION_CODE + 1)) else override_code=$((EXPECTED_ANDROID_VERSION_CODE - 1)) fi overridden="$(ANDROID_VERSION_CODE="$override_code" gradle --no-daemon -q -p android :app:printAndroidVersion)" grep -Fx "versionName=$EXPECTED_ANDROID_VERSION_NAME" <<< "$overridden" grep -Fx "versionCode=$override_code" <<< "$overridden" - name: Install native build helpers run: | cargo install cargo-ndk --version 4.1.2 --locked cargo install uniffi --version 0.32.0 --features cli --locked - 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 needs: build runs-on: ubuntu-24.04 env: ANDROID_RELEASE_STORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_STORE_PASSWORD }} ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }} ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }} steps: - uses: actions/checkout@v7 - uses: actions/setup-java@v5 with: distribution: temurin java-version: '17' # The action's default `packages` includes `tools`, which cmdline-tools # 16.0 no longer offers, so sdkmanager exits 1. The SDK pieces this # build needs are installed explicitly below. - uses: android-actions/setup-android@v3 with: packages: platform-tools log-accepted-android-sdk-licenses: false - name: Install native build prerequisites run: | sudo apt-get update sudo apt-get install --no-install-recommends \ autoconf automake build-essential cmake curl git gperf libdbus-1-dev \ libtool nasm ninja-build pkg-config python3 unzip wget xz-utils yasm # Not apt's meson: noble ships 1.3.2, and the fontconfig that the pinned # mpv-android revision builds requires >= 1.11. pipx install meson==1.12.1 - name: Install Android SDK and NDK run: >- sdkmanager "platforms;android-${ANDROID_API}" "build-tools;${ANDROID_BUILD_TOOLS}" "ndk;${ANDROID_NDK}" - id: toolchain uses: dtolnay/rust-toolchain@stable with: toolchain: '1.96' targets: aarch64-linux-android,x86_64-linux-android # 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-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 # and saved in steps of its own, so a later failure in Gradle does not # discard it the way a post-job cache save would. - id: libmpv uses: actions/cache/restore@v6 with: path: android/app/build/generated/mpv key: libmpv-${{ hashFiles('scripts/build-libmpv-android.sh') }} - name: Build libmpv run: bash scripts/build-libmpv-android.sh - if: steps.libmpv.outputs.cache-hit != 'true' uses: actions/cache/save@v6 with: path: android/app/build/generated/mpv key: ${{ steps.libmpv.outputs.cache-primary-key }} - uses: gradle/actions/setup-gradle@v4 with: gradle-version: '8.13' - name: Install native build helpers run: | cargo install cargo-ndk --version 4.1.2 --locked cargo install uniffi --version 0.32.0 --features cli --locked - name: Restore release keystore env: KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }} run: | test -n "$KEYSTORE_BASE64" test -n "$ANDROID_RELEASE_STORE_PASSWORD" test -n "$ANDROID_RELEASE_KEY_ALIAS" test -n "$ANDROID_RELEASE_KEY_PASSWORD" ANDROID_RELEASE_STORE_FILE="$RUNNER_TEMP/proton-stream-release.jks" echo "ANDROID_RELEASE_STORE_FILE=$ANDROID_RELEASE_STORE_FILE" >> "$GITHUB_ENV" printf '%s' "$KEYSTORE_BASE64" | base64 --decode > "$ANDROID_RELEASE_STORE_FILE" - 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 run: | version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n1)" mkdir -p dist/android cp android/app/build/outputs/apk/release/app-release.apk \ "dist/android/proton-stream-$version-android.apk" cp android/app/build/generated/mpv/mpv-android-source-*.tar.gz dist/android/ - uses: actions/upload-artifact@v7 with: name: android path: dist/android/* if-no-files-found: error - uses: actions/upload-artifact@v7 with: name: android-bundle path: android/app/build/outputs/bundle/release/*.aab if-no-files-found: error