Files
takana/docs/adr/0013-mirror-de-fuentes.md
T
SergioandClaude Opus 5 45b95f78b9 ADR 0013: mirror de fuentes — la URL es transporte, el sha256 es la identidad
`rsync` (404 de samba.org) y `musl` (musl.libc.org no responde) no se pueden construir hoy, y no por
culpa nuestra. Es el estado estacionario: una distro que construye TODO desde fuente tiene tantos
puntos de fallo como fuentes, y son servidores de terceros que nadie nos prometió mantener.

LA MEDIDA, peor de lo que parecía. Sobre 1167 fuentes (561 tarball + 606 git, 79 hosts):
github.com sostiene 742 — el 64% del corpus depende de UN host. Doce hosts sostienen el 89%. Y 43
hosts sostienen exactamente UNA receta cada uno: ahí es donde muerde el bit-rot lento.

El vigía, en su primera corrida: 8 URLs muertas de 1167. Tres de ellas —busybox, freetype,
freetype-shared— están SELLADAS Y EN USO: son el shell y las fuentes del escritorio que se capturó
hoy. Se salvan sólo porque el tarball sigue en la caché local de esta máquina.

LA URL NUNCA FUE LA IDENTIDAD, y el código ya lo sabía: `hash_inputs` usa `tarball:{sha256}` /
`git:{commit}` y el `..` descarta la URL; la caché se nombra `{sha256}.tar` con un comentario que
dice literalmente que cambiar de mirror no la invalida. Añadir mirrors NO re-hashea NADA. Faltaba el
mecanismo, no el diseño.

Orden: caché local → mirror propio → upstream. El mirror va ANTES, no como rescate: el sha256 se
verifica igual, así que no hay diferencia de contenido posible, y un mirror que sólo se usa cuando
upstream falla es un mirror que nadie prueba — se descubre roto el día que hace falta.

LO QUE HAY QUE HACER BIEN. Un mirror que sirve calladamente lo que upstream perdió convierte un
fallo ruidoso en silencio. Por eso construir y vigilar van SEPARADOS: `hammer build` nunca avisa
(sería ruido en 561 recetas), y `fuentes-vigia.sh` pide cabeceras, escribe
docs/state/fuentes-vigia.json y lo corre el latido. Sin ese contrapeso las URLs se mueren una a una
y el corpus queda irreconstruible con todo en verde — el mismo modo de fallo que dejó el grafo de
wlr 17 días anunciando un 121/121 falso.

 PROHIBIDO cambiar el sha256 para "arreglar" una URL muerta. Es la tentación natural ante un 404 y
no arregla una descarga: cambia lo que la distro construye. Otro sha256 es otro contenido, y la
receta seguiría diciendo `rsync 3.4.4` mientras construye otra cosa.

VERIFICADO DE PUNTA A PUNTA. El primer intento —construir busybox con el mirror puesto— dijo BUILD
OK y NO PROBÓ NADA: cache-hit del artefacto, cero bytes descargados. La prueba válida usa una receta
efímera con un sha256 que sí está en el mirror y una URL que ni resuelve por DNS. Sin HAMMER_MIRROR:
`curl (6) Could not resolve host`. Con él: sella, con el contenido real.

Mirror poblado: 126 objetos en el Storage Box que ya se paga. `cargo test -p hammer-build`: 5/5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016v9ozVm44p6DB7EMXeZK4o
2026-08-26 18:57:40 +00:00

8.5 KiB

ADR 0013 — Mirror de fuentes: la URL es transporte, el sha256 es la identidad

  • Estado: ACEPTADO — implementado en crates/hammer-build/src/fetch.rs y scripts/fuentes/.
  • Fecha: 2026-08-26
  • Frontera: fetch_tarball / fetch_git, scripts/fuentes/mirror-poblar.sh, scripts/fuentes/fuentes-vigia.sh.

Contexto

Dos recetas del corpus no se pueden construir hoy, y no por culpa nuestra:

  • rsync 3.4.4 → download.samba.org devuelve 404. La tarball ya no está donde la receta la busca.
  • musl 1.2.5 → musl.libc.org no responde desde gioser (curl 000, connection reset).

No es mala suerte: es el estado estacionario. Una distro que construye todo desde fuente tiene tantos puntos de fallo como fuentes distintas, y esos puntos son servidores de terceros que nadie nos prometió mantener. El problema sólo crece con el tiempo.

La medida, que es peor de lo que parece

Sobre las 1167 recetas del árbol (561 tarball + 606 git, 79 hosts distintos):

recetas host
742 github.com
104 download.kde.org
68 download.gnome.org
29 gitlab.freedesktop.org
23 ftp.gnu.org

Un solo host sostiene el 64% del corpus. Doce hosts sostienen el 89%. En el otro extremo, 43 hosts sostienen exactamente una receta cada uno: proyectos pequeños, dominios personales, cosas que se apagan sin aviso. Ahí es donde el bit-rot muerde despacio, y musl.libc.org y download.samba.org son justo eso.

Dicho de otra forma: la reproducibilidad del corpus está cerrada hacia adentro y completamente abierta hacia afuera. Tenemos hashes de todo, verificación bit a bit y un store direccionado por contenido — apoyado sobre 79 servidores ajenos.

Decisión

1. La URL nunca fue la identidad, y el código ya lo sabía

Recipe::hash_inputs calcula:

let source_id = match self.source.kind()? {
    SourceKind::Git { commit, .. } => format!("git:{commit}"),
    SourceKind::Tarball { sha256, .. } => format!("tarball:{sha256}"),
};

El .. descarta la URL. Y la caché de tarballs se nombra {sha256}.tar, con un comentario que ya lo dice: «dos URLs distintas con el mismo contenido se cachean una vez, y cambiar la URL sin cambiar el sha (mirror) no invalida la caché».

Añadir mirrors no re-hashea absolutamente nada. No es una consecuencia afortunada: es que la identidad del contenido siempre estuvo en el sha256 y el commit. La URL es una sugerencia sobre dónde buscar los bytes. Esto ya estaba diseñado; lo que faltaba era el mecanismo.

2. Orden de resolución: caché local → mirror propio → upstream

El mirror va antes que upstream, no después. Razones, en orden:

  • El sha256 se verifica igual en los tres casos, así que no hay diferencia de contenido posible. Preferir upstream «por pureza» no compra nada: compra latencia y un tercero en el camino.
  • Los workers de la granja son efímeros y descargan el corpus entero en cada ciclo. Pegarle 561 veces a servidores ajenos es a la vez frágil y de mala educación.
  • Un mirror sólo de rescate es un mirror que nadie prueba. Se descubre roto el día que hace falta, que es exactamente el día en que no se puede arreglar.

3. El mirror NO puede tapar el bit-rot: la vigilancia va SEPARADA

Ésta es la parte que hay que hacer bien, y es una lección cara de este mismo repo.

Un mirror que sirve calladamente lo que upstream ya perdió convierte un fallo ruidoso en silencio. Las URLs se irían muriendo una por una sin que nadie se entere, y el día que el mirror se pierda el corpus resultaría irreconstruible — con todos los indicadores en verde hasta ese momento. Es el mismo patrón que dejó el grafo de wlr congelado 17 días anunciando 121/121: una métrica que nadie refresca envejece hacia el optimismo.

Por eso construir y vigilar upstream son dos trabajos distintos y van en dos sitios distintos:

  • hammer build nunca avisa de que usó el mirror. No es su tarea y volvería el log inútil.
  • scripts/fuentes/fuentes-vigia.sh recorre las 1167 fuentes con curl -I (cabeceras, sin descargar) y escribe docs/state/fuentes-vigia.json: qué URLs siguen vivas, cuáles dan 404, cuáles no responden. Lo corre el latido, como el grafo de estado.

Una URL muerta deja de ser una emergencia en mitad de un build y pasa a ser una línea en un informe.

4. Poblar es un efecto secundario, no una tarea

Toda descarga verificada se promueve al mirror. El mirror se llena solo, construyendo. scripts/fuentes/mirror-poblar.sh hace la carga inicial desde work/tarballs y sirve para rellenar lo que falte.

El mirror vive en el Storage Box, bajo hammer/fuentes/, direccionado por contenido: hammer/fuentes/{sha256}.tar. Es el mismo Storage Box del respaldo (1 TiB, 938 G libres, ~€3,20/mes ya pagados) — no hay infraestructura nueva que mantener.

5. PROHIBIDO cambiar el sha256 para «arreglar» una URL muerta

La regla más importante del documento, porque es la tentación natural cuando un build falla con 404: buscar la tarball nueva, pegar el sha256 nuevo, seguir adelante.

Eso no arregla una descarga: cambia lo que la distro construye. Un sha256 distinto es otro contenido — otra versión, otro tarball re-empaquetado, o un compromiso de upstream. La receta seguiría llamándose rsync 3.4.4 y estaría construyendo otra cosa, con el agravante de que el artefacto resultante se sella como si tal cosa.

Ante una URL muerta, en este orden:

  1. Buscar los bytes originales — el mirror, la caché de otra máquina, un mirror público conocido. Si el sha256 casa, se actualiza la URL y listo, sin re-hasheo.
  2. Si no aparecen en ningún sitio, es un cambio de versión con su propia auditoría: se mira qué cambió, se actualiza version + sha256, y se acepta el re-hasheo en cascada que corresponda.
  3. Nunca, jamás, el paso 2 disfrazado de paso 1.

Cómo se activa

. scripts/fuentes/mirror-env.sh          # HAMMER_MIRROR + HAMMER_MIRROR_KEY
flock work/.farm-build.lock ./target/release/hammer --store ./store build <receta>

El mirror es ADITIVO: sin HAMMER_MIRROR el comportamiento es exactamente el de siempre. No se hornea un default en el binario a propósito — apuntar por defecto a una máquina concreta convertiría un fallo de red en un fallo de hammer para cualquiera que clone el repo.

⚠️ La ruta sftp lleva /~/. curl trata lo que sigue al host en sftp:// como ruta absoluta del servidor: …:23/hammer/fuentes busca en la raíz y da «(78) Could not open remote file for reading» aunque el fichero exista, que es un error que parece de permisos o de ausencia y es de ruta.

Verificado de punta a punta, no por inspección

Primer intento de prueba: construir busybox (upstream muerto) con el mirror puesto. Dijo BUILD OK y no probó nada — busybox ya estaba sellado, así que hubo cache-hit del artefacto y no se descargó un solo byte. La prueba válida es una receta cuyo artefacto NO exista:

  • receta efímera con el sha256 de un tarball que está en el mirror,
  • y una URL upstream que ni resuelve por DNS (https://este-host-no-existe.invalid/…).

Sin HAMMER_MIRROR: falla con curl (6) Could not resolve host. Con HAMMER_MIRROR: sella, y el artefacto tiene el contenido real. Los bytes no pudieron venir de ningún otro sitio.

Consecuencias

  • Cero re-hasheos. Verificado: recalculadas las 794 recetas del grafo wlr antes y después, 0 cambios.
  • El corpus deja de depender de que 79 terceros sigan sirviendo los mismos bytes.
  • La concentración en github.com (64%) no la arregla este ADR — la mitiga. Sigue siendo el riesgo estructural mayor del proyecto y merece su propia decisión. ADR 0006 (commits pineados) ya cubre la parte de que GitHub regenera los archive/<tag>.tar.gz, que es un problema distinto y peor: ahí los bytes cambian sin que cambie la URL.
  • Aparece una dependencia nueva: el Storage Box. Es aceptable porque es caché reconstruible, no la verdad: la verdad son las recetas en git. Si el mirror se pierde, se repuebla desde cualquier máquina que tenga work/tarballs.

Lo que este ADR NO decide

  • Qué hacer con la concentración en GitHub.
  • Si el mirror debe replicarse fuera de Hetzner (hoy el repo, el respaldo y el mirror están todos en la misma cuenta y el mismo proveedor).
  • El mirror de fuentes git: hoy sólo se espeja el tarball. Los 606 repos por commit siguen dependiendo del remoto. Es el siguiente eslabón, y es más grande.