Generar AppImages con AppImageKit

Para un proyecto estoy generando ejecutables para GNU/Linux, y el compilador me produce una carpeta con una distribución de archivos. Carpeta bin/ con el ejecutable, carpeta lib/ con las .so… Podría empaquetar eso en un .zip, podría aprender a generar un .deb o un .rpm… o podría aprovechar la ocasión para aprender a crear archivos AppImage.

AppImage es un formato ejecutable autocontenido para GNU/Linux. Es decir, el ejecutable y todas las dependencias (imágenes, bibliotecas dinámicas…) van dentro del propio archivo. La ventaja de esto es que acabas con el conflicto de versiones de bibliotecas dinámicas (la típica de que en GNU/Linux dos programas no se llevan bien porque uno espera que /usr/lib/libwhatever.so sea la versión 1.2.3 y otro espera que /usr/lib/libwhatever.so sea la versión 2.5.8). Al final tienes un único archivo, que haces ejecutable con chmod +x, y que cuando ejecutas funciona normal en cualquier distribución GNU/Linux. Como lo que también buscan conseguir Flatpak y Snap, vaya.

Para crear AppImages, utilicé AppImageKit. Es una herramienta que convierte un directorio en formato AppDir (es decir, con el esqueleto de la aplicación), en un archivo ejecutable de tipo AppImage. Aunque es muy flexible, a la vez es muy sencillo de empezar a usar.

Montando el AppDir

Primero voy a crear el AppDir. En su forma más simple hay que crear un directorio con tres archivos dentro:

$ ls -l
total 32
-rwxr-xr-x@ 1 danirod  staff   215 Jan 12 04:23 AppRun
-rw-r--r--@ 1 danirod  staff   159 Jan 12 04:23 es.danirod.rectball.desktop
-rw-r--r--@ 1 danirod  staff  5572 Jan 12 04:23 es.danirod.rectball.svg

En este ejemplo estoy creando una distribución AppImage de Rectball. Hay tres archivos:

  • El archivo AppRun es el script que se ejecuta cuando lanzas el AppImage, es decir, cuando luego hagas ./programa.AppImage o cuando utilices el lanzador de aplicaciones. Es como un script normal, y debería usarse para configurar el entorno (por ejemplo, darle un valor a las variables $PATH y $LD_LIBRARY_PATH, y finalmente hacer un exec del auténtico ejecutable.
  • El archivo .desktop permiten crear los metadatos de la aplicación. El archivo .desktop es la entrada de escritorio y sigue la Desktop Entry Specification de freedesktop.org. Si alguna vez has cacharreado en la carpeta /usr/share/applications o has modificado el lanzador de aplicaciones de tu distro sabrás a qué me refiero.
  • El archivo .svg es el icono de aplicación. Los exploradores de archivos de algunas distribuciones están empezando a mostrar el icono cuando detectan un archivo de tipo AppImage, y también hay herramientas como AppImageLauncher que detectan automáticamente archivos AppImage que haya en un directorio de tu ordenador y que se ocupan de crear lanzadores para el menú de aplicaciones en base al .desktop que hay en tu AppImage que usarán este icono. En teoría se debería poder usar .png por lo que he leído, pero no soy capaz de que muestre bien el icono si lo hago así, así que recomendaría .svg.

Dentro del AppDir es donde habrá que dejar los archivos de la aplicacion. Diría que lo más seguro es respetar la FHS y crear directorios como ./usr/bin, ./usr/lib o ./usr/share.

Es muy importante que el ejecutable sea independiente de ubicación. Es decir, si tu programa asume que siempre se va a estar ejecutando en /usr/bin/programa, o si usa rutas absolutas para acceder a cosas (por ejemplo, /usr/share/programa), las cosas van a salir mal. Esto es porque cuando se ejecuta un AppImage, la aplicación se ejecuta desde otra ubicación diferente. Igualmente, al principio deberías probar en distintas distribuciones y versiones que todo funcione bien, por ejemplo, probar en la versión de Ubuntu o Debian más antigua que quieras soportar.

Al final, con todo esto, lo que hay en el AppDir es en realidad algo como lo siguiente:

$ ls -l . usr usr/bin
.:
total 16
-rwxr-xr-x 1 root root  215 Jan 12 13:18 AppRun
-rw-r--r-- 1 root root  159 Jan 12 13:18 es.danirod.rectball.desktop
-rw-r--r-- 1 root root 5572 Jan 12 13:18 es.danirod.rectball.svg
drwxr-xr-x 4 root root  128 Jan 12 13:18 usr

usr:
total 0
drwxr-xr-x 3 root root  96 Jan 12 13:18 bin
drwxr-xr-x 6 root root 192 Jan 12 13:18 lib

usr/bin:
total 72
-rwxr-xr-x 1 root root 71912 Jan 12 13:18 Rectball

Montando el .desktop

El archivo .desktop se puede llamar como quieras, pero tiene que terminar en .desktop. Y tiene que ser un archivo con formato Desktop Entry. Puedes hacerlo a mano si sabes hacerlo, o puedes usar alguna herramienta para editar este tipo de archivos. (Por ejemplo, muchos editores de menú para GNOME pueden hacerlo, y KDE suele traer un Editor de menús preinstalado). Este es el mío:

Desktop Entry]
Type=Application
Name=Rectball
Comment=Match gems and collect points in this puzzle game
Categories=Game
Exec=Rectball

Si te das cuenta, la propiedad Exec apunta al nombre del ejecutable, pero no pone la ruta al mismo. En tu AppRun, como muestro un poco más abajo, tu script deberá configurar el PATH para absorber este archivo. Lo de poner una propiedad Icon para apuntar explícitamente al icono, he buscado AppImages en internet y veo que hay gente que lo hace, y gente que no lo hace. Yo no lo hago, pero tal vez no estaría de más. (Es más, me pregunto si no ponerlo es la razón de que tenga problemas para ver el icono cuando uso .png en vez de .svg…)

Montando el script AppRun

En cuanto al script AppRun, diría que lo más simple es partir del propio AppRun que hay en el repo de AppImageKit e ir tirando desde ahí. Como dije antes, es el script que se ejecuta, y tiene que configurar el PATH y el LD_LIBRARY_PATH antes de arrancar. Todo esto teniendo en cuenta que tu script no sabe desde qué ubicación real se está ejecutando hasta que alguien haga ./programa.AppImage en su terminal o lo lance de otro modo. El AppRun de referencia resuelve esto de una forma elegante:

#!/bin/sh
SELF=$(readlink -f "$0")
HERE=${SELF%/*}
export PATH="${HERE}/usr/bin/..."
export LD_LIBRARY_PATH="${HERE}/usr/lib/..."

La forma de invocar al ejecutable también la resuelve de forma peculiar: inspecciona el contenido del archivo .desktop, busca la propiedad Exec, e invoca directamente su valor:

EXEC=$(grep -e '^Exec=.*' "${HERE}"/*.desktop | head -n 1 | cut -d "=" -f 2 | cut -d " " -f 1)
exec "${EXEC}" "$@"

El AppImage de neovim es bastante más breve. Según el script que lo genera, lo soluciona con una única línea de script:

#!/bin/bash

unset ARGV0
exec "$(dirname "$(readlink  -f "${0}")")/usr/bin/nvim" ${@+"$@"}

Compilar la aplicación con AppImageKit

Desde el repo de AppImageKit se puede descargar una versión ejecutable. Convenientemente es un AppImage. El tag continuous se actualiza automáticamente cada vez que se sube versión, así que salvo que hayan mentido o cambien de idea en algún momento, debería ser seguro poner una ruta tipo https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage en un shellscript para automatizar el proceso.

Crear el AppImage me resultó tan fácil como usar el comando

./appimagekit-x86_64.AppImage [ruta del AppDir] [archivo de destino]

Así que al final yo lo invocaría en mi caso como ./appimagekit-x86_64.AppImage rectball.AppDir rectball-x86_64.AppImage. Para poder utilizar AppImage es necesario tener FUSE instalado en el sistema, lo cual es un punto importante cuando se usa Docker o algunos entornos de CI donde no se instala FUSE por defecto dentro de los contenedores.

El resultado es el ejecutable:

$ rectball-x86_64.AppImage
-rwxr-xr-x 1 root root 33423744 Jan 12 13:46 rectball-x86_64.AppImage

De nuevo, recomendaría llevarse este ejecutable a un par de ordenadores o máquinas virtuales con varias versiones de distros, no todas necesariamente nuevas, para asegurarse de que va bien.