Skip to content

Instantly share code, notes, and snippets.

@sng2c
Created July 31, 2026 05:53
Show Gist options
  • Select an option

  • Save sng2c/4ff603e0777ca4d9cbb51e01fe08dbcb to your computer and use it in GitHub Desktop.

Select an option

Save sng2c/4ff603e0777ca4d9cbb51e01fe08dbcb to your computer and use it in GitHub Desktop.
Android APK build env on Termux (aarch64) — qemu-user + x86_64 glibc sysroot (no proot/debian). Reimplementation of the PRoot-Debian gist, Termux-native.
#!/data/data/com.termux/files/usr/bin/bash
# RUN IN: Termux (aarch64), no root, no proot/debian.
# PURPOSE: install qemu-user-x86_64 + JDK 21, and build an x86_64 glibc
# sysroot (loader + libc/libstdc++/zlib/libgcc) from Debian trixie
# amd64 .deb packages. The sysroot lets qemu-user run the x86_64
# Android SDK binaries (which are dynamically linked against glibc).
set -euo pipefail
# --- 1. Termux packages -----------------------------------------------------
pkg update -y
pkg install -y qemu-user-x86-64 openjdk-21 file unzip
# (curl, dpkg-deb are present in a stock Termux.)
# --- 2. x86_64 glibc sysroot from Debian trixie amd64 ------------------------
SYSROOT="${SYSROOT:-$PREFIX/var/lib/x86_64-glibc-sysroot}"
mkdir -p "$SYSROOT"
WORK="$(mktemp -d)"; cd "$WORK"
echo "[1/2] fetching Debian trixie main/amd64 package index..."
curl -sLo Packages.gz \
https://deb.debian.org/debian/dists/trixie/main/binary-amd64/Packages.gz
gunzip -f Packages.gz
echo "[2/2] extracting glibc runtime into $SYSROOT ..."
for p in libc6 libgcc-s1 libstdc++6 zlib1g; do
fn="$(grep -A200 "^Package: $p$" Packages | awk '/^Filename:/{print $2; exit}')"
curl -sLo "$p.deb" "https://deb.debian.org/debian/$fn"
dpkg-deb -x "$p.deb" "$SYSROOT"
done
# Debian trixie is usr-merged: libs live under /usr/lib(/lib64). Add the
# classic /lib, /lib64 symlinks so an interpreter path of
# /lib64/ld-linux-x86-64.so.2 resolves under qemu -L <sysroot>.
cd "$SYSROOT"
for l in lib lib64 bin sbin; do [ -e "$l" ] || ln -s "usr/$l" "$l"; done
# --- 3. smoke test: run the x86_64 loader under qemu -------------------------
QEMU="$PREFIX/bin/qemu-x86_64"
LD="$SYSROOT/lib64/ld-linux-x86-64.so.2"
"$QEMU" -L "$SYSROOT" "$LD" --version | head -1
echo
echo "JAVA_HOME=$PREFIX/lib/jvm/java-21-openjdk"
echo "SYSROOT=$SYSROOT"
echo "QEMU=$QEMU"
echo "Run 02-install-sdk.sh next."
#!/data/data/com.termux/files/usr/bin/bash
# RUN IN: Termux (after 01)
# PURPOSE: download Android cmdline-tools and install platform-tools,
# platforms;android-35 and build-tools;35.0.0.
# NOTE: sdkmanager is JVM-based and runs natively on aarch64. The packages it
# downloads contain x86_64 ELF binaries, which 03 wraps with qemu.
set -euo pipefail
export JAVA_HOME="$PREFIX/lib/jvm/java-21-openjdk"
export ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
CMDTOOLS_BUILD=11076708
URL="https://dl.google.com/android/repository/commandlinetools-linux-${CMDTOOLS_BUILD}_latest.zip"
mkdir -p "$ANDROID_HOME/cmdline-tools"
cd "$ANDROID_HOME/cmdline-tools"
if [ ! -d latest/bin ]; then
echo "[1/3] downloading cmdline-tools..."
curl -sLo c.zip "$URL"
unzip -q c.zip
rm -rf latest
mv cmdline-tools latest
rm c.zip
else
echo "[1/3] cmdline-tools already present"
fi
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
echo "[2/3] accepting licenses..."
yes | sdkmanager --licenses >/dev/null
echo "[3/3] installing platform-tools / android-35 / build-tools 35.0.0..."
sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
echo "--- installed ---"
sdkmanager --list_installed
echo "Run 03-apply-qemu-wrappers.sh next."
#!/data/data/com.termux/files/usr/bin/bash
# Termux adaptation of gist 03-apply-qemu-wrappers.sh
#
# Replace x86_64 ELF executables in the Android SDK with shell wrappers that
# exec qemu-x86_64 -L <x86_64-glibc-sysroot> on the original (.x86) binary.
#
# * The wrapper keeps the EXACT SAME filename/path/args -> standard usage.
# * Idempotent: re-running after `sdkmanager` updates only wraps newly
# restored x86_64 ELFs; already-wrapped ones (.x86 sibling present) skipped.
set -eu
PREFIX="${PREFIX:-/data/data/com.termux/files/usr}"
ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
QEMU="$PREFIX/bin/qemu-x86_64"
SYSROOT="$PREFIX/var/lib/x86_64-glibc-sysroot"
WRAP_SH="$PREFIX/bin/sh"
[ -x "$QEMU" ] || { echo "FAIL: $QEMU missing" >&2; exit 1; }
[ -d "$SYSROOT" ] || { echo "FAIL: $SYSROOT missing" >&2; exit 1; }
[ -d "$ANDROID_HOME" ] || { echo "FAIL: $ANDROID_HOME missing" >&2; exit 1; }
wrap_one() {
local f abs info is_x86_elf
f="$1"
[ -f "$f" ] || return 0
# Skip our own backup files (would cause recursive .x86.x86 wrapping)
case "$f" in *.x86) return 0 ;; esac
abs="$(readlink -f "$f")" || return 0
info="$(file "$abs" 2>/dev/null)" || info=""
is_x86_elf=0
if echo "$info" | grep -q "x86-64" \
&& echo "$info" | grep -q "executable" \
&& ! echo "$info" | grep -q "shared object"; then
is_x86_elf=1
fi
if [ "$is_x86_elf" = 1 ]; then
# Case A: unwrapped x86_64 ELF executable -> move original aside to .x86
mv "$abs" "${abs}.x86"
elif [ -f "${abs}.x86" ]; then
# Case B: already wrapped (this file is the wrapper) -> refresh below
:
else
# Not an x86_64 ELF and no .x86 sibling -> shell/jar launcher/data -> skip
return 0
fi
# (Re)write the canonical wrapper. Original ELF lives at "${abs}.x86";
# the wrapper keeps the same name/path so invocation is unchanged.
[ -f "${abs}.x86" ] || return 0
cat > "$abs" <<EOF
#!$WRAP_SH
# Auto-generated by .setup/apply-qemu-wrappers.sh — do not edit.
# Runs the x86_64 original (.x86) under qemu-user with the x86_64 glibc sysroot.
unset LD_PRELOAD
exec $QEMU -L $SYSROOT "${abs}.x86" "\$@"
EOF
chmod +x "$abs"
echo "wrapped: $abs"
}
count_before=$(find "$ANDROID_HOME" -name '*.x86' -type f 2>/dev/null | wc -l | tr -d ' ')
# build-tools + platform-tools (top-level + 1 level deep)
while IFS= read -r -d '' p; do
wrap_one "$p"
done < <(find "$ANDROID_HOME/build-tools" "$ANDROID_HOME/platform-tools" \
-maxdepth 2 -type f -executable -print0 2>/dev/null)
# AGP bundles its own aapt2 into ~/.gradle/caches/<v>/transforms/.../aapt2-*-linux/
# Wrap those too (fallback; the aapt2FromMavenOverride in gradle.properties is
# the primary mechanism to force AGP onto the SDK's qemu-safe aapt2).
GRADLE_CACHE="$HOME/.gradle/caches"
if [ -d "$GRADLE_CACHE" ]; then
while IFS= read -r -d '' p; do
wrap_one "$p"
done < <(find "$GRADLE_CACHE" -type f -executable -print0 2>/dev/null)
fi
count_after=$(find "$ANDROID_HOME" -name '*.x86' -type f 2>/dev/null | wc -l | tr -d ' ')
echo "--- summary ---"
echo "wrapped binaries: $count_before -> $count_after"
# Smoke test one wrapped binary (standard invocation: `aapt2 version`)
AAPT2="$ANDROID_HOME/build-tools/35.0.0/aapt2"
if [ -x "$AAPT2" ]; then
echo "--- smoke test: aapt2 version ---"
"$AAPT2" version 2>&1 | head -3
fi
#!/data/data/com.termux/files/usr/bin/bash
# Termux adaptation of gist 04-setup-env.sh
# Persist JAVA_HOME / ANDROID_HOME / PATH into the shell rc files (idempotent)
# and set android.aapt2FromMavenOverride so AGP uses the SDK's qemu-safe aapt2.
set -eu
PREFIX="${PREFIX:-/data/data/com.termux/files/usr}"
JAVA_HOME="$PREFIX/lib/jvm/java-21-openjdk"
ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
MARKER_O="# >>> android-sdk env >>>"
MARKER_C="# <<< android-sdk env <<<"
[ -x "$JAVA_HOME/bin/javac" ] || { echo "FAIL: JDK not at $JAVA_HOME" >&2; exit 1; }
[ -d "$ANDROID_HOME/cmdline-tools/latest/bin" ] || { echo "FAIL: SDK not at $ANDROID_HOME" >&2; exit 1; }
BTV=$(ls -1 "$ANDROID_HOME/build-tools" 2>/dev/null | grep -E '^[0-9]' | sort -V | tail -1 || true)
# No symlinks: sdkmanager warns about any non-canonical build-tools location
# (it scans the whole ANDROID_HOME tree and finds source.properties through a
# symlink). Use the real versioned dir on PATH/override instead. Re-run this
# script after upgrading build-tools to refresh the version.
rm -f "$ANDROID_HOME/build-tools/latest" "$ANDROID_HOME/build-tools-latest"
if [ -n "$BTV" ]; then
BT="$ANDROID_HOME/build-tools/$BTV"
else
BT=""
fi
write_block() {
local file="$1"
touch "$file"
# Remove any previous block (idempotent refresh), then append the new one.
sed -i -e '/# >>> android-sdk env >>>/,/# <<< android-sdk env <<</d' "$file" 2>/dev/null || true
{
echo ""
echo "$MARKER_O"
echo "export JAVA_HOME=$JAVA_HOME"
echo "export ANDROID_HOME=$ANDROID_HOME"
echo "export PATH="$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$BT:\$PATH""
echo "$MARKER_C"
} >> "$file"
echo "env block written to $file"
}
# Cover both login shells (~/.profile) and interactive shells (~/.bashrc).
write_block "$HOME/.bashrc"
write_block "$HOME/.profile"
# gradle.properties: force AGP onto the SDK's (qemu-wrapped) aapt2.
mkdir -p "$HOME/.gradle"
GP="$HOME/.gradle/gradle.properties"
KEY="android.aapt2FromMavenOverride"
VAL="$BT/aapt2"
if [ -z "$BT" ]; then
echo "WARN: no build-tools found; skipping $KEY" >&2
elif [ -f "$GP" ] && grep -q "^$KEY=" "$GP"; then
sed -i -e "s|^$KEY=.*|$KEY=$VAL|" "$GP"
echo "updated $KEY in $GP"
else
echo "$KEY=$VAL" >> "$GP"
echo "added $KEY to $GP"
fi
echo
echo "Done. Open a new Termux session (or run: source ~/.bashrc) to activate."
echo " JAVA_HOME=$JAVA_HOME"
echo " ANDROID_HOME=$ANDROID_HOME"

Android APK build env on Termux (aarch64) — qemu-user + x86_64 glibc sysroot

Build Android APKs on a plain Termux (Android aarch64) install — no root, no PRoot, no Debian — by running the SDK's x86_64 build-tools under qemu-user-x86-64.

This is a Termux-native reimplementation of the PRoot-Debian setup in sng2c/067c4022…. The key realisation: Termux already ships qemu-user-x86-64 (and openjdk-21), so the only thing Debian was implicitly providing — an x86_64 glibc runtime for the dynamically-linked SDK binaries — can be supplied directly from a few Debian amd64 .deb packages. No container needed.

Why this is needed

Google only ships Android build-tools (aapt2, adb, zipalign, apksigner, d8 launcher, aidl, …) as x86_64 ELF linked against glibc. Termux runs on aarch64 with bionic, so those binaries can't run natively. We run them under qemu-user-x86-64 -L <x86_64-glibc-sysroot>.

The wrapper trick (usage stays standard)

Each x86_64 ELF executable is moved aside to <name>.x86 and replaced by a shell wrapper that keeps the exact same name/path:

#!/data/data/com.termux/files/usr/bin/sh
unset LD_PRELOAD                                  # drop Termux's aarch64 preload shim
exec /…/qemu-x86_64 -L /…/x86_64-glibc-sysroot "/…/<name>.x86" "$@"

Because the wrapper keeps the filename and forwards "$@", stdio and exit code verbatim, every tool is invoked exactly the standard way — aapt2 version, adb devices, zipalign -c -v 4 app.apk, ./gradlew assembleDebug (AGP execs the SDK aapt2 and hits the wrapper transparently). The wrapping is invisible; it does not change command names or usage — that is the whole point.

unset LD_PRELOAD avoids a noisy/unsupported attempt by the x86_64 loader to preload Termux's aarch64 libtermux-exec-ld-preload.so.

Run (fresh Termux, in order)

./01-install-qemu-jdk-sysroot.sh   # qemu-user-x86_64 + JDK 21 + x86_64 glibc sysroot
./02-install-sdk.sh                 # cmdline-tools + platform-tools + android-35 + build-tools 35.0.0
./03-apply-qemu-wrappers.sh         # wrap x86_64 ELF with qemu (idempotent)
./04-setup-env.sh                   # JAVA_HOME/ANDROID_HOME/PATH -> ~/.bashrc,~/.profile + gradle aapt2 override
source ~/.bashrc

Layout

Role Path
Android SDK (ANDROID_HOME) ~/android-sdk
x86_64 glibc sysroot $PREFIX/var/lib/x86_64-glibc-sysroot (libc6, libgcc-s1, libstdc++6, zlib1g)
qemu-user (x86_64) $PREFIX/bin/qemu-x86_64
JDK 21 (JAVA_HOME) $PREFIX/lib/jvm/java-21-openjdk
env persistence ~/.bashrc, ~/.profile
AGP aapt2 override ~/.gradle/gradle.properties

Standard usage (after source ~/.bashrc)

sdkmanager --list_installed
adb version
aapt2 version
zipalign -c -v 4 app-debug.apk
apksigner --version
./gradlew assembleDebug

Verified end-to-end on aarch64: aapt2 compile → aapt2 link → out.apk → zipalign -c → aapt2 dump strings all run under qemu with the sysroot.

Maintenance (scripts are idempotent)

After installing/restoring SDK packages:

sdkmanager "build-tools;36.0.0" …
bash ~/android-sdk/.setup/apply-qemu-wrappers.sh   # wraps new x86_64 ELF, skips already-wrapped

After changing the build-tools version, also re-run setup-env.sh (it auto-detects the highest installed build-tools and refreshes PATH + the gradle aapt2 override):

bash ~/android-sdk/.setup/apply-qemu-wrappers.sh
bash ~/android-sdk/.setup/setup-env.sh

If a binary needs an extra glibc library, add it to the sysroot:

dpkg-deb -x <pkg>_amd64.deb "$PREFIX/var/lib/x86_64-glibc-sysroot"

Notes / caveats

  • qemu user-mode emulation is per-process; builds are slower than on a real x86_64 machine (first assembleDebug can take several minutes). Gradle's daemon cache speeds up subsequent builds.
  • d8/r8/apksigner are JVM launchers and run natively (not wrapped); only the native x86_64 ELF tools are wrapped.
  • The android.aapt2FromMavenOverride gradle property forces AGP onto the SDK's (qemu-wrapped) aapt2 instead of its own bundled one.

Credits

Adapted from the PRoot-Debian gist by @sng2c — moved to a Termux-native qemu-user + self-built x86_64 glibc sysroot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment