Distribuyendo un programa hecho en Python

Más que un análisis completo de las tecnologías para permitir la distribución de programas hechos en Python, este post es casi una receta o colección de anotaciones para seguir un camino. Y es que es un camino que no me fué fácil recorrer, porque la mayoría de los mecanismos para distribuir código Python están pensadas para distribuir bibliotecas hechas en este lenguaje, y no programas.

¿Dónde está la diferencia? En dónde van las cosas.

Antes de seguir: para todo el armado usé distutils, que es lo que está en la biblioteca estándar. Le pegué una mirada a otras cosas como setuptools, distribute, etc, pero todas (aunque son más aptas para cosas más complejas) no me solucionaban el problema básico y me complicaban un poco la vida en otros aspectos.

¿Dónde van las cosas?

Volviendo a el lugar en dónde se instala el código Python... si uno quiere distribuir una biblioteca, la respuesta es sencilla: en el directorio de bibliotecas de Python de tu sistema. ¿En dónde particularmente? Bueno, depende de tu sistema; incluso en Linux esto fue cambiando y no es el mismo lugar siempre. En mi máquina tengo /usr/lib/python2.6/dist-packages/, que en parte apunta a /usr/share/pyshared/.

Igual, no importa la ubicación exacta: usando distutils (u otras alternativas) las bibliotecas van a parar al lugar correcto sin mayor esfuerzo.

¿Pero qué pasa si no es una biblioteca sino un programa? El primer detalle es que necesitamos un ejecutable que arranque nuestro programa. Distutils y amigos tienen esto bastante bien manejado, se les puede especificar un script, y terminan instalando todo de la siguiente manera:

script -> /usr/bin/</span>
todo el resto /usr/lib/python2.6/dist-packages/ (o similar)

Hasta acá todo bien, ¿no? No. Resulta que nuestro programa tiene imágenes, archivos de audio, etc, y está "mal visto" meter esos archivos "de datos" dentro del directorio de bibliotecas de Python. Entonces, lo que recomiendan por ahí es:

script -> /usr/bin/
archivos de datos -> /usr/share/
código python -> /usr/lib/python2.6/dist-packages/ (o similar)

Esto ya no es tan fácil de lograr, porque la distribución de archivos de datos es como un parche en los sistemas de distribución de bibliotecas.

Además, si nos vamos a poner quisquillosos de no meter archivos de datos en el directorio de bibliotecas, yo pregunto: ¿por qué meter código de nuestro programa, que no es una biblioteca, en el directorio de bibliotecas?

Entonces me embarqué en el siguiente capricho: quería que la distribución de mi programa vaya a parar a:

script -> /usr/bin/ todo el resto -> /usr/share/

Los archivos de datos, por supuesto, mezclados con "todo el resto".

Estructura de nuestro programa

Primero lo primero, ¿cómo organizamos nuestro proyecto? Yo tengo lo siguiente (simplificado, pueden ver toda la estructura en los archivos del proyecto Encuentro):

  • un directorio 'bin' donde tengo el script que arranca todo:

    bin/encuentro

esto es un archivo ejecutable que no hace mucho más que jugar un poco con los directorios y el sys.path para que se encuentre al resto del código Python de nuestro programa (en dos situaciones: cuando ejecutamos bin/encuentro desde el repositorio mientras estamos desarrollando, y cuando está instalado finalmente en el sistema), e inicializar alguna estructura básica y arrancarla, para que comience nuestro programa.

  • un directorio con el nombre de nuestro proyecto, con el resto del programa:

    encuentro/__init__.py
    encuentro/main.py
    encuentro/network.py
  • directorios con los archivos de datos, adentro de nuestro proyecto (no por separado), en este caso:

    encuentro/ui/main.glade
    encuentro/ui/preferences.glade
    encuentro/ui/update.glade

Una vez aclarado eso, quedan dos preguntas sencillas y una complicada por contestar: las sencillas son ¿cómo el script encuentra al resto del programa instalado? y ¿cómo accedemos a los archivos de datos desde nuestro código?.

La primera es usando una variable que se inyecta en el script en el momento de instalar el programa (ver más abajo el cuándo hacemos eso en setup.py).

La segunda es accediendo a los archivos de forma relativa al código. Yo tengo esto al principio del programa:

BASEDIR = os.path.dirname(__file__)

y luego hago cosas como:

data_file = os.path.join(BASEDIR, 'ui', 'preferences.glade')

Finalmente, la pregunta complicada: ¿cómo hacemos para que todo esto funcione?

Distribuyendo programas

En realidad, la respuesta no es tan complicada una vez que está resuelto (como tantas cosas en la vida).

Para incluir todos los archivos, en el setup.py, en la llamada a setup() hay que poner:

packages = ["encuentro"],
package_data = {"encuentro": ["ui/*.glade"]},
scripts = ["bin/encuentro"],

Fíjense como ahí declaro el paquete donde está mi programa, el script, y los archivos de datos. Pero hay un bug, hasta en Python 2.6 inclusive, que hace que para meter los archivos de datos con eso sólo no alcanza, y hay que declararlos también en el MANIFEST.in:

include encuentro/ui/*.glade

Para que todos estos archivos vayan a parar al lugar correcto, hay que hacer algo específico: una clase que acomoda cosas en el proceso de instalación. Pueden ver el detalle de esa clase en el setup.py de Encuentro, pero basicamente hace dos cosas:

  • Construye un directorio donde va a quedar todo con el prefijo indicado, "share" y el nombre del proyecto, y autocorrije el directorio de instalación con eso.

  • Guarda ese directorio de instalación nuevo en los scripts declarados, usando una cadena especial como bandera, de manera que al quedar el script instalado sabe dónde buscar el programa entero.

(importante: no olvidar declarar en la llamada a setup() a esta nueva clase como la clase que será usada para instalar!)

Finalmente, está bueno probar que todo funca bien. Las pruebas que yo hice fue crear el .tar.gz con python setup.py sdist, descomprimirlo en otro lado que nada que ver y hacer python setup.py install --prefix=/tmp (para que se instale en /tmp y probarlo ahí adentro) y también sudo python setup.py install (para que se instale en el sistema y probarlo así).

También, luego de hacer todo el proceso de packaging, cuando pbuilder me dejó el .deb, lo descomprimo y veo que la estructura está correcta y que la variable reemplazada en el script tiene el valor que debería; igual, la prueba de fuego con el .deb es instalarlo con dpkg -i y probar el programa.

Nota final: ahora me falta armar un .exe para que se pueda ejecutar en Windows, pero eso será otro post.

Comentarios Imprimir