granja: las tres wanted del perfil servidor, construidas — y popt, que faltaba debajo

`targets.toml` declaraba `chrony`, `cronie` y `logrotate` como raíces del perfil `servidor` con el
motivo escrito al lado, y ninguna tenía receta: el grafo las contaba como `wanted` y ésa era toda la
deuda que le quedaba al perfil. Ahora sellan las cuatro (popt es la hoja que `logrotate` exige:
incluye `<popt.h>` en la primera pantalla y su configure aborta sin él).

    servidor  97/97 → 101/101 listo, falta 0.  `wanted` desaparece de los totales.

Lo que se midió, no se supuso:
  · logrotate  estático, CERO NEEDED, corre, y las rutas de gzip quedan en `/bin` — que es donde
    busybox las deja de verdad; el default de upstream en Linux es `/usr/bin/gzip`, que en esta
    distro NO EXISTE y habría fallado en runtime diciendo «no se pudo comprimir».
  · chrony     `+CMDMON +REFCLOCK +RTC +PRIVDROP +IPV6`; `cap_set_proc` está en el ELF, o sea que
    libcap entró de verdad y chronyd suelta privilegios. Sale `-NTS -SECHASH` a propósito: NTS
    necesita nettle o gnutls y ninguna está en el corpus.
  · cronie     los cuatro binarios estáticos sin NEEDED, con inotify dentro, y `/bin/vi` pineado.

**El hilo que recorre las tres recetas es el mismo, y es el que valía la pena escribir:** los tres
`configure` DECIDEN MIRANDO EL LAB. `logrotate` trae `--with-selinux/--with-acl` en `[default=check]`;
`chrony` prueba nettle, gnutls, libcap, seccomp y editline; `cronie` resuelve el editor de
`crontab -e` con `AC_PATH_PROG([vi])` y lo graba en el binario. El lab NO entra en `hash_inputs` ⇒
dos labs distintos sellarían bytes distintos en la MISMA dirección del store y nada lo notaría.
Cada palanca va fijada en la receta —que sí entra en el hash— para que sea una decisión y no un
accidente del entorno.

Y una anotada en vez de tapada: cronie no reemplaza al `crond` de busybox por reflejo (busybox ya lo
pone en `/sbin`); agrega `@reboot`, crontabs por usuario, `/etc/cron.d` y anacron. Cuál va en cada
imagen es decisión de perfil — lo que faltaba era que la opción existiera en el corpus.
This commit is contained in:
Sergio
2026-09-11 17:13:42 +00:00
parent 8ad9f0490d
commit cc5aebd8c4
5 changed files with 304 additions and 43 deletions
+89 -43
View File
@@ -1,27 +1,24 @@
{
"schema": "hammer-build-state/1",
"totals": {
"recipes": 877,
"nodes": 882,
"sealed": 877,
"ajeno": 2,
"wanted": 3
"recipes": 881,
"nodes": 883,
"sealed": 881,
"ajeno": 2
},
"by_class": {
"ajeno": 2,
"c": 234,
"c": 238,
"go": 362,
"gui": 41,
"kernel": 6,
"rust": 234,
"wanted": 3
"rust": 234
},
"debt_by_class": {},
"by_queue": {
"corpus": {
"sealed": 877,
"ajeno": 2,
"wanted": 3
"sealed": 881,
"ajeno": 2
}
},
"by_profile": {
@@ -42,11 +39,11 @@
},
"servidor": {
"raices": 61,
"clausura": 97,
"sealed": 97
"clausura": 101,
"sealed": 101
}
},
"sin_perfil": 646,
"sin_perfil": 643,
"orphan_deps": [],
"topo_ok": true,
"nodes": {
@@ -1008,7 +1005,7 @@
"escritorio-sway",
"servidor"
],
"dependientes_total": 48
"dependientes_total": 52
},
"biome": {
"iname": "biome",
@@ -1285,7 +1282,7 @@
"escritorio-sway",
"servidor"
],
"dependientes_total": 8
"dependientes_total": 12
},
"bwrap": {
"iname": "bwrap",
@@ -2444,17 +2441,26 @@
},
"chrony": {
"iname": "chrony",
"version": null,
"link": null,
"compiler": null,
"deps": [],
"cls": "wanted",
"version": "4.9",
"link": "static",
"compiler": "zig-cc",
"deps": [
"binutils",
"busybox",
"libcap",
"make",
"pkgconf"
],
"cls": "c",
"queue": "corpus",
"hash": null,
"state": "wanted",
"foreign": false,
"hash": "b3:b9e26374f000ef5820bcc32bdb761b2b3e7306d1ed400c147271cfdbd77557e9",
"state": "sealed",
"blocked_by": [],
"unblocks": 0,
"perfiles": [],
"perfiles": [
"servidor"
],
"dependientes_total": 0
},
"cilium-cli": {
@@ -2871,17 +2877,25 @@
},
"cronie": {
"iname": "cronie",
"version": null,
"link": null,
"compiler": null,
"deps": [],
"cls": "wanted",
"version": "1.7.2",
"link": "static",
"compiler": "zig-cc",
"deps": [
"binutils",
"busybox",
"make",
"pkgconf"
],
"cls": "c",
"queue": "corpus",
"hash": null,
"state": "wanted",
"foreign": false,
"hash": "b3:387347b89b107d97febbb2692ce2d292e74bc7b1fba105676befe396cc3ebf21",
"state": "sealed",
"blocked_by": [],
"unblocks": 0,
"perfiles": [],
"perfiles": [
"servidor"
],
"dependientes_total": 0
},
"crossplane-cli": {
@@ -9313,7 +9327,7 @@
"escritorio-sway",
"servidor"
],
"dependientes_total": 3
"dependientes_total": 4
},
"libdisplay-info": {
"iname": "libdisplay-info",
@@ -10521,17 +10535,26 @@
},
"logrotate": {
"iname": "logrotate",
"version": null,
"link": null,
"compiler": null,
"deps": [],
"cls": "wanted",
"version": "3.22.0",
"link": "static",
"compiler": "zig-cc",
"deps": [
"binutils",
"busybox",
"make",
"pkgconf",
"popt"
],
"cls": "c",
"queue": "corpus",
"hash": null,
"state": "wanted",
"foreign": false,
"hash": "b3:09698cb83519dc2423b8aa1905fa5803955ee1c79598590c30ff5b5b14e5e84d",
"state": "sealed",
"blocked_by": [],
"unblocks": 0,
"perfiles": [],
"perfiles": [
"servidor"
],
"dependientes_total": 0
},
"lsd": {
@@ -10763,7 +10786,7 @@
"escritorio-sway",
"servidor"
],
"dependientes_total": 124
"dependientes_total": 128
},
"mako": {
"iname": "mako",
@@ -13127,7 +13150,7 @@
"escritorio-sway",
"servidor"
],
"dependientes_total": 406
"dependientes_total": 410
},
"plow": {
"iname": "plow",
@@ -13308,6 +13331,29 @@
"perfiles": [],
"dependientes_total": 0
},
"popt": {
"iname": "popt",
"version": "1.19",
"link": "static",
"compiler": "zig-cc",
"deps": [
"binutils",
"busybox",
"make",
"pkgconf"
],
"cls": "c",
"queue": "corpus",
"foreign": false,
"hash": "b3:7202a918f06efe0315267027b071f5380f77aa15ce59eac83b5f078e6edd24f4",
"state": "sealed",
"blocked_by": [],
"unblocks": 0,
"perfiles": [
"servidor"
],
"dependientes_total": 1
},
"prek": {
"iname": "prek",
"version": "0.3.11",
+70
View File
@@ -0,0 +1,70 @@
# chrony 4.9 — el reloj. `targets.toml` lo declara como raíz del perfil `servidor` con un motivo que
# no es comodidad: sin NTP un reloj que deriva rompe la validación de certificados TLS y la de firmas
# con expiración, y falla con errores que NO dicen «la hora», dicen «firma inválida». Era una de las
# tres `wanted` que el grafo contaba como deuda del perfil.
#
# El `configure` de chrony NO ES AUTOTOOLS: es un script a mano que ni siquiera acepta
# `--build`/`--host` (los imprime como «Unrecognized option» y sigue). Por eso esta receta no lleva
# la línea `--build=$CBUILD --host=$CHOST` que llevan todas las demás; lo que sí acepta —y es su
# equivalente— es `--host-system`/`--host-machine`, que se pinean para que la detección no salga de
# `uname` del lab.
#
# ⚠ TODO `--without-*` DE ACÁ ABAJO ESTÁ POR LA MISMA RAZÓN, Y NO ES PROLIJIDAD. El `configure` de
# chrony PRUEBA el entorno para casi todo (`try_nettle=1`, `try_gnutls=1`, `try_libcap=-1`,
# `try_seccomp=-1`, `try_editline=1`…): lo que encuentre instalado en el LAB cambia el binario. Y el
# lab NO entra en `hash_inputs` — dos labs distintos sellarían bytes distintos en la MISMA dirección
# del store sin que nada lo note. Fijar cada palanca convierte eso en una decisión de la receta, que
# sí entra en el hash. Enumeradas:
#
# --without-nettle --without-gnutls --without-nss --without-tomcrypt --disable-nts
# NTS (NTP sobre TLS) necesita nettle o gnutls, y NINGUNA de las dos está en el corpus. Se
# apaga a propósito y queda anotado: subir NTS es su propia unidad de trabajo —entra un árbol
# de criptografía entero— no algo que se resuelve de paso. Mientras tanto chronyd hace NTP
# clásico, que es lo que la caja necesita para que TLS no falle por la hora.
# --disable-readline --without-editline
# el editor de línea de `chronyc`. Ni readline ni editline están en el corpus; sin fijarlo, un
# lab que las tuviera cambiaría el binario.
# --without-seccomp
# el filtro de syscalls (`--enable-scfilter`) viene apagado de fábrica, pero el probe de
# libseccomp corre igual. Sin scfilter, encontrarla no aporta nada y sí mueve el hash.
#
# `libcap` SÍ entra como dep, y es la decisión contraria a las de arriba: chronyd suelta privilegios
# (`--disable-privdrop` NO se pasa) y para eso necesita `cap_*`. Declararla como dep la vuelve
# presente-por-contrato en vez de presente-por-suerte-del-lab, que es exactamente el mismo problema
# visto del otro lado. El `libcap` del corpus publica sólo `.a` (`SHARED=no`), así que enlaza
# estático y no deja NEEDED colgante.
#
# **`--with-user` se deja en `root`.** Lo natural sería `chrony`, pero un chronyd que hace `setuid`
# a un usuario que no existe en `/etc/passwd` no arranca, y la distro todavía no crea usuarios de
# servicio. Cambiarlo va junto con esa decisión, no antes.
#
# **Consecuencia medida de apagar la criptografía:** el binario sale `-SECHASH`, o sea que para la
# autenticación simétrica por clave sólo queda MD5. No es un descuido: `sechash` (SHA-2 y CMAC) se
# implementa contra nettle/gnutls/nss/tomcrypt, las mismas que no están. Lo que el binario SÍ trae,
# comprobado corriéndolo: `+CMDMON +REFCLOCK +RTC +PRIVDROP +IPV6`, es decir que `libcap` entró de
# verdad — `cap_set_proc`/`cap_from_text` están en el ELF — y chronyd suelta privilegios.
#
# La reproducibilidad ya la trae upstream: el `NTP_ERA_SPLIT` —que por default es «hace 50 años»
# calculado con `date` en tiempo de configure— respeta `SOURCE_DATE_EPOCH`, que el lab exporta. Sin
# eso el binario cambiaría cada día sin tocar la receta.
name = "chrony"
version = "4.9"
license = "GPL-2.0-only"
[source]
tarball = "https://chrony-project.org/releases/chrony-4.9.tar.gz"
sha256 = "4924c6f530105bcd5b9e9e33c48a2ae1bfd889222c8480bc41601110efc864d0"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
flags = []
[build.phases]
configure = './configure --host-system=Linux --host-machine=x86_64 --prefix=/usr --sysconfdir=/etc --sbindir=/usr/sbin --bindir=/usr/bin --without-nettle --without-gnutls --without-nss --without-tomcrypt --disable-nts --disable-readline --without-editline --without-seccomp'
compile = 'make -j"$(nproc)"'
install = "make install DESTDIR=/out"
[deps]
build = ["binutils", "busybox", "make", "pkgconf", "libcap"]
+54
View File
@@ -0,0 +1,54 @@
# cronie 1.7.2 — el disparador periódico. Es la tercera y última de las raíces que `targets.toml`
# declaraba para el perfil `servidor` sin receta detrás. El motivo escrito allí es directo: en una
# distro sin systemd no hay timers, y el latido de la granja son tres líneas de crontab.
#
# **Y busybox YA trae un `crond` — esto no lo reemplaza por reflejo.** El artefacto sellado de
# busybox pone `crond` en `/sbin`. Lo que cronie agrega y busybox no tiene: `@reboot`, crontabs por
# usuario con `crontab -e`, `/etc/cron.d`, `cronnext`, y `anacron` para las tareas que hay que
# recuperar cuando la máquina estuvo apagada. Cuál de los dos va en cada imagen es una decisión de
# perfil, no de esta receta; lo que hacía falta era que la opción EXISTIERA en el corpus en vez de
# descubrirse ausente el día que alguien la necesite.
#
# ⚠ EL DEFAULT QUE HABÍA QUE PINEAR SÍ O SÍ: `--with-editor`. Sin él, `configure.ac` hace
# `AC_PATH_PROG([vi])` — o sea que la ruta del editor que `crontab -e` lanza SALE DE BUSCAR `vi` EN
# EL LAB, y queda grabada en el binario con `AC_DEFINE_UNQUOTED`. El lab no entra en `hash_inputs`:
# dos labs con `vi` en rutas distintas sellarían bytes distintos en la MISMA dirección del store y
# nada lo notaría. Se fija en `/bin/vi`, que además es donde busybox lo deja de verdad (comprobado
# en su artefacto sellado); el fallback de autoconf era `/usr/bin/vi`, que en esta distro no existe.
#
# Lo demás son palancas de cosas que el corpus no tiene, apagadas explícitamente por la misma razón
# —que el probe no salga del entorno— y no porque estorben:
# --without-pam no hay Linux-PAM en el corpus. La autorización de `crontab` cae entonces a
# `/etc/cron.allow` + `/etc/cron.deny`, que es el mecanismo clásico y no
# necesita nada más.
# --without-selinux la distro no lleva SELinux.
# --without-audit no hay libaudit.
# --with-inotify esto se ENCIENDE: con inotify, crond se entera de un crontab editado sin
# esperar al minuto siguiente. Es una API del kernel, no una dep nueva.
# --enable-syscrontab `/etc/crontab` y `/etc/cron.d`, que es por donde entra el latido de la granja.
#
# `link = "static"`: binarios monolíticos como el resto de las herramientas de sistema.
#
# La licencia es compuesta y no se adivina: el grueso (`src/cron.c` y compañía) es ISC de Vixie/ISC,
# y `anacron` —que se construye— trae su propio `COPYING.anacron`, GPLv2-or-later.
name = "cronie"
version = "1.7.2"
license = "ISC AND GPL-2.0-or-later"
[source]
tarball = "https://github.com/cronie-crond/cronie/releases/download/cronie-1.7.2/cronie-1.7.2.tar.gz"
sha256 = "f1da374a15ba7605cf378347f96bc8b678d3d7c0765269c8242cfe5b0789c571"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
flags = []
[build.phases]
configure = './configure --build=$CBUILD --host=$CHOST --prefix=/usr --sysconfdir=/etc --localstatedir=/var --sbindir=/usr/sbin --with-editor=/bin/vi --with-inotify --without-pam --without-selinux --without-audit --enable-syscrontab'
compile = 'make -j"$(nproc)"'
install = "make install DESTDIR=/out && find /out -name '*.la' -delete"
[deps]
build = ["binutils", "busybox", "make", "pkgconf"]
+52
View File
@@ -0,0 +1,52 @@
# logrotate 3.22.0 — la rotación de logs. Es la última de las tres raíces que `targets.toml` declara
# para el perfil `servidor` y que no tenían receta: el grafo las contaba como `wanted` y la deuda del
# perfil era exactamente ésa. El motivo escrito allí sigue valiendo: en una caja que sirve, el log de
# acceso crece hasta llenar el disco, y Caddy rota el suyo por configuración pero nada más lo hace.
#
# DOS DEFAULTS DE `configure` QUE SE PINEAN A PROPÓSITO, y no por prolijidad:
#
# 1. **`--with-selinux=no --with-acl=no`.** Los dos vienen con `[default=check]`, o sea que el
# contenido del binario depende de si el LAB tiene `libselinux`/`libacl` instaladas. El lab NO
# entra en `hash_inputs` (deuda conocida del repo): dos labs distintos sellarían bytes distintos
# en la MISMA dirección del store y nada lo notaría. Fijarlo en `no` lo vuelve una decisión de la
# receta —que sí entra en el hash— en vez de un accidente del entorno. Y `no` es lo correcto acá:
# la distro no lleva SELinux ni ACLs POSIX.
#
# 2. **`--with-compress-command` / `--with-uncompress-command`.** `configure.ac` elige la ruta por
# sistema operativo y en Linux fija `/usr/bin/gzip`, que en esta distro NO EXISTE: el `gzip` y el
# `gunzip` los provee busybox y quedan en `/bin` (comprobado en el artefacto sellado de busybox).
# Un logrotate con `compress` en la config fallaría en runtime buscando un binario ausente, y el
# error sería «no se pudo comprimir», no «la ruta está mal». Se pinean a `/bin/…`.
#
# **Lo que NO se pinea, y por qué se deja anotado:** el `mail command` queda en su default
# `/bin/mail`, que en esta distro NO existe — busybox provee `sendmail`, no `mail`. No se cambia a
# `/bin/sendmail` porque no es un reemplazo: logrotate lo invoca como `mail -s <asunto> <destino>` y
# la interfaz de busybox es otra. Sólo afecta a configuraciones que usen la directiva `mail`, que la
# distro hoy no tiene; queda dicho para que se descubra leyendo y no en runtime.
#
# `--with-state-file-path` se deja en su default (`/var/lib/logrotate.status`): es ruta de ESTADO en
# la máquina viva, no algo que la imagen deba decidir.
#
# `link = "static"`: binario monolítico, como el resto de las herramientas de sistema. popt publica
# sólo `.a` justo para que acá no aparezca un NEEDED colgante.
name = "logrotate"
version = "3.22.0"
license = "GPL-2.0-or-later"
[source]
tarball = "https://github.com/logrotate/logrotate/releases/download/3.22.0/logrotate-3.22.0.tar.xz"
sha256 = "42b4080ee99c9fb6a7d12d8e787637d057a635194e25971997eebbe8d5e57618"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
flags = []
[build.phases]
configure = './configure --build=$CBUILD --host=$CHOST --prefix=/usr --with-selinux=no --with-acl=no --with-compress-command=/bin/gzip --with-uncompress-command=/bin/gunzip'
compile = 'make -j"$(nproc)"'
install = "make install DESTDIR=/out && find /out -name '*.la' -delete"
[deps]
build = ["binutils", "busybox", "make", "pkgconf", "popt"]
+39
View File
@@ -0,0 +1,39 @@
# popt 1.19 — el parser de opciones de línea de comando de rpm. Entra por UNA razón y sólo una:
# `logrotate` lo exige. Su `logrotate.c` hace `#include <popt.h>` en la primera pantalla y su
# `configure` aborta sin él; no hay flag para sacarlo ni fallback interno a getopt.
#
# Es la hoja de la cadena `logrotate` del perfil `servidor`, que el grafo pedía como `wanted` desde
# que se escribió `targets.toml` y que nadie había puesto.
#
# Tarball de release y no git: el árbol de git NO trae `configure` (lo genera `autoreconf`) y el
# corpus no tiene autoconf/automake — el mismo motivo por el que entran libmnl, jq y gmp.
#
# `--disable-nls`: popt traduce sus mensajes de error con gettext, y el corpus tiene `gettext-tiny`,
# cuyo `msgfmt` muere con SIGILL en algunos catálogos (los plurales del árabe). No vale la pena
# arriesgar eso por los mensajes de error de un parser de flags que sólo ve `logrotate`.
#
# `link = "static"` y `--disable-shared`: el consumidor es un binario de sistema que queremos
# monolítico (la estrategia de hidratación primaria). Publicar sólo `.a` evita además que
# `logrotate` salga con un NEEDED de `libpopt.so.0` que después nadie provea — la familia de fallos
# de `needed-colgante`.
name = "popt"
version = "1.19"
license = "MIT"
[source]
tarball = "http://ftp.rpm.org/popt/releases/popt-1.x/popt-1.19.tar.gz"
sha256 = "c25a4838fc8e4c1c8aacb8bd620edb3084a3d63bf8987fdad3ca2758c63240f9"
[build]
compiler = "zig-cc"
target = "x86_64-linux-musl"
link = "static"
flags = []
[build.phases]
configure = './configure --build=$CBUILD --host=$CHOST --prefix=/usr --disable-shared --enable-static --disable-nls --disable-rpath'
compile = 'make -j"$(nproc)"'
install = "make install DESTDIR=/out && find /out -name '*.la' -delete"
[deps]
build = ["binutils", "busybox", "make", "pkgconf"]