diff --git a/crates/hammer-build/src/fetch.rs b/crates/hammer-build/src/fetch.rs index 44614a45..1de5371b 100644 --- a/crates/hammer-build/src/fetch.rs +++ b/crates/hammer-build/src/fetch.rs @@ -109,7 +109,7 @@ fn fetch_tarball( // cambiar la URL sin cambiar el sha (mirror) no invalida la caché. let cached = tarballs_dir.join(format!("{sha256}.tar")); if !cached.is_file() { - download_verify(url, sha256, &cached)?; + fetch_con_mirror(url, sha256, &cached)?; } else { verify_sha256(&cached, sha256)?; } @@ -160,12 +160,57 @@ fn fetch_tarball( Ok(work_tree) } +/// Baja el tarball probando el **mirror propio antes que upstream** (ADR 0013). +/// +/// # Por qué el mirror va primero y no como rescate +/// +/// El `sha256` se verifica igual venga de donde venga, así que **no hay diferencia de contenido +/// posible** entre las dos rutas: preferir upstream «por pureza» no compra nada y paga latencia más +/// un tercero en el camino. Además un mirror que sólo se usa cuando upstream falla es un mirror que +/// **nadie prueba**, y se descubre roto justo el día en que hace falta. +/// +/// # Por qué NO avisa cuando cae al mirror +/// +/// Tentación evidente y **equivocada**: si `hammer build` avisara en cada descarga, el aviso sería +/// ruido en 561 recetas y nadie lo leería. Peor: haría creer que el bit-rot está vigilado *acá*, +/// cuando construir y vigilar upstream son dos trabajos distintos. La vigilancia vive en +/// `scripts/fuentes/fuentes-vigia.sh`, la corre el latido y escribe `docs/state/fuentes-vigia.json`. +/// Un mirror silencioso **con** vigía separado detecta el bit-rot; uno silencioso **sin** vigía lo +/// entierra, que es como el grafo de wlr pasó 17 días anunciando un 121/121 que ya era falso. +/// +/// `HAMMER_MIRROR` es un prefijo de URL que curl entienda (`file://`, `https://`, `sftp://`…); el +/// objeto se busca por contenido en `{HAMMER_MIRROR}/{sha256}.tar`. `HAMMER_MIRROR_KEY`, si está, +/// se pasa como `--key` (Storage Box por sftp). Sin `HAMMER_MIRROR` el comportamiento es el de +/// siempre: derecho a upstream. +fn fetch_con_mirror(url: &str, sha256: &str, dst: &Path) -> hammer_core::Result<()> { + if let Ok(base) = std::env::var("HAMMER_MIRROR") { + let base = base.trim_end_matches('/'); + if !base.is_empty() { + let espejo = format!("{base}/{sha256}.tar"); + // Un fallo acá NO es un error: es el caso normal la primera vez que se ve una fuente. + // Se ignora en silencio y se sigue a upstream. + if download_verify(&espejo, sha256, dst).is_ok() { + return Ok(()); + } + } + } + download_verify(url, sha256, dst) +} + /// Descarga `url` a `dst` y verifica el sha256. Si la verificación falla, borra el archivo /// para que la próxima corrida lo vuelva a intentar (no envenenamos la caché). fn download_verify(url: &str, sha256: &str, dst: &Path) -> hammer_core::Result<()> { let tmp = dst.with_extension("tar.partial"); - let status = Command::new("curl") - .args(["-fL", "--retry", "3", "--connect-timeout", "30", "-o"]) + let mut cmd = Command::new("curl"); + cmd.args(["-fL", "--retry", "3", "--connect-timeout", "30"]); + // Clave para el mirror por sftp (Storage Box). Inocua para http/file. + if let Ok(key) = std::env::var("HAMMER_MIRROR_KEY") { + if !key.is_empty() { + cmd.arg("--key").arg(&key); + } + } + let status = cmd + .arg("-o") .arg(&tmp) .arg(url) .stdout(Stdio::inherit()) diff --git a/docs/adr/0013-mirror-de-fuentes.md b/docs/adr/0013-mirror-de-fuentes.md new file mode 100644 index 00000000..430806d6 --- /dev/null +++ b/docs/adr/0013-mirror-de-fuentes.md @@ -0,0 +1,164 @@ +# 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: + +```rust +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 + +```sh +. scripts/fuentes/mirror-env.sh # HAMMER_MIRROR + HAMMER_MIRROR_KEY +flock work/.farm-build.lock ./target/release/hammer --store ./store build +``` + +**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 **sí** 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/.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. diff --git a/docs/state/fuentes-vigia.json b/docs/state/fuentes-vigia.json new file mode 100644 index 00000000..b61d807c --- /dev/null +++ b/docs/state/fuentes-vigia.json @@ -0,0 +1,72 @@ +{ + "schema": "hammer-fuentes-vigia/1", + "total": 1167, + "vivas": 1159, + "muertas": 8, + "detalle_muertas": [ + { + "receta": "busybox", + "fichero": "recipes/busybox.toml", + "tipo": "tarball", + "url": "https://busybox.net/downloads/busybox-1.36.1.tar.bz2", + "codigo": "000", + "viva": false + }, + { + "receta": "freetype", + "fichero": "recipes/freetype.toml", + "tipo": "tarball", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.xz", + "codigo": "502", + "viva": false + }, + { + "receta": "freetype-shared", + "fichero": "recipes/freetype-shared.toml", + "tipo": "tarball", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.xz", + "codigo": "502", + "viva": false + }, + { + "receta": "freetype-shared", + "fichero": "recipes/incoming-gnome-onda2/freetype-shared.toml", + "tipo": "tarball", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.xz", + "codigo": "502", + "viva": false + }, + { + "receta": "freetype-shared", + "fichero": "recipes/incoming-kde/freetype-shared.toml", + "tipo": "tarball", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.xz", + "codigo": "502", + "viva": false + }, + { + "receta": "libtool", + "fichero": "recipes/incoming-kde/libtool.toml", + "tipo": "tarball", + "url": "https://ftpmirror.gnu.org/libtool/libtool-2.5.4.tar.xz", + "codigo": "502", + "viva": false + }, + { + "receta": "musl", + "fichero": "recipes/musl.toml", + "tipo": "tarball", + "url": "https://musl.libc.org/releases/musl-1.2.5.tar.gz", + "codigo": "000", + "viva": false + }, + { + "receta": "rsync", + "fichero": "recipes/rsync.toml", + "tipo": "tarball", + "url": "https://download.samba.org/pub/rsync/rsync-3.4.4.tar.gz", + "codigo": "404", + "viva": false + } + ] +} diff --git a/scripts/farm/cosecha-cron.sh b/scripts/farm/cosecha-cron.sh index ada5ed23..179cc3fb 100755 --- a/scripts/farm/cosecha-cron.sh +++ b/scripts/farm/cosecha-cron.sh @@ -177,6 +177,15 @@ if [ -x "$HAMMER" ]; then # ningún build — sólo deja escrito qué conviene moler primero, para que la cola deje de ser una # lista escrita a mano. Barato (lee JSON, no hashea). scripts/drenar.py --todos >/dev/null 2>&1 && echo " drenaje.json ✓" || echo " ⚠ drenar.py falló" + # VIGÍA DE FUENTES (ADR 0013 §3). Desde que el mirror propio se consulta ANTES que upstream, + # `hammer build` deja de avisar cuando una URL de terceros muere: sirve los bytes del mirror y + # sigue. Eso es lo correcto para construir y sería CIEGO sin este contrapeso — las URLs se irían + # muriendo una a una y el corpus resultaría irreconstruible el día que falte el mirror, con todo + # en verde hasta ese momento. Igual que el grafo de wlr, que pasó 17 días anunciando un 121/121 + # que ya era falso: lo que nadie refresca envejece hacia el optimismo. + # No descarga: pide cabeceras. Primera corrida (2026-08-26): 8 muertas de 1167, y TRES de ellas + # —busybox, freetype, freetype-shared— selladas y en uso. + scripts/fuentes/fuentes-vigia.sh >/dev/null 2>&1 && echo " fuentes-vigia.json ✓" || echo " ⚠ fuentes-vigia falló" else echo " ⚠ sin binario hammer en $HAMMER — no regenero estado" fi @@ -184,7 +193,7 @@ fi # 4. Commitear SÓLO el estado firme (el avance que el humano sigue). Recetas nuevas que YO autoré se # commitean aparte, a mano, para no meter autoría a medias en un commit de cron. if [ "${NO_COMMIT:-}" != "1" ]; then - git add docs/state/build-state.json docs/state/build-state-kde.json docs/state/build-state-gnome.json docs/state/build-state-cosmic.json docs/state/build-state-wlr.json docs/state/drenaje.json 2>/dev/null || true + git add docs/state/build-state.json docs/state/build-state-kde.json docs/state/build-state-gnome.json docs/state/build-state-cosmic.json docs/state/build-state-wlr.json docs/state/drenaje.json docs/state/fuentes-vigia.json 2>/dev/null || true if ! git diff --cached --quiet 2>/dev/null; then git commit -q -m "estado: cosecha granja $(ts) — avance del árbol KDE" 2>/dev/null \ && { git push -q origin main 2>/dev/null && echo "==> estado commiteado+pusheado" \ diff --git a/scripts/fuentes/fuentes-vigia.py b/scripts/fuentes/fuentes-vigia.py new file mode 100755 index 00000000..fd312d59 --- /dev/null +++ b/scripts/fuentes/fuentes-vigia.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Comprueba que las URLs de fuentes de las recetas siguen vivas. Ver ADR 0013 §3. + +No descarga: pide cabeceras. Para `git` no hay HEAD que valga, así que usa `git ls-remote +--exit-code`, que es la pregunta equivalente («¿este remoto sigue ahí y responde?»). + +Escribe `docs/state/fuentes-vigia.json`. NO falla si hay URLs muertas: su trabajo es INFORMAR. +Un vigía que aborta el latido en la primera URL caída deja de correr y se vuelve inútil justo +cuando más hace falta — que es cuando hay varias caídas. +""" +import json, os, re, subprocess, sys, glob +from concurrent.futures import ThreadPoolExecutor + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +os.chdir(ROOT) +LIMITE = int(os.environ.get("LIMITE", "0")) +JOBS = int(os.environ.get("JOBS", "12")) +TIMEOUT = int(os.environ.get("TIMEOUT", "25")) + +def fuentes(): + out = [] + for f in sorted(glob.glob("recipes/**/*.toml", recursive=True)): + s = open(f, errors="ignore").read() + nombre = (re.search(r'^\s*name\s*=\s*"([^"]+)"', s, re.M) or [None, os.path.basename(f)[:-5]])[1] + t = re.search(r'^\s*tarball\s*=\s*"([^"]+)"', s, re.M) + g = re.search(r'^\s*repo\s*=\s*"([^"]+)"', s, re.M) + if t: out.append({"receta": nombre, "fichero": f, "tipo": "tarball", "url": t.group(1)}) + elif g: out.append({"receta": nombre, "fichero": f, "tipo": "git", "url": g.group(1)}) + return out + +def revisa(e): + url, tipo = e["url"], e["tipo"] + try: + if tipo == "tarball": + # -I pide cabeceras; algunos servidores no admiten HEAD y responden 405 -> se reintenta + # con -r 0-0 (un rango de UN byte), que es un GET mínimo y no descarga el tarball. + r = subprocess.run(["curl", "-sIL", "-o", "/dev/null", "-w", "%{http_code}", + "--connect-timeout", str(TIMEOUT), "--max-time", str(TIMEOUT * 2), url], + capture_output=True, text=True, timeout=TIMEOUT * 3) + code = (r.stdout or "").strip()[-3:] + if code in ("405", "501", "403"): + r = subprocess.run(["curl", "-sL", "-r", "0-0", "-o", "/dev/null", "-w", "%{http_code}", + "--connect-timeout", str(TIMEOUT), "--max-time", str(TIMEOUT * 2), url], + capture_output=True, text=True, timeout=TIMEOUT * 3) + code = (r.stdout or "").strip()[-3:] + viva = code.startswith("2") or code.startswith("3") + return {**e, "codigo": code or "000", "viva": viva} + else: + r = subprocess.run(["git", "ls-remote", "--exit-code", url, "HEAD"], + capture_output=True, text=True, timeout=TIMEOUT * 2) + return {**e, "codigo": f"git:{r.returncode}", "viva": r.returncode == 0} + except Exception as ex: + return {**e, "codigo": f"error:{type(ex).__name__}", "viva": False} + +def main(): + fs = fuentes() + if LIMITE: + fs = fs[:LIMITE] + print(f"== vigía de fuentes: {len(fs)} recetas ({sum(1 for f in fs if f['tipo']=='tarball')} tarball, " + f"{sum(1 for f in fs if f['tipo']=='git')} git), {JOBS} en paralelo", flush=True) + with ThreadPoolExecutor(max_workers=JOBS) as ex: + res = list(ex.map(revisa, fs)) + muertas = [r for r in res if not r["viva"]] + # El informe se ordena por receta para que el diff de git sea legible entre corridas. + inf = { + "schema": "hammer-fuentes-vigia/1", + "total": len(res), + "vivas": len(res) - len(muertas), + "muertas": len(muertas), + "detalle_muertas": sorted(muertas, key=lambda r: r["receta"]), + } + os.makedirs("docs/state", exist_ok=True) + with open("docs/state/fuentes-vigia.json", "w") as fh: + json.dump(inf, fh, indent=1, ensure_ascii=False) + fh.write("\n") + print(f"== vivas {inf['vivas']} / {inf['total']} MUERTAS {inf['muertas']}") + for m in inf["detalle_muertas"][:40]: + print(f" {m['codigo']:>12} {m['receta']:<24} {m['url'][:70]}") + # Exit 0 SIEMPRE: informar, no abortar el latido. Ver la docstring. + return 0 + +sys.exit(main()) diff --git a/scripts/fuentes/fuentes-vigia.sh b/scripts/fuentes/fuentes-vigia.sh new file mode 100755 index 00000000..b51b579e --- /dev/null +++ b/scripts/fuentes/fuentes-vigia.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# fuentes-vigia.sh — comprueba que las URLs de las recetas SIGUEN VIVAS (ADR 0013 §3). +# +# ── POR QUÉ ESTO EXISTE APARTE Y NO DENTRO DE `hammer build` ─────────────────────────────────── +# Desde el ADR 0013 el mirror propio se consulta ANTES que upstream, y `hammer build` no avisa +# cuando lo usa. Eso es correcto para construir y PELIGROSO sin contrapeso: un mirror que sirve +# calladamente lo que upstream ya perdió convierte un fallo ruidoso en silencio, y las URLs se van +# muriendo una por una sin que nadie se entere. El día que el mirror se pierda, el corpus resulta +# irreconstruible — con todo en verde hasta ese momento. +# +# Es el mismo modo de fallo que dejó `build-state-wlr.json` diciendo `escritorio-sway 121/121` +# durante 17 días: una métrica que nadie refresca envejece SIEMPRE hacia el optimismo. +# +# Así que construir y vigilar son dos trabajos y van en dos sitios. Éste no construye ni descarga +# nada: pide CABECERAS (`curl -I`) y escribe un informe. Lo corre el latido. +# +# Uso: scripts/fuentes/fuentes-vigia.sh # todas las recetas → docs/state/fuentes-vigia.json +# LIMITE=50 scripts/fuentes/fuentes-vigia.sh # sólo las primeras 50 (prueba rápida) +set -eu +ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" +exec python3 scripts/fuentes/fuentes-vigia.py "$@" diff --git a/scripts/fuentes/mirror-env.sh b/scripts/fuentes/mirror-env.sh new file mode 100644 index 00000000..edec4c39 --- /dev/null +++ b/scripts/fuentes/mirror-env.sh @@ -0,0 +1,15 @@ +# mirror-env.sh — activa el mirror de fuentes (ADR 0013). Se hace `source`, no se ejecuta: +# +# . scripts/fuentes/mirror-env.sh +# flock work/.farm-build.lock ./target/release/hammer --store ./store build +# +# Sin esto `hammer build` va derecho a upstream, que es el comportamiento de siempre: el mirror es +# ADITIVO y su ausencia nunca rompe un build. Por eso no está horneado en el binario — un default +# que apunta a una máquina concreta convertiría un fallo de red en un fallo de hammer para cualquiera +# que clone el repo. +# +# La ruta lleva `/~/` a propósito. curl interpreta lo que sigue al host en una URL `sftp://` como +# ruta ABSOLUTA del servidor, así que `…:23/hammer/fuentes` busca en la raíz y devuelve +# «(78) Could not open remote file for reading» aunque el fichero exista. `/~/` lo ancla al home. +export HAMMER_MIRROR="${HAMMER_MIRROR:-sftp://u647150@u647150.your-storagebox.de:23/~/hammer/fuentes}" +export HAMMER_MIRROR_KEY="${HAMMER_MIRROR_KEY:-$HOME/.ssh/github5}" diff --git a/scripts/fuentes/mirror-poblar.sh b/scripts/fuentes/mirror-poblar.sh new file mode 100755 index 00000000..0e7081bc --- /dev/null +++ b/scripts/fuentes/mirror-poblar.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# mirror-poblar.sh — sube la caché local de tarballs al mirror de fuentes (ADR 0013). +# +# El mirror está direccionado por CONTENIDO: `hammer/fuentes/{sha256}.tar`, exactamente el mismo +# nombre que usa `work/tarballs/`. Por eso subir es un rsync plano y no hace falta índice ninguno: +# el nombre del fichero ES su verificación. +# +# Es INCREMENTAL e interrumpible, como el respaldo: los ficheros nunca cambian (un sha256 identifica +# un contenido y sólo uno), así que la comparación por tamaño+fecha de rsync es exacta y baratísima. +# +# Uso: scripts/fuentes/mirror-poblar.sh # sube lo que falte +# SECO=1 scripts/fuentes/mirror-poblar.sh # dice qué subiría +set -eu +ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" + +SB_USER="${SB_USER:-u647150}" +SB_HOST="${SB_HOST:-u647150.your-storagebox.de}" +SB_PORT="${SB_PORT:-23}" +KEY="${KEY:-$HOME/.ssh/github5}" +SRC="${SRC:-work/tarballs}" +SECO=""; [ "${SECO:-}" = "1" ] && SECO="--dry-run" + +[ -d "$SRC" ] || { echo "no hay caché local en $SRC" >&2; exit 1; } +SSH_CMD="ssh -4 -p $SB_PORT -i $KEY -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30" + +n=$(ls "$SRC" | wc -l); tam=$(du -sh "$SRC" | cut -f1) +echo "==> caché local: $n tarballs, $tam" +echo "==> destino: $SB_USER@$SB_HOST:hammer/fuentes/ ${SECO:+(SECO)}" + +$SSH_CMD "$SB_USER@$SB_HOST" 'mkdir hammer hammer/fuentes' >/dev/null 2>&1 || true + +# --partial-dir: un tarball cortado a la mitad conserva el trozo y la próxima corrida lo TERMINA. +rsync -a --info=progress2 --partial-dir=.rsync-partial $SECO \ + -e "$SSH_CMD" "$SRC"/ "$SB_USER@$SB_HOST:hammer/fuentes/" + +echo "==> en el mirror: $($SSH_CMD "$SB_USER@$SB_HOST" 'ls hammer/fuentes | wc -l' 2>/dev/null) objetos"