Cartero ya está hecha en Meson

Este es un resumen de lo ocurrido en el tercer stream de desarrollo de Cartero, así como los commits que le he tirado hoy aprovechando que es festivo y que Meson al fin y al cabo sabía de antemano que iba a ser algo aburrido de integrar que no valía la pena hacer en vivo. Las cuatro horas de stream de ayer (yo prometí que el stream duraría hora y media, pero ciertamente volví a fallar) se pueden ver aquí.

  • Se ha migrado el código de la ventana y la aplicación a una clase propia.
  • Primeros pull requests integrados, este proyecto ya tiene más contributors.
  • El proyecto ya tiene icono, aunque sea provisional.
  • Ahora se usa meson para compilar el programa, y eso incluye más cosas.

Ahora se usan clases

Aunque es posible desarrollar aplicaciones GTK instanciando variables, no es lo normal. El código del programa podría estar basado en llamadas tipo:

var window = new Gtk.Window();
var button = new Gtk.Button();
window.set_child(button);
button.connect("clicked", on_click);

Sin embargo, en GTK lo habitual será que se acaben fabricando clases que extiendan de los widgets. Por ejemplo, crear una CarteroApplication que extienda de Gtk.Application, una CarteroWindow que extienda de Gtk.ApplicationWindow, o una CarteroRequestHeaders que extienda de Gtk.Box.

Esto es así incluso en lenguajes poco orientados a objetos como C o Rust. GObject está pensado para fabricar unos bindings neutros e interoperables, por ejemplo con respecto a los archivos XML o Blueprint que luego codifican la interfaz de usuario.

En fin, no sé si estoy muy cualificado para escribir un tutorial largo sobre cómo simular esta orientación a objetos en Rust en este momento, pese a que el streaming de ayer consistió en líneas generales en describirla igualmente. El libro de gtk-rs tiene un capítulo que cuenta todo esto de una forma que se entiende mejor.

El resumen es que el código queda más limpio y organizado, porque ya no son todo callbacks, sino métodos de una impl. En este commit se puede ver resumido todo el refactor donde paso de tener un único main.rs que instancia un montón de cosas a varios archivos (main.rs, app.rs y win.rs) donde se definen por separado los elementos de la interfaz de una forma más clara.

Primeros pull requests integrados

Finalmente integré el primer pull request ayer aprovechando que estaba en vivo. Hoy he integrado un segundo pull request que agrega un enlace a este devlog desde el archivo README (¡hola!).

No tengo ya la intención de esperarme a un vivo para aprobar e integrar pull requests, sino que lo voy a hacer tranquilamente cuando toque. Lo que sea importante, lo contaré en el siguiente directo tras integrarlo o lo pondré por escrito aquí.

Para asegurarme de que el roadmap del proyecto está claro y que no se trabaja en la oscuridad, ayer creé un kanban donde he dejado apuntadas las tareas pendientes que tengo en la cabeza. Así no ocurre que haga una cosa y resulta que alguien secretamente había decidido clonar el repositorio para hacerlo también.

El proyecto ya tiene icono

Le hice hoy un icono a Cartero con Inkscape y un poco de cariño.

El icono propuesto para Cartero representa un sobre que puede entrar y salir.

Con esto ahora cuando se instale la aplicación y se ejecute, por lo menos se mostrará un icono en el lanzador, barra de tareas, alt-tab…

El proyecto ahora usa Meson

A lo largo del día de hoy me basé en la plantilla de GTK y Rust que hay en el GitLab de GNOME y en otros meson.build que hay en el mismo sitio web para hacer un meson.build para mi propio proyecto.

Meson es una herramienta de compilación muy parecida a CMake. Si bien es verdad que Rust ya trae Cargo, Meson ha acabado haciendo falta porque existen muchas cosas a la hora de compilar una aplicación de este estilo que no tienen nada que ver con Rust y que por lo tanto no se suelen hacer a través de Cargo. Me estoy refiriendo a:

  • Compilar los archivos Blueprint traduciéndolos a archivos XML.
  • Empaquetar luego esos archivos XML a un archivo GResource (otro día os cuento esto en un artículo de blog).
  • Actualizar los archivos de traducción (con las cadenas de caracteres que son internacionalizables).
  • Generar los archivos .desktop y .xml para enviar a tiendas de aplicaciones (como GNOME Software o el AppCenter de elementaryOS).

Es por ello que al final tiene que entrar Meson. Meson tiene un módulo para hablar con Rust así que se le puede pedir a Meson que invoque ese cargo build por nosotros, pero se convierte en un paso de la compilación con Meson.

Al final tampoco tengo mucho más que decir sobre el código porque casi todo es copia y pega, aunque a medida que iba trayéndome el código de la plantilla, iba viéndole mucho sentido a lo que hacía. Estos son los commits que he tirado.

  • El primer commit integra Meson de forma bruta. En principio después de aplicar este commit, ejecutar ninja -C build invoca automáticamente cargo build para compilar el proyecto. A tener en cuenta que en función de si en Meson estamos usando modo desarrollo o no, el código de Rust se compila en modo release o en modo debug. Una particularidad de Meson es que no usa la carpeta target para descargar las dependencias, sino la propia carpeta de build.
  • Una vez hice eso, aproveché otro de los puntos fuertes de Meson: crear archivos a través de plantillas. Al igual que otros build tools, puedes pasar un archivo .in, que es un archivo de plantilla donde se hacen sustituciones cambiando algunas macros por valores que se obtienen en tiempo de compilación. Esto está descrito en este commit. He agregado un archivo llamado config.rs.in, que se usa como plantilla para crear un archivo llamado config.rs. La diferencia es que en el config.rs.in hay varios placeholders, como @APP_ID@ o @VERSION@, cuyo valor está definido realmente en el meson.build. Cuando Meson convierte este archivo al config.rs, sustituye los placeholders por el valor que se le asigna en el meson.build. Esto es muy interesante, por ejemplo, de cara a crear el valor del VERSION a partir del hash del commit de Git cuando se está trabajando en modo desarrollo.
  • Crear el archivo GResources es algo que realizo en este otro commit. Insisto en que ya hablaré por separado de cómo funciona este tipo de archivo, porque me ha dejado impresionado una vez que entiendes qué hace. Por anticipar, un archivo gresource es un bundle que empaqueta varios archivos de entrada (parecido a un .tar o un .zip). Cuando arrancas la aplicación, puedes hacer que ciertos assets (como una imagen, un sonido o incluso el XML que describe una interfaz de usuario) se carguen desde ese bundle. De este modo, en vez de incrustar la cadena de caracteres con el XML dentro del ejecutable, puedes dejarlo como un archivo aparte en /usr/share, y se carga de forma transparente sin mucho esfuerzo. Sinceramente, este paso es el único que no me hace mucha gracia. Lo malo ahora es que para poder ejecutar la aplicación, incluso en modo desarrollo, es necesario tener el archivo de recursos que describe la interfaz de usuario instalado en el sistema. Esto ralentizará un poco las cosas.
  • Agregar blueprint-compiler como dependencia. Así durante la compilación, se transforman automáticamente los archivos .blp a archivos .ui. Además, Meson puede descargarse blueprint-compiler de forma local, por lo que ya no es necesario instalarlo por separado.
  • Traducciones con gettext. Para internacionalizar, se crean unos archivos especiales con la extensión .mo, que se acaban instalando en /usr/share/locale. Cuando se inicia la aplicación, se llama a una función init del paquete gettext que carga en memoria el archivo de traducción correspondiente al idioma que tenga el ordenador. Cada vez que se quiera traducir una cadena de caracteres, se llama a la función de traducción, típicamente en C suele ser la función guión bajo, aunque en Rust será la función gettext. Por ejemplo, en vez de hacer printf("Hola") haces printf(_("Hola")). Con esto se busca en el archivo de traducción cómo se dice en tu idioma la cadena "Hola" y puedes ver internacionalizado el mensaje. Otro día cuento más sobre gettext también.
  • Y finalmente, he agregado un archivo .desktop y un icono para el programa, y ahora durante el paso de instalación se copian los archivos para que se pueda crear un lanzador del programa. El resultado es que ahora tras instalar la aplicación, se puede encontrar en el lanzador de aplicaciones con un icono, lo cual vendrá bien cuando la aplicación por fin haga algo.

Relajar las dependencias

Aunque eventualmente la aplicación se compilará desde un Flatpak, por aquello del sandboxing, no quiero que se deje de poder compilar de forma nativa en el sistema operativo.

He descargado una ISO de Debian 12, una de Ubuntu 22.04 y una de elementaryOS 7.1, y he tratado de compilar la aplicación desde los tres sistemas operativos en máquinas virtuales. Al validar que compilaban correctamente en todas, he identificado cuáles son las versiones más viejas de las dependencias (GTK, GLib y GtkSourceView) que permiten compilar la aplicación.

La razón de este cambio es que mi ordenador usa Arch Linux (btw), por lo que al final mi ordenador siempre va a tener las versiones más frescas de las dependencias, pero quiero que se pueda compilar también en sistemas operativos que no son rolling release, donde a lo mejor van un par de versiones por detrás.

Quien ganó en cuanto a antigüedad fue Ubuntu 22.04, por lo que ahora el meson.build indica que es necesario usar como mínimo las versiones de estas dependencias que vienen en Ubuntu 22.04.