Asahi Linux funciona genial en Apple Silicon hasta que intentas compilar una app de Android. El Java, el demonio de Gradle, el compilador de Kotlin, todo eso funciona. Entonces la compilación falla en un único binario llamado aapt2 con un error que parece indicar que el archivo está corrupto, cuando en realidad solo es la arquitectura equivocada.
Esta es la configuración que uso para desarrollar apps de Android en un Mac de la serie M con Asahi Linux. Envuelve el aapt2 x86-64 de Google con emulación en modo usuario de QEMU y sustituye el adb del SDK por una compilación nativa ARM64. Al terminar puedes ejecutar ./gradlew assembleDebug y adb install como en cualquier otra máquina.
Por qué aapt2 falla en ARM64
aapt2 (Android Asset Packaging Tool 2) es la herramienta que el Android Gradle Plugin (AGP) usa para compilar y enlazar tus recursos: los layouts XML, los drawables, los strings, todo lo de res/. Genera el R.java y la tabla de recursos compilada que acaba en el APK. Sin aapt2, no hay APK.
Google distribuye aapt2 únicamente como binario ELF x86-64. Llega desde Maven como com.android.tools.build:aapt2 y también viene en los build-tools del SDK. En una máquina aarch64 ese binario no se puede ejecutar directamente.
AGP intenta ser listo con esto. Si tu sistema tiene binfmt_misc configurado para despachar binarios x86-64 a QEMU automáticamente, a veces AGP simplemente funciona. En la práctica ese camino es frágil: puede que el dispatcher no esté configurado, la resolución de versiones puede ser inconsistente, y en una instalación limpia de Asahi la compilación se cuelga o falla con un mensaje confuso sobre un ejecutable “roto”. En vez de pelearme con binfmt_misc, ejecuto QEMU de forma explícita mediante un pequeño wrapper. Es determinista, es rápido (~0.07s por llamada) y sobrevive a las actualizaciones de AGP.
Qué necesitas
Estás en Asahi Linux, que está basado en Fedora, así que los nombres de paquetes de abajo son para dnf. He añadido los equivalentes de Debian/Ubuntu y Arch donde difieren.
- qemu-user (proporciona
qemu-x86_64) para la capa de emulación. - erofs-utils (proporciona
erofsfuse) para montar el sistema de archivos raíz x86-64. - El rootfs de FEX-Emu, que es un sysroot x86-64 ya hecho. Instala FEX y su rootfs con la herramienta de configuración de FEX, y obtendrás un archivo en
/usr/share/fex-emu/RootFS/default.erofsque contienelib64/ld-linux-x86-64.so.2y la glibc contra la que enlazaaapt2. (Cualquier rootfs x86-64 con ese enlazador dinámico sirve si no quieres FEX; mira la última sección.)
Paso 1: instala las dependencias
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
Luego instala FEX y descarga su rootfs con la herramienta propia de FEX, para que la imagen default.erofs exista en la ruta de arriba. Confirma que QEMU está en tu path:
qemu-x86_64 --version
Paso 2: obtén el script de instalación del wrapper
Tengo esto en un repo diminuto en github.com/IgnacioLD/aapt2-qemu. Descárgalo y, como siempre con un script que encontraste en internet, léelo antes de ejecutarlo:
curl -sLO https://raw.githubusercontent.com/IgnacioLD/aapt2-qemu/main/aapt2-qemu-setup.sh
less aapt2-qemu-setup.sh
No tiene nada mágico. En orden, hace lo siguiente:
- Comprueba que
qemu-x86_64está disponible. - Monta el rootfs de FEX con FUSE (
erofsfuse) en/tmp/aapt2-x86rootcomo sysroot x86-64 y verifica que contienelib64/ld-linux-x86-64.so.2. - Busca en
~/.gradle/cacheselaapt2x86-64 más reciente que AGP ya haya descargado. - Escribe un pequeño wrapper en
~/.local/share/aapt2-qemu/aapt2que ejecutaqemu-x86_64 -L <sysroot> <aapt2> "$@". - Establece
android.aapt2FromMavenOverrideen~/.gradle/gradle.propertiespara que AGP use el wrapper.
Paso 3: ejecútalo
sh aapt2-qemu-setup.sh
Si se queja de que no encontró ningún aapt2 en la caché de Gradle, ejecuta primero la compilación para que AGP lo descargue:
./gradlew assembleDebug # fallará al intentar ejecutar aapt2, no pasa nada
sh aapt2-qemu-setup.sh # ahora encuentra el binario e instala el wrapper
El montaje FUSE es efímero, así que tienes que ejecutar el script una vez por sesión (o tras un reinicio). La propiedad de Gradle que escribe sí es permanente.
Paso 4: compila
./gradlew assembleDebug
Esto ya debería compilar de principio a fin. Los recursos se procesan a través del aapt2 envuelto en QEMU y acaban en tu APK.
adb: no uses el del SDK
Esto hace tropezar a la gente. El adb que viene con el SDK de Android en $ANDROID_HOME/platform-tools/adb también es x86-64. En Asahi no se ejecuta de forma nativa. Android Studio te dirá felizmente que adb está roto por motivos que no acabas de rastrear.
Instala el adb nativo ARM64 desde tu distro en su lugar:
sudo dnf install android-tools
Debian/Ubuntu: sudo apt install adb
Arch: sudo pacman -S android-tools
Esto te da /usr/bin/adb, una compilación aarch64 de verdad. Asegúrate de que aparece primero en tu PATH, por delante de la copia del SDK, para que tanto la línea de comandos como Android Studio lo cojan. Puedes comprobar cuál se resuelve:
which adb # debería ser /usr/bin/adb
file $(which adb) # debería decir aarch64, ARM, no x86-64
Luego conecta un dispositivo por USB o configura la depuración inalámbrica y verifica:
adb devices
Si tu teléfono aparece ahí, tienes el ciclo completo: compilar con ./gradlew, desplegar con adb install, leer logs con adb logcat. Todo nativo, nada retransmitido por un emulador x86-64.
Por qué el wrapper tiene una regla rara con stderr
Esto conviene saberlo si alguna vez lo depuras. AGP se comunica con aapt2 como un demonio de larga duración. Hay dos comportamientos que el wrapper tiene que acertar exactamente, o AGP se rinde en silencio:
- Para
aapt2 version,aapt2imprime su banner de versión a stderr, pero AGP lee la versión desde stdout. Así que el wrapper fusiona stderr en stdout solo para ese subcomando. - Para todo lo demás, el wrapper deja stderr tal cual. AGP mantiene abierto el pipe de stderr del demonio y trata un EOF en él como un cuelgue. Si rediriges stderr, AGP concluye que
aapt2ha muerto.
Además, el nombre del wrapper tiene que terminar en aapt2. AGP valida la ruta del override con path.endsWith("aapt2") y rechaza cualquier otra cosa. Trivial, pero me costó una tarde.
Personalizar las rutas
El script lee tres variables de entorno opcionales, todas con valores por defecto razonables:
| Variable | Por defecto | Para qué sirve |
|---|---|---|
AAPT2_EROFS |
/usr/share/fex-emu/RootFS/default.erofs |
La imagen rootfs de FEX |
AAPT2_SYSROOT |
/tmp/aapt2-x86root |
Dónde montarla |
AAPT2_WRAPPER_DIR |
$HOME/.local/share/aapt2-qemu |
Dónde vive el wrapper |
AAPT2_SYSROOT=/custom/mount ./aapt2-qemu-setup.sh
Sin FEX
En realidad no necesitas FEX. El script solo necesita un sysroot x86-64 que contenga lib64/ld-linux-x86-64.so.2 y las librerías con las que enlaza aapt2. Si apuntas AAPT2_EROFS a un directorio normal en vez de a una imagen erofs, el script se salta el montaje FUSE y usa el directorio directamente:
AAPT2_EROFS=/ruta/a/un-rootfs-x86 ./aapt2-qemu-setup.sh
Cualquier rootfs x86-64 de Debian/Ubuntu, una imagen de Docker extraída a una carpeta, o una extracción manual, sirven.
En resumen
Eso es toda la configuración: QEMU envuelve aapt2, los android-tools de la distro te dan un adb nativo, y Asahi deja de ser una plataforma de segunda para el desarrollo en Android. La sobrecarga de emulación sobre aapt2 es despreciable porque el procesamiento de recursos es una fracción minúscula de la compilación, y todo lo demás, Gradle, el JDK, la toolchain, se ejecuta de forma nativa.
Si te atasca algo, el repo tiene el script completo y el README. Feliz hacking en ARM.