Mostrando entradas con la etiqueta rust. Mostrar todas las entradas
Mostrando entradas con la etiqueta rust. Mostrar todas las entradas

24 mayo 2023

Cómo parsear los parámetros de entrada de tu aplicación de consola en Rust usando Clap

En un artículo anterior, expliqué  cómo usar el módulo ArgParse module para leer los argumentos de consola de las aplicaciones desarrolladas en Python. Rust cuenta con crates especializados en el parseo de argumentos de consola. En este artículo vamos a ver cómo usar uno de esos crates: clap.

Una de las cosas maravillosas de Rust es que dentro de ese caparazón rustaceo, duro y árpero hay un corazón pythonista. Al usar clap encontrarás muchas de las funcionalidades de ArgParse. En realidad, los conceptos entre ambos son muy similares: mientras que los verbos recibían el nombre de subparser en ArgParse, en clap reciben el nombre de subcomandos, mientras que ArgParse llama argumentos a los que clap denomina simplemente arg. Por lo tanto, si estás acostumbrado a usar ArgParser, lo más probable es que te sientas en casa usando clap. Por todo lo anterior, voy a asumir que ya has leído mi artículo anterior sobre ArgParse y no voy a repetirme volviendo a explicar los mismos conceptos.

Como cualquier otro crate de Rust, para usar clap hay que incluirlo primero en el fichero Cargo.toml:

Después, ya podrás usar clap en tu código fuente. Para ilustrar las explicaciones, voy a usar como ejemplo el parseo que se realiza en mi proyecto cifra-rust. 

Como puedes ver allí, puedes usar clap directamente en tu función main(), pero yo prefiero abstraer ese proceso en una función aparte que retorne un struct Configuration específicamente desarrollado por mí. De esa manera, si en el futuro decidiese cambiar y pasar a utilizar otro crate de parseo de argumentos, el proceso sería mucho más fácil al haber reducido el acoplamiento. Por eso, tal y como también hago en Python, he definido una función llamada parse_arguments() que retorna un tipo Configuration. 

En el código se puede ver que el parser raíz se define en clap usando App::new(). Como en otros crates de Rust, clap hace un uso muy intenso del patrón constructor para configurar su parseo. De esa manera, puedes configurar la versión del comando, su autor y la descripción larga ("long_about"), entre otras opciones, tal y como se puede ver entre las líneas 277 y 280:


El comportamiento de clap se puede personalizar usando el método setting(). Un parámetro típico con el que llamar a ese método es AppSettings::ArgRequiredElseHelp, para mostrar la ayuda si se llama al comando sin argumento alguno:


Un subparser se crea llamando al método subcommand() y pasándole una nueva instancia de App:


Usando el método about() se puede definir una descripción corta, sobre el comando o sus argumentos, la cual aparecerá cuando se llame al comando principal con la opción --help.

Lo normal es que tanto el parser, como sus subparser, requieran argumentos. Los argumentos se definen a través del método arg(), en el parser al que pertenezca. Aquí tienes un ejemplo:


En el último ejemplo se puede ver que el subparser create (línea 285) tiene dos argumentos: dictionary_name (línea 287) e initial_words_file (línea 292). Fíjate que todas las llamadas a arg() usan como argumento una instancia de Arg, mientras que la configuración del argumento se hace usando el patrón constructor sobre las instancias de Arg.

El argumento dictionary_name es necesario porque está configurado como required(true) en la línea 288. Ten en cuenta que aunque aquí se utilice un flag, en general se suele recomendar que no se haga. Por definición, todos los argumentos obligatorios deberían ser posicionales (es decir, no deberían usar un flag), el único caso en el que el uso de un flag está bien visto con un argumento obligatorio es cuando ese argumento llama a una operación destructiva y pedirle el flag al usuario es una manera de asegurar que el usuario sabe lo que hace al tener que tomarse ese trabajo extra. Cuando se usa un argumento posicional, se puede usar el método index() para especificar la posición de ese argumento en relación a otros. En realidad, después me he enterado de que puedes dejar el método index() y en ese caso el índice se asignará según el orden de evaluación dentro del código. Precisamente lo que te permite index() e poder saltarte ese orden que sale de la evaluación del código.

Cuando se usa takes_value(true) oen un argumento, el valor facilitado desde consola se guarda en una clave llamada como el argumento. Por ejemplo, el  takes_value(true) de la línea 290 hace que el valor facilitado por el usuario se guarde en una clave llamada dictionary_name. Si estás usando un flag opcional sin valor (es decir un flag booleano), puedes usar takes_value(false) o sencillamente omitirlo.

La llamada al método value_name() es equivalente al parámetro metavar del ArgParse de Python. Te permite definir la cadena que representará al parámetro cuando se llame a la ayuda con el parámetro --help.

Lo raro es que los argumentos no usan el método about() para definir sus cadenas de ayuda, sino el método help().

Puedes ver cómo se define un argumento opcional de la línea 292 a la 298. Allí, la versión larga de flag se define con el método long() y la corta con el método short(). En este ejemplo, el argumento puede llamarse tanto como "--initial_words_file <PATH_TO_FILE_WITH_WORDS>" como "-i <PATH_TO_FILE_WITH_WORDS>".

Puede resultarte útil llamar al método validator(), ya que define una función que se aplicará sobre los argumentos facilitados para asegurar que cumplen las condiciones de lo que se espera recibir. La función que se utilice como validador deberá ser capaz de recibir un argumento string y deberá retornar un Result<(), String>. Si haces tus comprobaciones y encuentras correcto el argumento entonces debes retornar un Ok(()) o, en caso contario, un Err("Here you write your error message").

Encadenando métodos en argumentos anidados y en subcommandos puedes definir un árbol entero de comandos. Una vez que hayas acabado tienes que hacer una última llamada al método get_matches_from() del parser raíz. Para ello, debes pasarle al método un vector de con los strings de cada uno de los argumentos del comando:



Normalmente le pasarás el vector de strings que devuelve la función args(), el cual retorna un vector con cada argumento dado por el usuario al llamar a la aplicación desde la consola: 



Fíjate en que mi función main() está casi vacía. Eso es porque de esa manera puedo llamar a _main() (fíjate en el guión bajo) desde mis tests de integración, metiendo mi propio vector de argumentos para simular a un usuario llamando a la aplicación desde la consola.

los que get_matches_from() devuelve es un tipo ArgMatches. Ese tipo devuelve el valor de cada argumento si se le especifica el nombre de una clave. De las líneas 149 a 245 he implementado un método para crear un tipo Configuration usando el contenido del ArgMatches. 

Ten en cuenta que cuando sólo tienes un parser puedes obtener los valores usando el método value_of(). Pero si tienes subcomandos perimero tienes que conseguir el ArgMatches específico para esa rama de subcomando, haciendo una llamada al método subcommand_matches():


En el último ejemplo puede ver la manera de trabajar normal:
  • Profundizas obteniendo el ArgMatches de la rama en la que estás interesado. (líneas 160-161)
  • Una vez que tienes el ArgMatches que quieres, utilizas value_of() para conseguir el valor de un argumento concreto. (línea 164)
  • Para parámetros opcionales, puedes usar el método is_present() para comprobar si se facilitó el parámetro o no. (línea 165). 
Mediante estos métodos, puedes recuperar los valores de los argumentos facilitados por el usuario y construir la configuración necesaria para ejecutar tu aplicación.

Como puedes ver, puedes hacer un parser realmente potente con clap, con la misma funcionalidad que ofrecía ArgParse en Python, pero en este caso en un entorno de desarrollo con Rust.

15 marzo 2023

Cómo usar GitHub Actions para integración y despliegue continuos


En un artículo anterior ya expliqué cómo usar Travis CI para aplicar integración continua en tus proyectos. El problema con Travis es que ha cambiado sus términos de uso y se ha vuelto bastante incómodo para aplicarlo en proyectos open source. Ellos siguen diciendo que son gratuitos para proyectos open source pero en realidad tienes que suplicar por los créditos gratuitos cada vez que se agotan y te hacen probar que sigues cumpliendo con lo que entienden por un proyecto open source (he leído casos de desarrolladores que fueron descartados por el mero hecho de tener activados los GitHub Sponsors).

Por eso, he acabado buscando alternativas para mis proyectos. Dado que ya uso GitHub para mis proyectos open source, es natural probar su sistema de integración y despliegue continuos: GitHub Actions.

Conceptualmente, GitHub Actions es similar a Travis CI, por eso no voy a repetir los conceptos que desarrollé en el artículo de Travis CI.  Si quieres repasar esos conceptos y las razones para aplicar integración y despliegue continuos (CI/CD) puedes leer el artículo que he enlazado al comienzo de este. Por eso, vamos a enfocarnos en cómo usar GitHub Actions.

Como pasaba en Travis CI, todo lo que haces en GitHub Actions gira en torno a los ficheros yaml que vas creando en la carpeta .github/workflows de tu repositorio. GitHub buscará en esa carpeta los flujos de CI/CD que tiene que ejecutar. Cada uno de esos ficheros define un flujo de trabajo (workflow). Puedes tener múltiples flujos de trabajo para ser ejecutados como respuesta a diferentes eventos de la plataforma de GitHub, como pushes sobre determinadas ramas, pull requests recibidos, altas de nuevas incidencias de usuario (issues) y un largo etcétera.

La diferencia es que encuentro GitHub Actions bastante más cómodo y potente que Travis CI. Lo que diferencia a GitHub Action es que facilita la reutilización de componentes de manera masiva. Cada paso se puede encapsular y compartir entre flujos diferentes de trabajo o incluso con otros desarrolladores en GitHub para que lo apliquen a sus respectivos flujos de trabajo. Con tanta gente desarrollando y compartiendo en GitHub lo normal usar las tareas de otros (los que se denomina Actions), incluso más que implementarlas por uno mismo. A no ser que estés automatizando algo realmente raro, lo normal es que alguien lo haya implementado ya y lo haya compartido. En este artículo usaremos las acciones de otros desarrolladores e implementaremos nuestros propios pasos. Además, otra ventaja de GitHub Actions es que puedes reutilizar tus propios flujos de trabajo, o compartirlos con otros, de manera que no tengas que implementarlos desde cero en otros flujos de trabajo.

En este artículo nos enfocaremos al típico flujo de "probar -> empaquetar -> distribuir" y lo llamaremos  test_and_deploy.yaml (puedes llamarlo como quieras si te sientes creativo, pero intenta ser expresivo). Tienes el código fuente empleado en este artículo en este commit de mi proyecto cifra-rust en GitHub.

Para crear ese fichero yaml tienes dos opciones: crearlo en tu IDE y hacerle push como a cualquier otro fichero o bien crearlo usando el editor web integrado con GitHub. Para tu primera vez mi consejo es que uses el editor web, ya que te guía mejor a la hora de hacer funcionar tu primer fichero yaml. Teniendo en cuenta lo anterior, ve a tu repositorio en GitHub y pincha sobre la pestaña de Actions:


Allí, se te ofrece crear tu primer flujo, cuando aceptas se te ofrece usar una plantilla predefinida como punto de partida (GitHub tiene muchísimas, para diferentes tareas y lenguajes) o bien crear un flujo desde cero. Por aquello de aprender, nosotros vamos a elegir la opción de partir desde cero ("set up a workflow yourself"). Será entonces cuando entraremos al editor web. El flujo estará relleno con algo muy básico.

Ahora vamos a analizar el fichero yaml de Cifra-rust para ver lo que podemos hacer con las GitHub Actions.


Cabecera

En las primeras líneas (de la 1 a la 12) puedes ver el nombre de este flujo (en la etiqueta "name"). Utiliza un nombre expresivo, que identifique de un vistazo lo que hace el flujo. Este nombre será útil también para reutilizar este flujo en otros repositorios.

La etiqueta "on" define qué eventos disparan este flujo. En mi caso el flujo se dispara con los pushes y pull requests sobre mi rama staging. Hay muchos más eventos que puedes usar.

La etiqueta "workflow_dispatch" te permite disparar manualmente un flujo desde el interfaz web de GitHub. Suelo meterlo en todos mis flujos, no hace daño contar con esa opción.



Trabajos (Jobs)

Lo siguiente es la etiqueta "jobs" (línea 15) que es justo donde comienza el meollo de los flujos. Un flujo está compuesto de jobs. Cada job se ejecuta en una máquina virtual diferente (el runner) de manera que sus respectivas dependencias quedan encapsuladas. Esa encapsulación es buena para evitar que las dependencias de un job estropeen las dependencias y el sistema de ficheros de los otros jobs. Mi consejo es que intentes que cada job se centre en una única tarea (sí, aquí también aplica el principio de maximizar la cohesión).

Por defecto los jobs se ejecutan en paralelo a menos que se fijen dependencias explícitas entre ellos. Si necesitas que un job B se ejecute después de que el job A se finalice correctamente lo que tienes que hacer es usar la etiqueta "needs" para fijar que B necesita que A se complete correctamente para comenzar. Por ejemplo, en Cifra-rust los trabajos "merge_staging_and_master", "deploy_crates_io" y "generate_deb_package" necesitan que el trabajo "tests" finalice correctamente para que ellos puedan comenzar.  Puedes ver un ejemplo del uso de la etiqueta "needs" en la línea 53:

Como "deploy_debian_package" necesita respectivamente que "generate_deb_package" finalice antes, al final acabas con un árbol de ejecución como el siguiente:


Actions

Cada job se compone de uno o más pasos (steps). Un step es una secuencia de comandos de shell. Estos pueden comandos pueden ser los nativos del sistema operativo sobre el que estés ejecutando el job o bien scripts que hayas incluido en tu repositorio. De la línea 112 a la 115 podemos ver uno de estos steps:

En el step anterior estamos llamando a un script de la carpeta ci_scripts del repositorio. Fíjate en el pipe ("|") junto a la etiqueta "run". Sin ese pipe sólo podrías incluir un comando junto a la etiqueta, pero gracias a él se pueden meter varios comandos separados en líneas independientes (como en los steps de las líneas 44 a 46).

Si te encuentras repitiendo los mismos comandos en flujos diferentes entonces deberías pensar en encapsular esos comandos en una action. Como te puedes imaginar. las Actions son la clave de las GitHub Actions. Una action es un conjunto de comandos empaquetados para ser compartidos y reutilizados en diferentes flujos de trabajo (tuyos o de otros usuarios). Una action tiene entradas (inputs) y salidas (outputs), pero lo que pasa dentro de ella no es tu problema siempre y cuando funcione como se le supone. El cómo desarrollar tus propias actions y compartirlas con otros merece su propio artículo. En el próximo artículo convertiré ese step para generar el manpage en una action que pueda ser reutilizada.

A la derecha, el editor web de GitHub Actions tiene un buscador para encontrar actions útiles para la tarea que queramos hacer. Supongamos que quieres instalar el toolchain de Rust, en ese caso podemos hacer esta búsqueda:

Aunque el editor web de GitHub es realmente completo y sirve para encontrar errores en los ficheros yaml, su buscador carece de una manera de filtrar y reordenar sus resultados. Aún así es tu mejor opción para encontrar actions de otros para tus flujos.

Una vez que haces click en cualquier resultado del buscador, se te muestra un resumen acerca de que texto incluir en tu fichero yaml para usar esa action. Puedes conseguir una información aún más detallada haciendo click en el enlace "View full Marketplace listing".

Como con los step, una action usa una etiqueta "name" para describir lo que se supone que va a hacer la tarea, así como una "id" si ese action debe ser referenciado desde otros puntos del flujo de trabajo (mira por ejemplo la línea 42). Lo que diferencia a un step de una action es que este último utiliza la etiqueta "uses". Esa etiqueta referencia la action que queremos usar. El texto a usar en esa etiqueta difiere para cada action, pero puedes averiguar qué escribir allí consultando las instrucciones que salen en los resultados de la búsqueda para esa action. En esas instrucciones se suele describir las entradas que acepta la action. Esas entradas se incluyen con la etiqueta "with". Por ejemplo, en las líneas 23 a 27 usé una action para instalar el framework de compilación de Rust: 

Como puedes ver, en la etiqueta "uses" puedes decir qué versión de la action usar. Hay wildcards para usar la última versión pero lo mejor es usar una versión específica para evitar que los flujos se rompan por actualizaciones de los actions.

Los jobs se componen de actions encadenados en forma de pasos. Los pasos de un job se ejecutan secuencialmente. Si cualquiera de ellos falla, el job completo fracasa.


Compartir datos entre pasos y flujos

Aunque los steps de un flujo se ejecutan en la misma máquina virtual no pueden compartir variables de entorno porque cada step lanza un proceso de bash diferente. Hay dos maneras de fijar una variable de entorno en un step que necesite ser usado en otro step del mismo job:

  • Rápido y sucio: Concatenar la variable de entorno a la variable $GITHUB_ENV de manera que esa variable pueda ser accedida después usando el contexto env. Por ejemplo, en la línea 141 creamos la variable de entorno:

Esa variable de entorno es accedida en la línea 146, en el step siguiente:


  • Fijar el output del step: El problema con el último método es que aunque te permite compartir datos entre steps no te permite hacerlo entre jobs diferentes. Fijar los outputs de un step es algo más lento pero deja a tu step preparado para compartir datos no sólo con otros steps del mismo job, sino con steps de cualquier workflow. Para configurar una variable de entorno como el output de un step hace falta pasar el valor de esa variable a ::set-output y darle nombre a esa variable, seguida de su valor tras una pareja de dos puntos ("::"). Tienes un ejemplo de cómo hacerlo en la línea 46:
 
Fíjate en que ese step debe ser identificado con una etiqueta "id" para poder recuperar luego la variable compartida. Como ese step se identifica como "version_tag", la variable creada como "package_tag" puede ser recuperada luego desde otro step del mismo job usando:

${{ steps.version_tag.outputs.package_tag }}

En realidad, ese método se usa en la línea 48 para preparar esa variable para ser recuperada desde otro job. Recuerda que hasta ahora hemos visto que ese método sirve para pasar datos entre los steps de un mismo job. Para exportar datos desde un job, para ser usados desde otro job, tienes que declararlo primero como un output del job (líneas 47-48):


Fíjate en que en la última captura de pantalla, el nivel de indentación debe estar al mismo nivel que la etiqueta "steps" para que se pueda configurar a package_tag como un output del job.

Para recuperar ese output desde otro job, el job de destino debe declarar al job de origen en su etiqueta "needs". Después de eso, el valor puede ser recuperado usando el siguiente formato:

${{ needs.<needed_job>.outputs.<output_varieble_name> }} 

En nuestro ejemplo, "deploy_debian_package" necesita el valor exportado en la línea 48, por eso declara su job (test) como una dependencia en la línea 131:

Después de eso, ya puede leer esa variable en la línea 157:



Pasando ficheros entre jobs

A veces, pasar una variable no es suficiente porque necesitas producir ficheros en un job para que luego sean consumidos en otro job.

Puedes compartir ficheros entre steps de un mismo job porque esos steps comparten la misma máuina virtual. Pero entre jobs necesitas trasferir ficheros desde una máquina virtual a otra.

Cuando generas un fichero (un artefacto) en un job, puedes subirlo a un almacenamiento temporal compartido, para permitir que otros jobs del mismo flujo tengan acceso a ese artefacto. Para subir y descargar un artefacto hacia/desde ese almacenamiento temporal tienes dos actions predefinidas: upload-artifact and download-artifact.

En nuestro ejemplo, el job "generate_deb_package" genera un paquete debian requerido por "deploy_debian_package". Por eso, en las líneas 122 a 126 "generated_deb_package" sube ese paquete:

En el otro lado "deploy_debian_package" descarga el artefacto salvado en las líneas 132 a 136:



Usando el repositorio de código fuente

Por defecto empiezas cada job con una máquina virtual limpia. Para hacer que tu código fuente se descargue en cada máquina virtual tienes que utilizar una action denominada checkout. En nuestro ejemplo se usa como el primer step del job "tests" (línea 21) para hacer que el código se compile y se pruebe: 

Puedes hacer que se descargue el código de cualquier rama, pero si no especificas ninguna en concreto se descargará el de la rama que haya disparado el evento para arrancar el job. En nuestro ejemplo, esa rama es la de staging.


Ejecutando nuestro flujo

Tienes dos opciones para probar un flujo: puedes provocar el evento configurado para disparar el flujo (en nuestro ejemplo hacer push a la rama de staging) o se puede lanzar el flujo manualmente desde el interfaz web de GitHub (suponiendo que hayas incluido la etiqueta "workflow_dispatch" en tu fichero yml, como te aconsejé).

Para lanzar el flujo manualmente, ve a la pestaña Actions del repositorio y selecciona el flujo que quieres lanzar. Entonces verás el botón "Run workflow" a mano derecha:


Una vez que ya has pulsado el botón, el flujo comenzará y la lista de la pestaña mostrará que el flujo está activo. Si seleccionamos ese flujo en la lista se mostrará un árbol de ejecución como el que mostré antes. Haciendo click en cualquiera de las cajas mostrará los logs generados en tiempo real para cada paso del job. Es extremadamente fácil navegar entre los diferentes logs generados.


Conclusión

La verdad es que he encontrado las GitHub Actions muy agradables de usar. Su enfoque a la reutilización y a compartir las actions hace realmente fácil crear flujos extremadamente complejos, dado que a menos que estés desarrollando flujos realmente raros, lo más probable será que la mayor parte (si no todos) de los componentes que vayas a necesitar ya hayan sido implementados y compartidos por otros, en forma de actions. De esta manera hacer flujos complejos se convierte en una tarea fácil de unir piezas ya disponibles.

La documentación de GitHub Actions es realmente buena y su popularidad facilita encontrar respuestas online para cualquier duda que pudiera surgirte.

Además, la estructura de los fichero yml me ha parecido coherente y lógicas, por lo que es fácil coger los conceptos y alcanzar un buen nivel realmente rápido.

Siendo gratuito e ilimitado para repositorios open source, sospecho que voy a migrar todos mis flujos de CI/CD desde Travis a GitHub Actions.

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.