11 noviembre 2021

Cómo empaquetar aplicaciones Rust - Paquetes DEB


El lenguaje Rust es duro y áspero. Tu primer contacto con el compilador y con el borrow checker suele ser traumático hasta que te das cuenta de que en realidad están allí para protegerte de ti mismo. Una vez que tienes esa revelación empiezas a adorar a ese lenguaje.

Pero todo lo que está más allá del lenguaje en si mismo es amable (incluso diría que cómodo). Con cargo, compilar, probar, documentar, medir e incluso publicar en crates.io (el equivalente a Pypi en Rust) es una gozada. El empaquetado no es una excepción ya que se integra con cargo, una vez hecha la configuración que vamos a ver aquí.

Para empaquetar mi aplicación Rust en paqueted debian, utilizo cargo-deb. Para instalarlo sólo hay que teclear:

dante@Camelot:~~/Projects/cifra-rust/$ cargo install cargo-deb
Updating crates.io index
Downloaded cargo-deb v1.32.0
Downloaded 1 crate (63.2 KB) in 0.36s
Installing cargo-deb v1.32.0
Downloaded crc v1.8.1
Downloaded build_const v0.2.2
[...]
Compiling crossbeam-deque v0.8.1
Compiling xz2 v0.1.6
Compiling toml v0.5.8
Compiling cargo_toml v0.10.1
Finished release [optimized] target(s) in 53.21s
Installing /home/dante/.cargo/bin/cargo-deb
Installed package `cargo-deb v1.32.0` (executable `cargo-deb`)


dante@Camelot:~$

Hecho eso, ya podemos empezar a empaquetar aplicaciones sencillas. Por defecto, cargo deb saca la información de tu fichero Cargo.toml. De esa manera saca los siguientes campos:

  • name
  • version
  • license
  • license-file
  • description
  • readme
  • homepage
  • repository

Sin embargo, rara vez ocurre que tu aplicación no tiene dependencias en absoluto. Para configurar casos de uso más avanzados hay que crear una sección [package.metadata.deb] en tu Cargo.toml. En esa sección se pueden configurar los siguientes campos:

  • maintainer
  • copyright
  • changelog
  • depends
  • recommends
  • enhances
  • conflicts
  • breaks
  • replaces
  • provides
  • extended-description
  • extended-description-file
  • section
  • priority
  • assets
  • maintainer-scripts

Como ejemplo de todo esto, puedes consultar esta versión del fichero Cargo.toml de mi aplicación Cifra.

Ahí se puede leer la sección general de la que cargo-deb saca su información básica:

 

 

Cargo tiene una gran documentación donde se puede encontrar una explicación para todas las secciones y etiquetas posibles.

Ten en cuenta que cada ruta de ficheros que se incluya en el fichero Cargo.toml es relativa a dicho fichero Cargo.toml.

La sección específica para cargo-deb no necesita muchos parámetros para conseguir un paquete válido:


 

Las etiquetas para esta sección están documentadas en la página principal de cargo-deb.

Las etiquetas section y priority se usan para clasificar la aplicación en la jerarquía de Debian. Aunque en mi configuración las he fijado, creo que no son muy útiles los requisitos para publicar paquetes en los repositorios oficiales de Debian son superiores a los que se pueden conseguir con cargo-deb por el momento, por lo que cualquier paquete generado con cargo-deb acabará casi con total probabilidad en un repositorio personal al que no se le aplicará la jerarquía de debian para las aplicaciones.

En realidad, la etiqueta más importante es la de assets. Esta etiqueta te permite fijar que ficheros deben incluirse en el paquete y dónde deben dejarse al instalar. El formato del contenido de esa etiqueta es sencillo. Se trata de una lista de tuplas de tres elementos:

  • Ruta relativa al fichero a incluir en el paquete: Esa ruta es relativa a la ubicación del fichero Cargo.toml. 
  • Ruta absoluta donde ubicar el fichero en el ordenador del usuario.
  • Los permisos que debe tener el fichero en el ordenador del usuario.

Debería haber incluido una etiqueta "depends" para añadir las dependencias del paquete. Cifra depende de SQLlite3 y este no se encuentra en un paquete de Rust sino en un paquete de sistema, por lo que es una dependencia del paquete debian de Cifra. Si quisieras usar la etiqueta "depends" podrías hacerlo pero tendrías que usar el formato de dependencias de debian, aunque en realidad no es necesario porque cargo-deb es capaz de calcular las dependencias automáticamente aunque no uses esa etiqueta. Lo que hace es usar ldd contra el binario compilado de nuestra aplicación y luego busca con dpkg qué paquetes de sistema ofrecen las librerías detectadas por ldd.

Una vez que tienes tu configuración de cargo-deb en tu Cargo.toml, empaquetar tu paquete debian es tan sencillo como hacer:

dante@Camelot:~/Projects/cifra-rust/$ cargo deb
[...]
Finished release [optimized] target(s) in 0.17s
/home/dante/Projects/cifra-rust/target/debian/cifra_0.9.1_amd64.deb

dante@Camelot:~$

Como se puede ver en la salida del comando, se puede encontrar el paquete generado en una nueva carpeta dentro de la de tu proyecto denominada /target/debian/.

Cargo-deb es una herramienta muy útil cuya única pega es no ser capaz de cumplir los requisitos de Debian para empaquetar paquetes viables para ser incluidos en los repositorios oficiales.

Cómo escribir manpages con Markdown y Pandoc

Una aplicación de consola decente siempre cuenta con una manpage para documentar cómo usarla. Sin embargo, las interioridades de las manpages son bastantes arcanas, aunque para ser un formato de fichero de 1971 ha aguantado bastante bien.

Hoy en día hay dos manera estándar de aprender a usar un comando de consola (aparte de buscar en google ;) ): tecleando el nombre de la aplicación, seguido de "--help", para obtener un resumen de cómo usar la aplicación, o teclear "man" seguido del nombre de la aplicación para obtener información detallada de cómo usarla.

Para implementar "--help" en nuestra aplicación podemos incluir un parseado manual de "--help", aunque lo más recomendable es usar una librería como la de python ArgParse para parsear argumentos de usuario.

El enfoque de usar el comando "man" implica escribir una manpage de tu aplicación..

La manera estándar de crear manpages es usar el formato troff. Linux tiene su propio estándar de implementación de troff, denominado groff. Si quieres cacharrear para hacerte una idea de cómo es el formato con el que se escriben las manpages, puedes teclear lo siguiente como el contenido de un fichero que se llame, por ejemplo, corrupt.1:

.TH CORRUPT 1 .SH NAME corrupt \- modify files by randomly changing bits .SH SYNOPSIS .B corrupt [\fB\-n\fR \fIBITS\fR] [\fB\-\-bits\fR \fIBITS\fR] .IR file ... .SH DESCRIPTION .B corrupt modifies files by toggling a randomly chosen bit. .SH OPTIONS .TP .BR \-n ", " \-\-bits =\fIBITS\fR Set the number of bits to modify. Default is one bit.

Una vez salvado, ese fichero puede visualizarse conel comando man. Suponiendo que estemos en la misma carpeta que el fichero corrupt.1, podemos teclear:

dante@Camelot:~/$ man -l corrupt.1

La salida será: 

CORRUPT(1)                                                 General Commands Manual 

NAME
corrupt - modify files by randomly changing bits

SYNOPSIS
corrupt [-n BITS] [--bits BITS] file...

DESCRIPTION
corrupt modifies files by toggling a randomly chosen bit.

OPTIONS
-n, --bits=BITS
Set the number of bits to modify. Default is one bit.

CORRUPT(1)


 

Puedes eligir escribir directamente manpages para tu aplicación siguien, por ejemplo, esta pequeña chuleta

De todos modos, el formato troff/groff es bastante raro y en mi opinión mantener dos fuentes de información (tu README.md y tu manpage) no es sino fuente errores y un desperdicio de esfuerzos. Por eso sigo un enfoque diferente: escribo y mantengo actualizado mi README.md y luego lo convierto a una manpage. Es cierto que hay que mantener un formato estándar para que la conversión de README.md cuadre con lo que se espera de un manpage, pero al menos evitaremos teclear las mismas cosas dos veces, en dos ficheros diferentes, y con diferentes lenguajes de etiquetado.

La clave de la conversión es la herramienta denominada Pandoc. Esa herramienta es la navaja suiza de la conversión de documentos. La puedes usar para convertir entre muchos formatos documentales como word (docx), openoffice (odt), epub... o markdown (md) y groff man. Pandoc suele estar disponible en los repositorios estándar de la mayor parte de distribuciones de linux habituales, por lo que en un ubuntu no hay más que teclear:

dante@Camelot:~/$ sudo apt install pandoc

Una vez que Pandoc esté instalado, hay que respetar una serie de convenciones en nuestro README.md opara hacer que la conversión sea más sencilla. Para ilustrar las explicaciones de este artículo, vamos a seguir el ejemplo del fichero README.md de mi proyecto Cifra.

Como se puede ver, el README.md enlazado contiene medallas de GitHub justo al comienzo aunque las quitaremos antes de hacer la conversión, tal y como veremos más adelante. Todo lo demás está estructurado para cumplir con lo que se espera que tenga una manpage estándar.

Puede ser que lo más sospechoso sea la primera línea. No es normal encontrar una línea como esta en un README de GitHub:

En realidad, esa línea contiene metadatos para el visor de manpages. Tras el carácter "%" aparece el título del documento (generalmente el nombre de la aplicación), la sección del manual, una versión, un separador "|" y finalmente una cabecera. La sección del manual es 1 para los comandos de usuario, 2 para las llamadas de sistema y 3 para las funciones de C. Tus aplicaciones encajarán en la sección 1 el 99% de las veces. No he incluido una versión para Cifra en esa línea, pero lo podría haber hecho. Además, la cabecera indica a que categoría de documentación pertenece este manpage.

Tras esa línea, cada sección es de las habituales en un manpage, pero las únicas que deberían ser incluidas como obligatorias son: 

  • Name: El nombre del comando.
  • Synopsis: Un resumen de una línea explicando los argumentos y las opciones.
  • Description: Describe en detalle como usar el comando.

Otras secciones que se pueden añadir son:

  • Options: Opciones del comando.
  • Examples: Ejemplos de uso del comando.
  • Files: Útil si tu aplicación incluye ficheros de configuración.
  • Environment: Aquí se describe si la aplicaciónusa variables de entorno.
  • Bugs: ¿Se deben reportar los bugs detectados? Aquí se puede enlazar la página de issues de GitHub.
  • Authors: ¿Quién es el autor de esta pieza maestra?
  • See also: Referencias a otros manpages.
  • Copyright | License: Un buen lugar para incluir la licancia de tu aplicación.

Una vez decididas las secciones a incluir en el manpage, hay que escribirlas siguiendo un formato fácilmente convertible por pandoc en un manpage con una estructura estándar. Esa es la razón por la que en el README de ejemplo las secciones principales están todas marcadas con un nivle de indentación ("#"). Un problema con la indentación es que aunque en markdown se pueden conseguir múltiples niveles de indentación usando el carácter "#" ( "#" para los títulos principales, "##" para los subtítulos, "###" para las secciones, etc) esos subniveles sólo son reconocidos por Pandoc hasta el subnivel 2. Para conseguir más subniveles he tenido que tirar de las"pandoc lists", un formato que puedes usar en tu markdown y que luego lo reconoce Pandoc:

En las líneas 42 y 48 tenemos lo que Pandoc llama  "line blocks". Estos son líneas iniciadas por una barra vertical ( | ) seguidas por un espacio. Esos espacios entre la barra vertical y el comando se conservarán en el texto del manpage que genere Pandoc.

Todo lo demás del fichero README.md de ejemplo es formato markdown clásico.

Supongamos que hemos escrito todo nuestro README.md y que queremos hacer nuestra conversión. Para hacer eso podemos usar un script como este desde una carpeta temporal:

Las líneas 3 y 4 limpian cualquier fichero usado en una conversión anterior, mientras que la línea 5 es la que en realidad copia el README.md desde la carpeta de origen a la de destino.

La línea 6 borra cualquier medalla de GitHub que podamos tener en nuestro README.md.

La línea 7 es donde llamamos a Pandoc para realizar la conversión.

Hecho eso, ya tendremos un fichero que puede ser abierto con man:

dante@Camelot:~/$ man -l man/cifra.1

Aunque los manpages suelen estar comprimidos con gzip, como se puede ver con los manpages de nuestro sistema:

dante@Camelot:~/$ ls /usr/share/man/man1

Esa es la razón por la que la línea 8 del script comprime el manpage generado. A pesar de estar comprimido, el manpage sigue siendo leíble por man:

dante@Camelot:~/$ man -l man/cifra.1.gz

Aunque son unos cuantos pasos, se puede ver que son fácilmente automatizables mediante un script o como un paso más de tu flujo de integración y despliegue continuo.

Como se puede ver, con estos sencillos pasos sólo se necesita mantener actualizado el README.md, dado que el manpage puede ser generado a partir de él.