6 min de lectura

Desarrollar apps de Android en Asahi Linux

Cómo hacer funcionar el aapt2 del Android Gradle Plugin en Asahi Linux aarch64 a través de QEMU, con un adb funcional, para poder desarrollar apps de Android en Apple Silicon.

AndroidAsahi LinuxQEMUaarch64

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.erofs que contiene lib64/ld-linux-x86-64.so.2 y la glibc contra la que enlaza aapt2. (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:

  1. Comprueba que qemu-x86_64 está disponible.
  2. Monta el rootfs de FEX con FUSE (erofsfuse) en /tmp/aapt2-x86root como sysroot x86-64 y verifica que contiene lib64/ld-linux-x86-64.so.2.
  3. Busca en ~/.gradle/caches el aapt2 x86-64 más reciente que AGP ya haya descargado.
  4. Escribe un pequeño wrapper en ~/.local/share/aapt2-qemu/aapt2 que ejecuta qemu-x86_64 -L <sysroot> <aapt2> "$@".
  5. Establece android.aapt2FromMavenOverride en ~/.gradle/gradle.properties para 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, aapt2 imprime 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 aapt2 ha 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.