wandres.dev
CONSTRUCTOR · Build con Meson

Dependencias sin dolor

La cascada de búsqueda de dependency, el protocolo pkg-config que la sostiene, los subproyectos con wraps que rellenan los huecos y las políticas que deciden quién gana cuando la biblioteca existe en dos sitios a la vez.

⏱ 18 min

Resolver dependencias en C ha sido durante décadas el trabajo más ingrato del oficio: rutas de include cableadas a mano, flags de enlazado copiados de un foro, bibliotecas que existen con tres nombres distintos según el sistema. Meson no inventa una solución nueva; hace algo más útil, que es unificar bajo una sola función todos los mecanismos que ya existían y añadir un plan B cuando ninguno responde. El resultado es que dependency() casi siempre acierta, y cuando falla te dice exactamente por qué.

🎯 Al terminar esta lección sabrás
  • Recorrer la cascada de métodos que dependency() prueba y saber forzar uno concreto.
  • Leer y escribir archivos de pkg-config, y entender por qué son el sustrato de todo el sistema.
  • Declarar subproyectos con archivos wrap y encadenarlos como reserva automática.
  • Elegir una política de resolución consciente entre usar el sistema y empaquetar las fuentes.

La cascada de dependency

Una sola llamada esconde media docena de estrategias probadas en orden:

zlib = dependency('zlib', version: '>=1.2.11')
hilos = dependency('threads')
sqlite = dependency('sqlite3', required: false)
cripto = dependency('libcrypto', required: get_option('tls'))

executable('servidor', 'main.c',
    dependencies: [zlib, hilos, cripto])

Meson consulta primero pkg-config, que es el mecanismo dominante en sistemas tipo Unix. Si no encuentra nada, prueba los archivos de configuración de CMake, que muchas bibliotecas modernas instalan además del .pc. Después recurre a los detectores integrados que la propia herramienta trae escritos para casos que ningún protocolo cubre bien: hilos, OpenMP, Boost, LLVM, Qt, las bibliotecas de sistema de macOS. Por último, si existe un archivo wrap que lo provea, cae al subproyecto.

flowchart TB
A[dependency con nombre y version] --> B{pkg-config responde}
B -->|Si| Z[Objeto dependencia encontrado]
B -->|No| C{Archivo de configuracion de CMake}
C -->|Si| Z
C -->|No| D{Detector integrado disponible}
D -->|Si| Z
D -->|No| E{Existe wrap que la provee}
E -->|Si| F[Se compila como subproyecto]
F --> Z
E -->|No| G{Marcada como requerida}
G -->|No| H[Objeto no encontrado y build continua]
G -->|Si| X[Error de configuracion]

Cuando esa cascada acierta por el camino equivocado, se acota con method:

gl = dependency('gl', method: 'pkg-config')
llvm = dependency('llvm', method: 'config-tool', version: '>=17.0')
libm = meson.get_compiler('c').find_library('m', required: false)

find_library() es la salida para las bibliotecas que no publican metadatos de ninguna clase: busca el archivo por nombre en las rutas del enlazador y no sabe nada de versiones ni de flags. Es el último recurso, y conviene tratarlo como tal.

El argumento required cambia el contrato: con false, un fallo no detiene nada y devuelve un objeto no encontrado que sigue siendo pasable a un target, donde simplemente no aporta nada. De ahí que el idioma correcto sea preguntar y ramificar, no asumir:

if sqlite.found()
    conf.set10('CON_SQLITE', true)
    fuentes += files('src/persistencia_sqlite.c')
else
    fuentes += files('src/persistencia_memoria.c')
endif

Encadenar required: get_option('tls') con una opción de tipo feature es la forma más limpia de exponer esa decisión al usuario: los tres estados de la opción se traducen directamente a los tres comportamientos de la búsqueda, sin escribir un solo if.

pkg-config: el protocolo que sostiene todo

Debajo de la abstracción hay un formato de texto trivial. Un archivo .pc instalado en /usr/lib/pkgconfig o /usr/share/pkgconfig describe cómo consumir una biblioteca:

prefix=/usr
libdir=${prefix}/lib
includedir=${prefix}/include

Name: zlib
Description: zlib compression library
Version: 1.3.1
Libs: -L${libdir} -lz
Cflags: -I${includedir}

La herramienta pkg-config no hace más que leer esos archivos, resolver las variables y concatenar cadenas, pero resuelve el problema difícil: el cierre transitivo. El campo Requires encadena dependencias, de modo que preguntar por una biblioteca devuelve también los flags de todo lo que ella necesita, en el orden correcto y sin duplicados.

pkg-config --cflags --libs zlib          # los flags de esta biblioteca
pkg-config --print-requires libcurl      # sus dependencias declaradas
pkg-config --modversion openssl          # version instalada
PKG_CONFIG_PATH=/opt/mi_lib/lib/pkgconfig meson setup build

PKG_CONFIG_PATH es la palanca que resuelve el noventa por ciento de los casos raros: una biblioteca instalada en un prefijo no estándar deja de ser invisible en cuanto le indicas dónde vive su .pc. Y Requires.private frente a Requires codifica una distinción real: lo privado solo hace falta al enlazar estáticamente, porque en enlazado dinámico esas dependencias las arrastra la propia biblioteca. Entender esa diferencia explica por qué un build estático pide de pronto veinte flags que el dinámico no necesitaba.

Subproyectos y wraps

Cuando la biblioteca no está en el sistema, Meson puede compilarla desde las fuentes como parte de tu árbol. La descripción vive en subprojects/nombre.wrap:

[wrap-file]
directory = zlib-1.3.1
source_url = https://zlib.net/zlib-1.3.1.tar.gz
source_filename = zlib-1.3.1.tar.gz
source_hash = 9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
patch_filename = zlib_1.3.1-1_patch.zip
patch_hash = 0e9c1a0b52a9b7ce49b9e14e5b9e9d19d5a0b6a70e2f1f2b7de62d67a2e1a04d

[provide]
zlib = zlib_dep

La sección [provide] es la pieza que hace que todo esto sea invisible: declara que este subproyecto satisface el nombre zlib y expone su resultado en la variable zlib_dep. Con ella presente, tu dependency('zlib') original no cambia ni una letra; simplemente adquiere una reserva. La variante [wrap-git] sustituye la descarga de un tarball por un clon con revisión fijada, lo que resulta más cómodo durante el desarrollo y menos reproducible a largo plazo, porque una etiqueta de git se puede mover y un hash de tarball no.

El registro público WrapDB evita escribir esos archivos a mano:

meson wrap list                 # que hay disponible
meson wrap search sqlite
meson wrap install zlib         # escribe subprojects/zlib.wrap
meson subprojects update        # actualiza los ya descargados

Del lado del subproyecto, la única obligación es publicar un objeto de dependencia con declare_dependency(), exactamente igual que harías para consumo interno. Esa simetría es la razón de que casi cualquier proyecto que use Meson sea usable como subproyecto sin modificarlo: no hay una interfaz especial para ser dependencia, es la misma que ya usabas dentro de casa.

Conviene conocer dos comportamientos que sorprenden. El primero es que las opciones del subproyecto se fijan con el nombre cualificado, -Dzlib:tests=false, y que sus default_options nunca pisan las tuyas. El segundo es que Meson promociona subproyectos anidados: si dos dependencias tuyas traen cada una su copia de la misma biblioteca, puedes elevarla a subprojects/ con meson subprojects promote y ambas compartirán una sola instancia, que es la única forma de evitar dos copias del mismo símbolo en el binario final.

Políticas de resolución: quién decide

La existencia de dos orígenes posibles para la misma biblioteca convierte la resolución en una decisión de política, y Meson la expone como tal en la línea de órdenes en vez de esconderla en el código.

meson setup build --wrap-mode=nofallback      # solo el sistema, nunca descargar
meson setup build --wrap-mode=forcefallback   # siempre subproyectos, ignorar el sistema
meson setup build --wrap-mode=nodownload      # usa lo ya descargado, sin red
meson setup build --force-fallback-for=zlib   # una sola excepcion

Las dos posturas extremas tienen defensores legítimos y contextos donde son correctas. Un empaquetador de distribución exige nofallback: su trabajo consiste precisamente en que exista una copia de zlib en el sistema, parcheada y actualizada de forma centralizada, y una copia embebida en tu binario es un fallo de seguridad esperando a que alguien publique un aviso. Un equipo que produce binarios reproducibles para clientes prefiere forcefallback: quiere el mismo bit a bit en todas partes y no depender de qué versión trae cada máquina.

🏛️

Del sistema

Una copia compartida, parches de seguridad centralizados, binarios más pequeños. A cambio, versiones que no controlas.

📦

Subproyecto

Versión exacta y reproducible en cualquier máquina. A cambio, eres tú quien responde de cada aviso de seguridad.

Toda dependencia es un contrato que alguien tendrá que renegociar

La tentación al descubrir los wraps es declarar el problema resuelto: escribes cuatro líneas, la biblioteca aparece, el build funciona en cualquier máquina. Y algo real has ganado, porque has convertido en datos versionados lo que antes era una instrucción en un README que decía instala estas siete cosas. Pero no has eliminado la dependencia, has cambiado quién la administra, y con ella todo el trabajo que arrastra: cuando aparezca un desbordamiento explotable en esa biblioteca, la copia del sistema la arregla el mantenedor de la distribución mientras duermes y la copia embebida en tu binario la arreglas tú, si te enteras. Por eso las distribuciones libran esta batalla desde hace treinta años y por eso los ecosistemas que resolvieron la comodidad primero terminaron reinventando los avisos de seguridad, los ficheros de bloqueo y los árboles de dependencias auditables que Unix tenía desde el principio con menos ceremonia. Lo que Meson hace bien no es escoger un bando sino negarse a escogerlo: el mismo dependency('zlib') sirve a los dos mundos, y la decisión se toma en el momento de construir, por quien construye, que es quien conoce el contexto. Interioriza esa forma: cuando diseñes cualquier sistema con dependencias externas, separa la declaración de lo que necesitas de la política sobre de dónde sale. Mezclarlas es lo que produce proyectos que solo compilan en el portátil de quien los escribió.

⚔️ Resuelve una dependencia por los cuatro caminos
  1. Declara dependency('zlib') y localízala en tu sistema. Ejecuta pkg-config --cflags --libs zlib y compara con lo que Meson te informa en la salida de meson setup.
  2. Instala la biblioteca en un prefijo no estándar, comprueba que Meson deja de encontrarla y arréglalo solo con PKG_CONFIG_PATH.
  3. Añade subprojects/zlib.wrap con meson wrap install zlib y fuerza su uso con --force-fallback-for=zlib. Verifica en los logs que se compila desde las fuentes.
  4. Convierte una dependencia en opcional con una opción de tipo feature y comprueba los tres comportamientos: auto, enabled y disabled con la biblioteca ausente.
  5. Configura el mismo proyecto con --wrap-mode=nofallback y explica por escrito qué política acabas de adoptar y a quién beneficia.