Asahi Linux runs beautifully on Apple Silicon until you try to build an Android app. The Java, the Gradle daemon, the Kotlin compiler, all of that works. Then the build fails on a single binary called aapt2 with an error that looks like the file is corrupt, when really it’s just the wrong architecture.
This is the setup I use to develop Android apps on an M-series Mac running Asahi Linux. It wraps Google’s x86-64 aapt2 with QEMU user-mode emulation, and swaps the SDK’s adb for a native ARM64 build. By the end you can run ./gradlew assembleDebug and adb install like on any other machine.
Why aapt2 breaks on ARM64
aapt2 (Android Asset Packaging Tool 2) is the tool the Android Gradle Plugin (AGP) uses to compile and link your resources: the XML layouts, drawables, strings, everything in res/. It generates R.java and the compiled resource table that goes into the APK. No aapt2, no APK.
Google distributes aapt2 only as an x86-64 ELF binary. It comes down from Maven as com.android.tools.build:aapt2 and also ships in the SDK build-tools. On an aarch64 machine that binary won’t execute directly.
AGP tries to be clever about this. If your system has binfmt_misc configured to dispatch x86-64 binaries to QEMU automatically, AGP will sometimes just work. In practice that path is fragile: the dispatcher may not be set up, the version resolution can be inconsistent, and on a fresh Asahi install the build just hangs or fails with a confusing message about a “broken” executable. Rather than fight binfmt_misc, I run QEMU explicitly through a small wrapper. It’s deterministic, it’s fast (~0.07s per call), and it survives AGP upgrades.
What you need
You’re on Asahi Linux, which is Fedora-based, so the package names below are for dnf. I’ve added the Debian/Ubuntu and Arch equivalents where they differ.
- qemu-user (provides
qemu-x86_64) for the emulation layer. - erofs-utils (provides
erofsfuse) to mount the x86-64 root filesystem. - FEX-Emu’s rootfs, which is a ready-made x86-64 sysroot. Install FEX and its rootfs, and you get a file at
/usr/share/fex-emu/RootFS/default.erofscontaininglib64/ld-linux-x86-64.so.2and the glibc thataapt2links against. (Any x86-64 rootfs with that dynamic linker works if you don’t want FEX; see the last section.)
Step 1: install the dependencies
sudo dnf install -y qemu-user erofs-utils
Debian/Ubuntu: sudo apt install qemu-user-static erofs-utils
Arch: sudo pacman -S qemu-user-static erofs-utils
Then install FEX and pull its rootfs through FEX’s own setup tool, so the default.erofs image exists at the path above. Confirm QEMU is on your path:
qemu-x86_64 --version
Step 2: get the wrapper setup script
I keep this in a tiny repo at github.com/IgnacioLD/aapt2-qemu. Download it and, as always with a script you found online, read it before you run it:
curl -sLO https://raw.githubusercontent.com/IgnacioLD/aapt2-qemu/main/aapt2-qemu-setup.sh
less aapt2-qemu-setup.sh
There’s no magic in it. In order, it:
- Verifies
qemu-x86_64is available. - FUSE-mounts the FEX rootfs (
erofsfuse) at/tmp/aapt2-x86rootas the x86-64 sysroot, and checks it containslib64/ld-linux-x86-64.so.2. - Searches
~/.gradle/cachesfor the newest x86-64aapt2AGP already downloaded. - Writes a small wrapper at
~/.local/share/aapt2-qemu/aapt2that runsqemu-x86_64 -L <sysroot> <aapt2> "$@". - Sets
android.aapt2FromMavenOverridein~/.gradle/gradle.propertiesso AGP uses the wrapper.
Step 3: run it
sh aapt2-qemu-setup.sh
If it complains that no aapt2 was found in the Gradle cache, run the build once first so AGP downloads it:
./gradlew assembleDebug # this will fail when it tries to execute aapt2, that's fine
sh aapt2-qemu-setup.sh # now it finds the binary and installs the wrapper
The FUSE mount is ephemeral, so you need to run the script once per session (or after a reboot). The Gradle property it writes is permanent.
Step 4: build
./gradlew assembleDebug
That should now compile end to end. Resources get processed through the QEMU-wrapped aapt2 and land in your APK.
adb: don’t use the SDK one
This trips people up. The adb that ships with the Android SDK at $ANDROID_HOME/platform-tools/adb is also x86-64. On Asahi it won’t run natively. Android Studio will happily tell you adb is broken for reasons you can’t quite trace.
Install the native ARM64 adb from your distro instead:
sudo dnf install android-tools
Debian/Ubuntu: sudo apt install adb
Arch: sudo pacman -S android-tools
This gives you /usr/bin/adb, a real native aarch64 build. Make sure it comes first on your PATH, ahead of the SDK copy, so both the command line and Android Studio pick it up. You can check which one resolves:
which adb # should be /usr/bin/adb
file $(which adb) # should say aarch64, ARM, not x86-64
Then connect a device over USB or set up wireless debugging, and verify:
adb devices
If your phone shows up there, you have the full loop: build with ./gradlew, deploy with adb install, read logs with adb logcat. All native, none of it relayed through an x86-64 emulator.
Why the wrapper has a weird stderr rule
This bit is worth knowing if you ever debug it. AGP talks to aapt2 as a long-lived daemon. There are two behaviours the wrapper has to get exactly right, or AGP silently gives up:
- For
aapt2 version,aapt2prints its version banner to stderr, but AGP reads the version from stdout. So the wrapper merges stderr into stdout for that one subcommand. - For everything else, the wrapper leaves stderr alone. AGP holds the daemon’s stderr pipe open and treats EOF on it as a crash. If you redirect stderr, AGP concludes
aapt2died.
Also, the wrapper’s filename has to end in aapt2. AGP validates the override path with path.endsWith("aapt2") and rejects anything else. Trivial, but it cost me an afternoon.
Customising paths
The script reads three optional environment variables, all with sane defaults:
| Variable | Default | Purpose |
|---|---|---|
AAPT2_EROFS |
/usr/share/fex-emu/RootFS/default.erofs |
The FEX rootfs image |
AAPT2_SYSROOT |
/tmp/aapt2-x86root |
Where to mount it |
AAPT2_WRAPPER_DIR |
$HOME/.local/share/aapt2-qemu |
Where the wrapper lives |
AAPT2_SYSROOT=/custom/mount ./aapt2-qemu-setup.sh
Without FEX
You don’t actually need FEX. The script only needs an x86-64 sysroot containing lib64/ld-linux-x86-64.so.2 and the libraries aapt2 links against. If you point AAPT2_EROFS at a plain directory instead of an erofs image, the script skips the FUSE mount and uses the directory directly:
AAPT2_EROFS=/path/to/some-x86-rootfs ./aapt2-qemu-setup.sh
Any Debian/Ubuntu x86-64 rootfs, a Docker image extracted to a folder, or a manual extraction will do.
Wrapping up
That’s the whole setup: QEMU wraps aapt2, the distro android-tools gives you a native adb, and Asahi stops being an awkward second-class platform for Android development. The emulation overhead on aapt2 is negligible because resource processing is a tiny fraction of a build, and everything else, Gradle, the JDK, the toolchain, runs natively.
If you hit a snag, the repo has the full script and README. Happy hacking on ARM.