Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ jobs:
NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865"
CMAKE_BIN="$ANDROID_HOME/cmake/3.22.1/bin/cmake"

for ABI in arm64-v8a armeabi-v7a x86; do
# Only build arm64 and arm32. x86 removed to reduce APK size —
# modern emulators use x86_64 or ARM translation.
for ABI in arm64-v8a armeabi-v7a; 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" \
Expand Down Expand Up @@ -86,6 +88,9 @@ jobs:
echo "version=0.0.0" >> "$GITHUB_OUTPUT"
fi

# Always build Release for full optimization (trimming, AOT, compression).
# When a keystore is available we sign with it; otherwise the SDK produces
# a debug-signed Release APK that can be sideloaded for testing.
- name: Build Android APK (signed)
if: steps.keystore.outputs.has_keystore == 'true'
env:
Expand All @@ -105,10 +110,10 @@ jobs:
-p:AndroidSigningKeyPass="$ANDROID_KEY_PASS"
-p:AndroidSigningStorePass="$ANDROID_STORE_PASS"

- name: Build Android APK (unsigned)
- name: Build Android APK (unsigned Release)
if: steps.keystore.outputs.has_keystore != 'true'
run: >
dotnet publish -c Debug
dotnet publish -c Release
osu.Android/osu.Android.csproj
-f net10.0-android
-p:Version="${{ steps.version.outputs.version }}"
Expand All @@ -118,16 +123,29 @@ jobs:
- name: Find APK
id: find_apk
run: |
# Both paths build Release. Signed builds produce *-Signed.apk; unsigned
# builds produce the base APK name. Search publish dir first, then fallback.
if [ "${{ steps.keystore.outputs.has_keystore }}" == "true" ]; then
CONFIG="Release"
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
else
CONFIG="Debug"
APK=$(find "osu.Android/bin/Release/net10.0-android/publish" -maxdepth 1 -name "*.apk" 2>/dev/null | head -1)
if [ -z "$APK" ]; then
APK=$(find "osu.Android/bin/Release" -name "*.apk" | head -1)
fi
fi
PUBLISH_DIR="osu.Android/bin/$CONFIG/net10.0-android/publish"
APK=$(find "$PUBLISH_DIR" -maxdepth 1 -name "*.apk" 2>/dev/null | head -1)

if [ -z "$APK" ]; then
APK=$(find "osu.Android/bin/$CONFIG" -name "*.apk" | head -1)
echo "::error::Failed to locate APK. Listing bin directory:"
find osu.Android/bin -name "*.apk" -o -name "*.aab" 2>/dev/null || true
exit 1
fi

APK_SIZE=$(stat -c %s "$APK" 2>/dev/null || stat -f %z "$APK" 2>/dev/null || wc -c < "$APK")
APK_SIZE_MB=$((APK_SIZE / 1048576))
echo "Found APK: $APK ($APK_SIZE_MB MB)"
echo "apk_path=$APK" >> "$GITHUB_OUTPUT"

- name: Upload APK artifact
Expand Down
50 changes: 40 additions & 10 deletions osu.Android.props
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
<Project>
<PropertyGroup>
<SupportedOSPlatformVersion>33.0</SupportedOSPlatformVersion>
<RuntimeIdentifiers>android-x86;android-arm;android-arm64</RuntimeIdentifiers>
<!-- arm64-v8a covers 99%+ of modern Android devices.
armeabi-v7a is kept for older 32-bit devices.
x86 removed — only used by legacy emulators; modern x86_64 emulators
run arm64 binaries via native translation (no dedicated x86 build needed). -->
<RuntimeIdentifiers>android-arm;android-arm64</RuntimeIdentifiers>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<!-- CJK and Mideast encodings are needed for song metadata display.
Rare covers supplementary Unicode blocks. West is the default Latin set. -->
<MandroidI18n>CJK;Mideast;Rare;West;Other;</MandroidI18n>
<AndroidHttpClientHandlerType>Xamarin.Android.Net.AndroidMessageHandler</AndroidHttpClientHandlerType>
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<!-- Suppress XA0141 page-size warnings during build. The actual .so files are patched
to 16 KB alignment by build/PatchElfPageSize.targets, but the SDK check runs before
the patch in some build orderings. This suppression is harmless since we do fix them. -->
<!-- Suppress XA0141 page-size warnings. ELF patching is disabled for all configurations
(see PatchElfPageSizeEnabled below) to prevent APK signature corruption.
The XA0141 warning is therefore expected and harmless.
AndroidPageSize16KBCompatibilityCheck=false should suppress the check, but Android SDK
36.1.53 still emits XA0141 in some build orderings, so we also add it to NoWarn. -->
Comment on lines +19 to +21
<AndroidPageSize16KBCompatibilityCheck>false</AndroidPageSize16KBCompatibilityCheck>
<NoWarn>$(NoWarn);XA0141</NoWarn>
<!-- Disable ELF alignment patching for ALL configurations.
The custom PatchElfPageSize task can run concurrently with the Android packaging step
because MSBuild's BeforeTargets="Build" fires for every ABI sub-build, which often
overlaps with the main APK assembly pass. Patching .so files in the NuGet cache while
they are being read and packed into the APK can corrupt the ZIP/signing structures,
producing an APK with a null certificate array (INSTALL_PARSE_FAILED_NO_CERTIFICATES).
TODO: Re-enable once ppy.Veldrid.SPIRV ships 16 KB-aligned native libraries so no
patching is needed at all, or once the build targets are re-sequenced so
patching always completes before packaging starts. -->
<PatchElfPageSizeEnabled>false</PatchElfPageSizeEnabled>
</PropertyGroup>

<!-- Patch NuGet-provided .so files that ship with 4 KB ELF alignment to 16 KB.
Expand All @@ -26,10 +45,18 @@
<RunAOTCompilation>true</RunAOTCompilation>
<AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
<!-- Disable ELF patching in Release to prevent signature corruption -->
<PatchElfPageSizeEnabled>false</PatchElfPageSizeEnabled>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
<!-- Strip IL bodies from AOT-compiled assemblies. The runtime uses the native .so code
instead, and IL is only kept for non-AOT methods. This can save 20-30 MB. -->
<AndroidStripILAfterAOT>true</AndroidStripILAfterAOT>
<!-- Compress managed assemblies inside the APK (LZ4). Android extracts them on first run
but the download/APK size is significantly smaller. -->
<AndroidEnableAssemblyCompression>true</AndroidEnableAssemblyCompression>
<!-- Don't ship PDB files in the APK — they add ~15-20 MB.
Stack traces still work via embedded metadata. -->
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>

<ItemGroup Condition="'$(Configuration)' == 'Release'">
Expand All @@ -56,10 +83,13 @@
</PropertyGroup>

<!-- Fix for .NET 10 Android AOT misclassifying non-managed assets in runtime pack.
This is critical for Android 16 (API 36) which has stricter manifest/parsing rules.
We hook into multiple stages to ensure all native assets (including BASS and native bridge)
are correctly identified before the optimization/signing phases. -->
<Target Name="FixRuntimePackAssetTypes" AfterTargets="ResolveRuntimePackAssets;ComputeFilesToPublish;ComputeResolvedFilesToPublishList">
This is critical for Android 16 (API 36) Release/AOT builds where the linker expects
all non-managed assets to be marked as native before the optimization/signing phases.
Scoped to Release only — running this during Debug builds can interfere with how the
Android SDK classifies signing-related items, leading to APKs with null certificate
arrays (INSTALL_PARSE_FAILED_NO_CERTIFICATES). -->
<Target Name="FixRuntimePackAssetTypes" AfterTargets="ResolveRuntimePackAssets;ComputeFilesToPublish;ComputeResolvedFilesToPublishList"
Condition="'$(Configuration)' == 'Release'">
<ItemGroup>
<RuntimePackAsset Update="@(RuntimePackAsset)" Condition="'%(Extension)' != '.dll' AND '%(Extension)' != '.pdb'">
<AssetType>native</AssetType>
Expand Down
2 changes: 1 addition & 1 deletion osu.Android/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
<uses-sdk android:minSdkVersion="33" android:targetSdkVersion="36" />
<application android:extractNativeLibs="true" android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher" android:largeHeap="true">
<application android:extractNativeLibs="false" android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher" android:largeHeap="true">
<provider android:name="androidx.core.content.FileProvider" android:authorities="sh.ppy.osulazer.fileprovider" android:grantUriPermissions="true" android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
</provider>
Expand Down
4 changes: 2 additions & 2 deletions osu.Android/osu.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
<None Remove="Native\CMakeLists.txt" />
</ItemGroup>
<!-- Include pre-built native libraries when present (built by the release workflow or local NDK build).
When absent, native features (Oboe audio, Vulkan probe) are gracefully disabled at runtime. -->
When absent, native features (Oboe audio, Vulkan probe) are gracefully disabled at runtime.
x86 removed — only arm64 and arm32 are shipped (see RuntimeIdentifiers in osu.Android.props). -->
<ItemGroup>
<AndroidNativeLibrary Include="libs\arm64-v8a\libosu_native.so" Condition="Exists('libs\arm64-v8a\libosu_native.so')" Abi="arm64-v8a" />
<AndroidNativeLibrary Include="libs\armeabi-v7a\libosu_native.so" Condition="Exists('libs\armeabi-v7a\libosu_native.so')" Abi="armeabi-v7a" />
<AndroidNativeLibrary Include="libs\x86\libosu_native.so" Condition="Exists('libs\x86\libosu_native.so')" Abi="x86" />
</ItemGroup>
</Project>
Loading