Release Build #139
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 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: recursive | |
| 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: 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 }}" | |
| -p:CustomBeforeMicrosoftCommonTargets="${{ github.workspace }}/build/SuppressSubmoduleWarnings.targets" | |
| - 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 }}" | |
| SUBMOD="submodules/osu-framework/osu.Framework.Android/arm64-v8a" | |
| 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. Check that the osu-framework submodule is initialised and the AndroidNativeLibrary glob in osu.Android.props resolves correctly." | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "All required native libraries present β" | |
| # Verify the BASS libraries are the Android-built versions, not the Linux desktop | |
| # ones from the NativeLibs NuGet package. The Android libbass.so links against | |
| # libOpenSLES.so (Android audio API); the Linux one links against libc.so.6. | |
| echo "" | |
| echo "Verifying native library ABI compatibility..." | |
| TMPDIR=$(mktemp -d) | |
| unzip -q -o "$APK" "lib/arm64-v8a/libbass.so" -d "$TMPDIR" | |
| if readelf -d "$TMPDIR/lib/arm64-v8a/libbass.so" | grep -q "libOpenSLES.so"; then | |
| echo "β libbass.so is the correct Android build (links libOpenSLES.so)" | |
| else | |
| echo "::error::libbass.so in APK is NOT the Android build β it appears to be the Linux desktop version from ppy.osu.Framework.NativeLibs NuGet. The app will crash with DllNotFoundException." | |
| echo "::error::Ensure ppy.osu.Framework.NativeLibs has ExcludeAssets=native in osu.Android.props." | |
| readelf -d "$TMPDIR/lib/arm64-v8a/libbass.so" | grep NEEDED || true | |
| rm -rf "$TMPDIR" | |
| exit 1 | |
| fi | |
| rm -rf "$TMPDIR" | |
| - 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 }} |