forked from ppy/osu
-
Notifications
You must be signed in to change notification settings - Fork 0
400 lines (358 loc) · 17.7 KB
/
Copy pathrelease.yml
File metadata and controls
400 lines (358 loc) · 17.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
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 }}