From b6651310ea27249e5aa31166678d5add70856e86 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 16 Aug 2026 22:26:11 -0400 Subject: [PATCH] build(android): drop the x86_64 ABI, which no Android can run The fat APK's second half was 31 MB that cannot execute on any Android device. modernc.org/libc's Xlstat64 issues a raw lstat syscall on linux/amd64, and Android's seccomp policy forbids it because bionic never issues it, so the process takes SIGSYS the first time anything touches the database -- which for this app is startup. That is every x86_64 Android, x86 Chromebooks included, not merely the emulator. arm64 is structurally unaffected: the architecture has no lstat syscall at all, so modernc routes through fstatat. 27,059,130 bytes to 15,898,465, and one lib/ entry. Three places had to agree, and the third is what would have made this a silent no-op: abiFilters (what Gradle packages), android:package rather than package:fat (what Go *compiles* -- otherwise the library is still built and then discarded), and the native-code assertion in CI. That assertion is anchored, `native-code: 'arm64-v8a'$`, because without the anchor it also matches the fat APK's line and would pass on exactly the thing it exists to catch. Checked against a real artifact. Adding the ABI back, if modernc ever fixes Xlstat64, is those same three edits. --- .gitea/workflows/android-apk.yml | 15 ++++++++++----- Makefile | 9 +++++++-- build/android/app/build.gradle | 17 ++++++++++++++--- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.gitea/workflows/android-apk.yml b/.gitea/workflows/android-apk.yml index 74690e2..8fa051d 100644 --- a/.gitea/workflows/android-apk.yml +++ b/.gitea/workflows/android-apk.yml @@ -1,7 +1,7 @@ name: Build & publish the Android APK # The fifth workflow, and the second that publishes. It builds a signed -# fat APK (arm64-v8a + x86_64) on every version tag and puts it in +# arm64-v8a APK on every version tag and puts it in # Gitea's *generic* package registry, which — unlike the repository — is # readable without credentials. That is what lets an Obtainium client # poll a plain URL with no token and no public mirror of the source. @@ -225,7 +225,7 @@ jobs: # `$GITHUB_ENV` — where the `env:` dump is only masked for values # that are *verbatim* a secret, so a trimmed one could print in # clear — or repeating the trimming logic in both. - - name: Build the signed fat APK + - name: Build the signed APK working-directory: /src env: KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }} @@ -333,9 +333,14 @@ jobs: ls -la "$apk" "$bt/aapt2" dump badging "$apk" | sed -n '1p;/application-label:/p;/native-code/p' - # Both ABIs, or the artifact is not the fat APK it claims to be. - "$bt/aapt2" dump badging "$apk" | grep -q "native-code: 'arm64-v8a' 'x86_64'" || { - echo "the APK does not carry both ABIs" >&2; exit 1; } + # arm64 and *only* arm64. x86_64 Android cannot run this app + # (modernc's raw lstat against Android's seccomp filter, which + # is every x86_64 device and not merely the emulator), so an + # x86_64 slice would be ~31 MB that runs nowhere -- and its + # reappearance would mean someone had put the ABI back in + # app/build.gradle without knowing that. + "$bt/aapt2" dump badging "$apk" | grep -q "native-code: 'arm64-v8a'$" || { + echo "the APK's ABI set is not exactly arm64-v8a" >&2; exit 1; } # The identity the pipeline exists to keep stable. "$bt/aapt2" dump badging "$apk" | grep -q "versionCode='${{ steps.version.outputs.code }}'" || { diff --git a/Makefile b/Makefile index 5c2d9af..f341fc0 100644 --- a/Makefile +++ b/Makefile @@ -52,8 +52,13 @@ ANDROID_SDK ?= $(HOME)/Android/Sdk ANDROID_NDK ?= /opt/android-ndk ANDROID_ENV := ANDROID_HOME=$(ANDROID_SDK) ANDROID_SDK_ROOT=$(ANDROID_SDK) ANDROID_NDK_HOME=$(ANDROID_NDK) -android: build-frontend ## Build the fat APK (arm64 + x86_64) into bin/ - @$(ANDROID_ENV) PATH="$(TOOLBIN):$$PATH" go tool wails3 task android:package:fat +# `package`, not `package:fat`: x86_64 Android cannot run this app at +# all (modernc's raw lstat vs Android's seccomp -- see +# android-tier.md), so the second ABI was ~31 MB that could not run +# anywhere. app/build.gradle's abiFilters says the same thing to +# Gradle; both have to agree or the .so is built and then dropped. +android: build-frontend ## Build the arm64 APK into bin/ + @$(ANDROID_ENV) PATH="$(TOOLBIN):$$PATH" go tool wails3 task android:package android-setup: ## Install the SDK pieces and create the AVD (once, ~3.5GB) @$(ANDROID_ENV) ./scripts/android-emulator.sh setup diff --git a/build/android/app/build.gradle b/build/android/app/build.gradle index b8c2cd0..037ceeb 100644 --- a/build/android/app/build.gradle +++ b/build/android/app/build.gradle @@ -40,9 +40,21 @@ android { versionCode Integer.parseInt(System.getenv("YJ_VERSION_CODE") ?: "1") versionName System.getenv("YJ_VERSION") ?: "0.0.0" - // Configure supported ABIs + // **arm64 only, and x86_64 is not a gap.** `modernc.org/libc`'s + // Xlstat64 issues a raw lstat syscall on linux/amd64, which + // Android's seccomp policy forbids (bionic never issues it), so + // the process takes SIGSYS the first time anything touches the + // database -- which for this app is startup. That is every + // x86_64 Android, emulators and x86 Chromebooks alike, not just + // some. arm64 is structurally unaffected: the architecture has + // no lstat syscall at all, so modernc routes through fstatat. + // + // So the second ABI was ~31 MB of an artifact that could not run + // anywhere. If modernc fixes it, adding 'x86_64' back here and + // to the native-code assertion in android-apk.yml is the whole + // change. ndk { - abiFilters 'arm64-v8a', 'x86_64' + abiFilters 'arm64-v8a' } } @@ -93,7 +105,6 @@ android { packagingOptions { // Don't strip Go symbols in debug builds doNotStrip '*/arm64-v8a/libwails.so' - doNotStrip '*/x86_64/libwails.so' } }