fix(android): bake android24→26 clang shim into the docker image itself
Some checks failed
Mirror to GitHub / mirror (push) Failing after 37s
Build Release Binaries / build-amd64 (push) Failing after 3m36s

Build #13's PATH wrapper trick failed because tauri-cli invokes the linker
with an absolute path (/opt/android-sdk/ndk/.../bin/aarch64-linux-android24-
clang), which bypasses \$PATH entirely. The pthread_shim logs confirmed the
broken API-24 stubs were still being linked:

  WZP_pthread_shim: dlsym(RTLD_DEFAULT, pthread_create) returned NULL:
    libdl.a is a stub --- use libdl.so instead

Move the fix up a level — into the Dockerfile itself. On image build, for
each of the four android ABIs × {clang, clang++}, rename
`${abi}24-${suffix}` to `${abi}24-${suffix}.orig` and replace it with a
shell wrapper that exec()s `${abi}26-${suffix}`. Any call to the API-24
wrapper — via PATH, absolute path, or otherwise — now transparently runs
the API-26 wrapper, which uses the real libc.so/libdl.so bindings.

The old bash-c /tmp/wrappers workaround in build-tauri-android.sh is
removed now that the image handles it at the right layer.

Also add `--shell` to build-tauri-android.sh: opens an interactive docker
container on the remote with the same mounts/env as the build, so I can
iterate on cargo tauri android build / manually patch files / etc.
without the full git push → ssh → rebuild → install loop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-09 13:33:10 +04:00
parent 2718402e96
commit 5df9d418c9
2 changed files with 51 additions and 24 deletions

View File

@@ -74,6 +74,27 @@ RUN yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/nu
"platform-tools" \
2>&1 | grep -v '^\[' > /dev/null
# Work around broken API-24 libc/libdl stubs in the NDK. Rust's pre-compiled
# libstd.rlib for aarch64-linux-android links against pthread_create / dlsym
# from the sysroot. With --target=...24, those resolve to the API-24 "stub"
# libc.a/libdl.a that return "libdl.a is a stub --- use libdl.so instead" at
# runtime. API-26 has real dynamic bindings. Tauri-cli hard-codes the
# android24-clang path so env var overrides don't stick.
#
# Permanent fix inside the image: replace every `${abi}24-clang` binary with
# a shim that exec()s the corresponding `${abi}26-clang`. Any build using
# this image now transparently gets the API-26 sysroot even when the caller
# asks for API-24.
RUN set -eux; \
BIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin; \
for abi in aarch64-linux-android armv7a-linux-androideabi i686-linux-android x86_64-linux-android; do \
for suffix in clang clang++; do \
mv "$BIN/${abi}24-${suffix}" "$BIN/${abi}24-${suffix}.orig"; \
printf '#!/bin/sh\nexec "%s/%s26-%s" "$@"\n' "$BIN" "$abi" "$suffix" > "$BIN/${abi}24-${suffix}"; \
chmod +x "$BIN/${abi}24-${suffix}"; \
done; \
done
# Make SDK world-readable so builder user can access it
RUN chmod -R a+rX $ANDROID_HOME

View File

@@ -36,6 +36,7 @@ REBUILD_RUST=0
DO_PULL=1
DO_INIT=0
BUILD_RELEASE=0
DROP_SHELL=0
for arg in "$@"; do
case "$arg" in
--rust) REBUILD_RUST=1 ;;
@@ -43,6 +44,7 @@ for arg in "$@"; do
--no-pull) DO_PULL=0 ;;
--init) DO_INIT=1 ;;
--release) BUILD_RELEASE=1 ;;
--shell) DROP_SHELL=1 ;; # interactive debug shell inside container
-h|--help)
sed -n '3,30p' "$0"
exit 0
@@ -50,6 +52,34 @@ for arg in "$@"; do
esac
done
# ── --shell: drop into an interactive container for fast manual iteration ──
# The container is NOT --rm'd so you can keep hacking between invocations,
# and has the same mounts / env as the build path above so `cargo tauri
# android build ...` just works.
if [ "$DROP_SHELL" = "1" ]; then
log "Starting interactive shell in wzp-android-builder container..."
log " cd /build/source/desktop/src-tauri && cargo tauri android build --debug --target aarch64 --apk"
log " (exit the shell with ^D; container will be removed)"
ssh -t -A $SSH_OPTS "$REMOTE_HOST" "
set -euo pipefail
BASE=$BASE_DIR
# Make sure the source/cache is writable by uid 1000
sudo chown -R 1000:1000 \$BASE/data/source \$BASE/data/cache 2>/dev/null || true
docker run --rm -it \
--user 1000:1000 \
-v \$BASE/data/source:/build/source \
-v \$BASE/data/cache/cargo-registry:/home/builder/.cargo/registry \
-v \$BASE/data/cache/cargo-git:/home/builder/.cargo/git \
-v \$BASE/data/cache/target:/build/source/target \
-v \$BASE/data/cache/gradle:/home/builder/.gradle \
-v \$BASE/data/cache/android-home:/home/builder/.android \
-w /build/source/desktop/src-tauri \
wzp-android-builder \
bash
"
exit 0
fi
log() { echo -e "\033[1;36m>>> $*\033[0m"; }
ssh_cmd() { ssh -A $SSH_OPTS "$REMOTE_HOST" "$@"; }
@@ -166,30 +196,6 @@ docker run --rm \
wzp-android-builder \
bash -c '
set -euo pipefail
# ─── Linker wrappers ───────────────────────────────────────────────────────
# Tauri-cli hard-codes aarch64-linux-android24-clang as the Rust linker for
# the aarch64 target, which resolves pthread_create / dlsym / __init_tcb
# against the NDK API-24 *stub* libc.a/libdl.a. Those stubs are designed to
# crash if called — they only exist so the API-24 sysroot compiles, not so
# symbols actually work. API-26 has the real dynamic bindings to libc.so.
#
# Rather than fight tauri-cli to change which clang it invokes, we put a
# wrapper on $PATH that IS named android24-clang but exec()s the android26
# version. Same trick works for every ABI.
mkdir -p /tmp/wrappers
for abi in aarch64-linux-android armv7a-linux-androideabi i686-linux-android x86_64-linux-android; do
for suffix in clang clang++; do
cat > /tmp/wrappers/${abi}24-${suffix} <<WRAPPER
#!/bin/sh
exec /opt/android-sdk/ndk/26.1.10909125/toolchains/llvm/prebuilt/linux-x86_64/bin/${abi}26-${suffix} "\$@"
WRAPPER
chmod +x /tmp/wrappers/${abi}24-${suffix}
done
done
export PATH=/tmp/wrappers:$PATH
echo ">>> installed android24→android26 linker wrappers in /tmp/wrappers"
cd /build/source/desktop
echo ">>> npm install"