Instalación y distribución
El mapa de destinos de meson install y el papel de DESTDIR, la generación de pkg-config con SONAME y visibilidad controlada, el tarball verificado de meson dist y el modelo de tres máquinas de la compilación cruzada.
Compilar es la parte fácil. El momento en que un proyecto deja de ser tuyo y pasa a ser de otros es la instalación: a partir de ahí hay binarios en directorios que no controlas, headers que alguien incluirá durante años y un conjunto de símbolos que se convierte en una promesa difícil de retirar. Meson trata esa transición como lo que es, un problema de contratos, y te da las tres herramientas que lo hacen tratable: un mapa de destinos configurable, metadatos legibles por máquina y un modelo explícito de para qué máquina estás construyendo.
- Declarar qué se instala y dónde, y usar
DESTDIRpara preparar paquetes sin tocar el sistema. - Generar un archivo de pkg-config correcto y versionar la biblioteca con SONAME y visibilidad de símbolos.
- Producir un tarball reproducible con
meson disty entender qué verifica antes de dártelo. - Configurar una compilación cruzada distinguiendo las máquinas de construcción, anfitriona y objetivo.
Instalar: el mapa de destinos
Cada target decide si se instala, y Meson deduce el destino a partir de su tipo y del árbol de directorios estándar.
executable('mates-cli', 'src/cli.c',
dependencies: mates_dep,
install: true,
install_rpath: '$ORIGIN/../lib')
library('mates', fuentes,
install: true,
version: meson.project_version(),
soversion: '1')
install_headers('include/mates.h', 'include/mates_vector.h', subdir: 'mates')
install_data('data/tablas.bin', install_dir: get_option('datadir') / 'mates')
install_man('doc/mates.1')
Los destinos salen de opciones integradas que el usuario puede redefinir: prefix, bindir, libdir, includedir, datadir, sysconfdir. Un empaquetador de distribución configurará algo parecido a -Dprefix=/usr -Dlibdir=lib64 -Dsysconfdir=/etc, y tu proyecto obedecerá sin que hayas escrito ni una ruta absoluta. Escribirlas a mano es el error que convierte un proyecto en impaquetable.
La instalación de verdad casi nunca se hace sobre el sistema vivo:
meson install -C build --dry-run # que haria, sin hacerlo
DESTDIR=/tmp/paquete meson install -C build # instalacion escalonada
meson install -C build --tags devel # solo headers y archivos de desarrollo
meson install -C build --only-changed # no toca lo que ya esta al dia
DESTDIR antepone un prefijo a todos los destinos sin alterar las rutas que quedan grabadas dentro de los binarios. Esa distinción es la esencia del empaquetado: el binario cree que vive en /usr/lib porque ahí vivirá, pero los archivos aparecen bajo /tmp/paquete/usr/lib para que la herramienta de empaquetado los recoja. Confundir DESTDIR con prefix produce paquetes que buscan sus datos en el directorio temporal del constructor.
Las etiquetas de instalación separan lo que necesita quien ejecuta de lo que necesita quien compila contra tu biblioteca, y son lo que permite a las distribuciones partir un proyecto en el paquete normal y el paquete de desarrollo sin listas de archivos escritas a mano.
Queda un detalle que produce fallos misteriosos: el RPATH. Durante el desarrollo, Meson graba en los ejecutables una ruta de búsqueda apuntando al directorio de build, para que puedas ejecutarlos sin instalar nada. Al instalar la elimina, porque esa ruta no existirá en la máquina destino. Si tu binario necesita encontrar bibliotecas en una ubicación relativa a sí mismo, eso se declara con install_rpath y el token $ORIGIN, que el enlazador dinámico resuelve en ejecución.
Publicar una biblioteca: pkg-config, SONAME y visibilidad
Instalar los archivos no basta: hay que decirle al mundo cómo consumirlos. El módulo de pkg-config genera el .pc a partir de lo que ya sabe del target.
pkg = import('pkgconfig')
pkg.generate(mates,
name: 'mates',
filebase: 'mates',
description: 'Algebra lineal minima para sistemas empotrados',
version: meson.project_version(),
subdirs: 'mates',
requires: ['zlib'],
requires_private: ['libm'])
Pasar el objeto del target en lugar de escribir las rutas a mano es lo que mantiene el archivo sincronizado: si mañana cambias libdir o el nombre de la biblioteca, el .pc cambia con él. La separación entre requires y requires_private codifica una diferencia real: lo privado solo se emite cuando alguien enlaza estáticamente, porque en enlazado dinámico esas dependencias las arrastra tu propia biblioteca. Ponerlo todo en requires obliga a tus usuarios a arrastrar flags que no necesitan.
version y soversion
version es la versión completa; soversion es la del ABI y solo cambia cuando rompes compatibilidad binaria.
Visibilidad oculta
Con gnu_symbol_visibility: 'hidden' nada se exporta salvo lo que marques. Enlaza más rápido y fija tu superficie.
El archivo pc
Es el contrato legible por máquina: nombre, versión, flags y dependencias transitivas en un formato de texto.
Etiquetas de instalación
Separan el paquete de ejecución del de desarrollo sin listar archivos a mano.
El par version y soversion produce en Linux la cadena habitual: el archivo real con la versión completa, un enlace con la versión de ABI que es el nombre grabado dentro de los binarios que enlazan contigo, y un enlace sin número que solo usa el enlazador al construir. La regla es simple de enunciar y cara de incumplir: mientras no elimines ni cambies la firma de una función exportada, ni alteres el tamaño o la disposición de una struct pública, soversion no se toca. En cuanto lo hagas, debe subir, porque los binarios ya compilados contra la versión anterior deben seguir encontrando la biblioteca vieja.
La visibilidad es la otra mitad del contrato. Por defecto, en ELF todos los símbolos externos de tu biblioteca son visibles, incluidas las funciones auxiliares que nunca pensaste publicar. Compilar con visibilidad oculta invierte el criterio y te obliga a marcar de forma explícita lo que exportas, normalmente con una macro en el header público que se expande a __attribute__((visibility("default"))). La ganancia no es solo higiénica: reduce el tamaño de la tabla de símbolos, acelera la carga dinámica y evita que alguien acabe dependiendo de una función interna que después no puedes cambiar.
meson dist: el tarball que se comprueba a sí mismo
meson dist -C build
meson dist -C build --formats xztar,gztar,zip
meson dist -C build --include-subprojects
meson dist -C build --no-tests # rapido y desaconsejado
Lo interesante de meson dist no es que comprima archivos sino lo que hace antes de entregártelos. Consulta el control de versiones para saber qué está realmente bajo seguimiento, de modo que ningún archivo generado ni residuo local se cuela en el tarball. Luego lo extrae en un directorio limpio, lo configura, lo compila y ejecuta la suite de tests sobre esa copia. Solo si todo pasa produce el archivo y su suma de verificación.
Ese ciclo detecta la clase de error más embarazosa que existe en la distribución de software: el archivo que olvidaste añadir al repositorio y que compila en tu máquina porque lleva meses ahí. Es imposible de detectar compilando en tu árbol de trabajo, y es trivial de detectar compilando en un árbol recién extraído.
Compilación cruzada: tres máquinas, no dos
La mayoría de los sistemas de build tratan la compilación cruzada como una excepción. Meson la trata como el caso general del que la compilación nativa es una simplificación, y para eso nombra tres máquinas.
flowchart TB A[Maquina de construccion: donde corre meson y el compilador] B[Maquina anfitriona: donde correra el binario que produces] C[Maquina objetivo: solo si lo que produces es otro compilador] A -->|compila para| B B -->|generaria codigo para| C A -.herramientas nativas del propio build.-> A
En una compilación normal las tres coinciden y por eso nadie las distingue. En cuanto dejan de coincidir, todo se vuelve ambiguo: cuando tu build necesita un generador de código escrito en C, ese generador debe compilarse para la máquina de construcción, mientras el resto se compila para la anfitriona. Meson resuelve la ambigüedad con la clave native: true en el target correspondiente.
La descripción de la máquina anfitriona vive en un archivo aparte, fuera del proyecto:
[binaries]
c = 'aarch64-linux-gnu-gcc'
ar = 'aarch64-linux-gnu-ar'
strip = 'aarch64-linux-gnu-strip'
pkg-config = 'aarch64-linux-gnu-pkg-config'
exe_wrapper = 'qemu-aarch64-static'
[built-in options]
c_args = ['-march=armv8-a']
c_std = 'c23'
[properties]
sys_root = '/opt/sysroot-arm64'
pkg_config_libdir = '/opt/sysroot-arm64/usr/lib/pkgconfig'
[host_machine]
system = 'linux'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
meson setup build-arm --cross-file cross/aarch64.ini
meson compile -C build-arm
meson test -C build-arm # funciona si hay exe_wrapper
Dos entradas merecen atención especial. pkg_config_libdir reorienta la búsqueda de dependencias hacia el sysroot, sin lo cual Meson encontraría alegremente las bibliotecas de tu máquina y produciría un binario que no enlaza en ningún sitio. Y exe_wrapper declara cómo ejecutar un binario de la arquitectura ajena, normalmente mediante un emulador: con él puedes correr la suite de tests de una compilación cruzada, y sin él Meson te dirá con toda claridad que no puede.
Esa honestidad es el punto de diseño más fino de todo el sistema. Las comprobaciones de compilador que necesitan ejecutar un programa —averiguar el tamaño de un tipo, comprobar el orden de bytes— son imposibles al compilar en cruzado sin emulador. Otros sistemas las ejecutan de todos modos en la máquina equivocada y producen resultados falsos silenciosamente. Meson se niega, y te ofrece responderlas por adelantado en la sección [properties] del archivo, que es la única forma correcta: si la respuesta no se puede medir, tiene que declararse.
Existe una asimetría casi cruel entre la facilidad de exportar un símbolo y la dificultad de dejar de hacerlo. Añadir una función a una biblioteca cuesta una línea; retirarla cuesta una versión mayor, una nota de migración y la ruptura de todo el software ya compilado que la usaba, que en el caso de una biblioteca del sistema puede ser software cuyo código fuente nadie tiene. Por eso la visibilidad de símbolos, que parece un detalle de higiene, es en realidad una decisión de gobernanza: cuando dejas la visibilidad por defecto estás publicando cada función auxiliar de tu implementación, y a partir de ese momento alguien puede depender de ella sin que ni él ni tú lo sepáis, hasta que la cambies y algo remoto se rompa. La versión con visibilidad oculta y una macro de exportación no es más trabajo por gusto: es la forma de que tu superficie pública sea una lista que alguien escribió a propósito en lugar de un accidente de la compilación. Lo mismo vale para el SONAME, que es la única manera que tiene el sistema de saber que la promesa cambió, y para el archivo de pkg-config, que convierte el contrato en algo que una máquina puede leer y verificar. El patrón se repite muy lejos de C: en los campos de un endpoint que nadie documentó pero que un cliente ya consume, en la columna de una base de datos que un informe empezó a leer, en el formato de un log que se volvió la entrada de otro sistema. Publicar algo es transferir a otros el derecho a depender de ello. La disciplina que enseña una biblioteca en C es que ese derecho conviene concederlo de forma explícita, escrita y numerada, porque revocarlo después no depende de ti.
- Marca targets, headers y datos con
install: truey sus funciones, y ejecutameson install --dry-runpara revisar el mapa completo de destinos. - Instala en un directorio escalonado con
DESTDIRy examina el árbol resultante. Comprueba que ninguna ruta temporal quedó grabada dentro de los binarios. - Genera el
.pccon el módulo de pkgconfig y verifica desde otro proyecto quedependency()encuentra tu biblioteca usandoPKG_CONFIG_PATH. - Compila con
gnu_symbol_visibility: 'hidden', lista los símbolos exportados y compara la tabla con la de una compilación sin ocultar. Explica qué habías estado publicando sin querer. - Escribe un archivo cruzado para otra arquitectura, configura con
--cross-filey ejecuta la suite medianteexe_wrapper. Quita elexe_wrappery observa qué te dice Meson.