From 8325022dacec3b47de0d149c22c02909e8aadff3 Mon Sep 17 00:00:00 2001 From: laoqijun Date: Sun, 8 Nov 2026 06:53:37 +0800 Subject: [PATCH 1/6] build: fix assembleRelease and align libwnsteam.so to 16 KB pages The release build type was never exercised: every workflow in this repository builds Debug APKs, so two latent failures only show up on assembleRelease. 1. proot loader link. Release builds add -Wl,--gc-sections, which discards the unreferenced pokedata_workaround stub in src/loader/assembly.S. The loader-info.cmake step measures that stub's offset from _start and aborted the build with "pokedata_workaround or _start missing". Force the symbol into the image with -Wl,--undefined=pokedata_workaround. 2. R8. snakeyaml's optional JavaBeans introspection path references java.beans.*, which is not part of the platform, so minification failed with "Missing classes detected while running R8". Suppress those warnings; the code path is unreachable on Android. Also align libwnsteam.so to 16 KB pages. Gradle already passes -Wl,-z,max-page-size=16384 to the CMake targets, but cargo drives the NDK linker itself for the Rust library, so it was the only ELF in the APK still built for 4 KB pages. Verified: LOAD align is now 0x4000. Shipping the release variant also removes the Android 15+ 16 KB compatibility dialog, which the platform only shows for debuggable builds. --- app/proguard-rules.pro | 6 ++++++ app/src/main/cpp/proot/CMakeLists.txt | 4 ++++ app/src/main/cpp/wn-steam-client/CMakeLists.txt | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 8fab7dfc3..32380d10f 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -106,3 +106,9 @@ -dontwarn androidx.window.sidecar.SidecarInterface -dontwarn androidx.window.sidecar.SidecarProvider -dontwarn androidx.window.sidecar.SidecarWindowLayoutInfo + +# snakeyaml's optional JavaBeans introspection path is unreachable on Android +# (java.beans.* is not part of the platform), but R8 still resolves its +# references and fails the release build without these suppressions. +-dontwarn java.beans.** +-dontwarn org.yaml.snakeyaml.introspector.** diff --git a/app/src/main/cpp/proot/CMakeLists.txt b/app/src/main/cpp/proot/CMakeLists.txt index e6f7ca3e9..c9a0b67a1 100644 --- a/app/src/main/cpp/proot/CMakeLists.txt +++ b/app/src/main/cpp/proot/CMakeLists.txt @@ -20,7 +20,11 @@ add_executable(libproot-loader.so target_include_directories(libproot-loader.so PRIVATE src) target_compile_definitions(libproot-loader.so PRIVATE ${PROOT_DEFINES}) target_compile_options(libproot-loader.so PRIVATE ${PROOT_CFLAGS} -fPIC -ffreestanding -fno-stack-protector) +# Release builds add -Wl,--gc-sections; without a GC root the linker discards the +# unreferenced pokedata_workaround stub that loader-info.cmake measures, which made +# assembleRelease fail. -u keeps it in the image on every build type. target_link_options(libproot-loader.so PRIVATE -static -nostdlib -no-pie + -Wl,--undefined=pokedata_workaround -Wl,--build-id=none,-Ttext=0x2000000000,--rosegment,-z,noexecstack) # tracee/mem.c finds the loader's pokedata_workaround stub by its offset from _start. diff --git a/app/src/main/cpp/wn-steam-client/CMakeLists.txt b/app/src/main/cpp/wn-steam-client/CMakeLists.txt index 949f00483..ac196d6df 100644 --- a/app/src/main/cpp/wn-steam-client/CMakeLists.txt +++ b/app/src/main/cpp/wn-steam-client/CMakeLists.txt @@ -56,6 +56,10 @@ set(WNSTEAM_RUST_OUT "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libwnsteam.so") set(WNSTEAM_RUST_LINKER "${ANDROID_NDK}/toolchains/llvm/prebuilt/${WNSTEAM_NDK_HOST_TAG}/bin/${WNSTEAM_RUST_TARGET}${WNSTEAM_ANDROID_API}-clang${WNSTEAM_LINKER_SUFFIX}") +# Android 15+ ships 16 KB page-size kernels. Cargo drives the NDK linker itself +# and therefore does not inherit Gradle's CMake linker flags, so ask for 16 KB +# LOAD alignment explicitly here; otherwise libwnsteam.so would be the only ELF +# in the APK still built for 4 KB pages. add_custom_command( OUTPUT "${WNSTEAM_RUST_OUT}" COMMAND "${CMAKE_COMMAND}" -E env @@ -64,6 +68,7 @@ add_custom_command( "AR_aarch64_linux_android=${ANDROID_NDK}/toolchains/llvm/prebuilt/${WNSTEAM_NDK_HOST_TAG}/bin/llvm-ar${WNSTEAM_AR_SUFFIX}" "${CARGO_EXECUTABLE}" rustc --release --target "${WNSTEAM_RUST_TARGET}" --lib -- -Clink-arg=-Wl,-soname,libwnsteam.so + -Clink-arg=-Wl,-z,max-page-size=16384 COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${WNSTEAM_RUST_LIB}" "${WNSTEAM_RUST_OUT}" From 3edbc46d83b8af6cfad5f493300a87ee604ba548 Mon Sep 17 00:00:00 2001 From: laoqijun Date: Sun, 8 Nov 2026 07:07:53 +0800 Subject: [PATCH 2/6] ci: build and publish release APKs from GitHub runners No workflow in this repository produces a *release* (non-debuggable) APK: release.yml, tag-apk-artifacts.yml and pr-ci.yml all invoke assemble*Debug. Debug builds are the only ones Android shows the 16 KB page-size compatibility dialog for, so a release variant is what users actually want to sideload. Adds a workflow that builds :app:assembleStandardRelease on GitHub's runners and publishes the APK as a GitHub Release: - triggers: manual dispatch (optional tag input) or a push of a fork-v* tag - installs NDK 27.3.13750724 / CMake 3.22.1 / platform 35, pulls LFS assets and adds the aarch64-linux-android Rust target before building - signs with the KEYSTORE_BASE64 / KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD secrets when they exist, otherwise generates an ephemeral key and says so in the release notes, because a different key means users must uninstall first - fails the run if the produced APK is debuggable, or if libwnsteam.so lost its 16 KB LOAD alignment (cargo bypasses Gradle's CMake linker flags, so that regression is easy to reintroduce) --- .github/workflows/fork-release.yml | 170 +++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 .github/workflows/fork-release.yml diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml new file mode 100644 index 000000000..56f95ccee --- /dev/null +++ b/.github/workflows/fork-release.yml @@ -0,0 +1,170 @@ +name: Build APK and publish release + +# Builds a signed *release* APK on GitHub's runners and publishes it as a +# GitHub Release asset. The existing release.yml only builds debug APKs and +# needs a maintainer keystore, so forks had no way to ship a non-debuggable +# build from CI. +# +# Signing: set the KEYSTORE_BASE64 / KEYSTORE_PASSWORD / KEY_ALIAS / +# KEY_PASSWORD repository secrets to sign with a stable key. Without them a +# throwaway key is generated so the build still succeeds; the release notes +# record which key was used so users know whether they can install it over an +# existing copy. + +on: + workflow_dispatch: + inputs: + version: + description: 'Release tag to publish, e.g. fork-v1.0.0 (default: fork-)' + required: false + default: '' + push: + tags: + - 'fork-v*' + +permissions: + contents: write + +concurrency: + group: fork-release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - name: Clone repository + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 1 + lfs: false + persist-credentials: false + + - name: Pull LFS files + run: git lfs pull + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Install Rust Android target + run: rustup target add aarch64-linux-android + + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + + - name: Install NDK and CMake + run: | + set -euo pipefail + yes | sdkmanager --licenses >/dev/null 2>&1 || true + sdkmanager "ndk;27.3.13750724" "cmake;3.22.1" "platforms;android-35" "build-tools;35.0.0" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Configure signing + id: signing + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + run: | + set -euo pipefail + if [ -n "${KEYSTORE_BASE64:-}" ]; then + printf '%s' "$KEYSTORE_BASE64" | base64 -d > release.jks + store_password="${KEYSTORE_PASSWORD:?KEYSTORE_PASSWORD is required with KEYSTORE_BASE64}" + key_password="${KEY_PASSWORD:-$store_password}" + key_alias="${KEY_ALIAS:?KEY_ALIAS is required with KEYSTORE_BASE64}" + printf 'CI signed with the repository keystore secret.\n' > signing-note.txt + echo "stable=yes" >> "$GITHUB_OUTPUT" + else + echo "::warning::No KEYSTORE_BASE64 secret; generating a throwaway signing key. Users cannot install this APK over an existing copy signed with a different key." + keytool -genkeypair -keystore release.jks -alias winnative \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -storepass ephemeral123 -keypass ephemeral123 \ + -dname "CN=WinNative CI (ephemeral), OU=CI, O=CI, L=CI, C=CI" >/dev/null 2>&1 + store_password=ephemeral123 + key_password=ephemeral123 + key_alias=winnative + printf 'Signed with a throwaway CI key; uninstall any existing copy first.\n' > signing-note.txt + echo "stable=no" >> "$GITHUB_OUTPUT" + fi + cat > signing.properties < badging.txt + if grep -q "application-debuggable" badging.txt; then + echo "::error::release APK is debuggable; Android would show the 16 KB compatibility warning" + exit 1 + fi + # libwnsteam.so is produced by cargo, which drives the NDK linker + # itself; a regression there would silently fall back to 4 KB pages. + rm -rf apk-lib && mkdir apk-lib + unzip -o -q "$apk" 'lib/arm64-v8a/libwnsteam.so' -d apk-lib + readelf=$(find "$ANDROID_SDK_ROOT/ndk/27.3.13750724" -name llvm-readelf | head -1) + align=$("$readelf" -lW apk-lib/lib/arm64-v8a/libwnsteam.so | awk '/LOAD/{print $NF; exit}') + echo "libwnsteam.so LOAD align: $align" + if [ "$align" != "0x4000" ]; then + echo "::error::libwnsteam.so is not 16 KB aligned (align=$align)" + exit 1 + fi + + - name: Work out release tag + id: tag + run: | + set -euo pipefail + tag="${{ inputs.version }}" + if [ -z "$tag" ]; then + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + tag="${GITHUB_REF_NAME}" + else + tag="fork-${GITHUB_RUN_NUMBER}" + fi + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Publish release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: WinNative ${{ steps.tag.outputs.tag }} + draft: false + prerelease: true + body: | + Release APK built from `${{ github.sha }}` by the + `Build APK and publish release` workflow on GitHub's runners. + + - flavour: `standard` (application id `com.winnative.cmod`) + - build type: **release** (not debuggable, so Android does not show the + 16 KB page-size compatibility warning) + - Steam download fixes: CDN auth token (`GetCDNAuthToken#1`), logon + `cell_id` reuse, Asia-Pacific CDN fallback, adaptive host scheduling + with a pre-flight reachability probe + + $(cat signing-note.txt) + + `libcrypto.so`, `libssl.so` and `libcurl.so` still come from the + `com.android.ndk.thirdparty` prefab artefacts, which are built for + 4 KB pages; devices booted with a 16 KB page-size kernel are + therefore not supported yet. + files: app/build/outputs/apk/standard/release/app-standard-release.apk + fail_on_unmatched_files: true From cbc5df89e1e243f137e4285e659045ea9e7019c3 Mon Sep 17 00:00:00 2001 From: laoqijun Date: Sun, 8 Nov 2026 07:16:17 +0800 Subject: [PATCH 3/6] ci: use the runner's preinstalled Android SDK android-actions/setup-android@v3 fails on current ubuntu-latest images: it invokes sdkmanager to install the "tools" package, which no longer exists ("Warning: Failed to find package 'tools'"), so the job aborted before the build ever started. The other workflows in this repository already rely on the SDK that ships with the runner image; do the same, resolve sdkmanager from it, and fall back to letting Gradle install the NDK when it is absent. --- .github/workflows/fork-release.yml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index 56f95ccee..eab3b6ade 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -54,14 +54,28 @@ jobs: - name: Install Rust Android target run: rustup target add aarch64-linux-android - - name: Set up Android SDK - uses: android-actions/setup-android@v3 - + # The ubuntu-latest image already ships an Android SDK; android-actions/ + # setup-android@v3 fails on it because it insists on installing the + # long-removed `tools` package. Use the preinstalled SDK like the other + # workflows in this repository do. - name: Install NDK and CMake run: | set -euo pipefail - yes | sdkmanager --licenses >/dev/null 2>&1 || true - sdkmanager "ndk;27.3.13750724" "cmake;3.22.1" "platforms;android-35" "build-tools;35.0.0" + sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}" + if [ -z "$sdk" ] || [ ! -d "$sdk" ]; then + echo "::error::No Android SDK on this runner (ANDROID_SDK_ROOT/ANDROID_HOME unset)" + exit 1 + fi + echo "ANDROID_SDK_ROOT=$sdk" >> "$GITHUB_ENV" + echo "ANDROID_HOME=$sdk" >> "$GITHUB_ENV" + sdkmanager_bin="$(find "$sdk/cmdline-tools" -maxdepth 3 -name sdkmanager -type f 2>/dev/null | sort -V | tail -1)" + if [ -z "$sdkmanager_bin" ]; then + echo "::warning::sdkmanager not found under $sdk; letting Gradle install NDK and CMake" + exit 0 + fi + echo "Using $sdkmanager_bin" + yes | "$sdkmanager_bin" --licenses >/dev/null 2>&1 || true + "$sdkmanager_bin" "ndk;27.3.13750724" "cmake;3.22.1" "platforms;android-35" "build-tools;35.0.0" - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 From 5da3b0e32d2a99473b6e9abce184f316368e064a Mon Sep 17 00:00:00 2001 From: laoqijun Date: Sun, 8 Nov 2026 07:36:04 +0800 Subject: [PATCH 4/6] ci: make the APK verification step fail loudly The previous run built the release APK successfully but the verification step exited 74 without printing anything, so the log gave no clue which check failed. Drop `set -e` in favour of explicit checks with messages, resolve aapt2/llvm-readelf through globs (the find|sort|tail pipelines can abort under pipefail), and echo each decision so a failure is diagnosable from the log. --- .github/workflows/fork-release.yml | 42 ++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index eab3b6ade..53d5889b7 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -121,21 +121,47 @@ jobs: - name: Verify APK run: | - set -euo pipefail + # Deliberately not using `set -e`: an unexpected non-zero status in a + # helper command used to abort this step silently. Check explicitly and + # always print what was decided. + set -uo pipefail apk="app/build/outputs/apk/standard/release/app-standard-release.apk" - test -f "$apk" || { echo "::error::APK not found at $apk"; exit 1; } - badging=$(find "$ANDROID_SDK_ROOT/build-tools" -maxdepth 1 -mindepth 1 -type d | sort -V | tail -1)/aapt2 - "$badging" dump badging "$apk" > badging.txt + if [ ! -f "$apk" ]; then + echo "APK directory contents:" + ls -R app/build/outputs/apk || true + echo "::error::APK not found at $apk" + exit 1 + fi + echo "APK: $apk ($(stat -c%s "$apk") bytes)" + + build_tools="$(ls -d "$ANDROID_SDK_ROOT"/build-tools/* 2>/dev/null | sort -V | tail -1)" + echo "build-tools: ${build_tools:-}" + if [ -z "${build_tools:-}" ] || [ ! -x "$build_tools/aapt2" ]; then + echo "::error::aapt2 not found under $ANDROID_SDK_ROOT/build-tools" + exit 1 + fi + if ! "$build_tools/aapt2" dump badging "$apk" > badging.txt; then + echo "::error::aapt2 dump badging failed" + exit 1 + fi if grep -q "application-debuggable" badging.txt; then echo "::error::release APK is debuggable; Android would show the 16 KB compatibility warning" exit 1 fi + echo "APK is not debuggable" + # libwnsteam.so is produced by cargo, which drives the NDK linker # itself; a regression there would silently fall back to 4 KB pages. - rm -rf apk-lib && mkdir apk-lib - unzip -o -q "$apk" 'lib/arm64-v8a/libwnsteam.so' -d apk-lib - readelf=$(find "$ANDROID_SDK_ROOT/ndk/27.3.13750724" -name llvm-readelf | head -1) - align=$("$readelf" -lW apk-lib/lib/arm64-v8a/libwnsteam.so | awk '/LOAD/{print $NF; exit}') + rm -rf apk-lib + mkdir -p apk-lib + unzip -o -q "$apk" "lib/arm64-v8a/libwnsteam.so" -d apk-lib + readelf_bin="$(ls -d "$ANDROID_SDK_ROOT"/ndk/27.3.13750724/toolchains/llvm/prebuilt/*/bin/llvm-readelf 2>/dev/null | head -1)" + echo "llvm-readelf: ${readelf_bin:-}" + if [ -z "${readelf_bin:-}" ] || [ ! -x "$readelf_bin" ]; then + echo "::error::llvm-readelf not found in NDK 27.3.13750724" + exit 1 + fi + align="$("$readelf_bin" -lW apk-lib/lib/arm64-v8a/libwnsteam.so | awk '/LOAD/{print $NF; exit}')" echo "libwnsteam.so LOAD align: $align" if [ "$align" != "0x4000" ]; then echo "::error::libwnsteam.so is not 16 KB aligned (align=$align)" From 65c301733c1e7512e7d49fdc356055f10acc7b45 Mon Sep 17 00:00:00 2001 From: laoqijun Date: Sun, 8 Nov 2026 08:01:33 +0800 Subject: [PATCH 5/6] build: keep the JNI surface so release builds do not crash The first release APK built from this branch crashed as soon as the setup wizard tried to extract imagefs: F runtime.cc: Throwing new exception 'no non-static method "Lcom/winlator/cmod/shared/util/OnExtractFileListener;.mapsExtractedFiles()Z"' NoSuchMethodError: no non-static method "Lcom/winlator/cmod/shared/util/OnExtractFileListener;.onExtractFileProgress(Ljava/io/File;J)V" R8 cannot see calls that cross the JNI boundary, so it treats the callbacks that only native code invokes as dead code and drops them. `-dontobfuscate` alone was not enough: OnExtractFileListener.onExtractFileProgress, mapsExtractedFiles, reportsExtractedBytesOnly and onExtractedBytes were still missing from the dex. Keep the app's own classes intact and let R8 shrink only third-party code. The JNI surface is wide in both directions: libwinlator looks methods up with FindClass/GetMethodID (OnExtractFileListener, WaylandCompositor's static frame callbacks, XConnectorEpoll's ConnectionHandler, GPUImage.setStride, the Downloader progress listener) and the Rust Steam client calls Kotlin listeners by name (onStateChanged, onAuthResult, onComplete, onProgress, ...). Also fixes the CI verification step: it ran under `bash -e` and died with exit 74 while piping llvm-readelf into `awk 'exit'` (SIGPIPE). Write the headers to a file instead, add an ERR trap that names the failing line, and keep the explicit not-debuggable and 16 KB alignment assertions. --- .github/workflows/fork-release.yml | 21 ++++++++++++++------- app/proguard-rules.pro | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/.github/workflows/fork-release.yml b/.github/workflows/fork-release.yml index 53d5889b7..0453c0eca 100644 --- a/.github/workflows/fork-release.yml +++ b/.github/workflows/fork-release.yml @@ -121,9 +121,9 @@ jobs: - name: Verify APK run: | - # Deliberately not using `set -e`: an unexpected non-zero status in a - # helper command used to abort this step silently. Check explicitly and - # always print what was decided. + # The runner invokes this script with `bash -e`, so any single failing + # command aborts silently. Report the failing line instead. + trap 'status=$?; echo "::error::verify APK failed at line $LINENO (status $status)"' ERR set -uo pipefail apk="app/build/outputs/apk/standard/release/app-standard-release.apk" if [ ! -f "$apk" ]; then @@ -154,17 +154,24 @@ jobs: # itself; a regression there would silently fall back to 4 KB pages. rm -rf apk-lib mkdir -p apk-lib - unzip -o -q "$apk" "lib/arm64-v8a/libwnsteam.so" -d apk-lib + if ! unzip -o -q "$apk" "lib/arm64-v8a/libwnsteam.so" -d apk-lib; then + echo "::error::could not extract libwnsteam.so from the APK" + exit 1 + fi + ls -l apk-lib/lib/arm64-v8a/ readelf_bin="$(ls -d "$ANDROID_SDK_ROOT"/ndk/27.3.13750724/toolchains/llvm/prebuilt/*/bin/llvm-readelf 2>/dev/null | head -1)" echo "llvm-readelf: ${readelf_bin:-}" if [ -z "${readelf_bin:-}" ] || [ ! -x "$readelf_bin" ]; then echo "::error::llvm-readelf not found in NDK 27.3.13750724" exit 1 fi - align="$("$readelf_bin" -lW apk-lib/lib/arm64-v8a/libwnsteam.so | awk '/LOAD/{print $NF; exit}')" - echo "libwnsteam.so LOAD align: $align" + # Write to a file instead of piping into awk: an early awk exit sends + # SIGPIPE to readelf and aborts the step under `bash -e`. + "$readelf_bin" -lW apk-lib/lib/arm64-v8a/libwnsteam.so > readelf-headers.txt + align="$(awk '/LOAD/ { print $NF; exit }' readelf-headers.txt)" + echo "libwnsteam.so LOAD align: ${align:-}" if [ "$align" != "0x4000" ]; then - echo "::error::libwnsteam.so is not 16 KB aligned (align=$align)" + echo "::error::libwnsteam.so is not 16 KB aligned (align=${align:-})" exit 1 fi diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 32380d10f..82da6c154 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -112,3 +112,29 @@ # references and fails the release build without these suppressions. -dontwarn java.beans.** -dontwarn org.yaml.snakeyaml.introspector.** + +# The app talks to its own native code in both directions: ~20 Java/Kotlin +# classes declare native/external methods that resolve to exported +# Java___ symbols, and libwinlator/libwaylandcomp/ +# libwnsteam look callbacks up by name with GetMethodID (for example +# OnExtractFileListener.onExtractFileProgress, WaylandCompositor.onGameFrame, +# GPUImage.setStride, XConnectorEpoll.ConnectionHandler.handleNewConnection). +# Renaming any of those makes the lookup throw NoSuchMethodError at runtime, +# which is exactly what a release build hit while extracting imagefs. +# Keep names so the JNI surface stays addressable; shrinking and optimisation +# still run. +-dontobfuscate + +# R8 cannot see calls that cross the JNI boundary, so it treats every Java method +# that only native code calls as dead and removes it. That broke imagefs +# extraction in the first release build: libwinlator looked up +# OnExtractFileListener.onExtractFileProgress with GetMethodID and got +# NoSuchMethodError because the method had been shrunk away. The same applies to +# the other direction - WaylandCompositor's static frame callbacks, the Xconnector +# connection handlers, GPUImage.setStride, and the Kotlin listeners that the Rust +# Steam client calls back (onProgress, onComplete, onAuthResult, ...). +# Keep the app's own classes intact and let R8 shrink only third-party code. +-keep class com.winlator.** { *; } +-keep class com.winnative.** { *; } +-keep class org.love2d.** { *; } + From c7d79e1952fdb8a86f07a858656e3529e601a00f Mon Sep 17 00:00:00 2001 From: laoqijun Date: Sun, 8 Nov 2026 10:20:00 +0800 Subject: [PATCH 6/6] build: keep androidx.security.crypto and Tink for release builds Once release builds actually run, EncryptedSharedPreferences construction fails because Tink registers its algorithm implementations reflectively and R8 strips them. Keep the crypto/Tink classes so the encrypted preference store works in a minified build instead of falling back to plain preferences. --- app/proguard-rules.pro | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 82da6c154..ea76ee0a1 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -138,3 +138,11 @@ -keep class com.winnative.** { *; } -keep class org.love2d.** { *; } + +# androidx.security.crypto builds its keys through Tink, which registers +# algorithm implementations reflectively. Without these keeps, a release build +# loses them and EncryptedSharedPreferences.create() throws at runtime, taking +# every Activity down through PrefManager's initialization. +-keep class androidx.security.crypto.** { *; } +-keep class com.google.crypto.tink.** { *; } +-dontwarn com.google.crypto.tink.**