Skip to content

Commit 0802f04

Browse files
authored
Merge pull request #198 from winnerspiros/copilot/fix-installation-failed-apk
Optimize Android APK: always build Release, drop x86, enable IL stripping + compression
2 parents f1a8590 + 288b1ba commit 0802f04

4 files changed

Lines changed: 69 additions & 21 deletions

File tree

.github/workflows/release.yml

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ jobs:
4848
NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865"
4949
CMAKE_BIN="$ANDROID_HOME/cmake/3.22.1/bin/cmake"
5050
51-
for ABI in arm64-v8a armeabi-v7a x86; do
51+
# Only build arm64 and arm32. x86 removed to reduce APK size —
52+
# modern emulators use x86_64 or ARM translation.
53+
for ABI in arm64-v8a armeabi-v7a; do
5254
echo "::group::Building osu_native for $ABI"
5355
"$CMAKE_BIN" -B "build-native/$ABI" -S osu.Android/Native \
5456
-DCMAKE_TOOLCHAIN_FILE="$NDK_HOME/build/cmake/android.toolchain.cmake" \
@@ -86,6 +88,9 @@ jobs:
8688
echo "version=0.0.0" >> "$GITHUB_OUTPUT"
8789
fi
8890
91+
# Always build Release for full optimization (trimming, AOT, compression).
92+
# When a keystore is available we sign with it; otherwise the SDK produces
93+
# a debug-signed Release APK that can be sideloaded for testing.
8994
- name: Build Android APK (signed)
9095
if: steps.keystore.outputs.has_keystore == 'true'
9196
env:
@@ -105,10 +110,10 @@ jobs:
105110
-p:AndroidSigningKeyPass="$ANDROID_KEY_PASS"
106111
-p:AndroidSigningStorePass="$ANDROID_STORE_PASS"
107112
108-
- name: Build Android APK (unsigned)
113+
- name: Build Android APK (unsigned Release)
109114
if: steps.keystore.outputs.has_keystore != 'true'
110115
run: >
111-
dotnet publish -c Debug
116+
dotnet publish -c Release
112117
osu.Android/osu.Android.csproj
113118
-f net10.0-android
114119
-p:Version="${{ steps.version.outputs.version }}"
@@ -118,16 +123,29 @@ jobs:
118123
- name: Find APK
119124
id: find_apk
120125
run: |
126+
# Both paths build Release. Signed builds produce *-Signed.apk; unsigned
127+
# builds produce the base APK name. Search publish dir first, then fallback.
121128
if [ "${{ steps.keystore.outputs.has_keystore }}" == "true" ]; then
122-
CONFIG="Release"
129+
APK=$(find "osu.Android/bin/Release/net10.0-android/publish" -maxdepth 1 -name "*-Signed.apk" 2>/dev/null | head -1)
130+
if [ -z "$APK" ]; then
131+
APK=$(find "osu.Android/bin/Release" -name "*-Signed.apk" | head -1)
132+
fi
123133
else
124-
CONFIG="Debug"
134+
APK=$(find "osu.Android/bin/Release/net10.0-android/publish" -maxdepth 1 -name "*.apk" 2>/dev/null | head -1)
135+
if [ -z "$APK" ]; then
136+
APK=$(find "osu.Android/bin/Release" -name "*.apk" | head -1)
137+
fi
125138
fi
126-
PUBLISH_DIR="osu.Android/bin/$CONFIG/net10.0-android/publish"
127-
APK=$(find "$PUBLISH_DIR" -maxdepth 1 -name "*.apk" 2>/dev/null | head -1)
139+
128140
if [ -z "$APK" ]; then
129-
APK=$(find "osu.Android/bin/$CONFIG" -name "*.apk" | head -1)
141+
echo "::error::Failed to locate APK. Listing bin directory:"
142+
find osu.Android/bin -name "*.apk" -o -name "*.aab" 2>/dev/null || true
143+
exit 1
130144
fi
145+
146+
APK_SIZE=$(stat -c %s "$APK" 2>/dev/null || stat -f %z "$APK" 2>/dev/null || wc -c < "$APK")
147+
APK_SIZE_MB=$((APK_SIZE / 1048576))
148+
echo "Found APK: $APK ($APK_SIZE_MB MB)"
131149
echo "apk_path=$APK" >> "$GITHUB_OUTPUT"
132150
133151
- name: Upload APK artifact

osu.Android.props

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

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

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

5885
<!-- Fix for .NET 10 Android AOT misclassifying non-managed assets in runtime pack.
59-
This is critical for Android 16 (API 36) which has stricter manifest/parsing rules.
60-
We hook into multiple stages to ensure all native assets (including BASS and native bridge)
61-
are correctly identified before the optimization/signing phases. -->
62-
<Target Name="FixRuntimePackAssetTypes" AfterTargets="ResolveRuntimePackAssets;ComputeFilesToPublish;ComputeResolvedFilesToPublishList">
86+
This is critical for Android 16 (API 36) Release/AOT builds where the linker expects
87+
all non-managed assets to be marked as native before the optimization/signing phases.
88+
Scoped to Release only — running this during Debug builds can interfere with how the
89+
Android SDK classifies signing-related items, leading to APKs with null certificate
90+
arrays (INSTALL_PARSE_FAILED_NO_CERTIFICATES). -->
91+
<Target Name="FixRuntimePackAssetTypes" AfterTargets="ResolveRuntimePackAssets;ComputeFilesToPublish;ComputeResolvedFilesToPublishList"
92+
Condition="'$(Configuration)' == 'Release'">
6393
<ItemGroup>
6494
<RuntimePackAsset Update="@(RuntimePackAsset)" Condition="'%(Extension)' != '.dll' AND '%(Extension)' != '.pdb'">
6595
<AssetType>native</AssetType>

osu.Android/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version='1.0' encoding='utf-8'?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
33
<uses-sdk android:minSdkVersion="33" android:targetSdkVersion="36" />
4-
<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">
4+
<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">
55
<provider android:name="androidx.core.content.FileProvider" android:authorities="sh.ppy.osulazer.fileprovider" android:grantUriPermissions="true" android:exported="false">
66
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" />
77
</provider>

osu.Android/osu.Android.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@
2727
<None Remove="Native\CMakeLists.txt" />
2828
</ItemGroup>
2929
<!-- Include pre-built native libraries when present (built by the release workflow or local NDK build).
30-
When absent, native features (Oboe audio, Vulkan probe) are gracefully disabled at runtime. -->
30+
When absent, native features (Oboe audio, Vulkan probe) are gracefully disabled at runtime.
31+
x86 removed — only arm64 and arm32 are shipped (see RuntimeIdentifiers in osu.Android.props). -->
3132
<ItemGroup>
3233
<AndroidNativeLibrary Include="libs\arm64-v8a\libosu_native.so" Condition="Exists('libs\arm64-v8a\libosu_native.so')" Abi="arm64-v8a" />
3334
<AndroidNativeLibrary Include="libs\armeabi-v7a\libosu_native.so" Condition="Exists('libs\armeabi-v7a\libosu_native.so')" Abi="armeabi-v7a" />
34-
<AndroidNativeLibrary Include="libs\x86\libosu_native.so" Condition="Exists('libs\x86\libosu_native.so')" Abi="x86" />
3535
</ItemGroup>
3636
</Project>

0 commit comments

Comments
 (0)