From 6532e3a6c90d1833e5ae7f09e5bd2b01168cc06b Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Fri, 17 Jul 2026 10:17:41 +0200 Subject: [PATCH 1/3] native: align .so LOAD segments to 16 KB for targetSdk 35+ Play checks two things and we only satisfied one. ZIP-entry alignment already passed (zipalign -P 16, extractNativeLibs=false), which is what our notes recorded. ELF LOAD alignment did not: every shipped .so linked at 4 KB, so an upload at targetSdk 35 would be rejected. lld shifts vaddr rather than file offsets, so the .so bytes and the release APK size are identical either way. NDK r28 makes 16 KB the default, but it is not needed here and can stay a separate bump. The CI guard is there because this regresses silently: nothing in a normal build reports segment alignment. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Xavier Roche --- .github/workflows/android.yml | 5 ++++ app/src/main/jni/Application.mk | 3 ++ tools/ci/check-so-alignment.sh | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100755 tools/ci/check-so-alignment.sh diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 5b32ef7..33bdc60 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -84,6 +84,11 @@ jobs: run: tools/ci/fetch-openssl-statics.sh # picks up $OPENSSL_ANDROID_ROOT from the image - name: Gradle assemble + lint run: ./gradlew --no-daemon assembleDebug assembleRelease lint + - name: Check 16 KB .so alignment + run: | + for apk in app/build/outputs/apk/*/*.apk; do + tools/ci/check-so-alignment.sh "$apk" + done - uses: actions/upload-artifact@v4 with: name: apk diff --git a/app/src/main/jni/Application.mk b/app/src/main/jni/Application.mk index 31afe2e..08db648 100755 --- a/app/src/main/jni/Application.mk +++ b/app/src/main/jni/Application.mk @@ -7,3 +7,6 @@ # at API 24+. APP_ABI := arm64-v8a x86_64 APP_PLATFORM := android-24 + +# Play requires 16 KB-aligned LOAD segments at targetSdk 35+; r26d's lld still defaults to 4 KB. +APP_LDFLAGS := -Wl,-z,max-page-size=16384 diff --git a/tools/ci/check-so-alignment.sh b/tools/ci/check-so-alignment.sh new file mode 100755 index 0000000..975e2df --- /dev/null +++ b/tools/ci/check-so-alignment.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# Fail if any packaged .so has a LOAD segment aligned below 16 KB. Play rejects those +# at targetSdk 35+, and nothing in a normal build says so — the regression is silent. +set -euo pipefail + +APK="${1:?usage: $0 }" +WANT=16384 + +# The toolchain image carries the NDK but no binutils, so fall back to its llvm-readelf. +READELF="$(command -v llvm-readelf || command -v readelf || true)" +if [ -z "$READELF" ]; then + for cand in "${ANDROID_NDK_ROOT:-${ANDROID_NDK_HOME:-/nonexistent}}"/toolchains/llvm/prebuilt/*/bin/llvm-readelf; do + [ -x "$cand" ] && READELF="$cand" && break + done +fi +[ -n "$READELF" ] || { + echo "check-so-alignment: no readelf/llvm-readelf found" >&2 + exit 1 +} + +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +unzip -q -o "$APK" 'lib/*' -d "$tmp" + +mapfile -t sos < <(find "$tmp/lib" -name '*.so' | sort) +# An empty list would otherwise "pass" every check below. +[ "${#sos[@]}" -gt 0 ] || { + echo "check-so-alignment: no .so inside $APK" >&2 + exit 1 +} + +rc=0 +for so in "${sos[@]}"; do + name="${so#"$tmp"/}" + aligns="$("$READELF" -lW "$so" | awk '$1 == "LOAD" { print $NF }')" + [ -n "$aligns" ] || { + echo "check-so-alignment: no LOAD segments in $name" >&2 + exit 1 + } + for a in $aligns; do + if [ "$((a))" -lt "$WANT" ]; then + echo "FAIL $name: LOAD p_align $a < $WANT" + rc=1 + fi + done +done + +[ "$rc" -eq 0 ] && echo "16 KB aligned: ${#sos[@]} .so in $(basename "$APK")" +exit "$rc" From f3d0778c3de5f797c543f578718e5844741b9be5 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Fri, 17 Jul 2026 12:19:49 +0200 Subject: [PATCH 2/3] ci: assert the alignment guard walked every .so, not just one Review found the guard could pass on a subset. find runs inside a process substitution, where set -e cannot see its exit status and pipefail does not reach, so a truncated walk checked whatever it got and still printed the success banner. The old empty-list test only ever proved that at least one .so turned up. Comparing the walk against the archive's own listing asserts completeness instead, and catches unzip under-extracting for free. Confirmed against a find stub that emits one path and exits 1: the previous guard reported "16 KB aligned: 1 .so" and exited 0, this one exits 1. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Xavier Roche --- tools/ci/check-so-alignment.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/ci/check-so-alignment.sh b/tools/ci/check-so-alignment.sh index 975e2df..93d9d85 100755 --- a/tools/ci/check-so-alignment.sh +++ b/tools/ci/check-so-alignment.sh @@ -23,12 +23,19 @@ tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT unzip -q -o "$APK" 'lib/*' -d "$tmp" +# Cross-check the walk against the archive listing. find's exit status is invisible to set -e +# from inside a process substitution, so a truncated walk would silently check a subset and +# still report success; comparing counts asserts completeness, not merely non-emptiness. +listed="$(unzip -Z1 "$APK" 'lib/*.so' | wc -l)" || + { + echo "check-so-alignment: cannot list $APK" >&2 + exit 1 + } mapfile -t sos < <(find "$tmp/lib" -name '*.so' | sort) -# An empty list would otherwise "pass" every check below. -[ "${#sos[@]}" -gt 0 ] || { - echo "check-so-alignment: no .so inside $APK" >&2 +if [ "$listed" -le 0 ] || [ "${#sos[@]}" -ne "$listed" ]; then + echo "check-so-alignment: $APK lists $listed .so, walked ${#sos[@]}" >&2 exit 1 -} +fi rc=0 for so in "${sos[@]}"; do From a7872c21fda70ed0b238abcaeae1e1491cc3bca9 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Fri, 17 Jul 2026 14:41:15 +0200 Subject: [PATCH 3/3] ci: name the empty-lib case, and only walk regular files Review follow-ups, none of them blocking. The zero-.so APK, which is the likelier regression here, died in the extraction step with unzip's own "filename not matched" and rc 11 before any of our checks ran; it now says what actually happened, and a genuinely unreadable archive is reported separately rather than sharing that message. find grows -type f so a directory named foo.so cannot reach readelf. That also removes one of the two ways a count could agree over different sets, the other needing a duplicate entry name that no Android build tool emits. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Xavier Roche --- tools/ci/check-so-alignment.sh | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/tools/ci/check-so-alignment.sh b/tools/ci/check-so-alignment.sh index 93d9d85..52ad62c 100755 --- a/tools/ci/check-so-alignment.sh +++ b/tools/ci/check-so-alignment.sh @@ -21,18 +21,26 @@ fi tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT -unzip -q -o "$APK" 'lib/*' -d "$tmp" +mkdir -p "$tmp/lib" +# 11 is "nothing matched", which the count check below reports in its own words. +rc=0 +unzip -q -o "$APK" 'lib/*' -d "$tmp" || rc=$? +if [ "$rc" -ne 0 ] && [ "$rc" -ne 11 ]; then + echo "check-so-alignment: cannot extract $APK (unzip rc=$rc)" >&2 + exit 1 +fi # Cross-check the walk against the archive listing. find's exit status is invisible to set -e # from inside a process substitution, so a truncated walk would silently check a subset and # still report success; comparing counts asserts completeness, not merely non-emptiness. -listed="$(unzip -Z1 "$APK" 'lib/*.so' | wc -l)" || - { - echo "check-so-alignment: cannot list $APK" >&2 - exit 1 - } -mapfile -t sos < <(find "$tmp/lib" -name '*.so' | sort) -if [ "$listed" -le 0 ] || [ "${#sos[@]}" -ne "$listed" ]; then +# unzip -Z1 exits 11 when the pattern matches nothing, which is the likely regression here +# (a build shipping no native libs at all), so name that case rather than blame the archive. +listed="$(unzip -Z1 "$APK" 'lib/*.so' | wc -l)" || { + echo "check-so-alignment: no lib/*.so in $APK, or it is unreadable" >&2 + exit 1 +} +mapfile -t sos < <(find "$tmp/lib" -type f -name '*.so' | sort) +if [ "${#sos[@]}" -ne "$listed" ]; then echo "check-so-alignment: $APK lists $listed .so, walked ${#sos[@]}" >&2 exit 1 fi