El problema real no es el coste de la tanda: es que una pregunta a las 3am son 8
horas de idle. Estas 3 piezas están hechas para que la noche no se desperdicie.
harkaq-campana.sh (worker) bucle autónomo: construye cada receta bajo la
jaula y guarda los veredictos crudos. Idempotente, saltea
lo medido, duerme y re-escanea. NUNCA bloquea esperando: un
build que falla no detiene la cola (y su veredicto interesa
igual — es cuando más importa saber qué tocó fuera). Gatea
por ABI>=7: moler la noche para producir "no sabemos" es
peor que no moler.
harvest-harkaq.sh (hub, cron) hermano de harvest-go.sh: baja veredictos,
clasifica contra el store COMPLETO, aplica las deps
DECLARABLES y commitea con `git add` explícito. La deuda
IRREDUCIBLE NO se toca (declarar algo que el store no
provee rompe el build en vez de arreglarlo) → needs-review.
harkaq-add-deps.py editor de recetas conservador: idempotente, preserva las
deps existentes, y ANTE LA DUDA NO TOCA (valida que el
resultado siga parseando como TOML y no encoja). Corre de
noche sobre la fuente de verdad del catálogo: un fichero mal
editado a las 3am no da un error, da una receta corrupta que
nadie mira hasta el lunes.
POR QUÉ NO HAY REGEX EN EL CAMINO CRÍTICO: intenté sacar la lista de "a quién le
falta declarar make" con uno y falló dos veces en la misma tarde. `\bmake\b`
colaba `cargo-make` (el guión ES límite de palabra). La versión estrecha perdía
LAS CINCO recetas donde harkaq había medido la deuda de verdad (`compile = "make
…"`: el carácter previo es una comilla). El número bailó 69→45→99 según el
retoque. Construí una herramienta de medición y después intenté adivinar con un
regex — la lista buena la da el kernel. El regex queda sólo para ORDENAR la cola.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
3.1 KiB
Python
Executable File
81 lines
3.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""harkaq-add-deps — añade deps de build a una receta, sin romperla. (Cosecha desatendida.)
|
|
|
|
harkaq-add-deps.py recipes/zlib.toml make binutils
|
|
|
|
Idempotente: si la dep ya está, no toca nada y sale 1 (nada que commitear).
|
|
|
|
Corre DESATENDIDO y de noche, editando ficheros que son la fuente de verdad del catálogo. Por eso
|
|
la regla es: **ante la duda, no tocar**. Un fichero mal editado a las 3am no da un error, da una
|
|
receta corrupta que nadie mira hasta el lunes. Cada caso que no se entiende sale sin cambios y con
|
|
motivo, que es la misma disciplina de `SinEvidencia`: no afirmar lo que no se sabe.
|
|
"""
|
|
import re
|
|
import sys
|
|
|
|
|
|
def anadir(path, nuevas):
|
|
with open(path) as f:
|
|
s = f.read()
|
|
orig = s
|
|
|
|
m = re.search(r'^\[deps\]\s*$(.*?)(?=^\[|\Z)', s, re.M | re.S)
|
|
if m:
|
|
bloque = m.group(1)
|
|
mb = re.search(r'^(\s*build\s*=\s*)\[([^\]]*)\]', bloque, re.M)
|
|
if mb:
|
|
ya = re.findall(r'"([^"]+)"', mb.group(2))
|
|
falta = [d for d in nuevas if d not in ya]
|
|
if not falta:
|
|
return False, "ya estaban declaradas"
|
|
lista = ya + falta
|
|
nuevo = mb.group(1) + "[" + ", ".join(f'"{d}"' for d in lista) + "]"
|
|
s = s[:m.start(1)] + bloque[:mb.start()] + nuevo + bloque[mb.end():] + s[m.end(1):]
|
|
else:
|
|
# Hay [deps] pero sin `build = [...]` (p.ej. sólo `runtime`). Insertar la línea.
|
|
linea = "build = [" + ", ".join(f'"{d}"' for d in nuevas) + "]\n"
|
|
s = s[:m.start(1)] + "\n" + linea + bloque.lstrip("\n") + s[m.end(1):]
|
|
else:
|
|
# Sin [deps]: se añade al final. `[build.phases]` suele ser la última sección y sus strings
|
|
# multilínea (''' … ''') se comerían un bloque puesto después ⇒ va ANTES de la primera
|
|
# sección [build...] si existe, y si no, al final.
|
|
bloque = "\n[deps]\nbuild = [" + ", ".join(f'"{d}"' for d in nuevas) + "]\n"
|
|
mfase = re.search(r'^\[build\.phases\]', s, re.M)
|
|
if mfase:
|
|
s = s[:mfase.start()] + bloque.lstrip("\n") + "\n" + s[mfase.start():]
|
|
else:
|
|
s = s.rstrip("\n") + "\n" + bloque
|
|
|
|
if s == orig:
|
|
return False, "sin cambios"
|
|
|
|
# Guarda: sólo escribir si el TOML sigue pareciendo un TOML y no perdimos nada. Un parser de
|
|
# verdad sería mejor, pero tomllib no ESCRIBE y re-emitir con un writer reordenaría el fichero
|
|
# entero (y con él, comentarios que valen oro en este repo).
|
|
try:
|
|
import tomllib
|
|
tomllib.loads(s)
|
|
except Exception as e:
|
|
return False, f"el resultado no parsea como TOML ({e}) — NO se toca"
|
|
if len(s) < len(orig):
|
|
return False, "el resultado es más corto que el original — NO se toca"
|
|
|
|
with open(path, "w") as f:
|
|
f.write(s)
|
|
return True, "ok"
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 3:
|
|
print(__doc__.split("\n")[2].strip(), file=sys.stderr)
|
|
return 2
|
|
path, nuevas = sys.argv[1], sys.argv[2:]
|
|
ok, motivo = anadir(path, nuevas)
|
|
if not ok:
|
|
print(f" ·· {path}: {motivo}", file=sys.stderr)
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|