Files
takana/scripts/fuentes/mirror-git-poblar.py
Sergio 476168bb07 takana etapa 5c: comentarios de scripts, MOTD, y un BUG que introdujo la etapa 4
250 líneas de comentario en 151 scripts. Control verificado: el diff no toca
NI UNA línea que no empiece por #, y la sintaxis de los 151 pasa.

El barrido saltea heredocs y cadenas triples, y el guardián DISPARÓ 3 veces:
las tres eran el MOTD que el script escribe DENTRO de la imagen construida —
texto del producto, no comentario del script. Se cambiaron aparte y a
propósito, que es rebranding, no limpieza.

Y el hallazgo caro:  casaba contra ,
que es el TARGET de tracing — o sea el module_path!, o sea el nombre del crate.
La etapa 4 lo movió a  y el script quedó casando NADA. No fallaba:
imprimía cero atribuciones, indistinguible de un log sin problemas. Comprobado
con el binario (RUST_LOG=info sobre zlib), no deducido. Ahora acepta las dos, y
tiene que seguir aceptándolas porque los logs viejos en disco dicen la vieja.

Además 14 rutas de módulo  en docs, que el barrido anterior no tocó
porque  no es frontera de palabra.
2026-09-09 19:28:48 +00:00

138 lines
6.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Genera y sube bundles shallow de las fuentes git del corpus. Ver ADR 0013 y el .sh hermano.
Es INCREMENTAL: consulta primero qué commits ya están en el mirror y sólo trabaja los que faltan.
Interrumpible sin penalización — cada bundle es independiente.
"""
import os, re, subprocess, sys, glob, tempfile, shutil
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
os.chdir(ROOT)
SB_USER = os.environ.get("SB_USER", "u647150")
SB_HOST = os.environ.get("SB_HOST", "u647150.your-storagebox.de")
SB_PORT = os.environ.get("SB_PORT", "23")
KEY = os.environ.get("KEY", os.path.expanduser("~/.ssh/github5"))
LIMITE = int(os.environ.get("LIMITE", "0"))
SOLO = os.environ.get("SOLO", "")
DEST = "hammer/fuentes-git"
SSH = ["ssh", "-4", "-p", SB_PORT, "-i", KEY, "-o", "StrictHostKeyChecking=accept-new",
"-o", "ConnectTimeout=30"]
def fuentes_git():
vistos, out = set(), []
for f in sorted(glob.glob("recipes/**/*.toml", recursive=True)):
s = open(f, errors="ignore").read()
repo = re.search(r'^\s*repo\s*=\s*"([^"]+)"', s, re.M)
com = re.search(r'^\s*commit\s*=\s*"([0-9a-fA-F]{7,40})"', s, re.M)
if not (repo and com):
continue
nombre = (re.search(r'^\s*name\s*=\s*"([^"]+)"', s, re.M) or [None, os.path.basename(f)[:-5]])[1]
c = com.group(1).lower()
if c in vistos: # el mismo commit desde dos colas = un solo bundle
continue
vistos.add(c)
out.append({"receta": nombre, "repo": repo.group(1), "commit": c})
return out
def ya_en_mirror():
r = subprocess.run(SSH + [f"{SB_USER}@{SB_HOST}", f"ls {DEST}"],
capture_output=True, text=True, timeout=180)
return {l.strip()[:-7] for l in r.stdout.splitlines() if l.strip().endswith(".bundle")}
def bundle(e, tmpdir):
"""Clona --depth 1 el commit exacto y emite un bundle.
Devuelve (ruta, None) si sale bien y (None, motivo) si no. El motivo es el stderr REAL de git,
no una conjetura: un fallo acá puede ser el servidor negando el fetch por sha suelto, pero
tambien un timeout, DNS, o un rate-limit, y cada uno se arregla distinto. Ver ADR 0013.
"""
d = os.path.join(tmpdir, "r")
shutil.rmtree(d, ignore_errors=True)
os.makedirs(d)
def git(*a, **kw):
return subprocess.run(["git", "-C", d] + list(a), capture_output=True, text=True,
timeout=kw.get("t", 900))
def paso(etiqueta, *a, **kw):
try:
r = git(*a, **kw)
except subprocess.TimeoutExpired:
return f"{etiqueta}: timeout"
return None if r.returncode == 0 else f"{etiqueta}: {(r.stderr or r.stdout).strip().splitlines()[-1][:160] if (r.stderr or r.stdout).strip() else 'rc=' + str(r.returncode)}"
out = os.path.join(tmpdir, f'{e["commit"]}.bundle')
for etiqueta, args, kw in (
("init", ("init", "-q"), {}),
("remote", ("remote", "add", "origin", e["repo"]), {}),
# --depth 1 del SHA exacto: el servidor debe permitir fetch por sha (uploadpack.allowReachableSHA1).
("fetch", ("fetch", "-q", "--depth", "1", "origin", e["commit"]), {"t": 1800}),
):
motivo = paso(etiqueta, *args, **kw)
if motivo: return None, motivo
# El campo `commit` de una receta NO siempre es un commit: puede pinear un objeto TAG anotado.
# Un SHA de tag es una identidad igual de buena (`git archive` lo acepta) y por eso hash_inputs
# no lo distingue, pero `refs/heads/*` sólo apunta a commits. Se pela para la rama y, si hubo
# que pelar, el objeto tag viaja aparte: sin él, el `cat-file -e <commit>` del lado de takana
# falla y el mirror queda correcto-pero-inútil justo para estas recetas.
r = git("rev-parse", e["commit"] + "^{commit}")
if r.returncode != 0:
return None, f"rev-parse: {(r.stderr or '').strip().splitlines()[-1][:160]}"
pelado = r.stdout.strip()
refs = ["refs/heads/hammer"]
pasos = [("update-ref", ("update-ref", "refs/heads/hammer", pelado), {})]
if pelado != e["commit"]:
refs.append("refs/tags/hammer-objeto")
pasos.append(("update-ref-tag", ("update-ref", "refs/tags/hammer-objeto", e["commit"]), {}))
pasos.append(("bundle", tuple(["bundle", "create", out] + refs), {"t": 1800}))
for etiqueta, args, kw in pasos:
motivo = paso(etiqueta, *args, **kw)
if motivo: return None, motivo
return (out, None) if os.path.exists(out) else (None, "bundle: git dijo OK pero no hay fichero")
def main():
fs = fuentes_git()
if SOLO: fs = [e for e in fs if e["receta"] == SOLO]
print(f"== fuentes git en el corpus: {len(fs)} commits distintos", flush=True)
try:
hay = ya_en_mirror()
except Exception as ex:
print(f"!! no pude listar el mirror ({ex}); asumo vacío", flush=True); hay = set()
print(f"== ya en el mirror: {len(hay)}", flush=True)
faltan = [e for e in fs if e["commit"] not in hay]
if LIMITE: faltan = faltan[:LIMITE]
print(f"== a espejar ahora: {len(faltan)}", flush=True)
subprocess.run(SSH + [f"{SB_USER}@{SB_HOST}", f"mkdir hammer {DEST}"],
capture_output=True, text=True)
ok = fallo = 0
tmpdir = tempfile.mkdtemp(prefix="hammer-git-mirror-")
try:
for i, e in enumerate(faltan, 1):
# Placeholder deliberado (llimphi-counter): su commit es 000…0 y no se puede espejar
# nunca. Se distingue del fallo real a propósito — un ✗ permanente en cada tanda entrena
# a no mirar los rojos, y entonces el que sí importa pasa desapercibido.
if set(e["commit"]) == {"0"}:
print(f" [{i}/{len(faltan)}] {e['receta']:<24} · placeholder, no espejable", flush=True)
continue
b, motivo = bundle(e, tmpdir)
if not b:
fallo += 1
print(f" [{i}/{len(faltan)}] {e['receta']:<24}{motivo}", flush=True)
continue
mb = os.path.getsize(b) / 1e6
r = subprocess.run(["rsync", "-a", "-e", " ".join(SSH), b,
f"{SB_USER}@{SB_HOST}:{DEST}/"], capture_output=True, text=True, timeout=3600)
if r.returncode == 0:
ok += 1
print(f" [{i}/{len(faltan)}] {e['receta']:<24}{mb:6.1f} MB", flush=True)
else:
fallo += 1
print(f" [{i}/{len(faltan)}] {e['receta']:<24} ✗ subida: {r.stderr.strip()[:60]}", flush=True)
os.remove(b)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
print(f"== espejados {ok} fallidos {fallo}")
return 0
sys.exit(main())