Release Build #154
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build Android APK | |
| on: | |
| push: | |
| tags: | |
| - '*.*.*' | |
| - '!*-*' | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Version number (e.g. 1.0.0). Leave empty for auto-increment." | |
| required: false | |
| default: "" | |
| type: string | |
| create_release: | |
| description: "Create a GitHub Release with the APK?" | |
| required: false | |
| default: true | |
| type: boolean | |
| keystore_base64: | |
| description: "Base64-encoded keystore (from SAVE-THESE-SECRETS.txt). Overrides ANDROID_KEYSTORE_BASE64 secret." | |
| required: false | |
| default: "" | |
| type: string | |
| key_alias: | |
| description: "Signing key alias (from SAVE-THESE-SECRETS.txt). Overrides ANDROID_SIGNING_KEY_ALIAS secret." | |
| required: false | |
| default: "" | |
| type: string | |
| key_password: | |
| description: "Signing key password (from SAVE-THESE-SECRETS.txt). Overrides ANDROID_SIGNING_KEY_PASSWORD secret." | |
| required: false | |
| default: "" | |
| type: string | |
| jobs: | |
| build-android: | |
| name: Build Android APK | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: write | |
| packages: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: microsoft | |
| java-version: 17 | |
| - name: Install .NET 10.0.x | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: "10.0.x" | |
| - name: Install .NET Android workload | |
| run: dotnet workload install android | |
| - name: Authenticate to GitHub Packages | |
| run: dotnet nuget update source winnerspiros-github --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text | |
| - name: Ensure Android NDK and CMake are available | |
| run: | | |
| # The ubuntu-latest runner has $ANDROID_HOME pre-installed. | |
| # sdkmanager is not on PATH; use the full path. | |
| SDKMANAGER="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" | |
| # Install latest stable NDK (r29) + CMake. | |
| yes | "$SDKMANAGER" --licenses > /dev/null 2>&1 || true | |
| "$SDKMANAGER" --install "ndk;29.0.14206865" "cmake;3.22.1" > /dev/null 2>&1 | |
| - name: Build native library (libosu_native.so) | |
| run: | | |
| NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865" | |
| CMAKE_BIN="$ANDROID_HOME/cmake/3.22.1/bin/cmake" | |
| # arm64 only β matches RuntimeIdentifiers in osu.Android.props. | |
| for ABI in arm64-v8a; do | |
| echo "::group::Building osu_native for $ABI" | |
| "$CMAKE_BIN" -B "build-native/$ABI" -S osu.Android/Native \ | |
| -DCMAKE_TOOLCHAIN_FILE="$NDK_HOME/build/cmake/android.toolchain.cmake" \ | |
| -DANDROID_ABI="$ABI" \ | |
| -DANDROID_PLATFORM=android-33 \ | |
| -DCMAKE_BUILD_TYPE=Release | |
| "$CMAKE_BIN" --build "build-native/$ABI" --config Release -j "$(nproc)" | |
| mkdir -p "osu.Android/libs/$ABI" | |
| cp "build-native/$ABI/libosu_native.so" "osu.Android/libs/$ABI/" | |
| echo "::endgroup::" | |
| done | |
| echo "Native libraries built successfully:" | |
| find osu.Android/libs -name "*.so" -exec ls -lh {} \; | |
| - name: Decode or generate keystore | |
| id: keystore | |
| run: | | |
| KS_PATH="${{ github.workspace }}/osu.Android/osu.keystore" | |
| # Priority: workflow_dispatch inputs > repository secrets > auto-generate. | |
| # This lets users paste values from SAVE-THESE-SECRETS.txt directly into | |
| # the "Run workflow" form so they don't need to set up repo secrets. | |
| # Note: the auto-generated keystore uses the same password for both key and | |
| # store, so a single key_password input covers both. Users with separate | |
| # passwords should use repository secrets instead of workflow inputs. | |
| EFFECTIVE_KS="${INPUT_KEYSTORE_BASE64:-$KEYSTORE_BASE64}" | |
| EFFECTIVE_ALIAS="${INPUT_KEY_ALIAS:-$KEY_ALIAS_SECRET}" | |
| EFFECTIVE_PASS="${INPUT_KEY_PASSWORD:-$KEY_PASS_SECRET}" | |
| EFFECTIVE_STORE_PASS="${INPUT_KEY_PASSWORD:-${STORE_PASS_SECRET:-$EFFECTIVE_PASS}}" | |
| # Mask passwords so they never appear in logs. | |
| if [ -n "$EFFECTIVE_PASS" ]; then echo "::add-mask::$EFFECTIVE_PASS"; fi | |
| if [ -n "$EFFECTIVE_STORE_PASS" ]; then echo "::add-mask::$EFFECTIVE_STORE_PASS"; fi | |
| if [ -n "$EFFECTIVE_KS" ]; then | |
| # ββ User provided a keystore (via input or secret) ββββββββββββββ | |
| echo "$EFFECTIVE_KS" | base64 --decode > "$KS_PATH" | |
| echo "has_keystore=true" >> "$GITHUB_OUTPUT" | |
| echo "generated=false" >> "$GITHUB_OUTPUT" | |
| echo "key_alias=${EFFECTIVE_ALIAS:-osu-release}" >> "$GITHUB_OUTPUT" | |
| echo "key_pass=${EFFECTIVE_PASS}" >> "$GITHUB_OUTPUT" | |
| echo "store_pass=${EFFECTIVE_STORE_PASS}" >> "$GITHUB_OUTPUT" | |
| if [ -n "$INPUT_KEYSTORE_BASE64" ]; then | |
| echo "β Using keystore from workflow dispatch inputs." | |
| else | |
| echo "β Using saved keystore from repository secrets." | |
| fi | |
| else | |
| # ββ No keystore β auto-generate one for this build ββββββββββββββ | |
| # The APK will install fine on any device, but UPDATING from a | |
| # previous build signed with a DIFFERENT key will fail. | |
| # To avoid that, save the generated keystore as a secret | |
| # (instructions are printed at the end of the build). | |
| AUTO_PASS="osu-$(openssl rand -hex 12)" | |
| echo "::add-mask::$AUTO_PASS" | |
| keytool -genkeypair \ | |
| -keystore "$KS_PATH" \ | |
| -storepass "$AUTO_PASS" \ | |
| -keypass "$AUTO_PASS" \ | |
| -alias osu-release \ | |
| -keyalg RSA -keysize 2048 -validity 10000 \ | |
| -dname "CN=osu! Android,O=osu,C=US" 2>/dev/null | |
| echo "has_keystore=true" >> "$GITHUB_OUTPUT" | |
| echo "generated=true" >> "$GITHUB_OUTPUT" | |
| echo "key_alias=osu-release" >> "$GITHUB_OUTPUT" | |
| echo "key_pass=$AUTO_PASS" >> "$GITHUB_OUTPUT" | |
| echo "store_pass=$AUTO_PASS" >> "$GITHUB_OUTPUT" | |
| # Export base64 and password into a single instructions file | |
| # so the user only needs to download one artifact. | |
| { | |
| echo "=== osu! Android Signing Keystore ===" | |
| echo "" | |
| echo "ANDROID_KEYSTORE_BASE64 value (copy everything on the next line):" | |
| base64 -w 0 "$KS_PATH" | |
| echo "" | |
| echo "" | |
| echo "ANDROID_SIGNING_KEY_ALIAS value:" | |
| echo "osu-release" | |
| echo "" | |
| echo "ANDROID_SIGNING_KEY_PASSWORD value:" | |
| echo "$AUTO_PASS" | |
| echo "" | |
| echo "ANDROID_SIGNING_STORE_PASSWORD value:" | |
| echo "$AUTO_PASS" | |
| } > "${{ github.workspace }}/SAVE-THESE-SECRETS.txt" | |
| echo "::warning::No signing keystore secret found β auto-generated one for this build." | |
| echo "::warning::See the end of this job for instructions to save it for future builds." | |
| fi | |
| env: | |
| KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} | |
| KEY_ALIAS_SECRET: ${{ secrets.ANDROID_SIGNING_KEY_ALIAS }} | |
| KEY_PASS_SECRET: ${{ secrets.ANDROID_SIGNING_KEY_PASSWORD }} | |
| STORE_PASS_SECRET: ${{ secrets.ANDROID_SIGNING_STORE_PASSWORD }} | |
| INPUT_KEYSTORE_BASE64: ${{ inputs.keystore_base64 }} | |
| INPUT_KEY_ALIAS: ${{ inputs.key_alias }} | |
| INPUT_KEY_PASSWORD: ${{ inputs.key_password }} | |
| - name: Determine version | |
| id: version | |
| run: | | |
| # Priority: manual input > tag name > auto-increment from run number | |
| INPUT_VERSION="${{ inputs.version }}" | |
| REF="${{ github.ref_name }}" | |
| if [[ -n "$INPUT_VERSION" && "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| VERSION="$INPUT_VERSION" | |
| echo "π¦ Using manually specified version: $VERSION" | |
| elif [[ "$REF" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | |
| VERSION="$REF" | |
| echo "π¦ Using version from tag: $VERSION" | |
| else | |
| # Auto-generate: YYYY.MMDD.run_number | |
| VERSION="$(date -u +%Y).$(date -u +%-m%d).${{ github.run_number }}" | |
| echo "π¦ Auto-generated version: $VERSION" | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "tag=v$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: Build Android APK | |
| run: > | |
| dotnet publish -c Release | |
| osu.Android/osu.Android.csproj | |
| -f net10.0-android | |
| -p:Version="${{ steps.version.outputs.version }}" | |
| -p:ApplicationDisplayVersion="${{ steps.version.outputs.version }}" | |
| -p:ApplicationVersion="${{ github.run_number }}" | |
| -p:AndroidKeyStore=true | |
| -p:AndroidSigningKeyStore="${{ github.workspace }}/osu.Android/osu.keystore" | |
| -p:AndroidSigningKeyAlias="${{ steps.keystore.outputs.key_alias }}" | |
| -p:AndroidSigningKeyPass="${{ steps.keystore.outputs.key_pass }}" | |
| -p:AndroidSigningStorePass="${{ steps.keystore.outputs.store_pass }}" | |
| - name: Find and rename APK | |
| id: find_apk | |
| run: | | |
| APK=$(find "osu.Android/bin/Release/net10.0-android/publish" -maxdepth 1 -name "*-Signed.apk" 2>/dev/null | head -1) | |
| if [ -z "$APK" ]; then | |
| APK=$(find "osu.Android/bin/Release" -name "*-Signed.apk" | head -1) | |
| fi | |
| if [ -z "$APK" ]; then | |
| echo "::error::Failed to locate signed APK. Listing bin directory:" | |
| find osu.Android/bin -name "*.apk" -o -name "*.aab" 2>/dev/null || true | |
| exit 1 | |
| fi | |
| # Rename to a clean versioned filename for the release | |
| VERSION="${{ steps.version.outputs.version }}" | |
| FINAL_APK="$(dirname "$APK")/osu-lazer-$VERSION.apk" | |
| cp "$APK" "$FINAL_APK" | |
| APK_SIZE=$(stat -c %s "$FINAL_APK" 2>/dev/null || stat -f %z "$FINAL_APK" 2>/dev/null || wc -c < "$FINAL_APK") | |
| APK_SIZE_MB=$((APK_SIZE / 1048576)) | |
| echo "Found APK: $FINAL_APK ($APK_SIZE_MB MB)" | |
| echo "apk_path=$FINAL_APK" >> "$GITHUB_OUTPUT" | |
| - name: Verify APK signature | |
| run: | | |
| APK="${{ steps.find_apk.outputs.apk_path }}" | |
| APKSIGNER="$ANDROID_HOME/build-tools/$(ls "$ANDROID_HOME/build-tools" | sort -V | tail -1)/apksigner" | |
| if "$APKSIGNER" verify --print-certs "$APK"; then | |
| echo "APK signature verified β" | |
| else | |
| echo "::error::APK signature verification failed. The APK may not install correctly." | |
| exit 1 | |
| fi | |
| - name: Verify native libraries in APK | |
| run: | | |
| APK="${{ steps.find_apk.outputs.apk_path }}" | |
| echo "Checking APK for required native libraries..." | |
| NATIVE_LIBS=$(unzip -l "$APK" | grep "lib/arm64-v8a/.*\.so" || true) | |
| echo "$NATIVE_LIBS" | |
| echo "" | |
| MISSING=0 | |
| for LIB in libbass.so libbass_fx.so libbassmix.so; do | |
| if echo "$NATIVE_LIBS" | grep -q "$LIB"; then | |
| echo "β $LIB found" | |
| else | |
| echo "::error::$LIB is MISSING from the APK β the app will crash at startup (DllNotFoundException)." | |
| MISSING=1 | |
| fi | |
| done | |
| if [ "$MISSING" -ne 0 ]; then | |
| echo "" | |
| echo "::error::One or more required native libraries are missing from the APK. Ensure ppy.osu.Framework.Android was properly restored from the winnerspiros GitHub Packages feed." | |
| exit 1 | |
| fi | |
| # Architecture sanity check: a previous build packaged the Linux-glibc | |
| # libbass.so from ppy.osu.Framework.NativeLibs into lib/arm64-v8a/, which | |
| # passed the name check above but failed at runtime with | |
| # System.DllNotFoundException: bass because Android's bionic linker cannot | |
| # resolve glibc-only symbols. Both Linux and Android builds report | |
| # identically as "ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV)" | |
| # via file(1), so we rely on the presence of GLIBC_ versioned symbols | |
| # (which exist only in glibc-linked binaries) to distinguish them. | |
| echo "" | |
| echo "Verifying native library architectures (must be Android arm64, not Linux glibc)..." | |
| TMPDIR=$(mktemp -d) | |
| trap 'rm -rf "$TMPDIR"' EXIT | |
| BAD=0 | |
| for LIB in libbass.so libbass_fx.so libbassmix.so; do | |
| unzip -p "$APK" "lib/arm64-v8a/$LIB" > "$TMPDIR/$LIB" | |
| FILE_INFO=$(file "$TMPDIR/$LIB") | |
| echo " $LIB: $FILE_INFO" | |
| # Must be a 64-bit aarch64 ELF shared object. | |
| if ! echo "$FILE_INFO" | grep -qE "ELF 64-bit.*aarch64|ELF 64-bit.*ARM aarch64"; then | |
| echo "::error::$LIB is not a 64-bit aarch64 ELF β runtime DllNotFoundException will occur." | |
| BAD=1 | |
| continue | |
| fi | |
| # Reliable Linux-vs-Android distinguisher: GLIBC_ versioned symbols | |
| # (e.g. memcpy@@GLIBC_2.17) appear only in glibc-linked Linux binaries. | |
| # Android's bionic libc uses no symbol versioning. | |
| if strings "$TMPDIR/$LIB" | grep -q "^GLIBC_"; then | |
| echo "::error::$LIB references GLIBC_ versioned symbols β this is the Linux ELF from ppy.osu.Framework.NativeLibs runtimes/linux-arm64/native/, not the Android ELF from ppy.osu.Framework.Android. Android's bionic linker cannot load it; the app will crash at startup with System.DllNotFoundException: bass." | |
| BAD=1 | |
| continue | |
| fi | |
| echo " β $LIB is a valid Android arm64 ELF" | |
| done | |
| if [ "$BAD" -ne 0 ]; then | |
| echo "" | |
| echo "::error::One or more native libraries in the APK are NOT valid Android arm64 binaries. This usually means desktop runtime .so files (e.g. from ppy.osu.Framework.NativeLibs runtimes/linux-arm64/native/) leaked into lib/arm64-v8a/ during packaging. See osu.Android.props FixRuntimePackAssetTypes target." | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "All required native libraries present and valid β" | |
| - name: Upload APK artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: osu-android-${{ steps.version.outputs.version }} | |
| path: ${{ steps.find_apk.outputs.apk_path }} | |
| if-no-files-found: error | |
| # When the keystore was auto-generated, upload it so the user can save it | |
| # as a repository secret for consistent signing across builds. | |
| - name: Upload generated keystore | |
| if: steps.keystore.outputs.generated == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: osu-signing-keystore | |
| path: | | |
| ${{ github.workspace }}/osu.Android/osu.keystore | |
| ${{ github.workspace }}/SAVE-THESE-SECRETS.txt | |
| retention-days: 7 | |
| - name: Print keystore setup instructions | |
| if: steps.keystore.outputs.generated == 'true' | |
| run: | | |
| echo "" | |
| echo "==================================================================" | |
| echo " β οΈ YOUR APK WAS SIGNED WITH AN AUTO-GENERATED KEYSTORE" | |
| echo "==================================================================" | |
| echo "" | |
| echo " β The APK will install fine on any device." | |
| echo "" | |
| echo " β οΈ BUT β if you build again without saving this keystore," | |
| echo " Android will REFUSE to update (different signing certificate)." | |
| echo "" | |
| echo " To keep your APK updatable across builds:" | |
| echo "" | |
| echo " 1. Download the 'osu-signing-keystore' artifact from this run" | |
| echo " 2. Open 'SAVE-THESE-SECRETS.txt' β it contains all 4 values" | |
| echo " 3. Go to: Settings β Secrets and variables β Actions" | |
| echo " 4. Create these 4 secrets with the values from the file:" | |
| echo "" | |
| echo " β’ ANDROID_KEYSTORE_BASE64" | |
| echo " β’ ANDROID_SIGNING_KEY_ALIAS" | |
| echo " β’ ANDROID_SIGNING_KEY_PASSWORD" | |
| echo " β’ ANDROID_SIGNING_STORE_PASSWORD" | |
| echo "" | |
| echo " β‘ After saving the secrets, all future builds will use the" | |
| echo " same keystore automatically β no more setup needed." | |
| echo "" | |
| echo " π‘ QUICK OPTION: You can also paste the values from" | |
| echo " SAVE-THESE-SECRETS.txt directly into the workflow" | |
| echo " dispatch inputs (keystore_base64, key_alias, key_password)" | |
| echo " when running the 'Build Android APK' workflow manually." | |
| echo "==================================================================" | |
| # Create a GitHub Release with the APK attached. | |
| # Works for both tag pushes and manual workflow_dispatch runs. | |
| # For manual runs: creates a tag automatically. | |
| - name: Create GitHub Release | |
| if: inputs.create_release != false | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: ${{ steps.version.outputs.tag }} | |
| name: "osu! Android ${{ steps.version.outputs.version }}" | |
| files: ${{ steps.find_apk.outputs.apk_path }} | |
| generate_release_notes: true | |
| body: | | |
| ## osu! Android v${{ steps.version.outputs.version }} | |
| ### Download | |
| Grab the APK below and install it on your Android device. | |
| > **Minimum Android version:** 13 (API 33) | |
| > **Architecture:** arm64-v8a | |
| make_latest: true | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |