Python y Apk (nuevo)

Tienes un python en Python que funciona en terminal y quieres verlo como un icono más en el móvil. 

La pregunta parece simple, pero la respuesta tiene trampa: no todos los scripts se pueden empaquetar, y la herramienta que lo hace no es la que imaginas.

Partimos de un caso real: un clon en pygame de Night Pilot 64, aquel type-in de William Fong publicado en Commodore Horizons en agosto de 1984. Un único archivo .py, sin más dependencia que pygame. El objetivo: un .apk instalable.

Lo primero: no todo .py vale

Antes de perder una tarde conviene saber si tu script es empaquetable. Lo que se traslada a Android es el intérprete de Python más las bibliotecas que uses, compiladas para ARM. Y ahí no está todo:

Si tu script usa…¿Se puede hacer APK?
KivySí, es el caso para el que se diseñó toda esta cadena de herramientas.
pygameSí, con la receta oficial sobre SDL2. Es el caso de este artículo.
TkinterNo. Tk no existe en Android. Habría que reescribir la interfaz.
PyQt / PySideEn la práctica, no por esta vía.
Solo consola, sin interfazTécnicamente sí, pero no verás nada: Android no te da una terminal.

Las tres vías, de menos a más esfuerzo

VíaQué consiguesCoste
Pydroid 3Jugar en el móvil hoy mismo. No hay APK: ejecutas el .py dentro de la app.Cinco minutos.
Buildozer en LinuxEl APK de verdad, con su icono.De 20 a 40 minutos la primera compilación.
GitHub ActionsEl mismo APK, compilado en la nube.Subir cuatro archivos.

Recomiendo empezar por Pydroid 3 aunque quieras el APK: instalas pygame desde su menú Pip, abres el archivo y pulsas ▶. Si ahí no funciona, tampoco funcionará empaquetado, y habrás descubierto los problemas de interfaz en cinco minutos en lugar de en cuarenta.

Qué hace realmente Buildozer (y qué no hace Android Studio)

Este es el malentendido más común. Android Studio no compila un proyecto Python. No abras la carpeta desde su interfaz esperando un botón de Build: no es un proyecto Gradle.

Quien hace el trabajo es Buildozer, que a su vez maneja a python-for-android (abreviado p4a). La cadena, por dentro:

  1. p4a descarga el SDK y el NDK de Android y compila CPython para ARM.
  2. Compila cada dependencia siguiendo una receta: un archivo Python que sabe cómo cruzar esa biblioteca a Android. Existen recetas para numpy, Pillow, SDL2, pygame…
  3. Envuelve todo en un bootstrap (para pygame, el de SDL2), que es la parte Java que arranca el intérprete y le da una ventana.
  4. Genera un proyecto Gradle por dentro y lo compila. El resultado es el APK.

Android Studio sí sirve de despensa: su SDK Manager te instala el NDK y la plataforma que Buildozer necesita, y su Java integrado te saca de más de un apuro con las versiones de JDK.

Paso 1: adaptar el script

Un juego de escritorio no se convierte en app móvil solo por empaquetarlo. Hay cuatro cosas que tocar, y ninguna es opcional:

El archivo se debe llamar main.py

python-for-android busca ese nombre como punto de entrada. Renombrar y listo.

No hay teclado

Este es el cambio de verdad. Si tu juego se controla con teclas, en el móvil no se controla con nada. La solución: detectar Android y dibujar una botonera táctil. La detección es una línea:

import os, sys
ANDROID = hasattr(sys, "getandroidapilevel") or "ANDROID_ARGUMENT" in os.environ

Y conviene unificar las acciones en una sola tabla, para que el teclado y los botones llamen exactamente al mismo sitio:

def act(nombre):
    if nombre == "subir":   avion.morro_arriba()
    elif nombre == "bajar": avion.morro_abajo()
    elif nombre == "tren":  avion.tren()
    # ...

# El teclado entra por aquí
KEYMAP = {pygame.K_w: "subir", pygame.K_x: "bajar", pygame.K_SPACE: "tren"}

# Y el dedo, por aquí
for ev in pygame.event.get():
    if ev.type == pygame.KEYDOWN:
        accion = KEYMAP.get(ev.key)
        if accion:
            act(accion)
    elif ev.type == pygame.MOUSEBUTTONDOWN:
        for rect, etiqueta, accion in botones:
            if rect.collidepoint(ev.pos):
                act(accion)
                break

Detalle útil: en pygame 2 sobre SDL2, un toque en la pantalla genera también eventos de ratón. Escuchando MOUSEBUTTONDOWN cubres el móvil y el ratón del escritorio con el mismo código, sin tocar FINGERDOWN.

La pantalla no mide lo que tú decidiste

Olvídate de set_mode((960, 600)). En el móvil se pide la pantalla completa y se escala el juego a lo que haya:

info = pygame.display.Info()
pantalla = pygame.display.set_mode((info.current_w, info.current_h), pygame.FULLSCREEN)

# Se dibuja en una superficie pequeña de tamaño fijo y se escala al final:
base = pygame.Surface((480, 300))
...
pantalla.blit(pygame.transform.scale(base, (destino.w, destino.h)), destino.topleft)

Dibujar siempre en una superficie de tamaño fijo y escalar al final tiene premio doble: la maquetación no depende del dispositivo, y al ser transform.scale un escalado por vecino más próximo, el pixel art se mantiene cuadrado en lugar de emborronarse.

Android no tiene tus tipografías

pygame.font.match_font("dejavusansmono") devuelve None en el móvil y caes en la fuente interna de pygame, que no es monoespaciada: si tu interfaz alinea columnas, se descuadra. La solución es incluir el .ttf junto al script y cargarlo desde ahí:

ruta = os.path.join(os.path.dirname(os.path.abspath(__file__)), "DejaVuSansMono.ttf")
fuente = pygame.font.Font(ruta, 14) if os.path.exists(ruta) else pygame.font.Font(None, 14)

Recuerda declarar la extensión en el spec o no se empaquetará. Y comprueba la licencia de la fuente: DejaVu deriva de Bitstream Vera y permite redistribución.

Paso 2: el buildozer.spec

Se genera con buildozer init y trae decenas de opciones comentadas. Estas son las que importan:

[app]
title = Night Pilot 64
package.name = nightpilot64
package.domain = org.retro.nightpilot

source.dir = .
source.include_exts = py,ttf,png
version = 1.0

# La receta oficial de pygame va sobre SDL2. No hace falta receta propia.
requirements = python3,pygame

orientation = landscape
fullscreen = 1

android.api = 34
android.minapi = 21
android.ndk = 25b
android.archs = arm64-v8a, armeabi-v7a
android.accept_sdk_license = True

# El juego no necesita ningún permiso: ni red, ni ficheros, ni sensores.
android.permissions =

p4a.bootstrap = sdl2

[buildozer]
log_level = 2

Tres apuntes sobre estas líneas:

  • source.include_exts decide qué entra en el APK. Si tu juego carga imágenes o sonidos y te los dejas fuera, compilará bien y fallará al arrancar.
  • android.permissions vacío es una virtud: un juego que no pide permisos no asusta a nadie al instalarse. No añadas INTERNET por inercia.
  • android.archs multiplica el tamaño. Con solo arm64-v8a cubres cualquier móvil de los últimos años y el APK adelgaza bastante.

Reaprovechar el SDK de Android Studio

Por defecto Buildozer se descarga su propio SDK y NDK en ~/.buildozer, y es lo más seguro porque controla las versiones. Si ya los tienes, ahorras varios gigas apuntando a ellos:

android.sdk_path = /home/TUUSUARIO/Android/Sdk
android.ndk_path = /home/TUUSUARIO/Android/Sdk/ndk/25.1.8937393

El NDK se instala desde Settings → Android SDK → SDK Tools, marcando Show Package Details para poder elegir la versión exacta.

Paso 3: preparar Ubuntu

sudo apt update
sudo apt install -y git zip unzip openjdk-17-jdk python3-pip python3-venv \
     autoconf libtool pkg-config zlib1g-dev libncurses-dev cmake libffi-dev libssl-dev

Y aquí el detalle que hace tropezar a medio mundo en Ubuntu moderno: desde la 23.04, instalar paquetes con pip fuera de un entorno virtual falla con externally-managed-environment. No fuerces con --break-system-packages; usa un entorno virtual, que además te permite tener varias versiones de Buildozer sin ensuciar el sistema:

python3 -m venv ~/.venvs/buildozer
source ~/.venvs/buildozer/bin/activate
pip install --upgrade pip buildozer cython

Cython no es opcional. Aunque tu proyecto no lo use, python-for-android lo necesita en la máquina que compila para generar el código puente de varias recetas.

Paso 4: compilar

cd night-pilot-apk
buildozer android debug

Y a esperar. La primera vez tarda entre 20 y 40 minutos porque descarga el SDK y el NDK y compila Python, SDL2 y pygame desde el código fuente. Las siguientes son de minutos: todo queda cacheado en ~/.buildozer.

El APK aparece en bin/. Con el móvil conectado por USB y la depuración USB activada, esto compila, instala, lanza y te deja viendo el registro del dispositivo:

buildozer android debug deploy run logcat

Ese logcat final es tu única ventana a lo que pasa dentro. Cuando la app arranque y se cierre sola sin explicación, la traza de Python aparecerá ahí.

Errores frecuentes

SíntomaCausa y arreglo
externally-managed-environmentUbuntu 23.04 o superior. Instala Buildozer en un entorno virtual.
Aidl not found / falta una build-toolFalta parte del SDK. Deja que Buildozer se lo descargue, o instálalo desde el SDK Manager.
Errores de JDK o de GradleVersión de Java equivocada. Usa la 17: export JAVA_HOME=/opt/android-studio/jbr si tienes Android Studio.
Falla al compilar una recetaCasi siempre, incompatibilidad con la versión del NDK. Fija android.ndk = 25b y vuelve a probar.
El APK instala pero se cierra al abrirExcepción de Python. Míralo con buildozer android logcat.
Se ve un trozo de pantalla o los controles quedan fueraEstás usando un tamaño de ventana fijo. Vuelve al paso 1.

Aviso sincero: la receta de pygame es la pieza más frágil de toda la cadena. Es la que más se rompe cuando cambian las versiones del NDK, y por eso conviene fijarlas en el spec en lugar de dejar que Buildozer elija. Si la compilación revienta justo ahí, la alternativa conocida es usar pygame-ce con una receta propia en una carpeta p4a-recipes/.

La vía sin instalar nada: que lo compile GitHub

Si no tienes Linux a mano, o simplemente quieres compilar desde un sistema limpio y reproducible, un flujo de GitHub Actions hace el trabajo. Este archivo va en .github/workflows/build-apk.yml:

name: Compilar APK
on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  apk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/cache@v4
        with:
          path: |
            ~/.buildozer
            .buildozer
          key: buildozer-${{ runner.os }}-${{ hashFiles('buildozer.spec') }}

      - uses: ArtemSBulgakov/buildozer-action@v1
        id: buildozer
        with:
          command: buildozer android debug

      - uses: actions/upload-artifact@v4
        with:
          name: apk
          path: ${{ steps.buildozer.outputs.filename }}

Subes el repositorio, entras en la pestaña Actions y, cuando el flujo termine en verde, descargas el artefacto. La caché es importante: sin ella, cada compilación vuelve a bajarse el NDK entero.

Una última cosa: ese APK no está firmado

buildozer android debug produce un APK de depuración. Te lo instalas tú permitiendo orígenes desconocidos, pero no sirve para Play Store. Para eso hace falta generar un almacén de claves propio y compilar en modo release:

buildozer android release

El APK resultante hay que firmarlo con apksigner usando tu clave. Guarda ese almacén de claves como oro: si lo pierdes, no podrás publicar actualizaciones de tu propia aplicación nunca más.

Resumiendo

El camino de un .py a un .apk es Buildozer, no Android Studio. La compilación es lenta pero mecánica; lo que de verdad cuesta trabajo es lo de antes: aceptar que en el móvil no hay teclado, que la pantalla no mide lo que tú quieres y que no están tus fuentes. Si resuelves eso, el empaquetado es esperar.

El clon de Night Pilot 64 usado como ejemplo es un único archivo de Python con pygame, que funciona igual en escritorio y en Android. El type-in original es de William Fong, publicado en Commodore Horizons nº 8 (agosto de 1984).

Comentarios

Entradas populares