diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d510dc7..2c5cde1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -313,3 +313,21 @@ jobs: "$ANDROID_HOME/build-tools/37.0.0/apksigner" verify --verbose \ "build/app/outputs/flutter-apk/app-${abi}-release.apk" done + + - name: Record Android symbol provenance + run: | + test -s build/symbols/android/app.android-arm64.symbols + test -s build/symbols/android/app.android-arm.symbols + test -s build/app/outputs/mapping/release/mapping.txt + git rev-parse HEAD > build/symbols/android/commit.txt + sha256sum build/app/outputs/flutter-apk/*release.apk > build/symbols/android/apk-sha256.txt + + - name: Retain Android debug symbols + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: android-symbols-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} + if-no-files-found: error + retention-days: 90 + path: | + build/symbols/android/ + build/app/outputs/mapping/release/mapping.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9f1e0ed..3779bf58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -227,6 +227,24 @@ jobs: "$apk_dir/SpotiFLAC-${VERSION}-${abi}.apk" done + - name: Record Android symbol provenance + run: | + test -s build/symbols/android/app.android-arm64.symbols + test -s build/symbols/android/app.android-arm.symbols + test -s build/app/outputs/mapping/release/mapping.txt + git rev-parse HEAD > build/symbols/android/commit.txt + sha256sum build/app/outputs/flutter-apk/SpotiFLAC-*.apk > build/symbols/android/apk-sha256.txt + + - name: Retain Android debug symbols + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: android-symbols-${{ needs.get-version.outputs.version }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} + if-no-files-found: error + retention-days: 90 + path: | + build/symbols/android/ + build/app/outputs/mapping/release/mapping.txt + - name: Upload APK artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 with: diff --git a/README.md b/README.md index a84ab873..8fd0a0cf 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,18 @@ remain the default. To build only ARM64, set run CocoaPods and the app build with `SPOTIFLAC_DISCORD_SDK=0`; an existing Pods installation must be regenerated with that setting. +Android release builds, including plain `flutter build apk --release`, keep Dart +debug data in `build/symbols/android/app..symbols` to reduce APK +size without obfuscation. An explicit `--split-debug-info` path overrides this +default; `--analyze-size` retains Flutter's normal behavior. Keep these symbols +with the matching APK before another local build replaces them. Decode a trace +with `flutter symbolize --debug-info= --input=crash.txt`. + +CI and release jobs retain an `android-symbols-…` artifact with the commit, APK +SHA-256 hashes, Dart symbols, and R8 mapping, identified by run and attempt. +Archive it before its 90-day retention expires and match the reported APK hash +when choosing symbols; equal version numbers do not guarantee a match. + For native iOS codec checks, see the [FFmpeg capability probe](scripts/README_ios_ffmpeg_capabilities.md). --- diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 872ecac6..2411dfb6 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -8,6 +8,13 @@ plugins { id("dev.flutter.flutter-gradle-plugin") } +// Flutter reads this when configuring variant tasks, after this build script. +// Keep release AOT debug data outside libapp.so; explicit paths and size analysis take precedence. +if (!project.hasProperty("split-debug-info") && !project.hasProperty("code-size-directory")) { + project.extensions.extraProperties["split-debug-info"] = + rootProject.file("../build/symbols/android").absolutePath +} + val keystorePropertiesFile = rootProject.file("key.properties") val keystoreProperties = Properties() if (keystorePropertiesFile.exists()) { diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index 78a408c7..d93e796e 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -24,4 +24,12 @@ add_library(spotiflac_audio SHARED native_audio.cpp) target_compile_features(spotiflac_audio PRIVATE cxx_std_17) target_compile_options(spotiflac_audio PRIVATE -Wall -Wextra -Werror -fvisibility=hidden) target_link_libraries(spotiflac_audio PRIVATE oboe WavPack::WavPack log) -target_link_options(spotiflac_audio PRIVATE "-Wl,-z,max-page-size=16384") +# Only JNI is public; static Oboe/WavPack/C++ implementation symbols must not +# keep otherwise unused code alive. APS2 is supported by our minimum API 24. +target_link_options(spotiflac_audio PRIVATE + "-Wl,-z,max-page-size=16384" + "-Wl,--version-script,${CMAKE_CURRENT_SOURCE_DIR}/native_audio.map" + "-Wl,--icf=safe" + "-Wl,--pack-dyn-relocs=android") +set_property(TARGET spotiflac_audio APPEND PROPERTY LINK_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/native_audio.map") diff --git a/android/app/src/main/cpp/native_audio.map b/android/app/src/main/cpp/native_audio.map new file mode 100644 index 00000000..276e3fc7 --- /dev/null +++ b/android/app/src/main/cpp/native_audio.map @@ -0,0 +1,17 @@ +{ + global: + Java_com_zarz_spotiflac_NativeAudio_openOboe; + Java_com_zarz_spotiflac_NativeAudio_bitsOboe; + Java_com_zarz_spotiflac_NativeAudio_deviceOboe; + Java_com_zarz_spotiflac_NativeAudio_writeOboe; + Java_com_zarz_spotiflac_NativeAudio_startOboe; + Java_com_zarz_spotiflac_NativeAudio_flushOboe; + Java_com_zarz_spotiflac_NativeAudio_framesOboe; + Java_com_zarz_spotiflac_NativeAudio_closeOboe; + Java_com_zarz_spotiflac_NativeAudio_openWavPack; + Java_com_zarz_spotiflac_NativeAudio_infoWavPack; + Java_com_zarz_spotiflac_NativeAudio_readWavPack; + Java_com_zarz_spotiflac_NativeAudio_seekWavPack; + Java_com_zarz_spotiflac_NativeAudio_closeWavPack; + local: *; +}; diff --git a/assets/images/logo-transparent.png b/assets/images/logo-transparent.png index faf19f67..f05f924f 100644 Binary files a/assets/images/logo-transparent.png and b/assets/images/logo-transparent.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png index 49bbdb40..43073c2e 100644 Binary files a/assets/images/logo.png and b/assets/images/logo.png differ diff --git a/scripts/build_rust_backend.sh b/scripts/build_rust_backend.sh index 48a10b2f..fd84cc7a 100644 --- a/scripts/build_rust_backend.sh +++ b/scripts/build_rust_backend.sh @@ -98,8 +98,10 @@ if [[ "$PLATFORM" == "android" ]]; then done rustup target add "${ANDROID_TARGETS[@]}" - # Align ELF segments for Android devices using 16 KB pages. - export RUSTFLAGS="${RUSTFLAGS:-} -C link-arg=-Wl,-z,max-page-size=16384" + # Align ELF segments for 16 KB pages and pack relocations (Android API 23+). + # Keep APS2 rather than RELR, which requires API 28; our minimum is API 24. + # Fold only identical functions whose addresses are not significant. + export RUSTFLAGS="${RUSTFLAGS:-} -C link-arg=-Wl,-z,max-page-size=16384 -C link-arg=-Wl,--pack-dyn-relocs=android -C link-arg=-Wl,--icf=safe" for target in "${ANDROID_TARGETS[@]}"; do cargo build --locked --release -p spotiflac-mobile --target "$target" case "$target" in diff --git a/third_party/spotiflac_discord/android/CMakeLists.txt b/third_party/spotiflac_discord/android/CMakeLists.txt index 0851387b..fa78a97f 100644 --- a/third_party/spotiflac_discord/android/CMakeLists.txt +++ b/third_party/spotiflac_discord/android/CMakeLists.txt @@ -4,7 +4,14 @@ set(CMAKE_CXX_STANDARD 20) option(SPOTIFLAC_DISCORD_SDK "Include the staged Discord Social SDK" ON) add_library(spotiflac_discord SHARED src/main/cpp/presence.cpp) target_compile_options(spotiflac_discord PRIVATE -Wall -Wextra -Werror -fvisibility=hidden) -target_link_options(spotiflac_discord PRIVATE "-Wl,-z,max-page-size=16384") +# Keep every JNI entry point while hiding the wrapper's static C++ runtime. +target_link_options(spotiflac_discord PRIVATE + "-Wl,-z,max-page-size=16384" + "-Wl,--version-script,${CMAKE_CURRENT_SOURCE_DIR}/presence.map" + "-Wl,--icf=safe" + "-Wl,--pack-dyn-relocs=android") +set_property(TARGET spotiflac_discord APPEND PROPERTY LINK_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/presence.map") set(SDK "${CMAKE_CURRENT_SOURCE_DIR}/../sdk/android") if(SPOTIFLAC_DISCORD_SDK AND EXISTS "${SDK}/jni/${ANDROID_ABI}/libdiscord_partner_sdk.so") add_library(discord_partner_sdk SHARED IMPORTED) diff --git a/third_party/spotiflac_discord/android/presence.map b/third_party/spotiflac_discord/android/presence.map new file mode 100644 index 00000000..5efb3899 --- /dev/null +++ b/third_party/spotiflac_discord/android/presence.map @@ -0,0 +1,9 @@ +{ + global: + Java_com_zarz_spotiflac_discord_DiscordPresencePlugin_nativeStart; + Java_com_zarz_spotiflac_discord_DiscordPresencePlugin_nativeTick; + Java_com_zarz_spotiflac_discord_DiscordPresencePlugin_nativeUpdate; + Java_com_zarz_spotiflac_discord_DiscordPresencePlugin_nativeClear; + Java_com_zarz_spotiflac_discord_DiscordPresencePlugin_nativeStop; + local: *; +};