Files
hammer/scripts/farm/harkaq-add-deps.py
T
sergioandClaude Opus 4.8 f783f0107d harkaq: campaña desatendida — medir→declarar→verificar, sin regex y sin preguntas
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>
2026-07-15 22:53:57 -04:00

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())