DEV Community

Cover image for Termux proot to avf flutter build
Koji Ishida
Koji Ishida

Posted on

Termux proot to avf flutter build

Termux+proot Claude Code was too slow

So I moved to Android's built-in Linux VM (AVF) and somehow got a Flutter APK to build

TL;DR

  • Claude Code / Flutter builds inside Termux + proot-distro were painfully slow
  • Moved to Android's Virtualization Framework (AVF) "Linux Development Environment" instead — real Linux kernel, no more syscall translation overhead
  • The migration turned into a chain of "x86_64 binary doesn't run on ARM64 host" problems
  • Eventually got flutter build apk --debug working on AVF.

1. Claude Code on proot is sluggish

My setup was Termux + proot-distro (Ubuntu) running Claude Code, and it was noticeably heavy. Turns out this isn't a config issue, it's structural: proot runs on top of Android's actual kernel and intercepts every single syscall via ptrace to fake a "real Ubuntu" environment. That interception itself burns CPU, and it's not even 100% compatible — some syscalls behave differently or just don't work.

2. Why AVF

Anthropic's official native installer ships a glibc binary, which is fundamentally incompatible with Termux's Bionic libc at the ABI level, so it won't even run there without proot. AVF (Android's Virtualization Framework) sidesteps this entirely: it's a built-in pKVM-based Linux VM feature. Unlike proot (pseudo-virtualization via syscall translation), it's a genuine, isolated Linux kernel with no translation overhead.

proot AVF
What it actually is Android kernel + syscall translation layer Real Linux kernel (separate VM)
Speed Translation overhead Native-ish
Official installer Breaks (glibc mismatch) Just works
Effect on existing setup Fully isolated, coexists fine with Termux/proot

3. Setting up AVF

  1. Settings → About phone → tap the build number 7 times (enables Developer Options)
  2. Settings → System → Developer options → toggle Linux Development Environment
  3. A "Terminal" app shows up on your home screen — launch it, it'll pull down a Debian image

This is a standard Android feature, so if you don't like it, just flip the toggle off, or reset the VM from the Terminal app's settings. It's fully isolated from your existing Termux/proot setup, so nothing there gets touched.

4. Installing Claude Code

Once the VM was up, the official installer went through clean:

curl -fsSL https://claude.ai/install.sh | bash
export PATH="$HOME/.local/bin:$PATH"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
claude --version
Enter fullscreen mode Exit fullscreen mode

5. Getting Flutter Web running

Cloned the Flutter SDK and spun up a plain sample app to test with:

flutter create sample_app
cd sample_app
flutter config --enable-web
flutter pub get
flutter run -d web-server --web-port=8080 --web-hostname=0.0.0.0
Enter fullscreen mode Exit fullscreen mode

AVF runs in its own isolated network namespace, so opening 0.0.0.0:8080 inside the VM doesn't automatically become reachable from the Android browser. Had to open Settings → Port control in the Terminal app to allow the port (starting the server first, then opening Port control, usually gets it detected).

Once that was sorted, http://localhost:8080 loaded the default Flutter counter app fine — Web verification done.

6. Architecture mismatches building the APK

AVF runs on ARM64 (aarch64), but a bunch of tools in the Android SDK/Gradle ecosystem only ship official x86_64 binaries. That mismatch bit me three separate times, and each one had to be tracked down and fixed before the next one even showed up.

(Quick note on reproducing this: the aapt2 issue in 6-1 shows up on basically any Flutter Android build, native code or not. The cmake/NDK issues in 6-2 and 6-3 only kick in once a native build is involved — mine came from the jni pub.dev package for FFI. flutter create --template=plugin_ffi my_native_plugin gives you a minimal repro with the same CMakeLists.txt build path.)

6-1. aapt2 turned out to be an x86_64 binary

AAPT2 aapt2-9.0.1-14304508-linux Daemon #0: Unexpected error output: 
.../aapt2: 1: Syntax error: ")" unexpected
Enter fullscreen mode Exit fullscreen mode

Checked with file: host is aarch64, but the aapt2 pulled from Maven is x86-64.

$ uname -m
aarch64
$ file .../aapt2
...: ELF 64-bit LSB pie executable, x86-64, ...
Enter fullscreen mode Exit fullscreen mode

AGP 9.x fetches aapt2 from Maven directly instead of the SDK, and that Maven artifact just isn't built for ARM64 Linux.

Found a project called android-arm-build-tools (commit451.com) that cross-compiles aapt2/aidl/zipalign/split-select for ARM64 by cloning ~40 AOSP repos. Installed that, then pointed gradle.properties at it:

android.aapt2FromMavenOverride=/path/to/arm64/aapt2
Enter fullscreen mode Exit fullscreen mode

6-2. cmake was also x86_64

Same story with the SDK-bundled cmake (3.22.1) when the native build (the jni package) kicked in. Fixed by swapping in Debian's apt cmake, which is ARM64-native:

sudo apt install -y cmake ninja-build
Enter fullscreen mode Exit fullscreen mode

This override has to go in local.properties, not gradle.properties — cost me a wasted build cycle figuring that out:

cmake.dir=/usr
Enter fullscreen mode Exit fullscreen mode

6-3. The NDK's clang was x86_64 too

Even with cmake fixed, the NDK's bundled clang compiler itself was linux-x86_64 and just wouldn't run:

.../toolchains/llvm/prebuilt/linux-x86_64/bin/clang: Exec format error
Enter fullscreen mode Exit fullscreen mode

Google doesn't officially ship an ARM64-native Linux NDK, so binary-swapping wasn't an option here. Switched tack and emulated the x86_64 binary with qemu-user-static instead:

sudo apt install -y qemu-user-static binfmt-support
Enter fullscreen mode Exit fullscreen mode

Still got an error after that:

x86_64-binfmt-P: Could not open '/lib64/ld-linux-x86-64.so.2': No such file or directory
Enter fullscreen mode Exit fullscreen mode

qemu itself was working, but the x86_64 binary it was trying to run needed x86_64 versions of shared libraries (glibc etc.) that simply didn't exist on ARM64. Fixed by pulling in x86_64 libs via Debian's multiarch support:

sudo dpkg --add-architecture amd64
sudo apt update
sudo apt install -y libc6:amd64 libstdc++6:amd64 zlib1g:amd64
Enter fullscreen mode Exit fullscreen mode

6-4. Build succeeds (finally)

$ flutter build apk --debug
Running Gradle task 'assembleDebug'...    111.8s
✓ Built build/app/outputs/flutter-apk/app-debug.apk
Enter fullscreen mode Exit fullscreen mode

7. Summary

Symptom Cause Fix
aapt2 won't start Maven's aapt2 is x86_64-only Swap in an ARM64-native build
cmake won't start SDK's bundled cmake is x86_64-only Swap in apt's ARM64-native cmake, point cmake.dir in local.properties
NDK clang won't run No official ARM64-native NDK from Google qemu-user-static + multiarch (amd64 libs) to emulate it

A lot of the AGP/NDK toolchain still treats ARM64 Linux as a second-class citizen. This whole exercise was basically peeling back one x86_64 assumption at a time until the toolchain would actually run on an ARM64 host.

If you only need Flutter Web verification, moving to AVF is a clear win — noticeably faster, not much setup pain. APK builds are a different story: they need all the workarounds above, so it's more effort than it sounds going in. For now I'm using AVF for Web checks and keeping proot (or this patched-up AVF setup) around for APK builds.


Appendix

Stuff that came up during this whole process but isn't really part of the "make the toolchain work on ARM64" story — filing it here in case you hit the same thing.

Terminal crashing on basic commands right after initial setup

Right after my first setup, curl -fsSL https://claude.ai/install.sh | bash produced nothing. ping worked fine, but curl itself crashed with Illegal instruction. Swapped to wgetSegmentation fault. Reinstalled curl — same crash.

I don't have solid evidence this is a known/common AVF issue rather than a one-off corrupted download on my end, but if you hit something similar (basic commands crashing for no obvious reason, right after a fresh install), a VM reset fixed it immediately for me: Terminal app → Settings → Recovery → "Reset to initial version." Since I hadn't installed anything yet, there was nothing to lose.

Random disconnects every ~3 minutes, even with the screen on

Not a screen-off/backgrounding issue — the Terminal app kept flipping to "Reconnect" mid-session regardless. Turned out to be memory pressure: the default VM memory allocation is 1024MB, nowhere near enough for a Gradle build (JVM + native compilation). Bumping it up to ~3GB in Terminal app → Settings → Advanced → Memory size fixed it. Couldn't push it all the way to 4GB — that ceiling depends on how much free RAM the phone itself has left at the time.

Top comments (1)

Collapse
 
gnomeman4201 profile image
GnomeMan4201

Really good write up. I went down a related path building LLVM 13 and manually linking AArch64 binaries inside Termux + PRoot, so your move to AVF immediately caught my attention.

What I especially liked is that you didn’t stop at “the toolchain doesn’t work on ARM64.” You traced the failure through aapt2, CMake, and the NDK, then chose a native ARM64 replacement where one existed and QEMU/multiarch only where it didn’t. Separating the general Flutter failure from the FFI specific CMake and NDK problems was also a useful detail.

Did you capture even a rough build time comparison using the same project under PRoot and AVF? I’d be curious how much of AVF’s advantage remains once qemu-user static enters the native build path.