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.
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>.
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.
./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| 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 |
sdkmanager --list_installed
adb version
aapt2 version
zipalign -c -v 4 app-debug.apk
apksigner --version
./gradlew assembleDebugVerified end-to-end on aarch64: aapt2 compile → aapt2 link → out.apk → zipalign -c → aapt2 dump strings all run under qemu with the sysroot.
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-wrappedAfter 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.shIf 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"- qemu user-mode emulation is per-process; builds are slower than on a real
x86_64 machine (first
assembleDebugcan take several minutes). Gradle's daemon cache speeds up subsequent builds. d8/r8/apksignerare JVM launchers and run natively (not wrapped); only the native x86_64 ELF tools are wrapped.- The
android.aapt2FromMavenOverridegradle property forces AGP onto the SDK's (qemu-wrapped) aapt2 instead of its own bundled one.
Adapted from the PRoot-Debian gist by @sng2c — moved to a Termux-native qemu-user + self-built x86_64 glibc sysroot.