yupana: radio mezclaba las aristas de build con las de runtime y sobreestimaba el coste
`radio gcc-libs` contestaba «9 dependientes, 9 sellados caen a deuda», y con ese número avisé de una cascada de horas —firefox con PGO dos veces, waterfox, atuq—. La respuesta correcta es CERO: los cinco declaran gcc-libs en `deps.runtime`, y **`runtime` no entra en `hash_inputs`**, así que re-sellarla no re-hashea a nadie. Se vio al mirar el store después del cambio: los cinco seguían al día. El grafo de `_deps` une build+runtime a propósito —eso es el CIERRE de una imagen, y esa unión arregló en su día que firefox arrancara sin libstdc++—, pero `radio` responde otra pregunta: «¿a quién obligo a reconstruir?». Ahora usa dos grafos: la cascada sale del de BUILD, y las imágenes del de la unión, que es lo correcto para cada uno. Los que dependen sólo por runtime se siguen informando, nombrados, diciendo que NO se reconstruyen. Controles: gcc-libs ⇒ 0 de cascada y 9 sólo-runtime; zlib ⇒ 126 directos y 391 transitivos (sigue viendo las de verdad); zsh ⇒ 0. Y `test-yupana-radio.py` pasa: libdrm sigue cruzando las 5 colas. Un número que sobreestima frena arreglos correctos, que es justo lo que casi pasa acá. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+47
-8
@@ -75,16 +75,23 @@ sys.path.insert(0, str(ROOT / "scripts"))
|
|||||||
import targets as targets_mod
|
import targets as targets_mod
|
||||||
|
|
||||||
|
|
||||||
def cargar_colas():
|
def cargar_colas(solo_build=False):
|
||||||
"""{cola: {nombre: [deps de build]}} sobre TODAS las colas del disco. `corpus` = recipes/;
|
"""{cola: {nombre: [deps]}} sobre TODAS las colas del disco. `corpus` = recipes/;
|
||||||
`incoming-<x>` = recipes/incoming-<x>/. Es el grafo completo, sin recortar por vista."""
|
`incoming-<x>` = recipes/incoming-<x>/. Es el grafo completo, sin recortar por vista.
|
||||||
|
|
||||||
|
`solo_build=True` devuelve **únicamente las aristas de build**. La distinción no es cosmética y
|
||||||
|
la pide `radio`: `deps.runtime` **NO entra en el ArtifactHash**, así que un cambio en una receta
|
||||||
|
NO re-hashea a quien la consume sólo en runtime. Con la unión, `radio gcc-libs` contestaba
|
||||||
|
«9 sellados caen a deuda» —firefox con PGO dos veces, horas de granja— y la respuesta correcta
|
||||||
|
era CERO: los cinco la declaran en `runtime`. Medido el 2026-09-18."""
|
||||||
|
lee = _deps_build if solo_build else _deps
|
||||||
colas = {}
|
colas = {}
|
||||||
for f in sorted(glob.glob(str(ROOT / "recipes/*.toml"))):
|
for f in sorted(glob.glob(str(ROOT / "recipes/*.toml"))):
|
||||||
n = os.path.basename(f)[:-5]
|
n = os.path.basename(f)[:-5]
|
||||||
colas.setdefault("corpus", {})[n] = _deps(f)
|
colas.setdefault("corpus", {})[n] = lee(f)
|
||||||
for f in sorted(glob.glob(str(ROOT / "recipes/incoming-*/*.toml"))):
|
for f in sorted(glob.glob(str(ROOT / "recipes/incoming-*/*.toml"))):
|
||||||
q = os.path.basename(os.path.dirname(f))
|
q = os.path.basename(os.path.dirname(f))
|
||||||
colas.setdefault(q, {})[os.path.basename(f)[:-5]] = _deps(f)
|
colas.setdefault(q, {})[os.path.basename(f)[:-5]] = lee(f)
|
||||||
return colas
|
return colas
|
||||||
|
|
||||||
|
|
||||||
@@ -104,6 +111,16 @@ def _deps(f):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _deps_build(f):
|
||||||
|
"""Sólo las aristas de BUILD: las únicas que propagan un re-hash (`runtime` está fuera de
|
||||||
|
`hash_inputs`, igual que `license`). Para el cierre de una imagen hace falta la unión —eso es
|
||||||
|
`_deps`—; para «¿a quién obligo a reconstruir?» hace falta esto."""
|
||||||
|
try:
|
||||||
|
return sorted(set(tomllib.load(open(f, "rb")).get("deps", {}).get("build", []) or []))
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
# --- INVARIANTE (c): CANÓNICO — sin duplicados accidentales entre orígenes ------------------------
|
# --- INVARIANTE (c): CANÓNICO — sin duplicados accidentales entre orígenes ------------------------
|
||||||
# Sufijos que en takana marcan una VARIANTE deliberada del mismo paquete (misma fuente, otra decisión
|
# Sufijos que en takana marcan una VARIANTE deliberada del mismo paquete (misma fuente, otra decisión
|
||||||
# de build: estático/shared, kernel por perfil, mesa por backend). Compartir sha con estos NO es un
|
# de build: estático/shared, kernel por perfil, mesa por backend). Compartir sha con estos NO es un
|
||||||
@@ -687,7 +704,14 @@ def radio(nombre, como_json=False):
|
|||||||
if not ubic:
|
if not ubic:
|
||||||
print(f"no hay receta `{nombre}` en ninguna cola", file=sys.stderr)
|
print(f"no hay receta `{nombre}` en ninguna cola", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
rev = dependientes(colas)
|
# ⚠ DOS GRAFOS, Y LA DIFERENCIA ES EL NÚMERO QUE LA GENTE USA PARA DECIDIR (2026-09-18).
|
||||||
|
# El de BUILD es el que propaga un re-hash; el de la unión (build+runtime) es el del cierre de
|
||||||
|
# una imagen. `radio` mezclaba los dos y contestaba «9 sellados caen a deuda» para `gcc-libs`,
|
||||||
|
# cuando la respuesta correcta es CERO: sus cinco consumidores la declaran en `runtime`, que no
|
||||||
|
# entra en `hash_inputs`. Ese número de más casi cuesta reconstruir firefox con PGO dos veces.
|
||||||
|
rev_b = dependientes(cargar_colas(solo_build=True)) # cascada de rebuild
|
||||||
|
rev_u = dependientes(colas) # cierre / imágenes
|
||||||
|
rev = rev_b
|
||||||
memb = membresia(colas, targets_mod.load())
|
memb = membresia(colas, targets_mod.load())
|
||||||
|
|
||||||
resultado = {}
|
resultado = {}
|
||||||
@@ -701,9 +725,19 @@ def radio(nombre, como_json=False):
|
|||||||
continue
|
continue
|
||||||
vistos.add(n)
|
vistos.add(n)
|
||||||
pila.extend(rev.get(n, set()))
|
pila.extend(rev.get(n, set()))
|
||||||
|
# Las imágenes salen del grafo de la UNIÓN: para que una imagen ARRANQUE hacen falta también
|
||||||
|
# las deps de runtime, aunque no fuercen rebuild.
|
||||||
|
vistos_u, pila_u = set(), list(rev_u.get(nodo, set()))
|
||||||
|
while pila_u:
|
||||||
|
n = pila_u.pop()
|
||||||
|
if n in vistos_u:
|
||||||
|
continue
|
||||||
|
vistos_u.add(n)
|
||||||
|
pila_u.extend(rev_u.get(n, set()))
|
||||||
imgs = set()
|
imgs = set()
|
||||||
for n in vistos | {nodo}:
|
for n in vistos_u | {nodo}:
|
||||||
imgs |= memb.get(n, set())
|
imgs |= memb.get(n, set())
|
||||||
|
solo_runtime = sorted(vistos_u - vistos)
|
||||||
# Cuántos de los transitivos YA están sellados: ésos son los que HOY están al día y el
|
# Cuántos de los transitivos YA están sellados: ésos son los que HOY están al día y el
|
||||||
# re-hash MANDA A DEUDA (los que ya están en deuda igual habría que reconstruirlos). El
|
# re-hash MANDA A DEUDA (los que ya están en deuda igual habría que reconstruirlos). El
|
||||||
# radio verdadero de rebuild forzado por el cambio.
|
# radio verdadero de rebuild forzado por el cambio.
|
||||||
@@ -711,6 +745,8 @@ def radio(nombre, como_json=False):
|
|||||||
resultado[f"{nodo[0]}/{nodo[1]}"] = dict(
|
resultado[f"{nodo[0]}/{nodo[1]}"] = dict(
|
||||||
directos=len(directos), transitivos=len(vistos),
|
directos=len(directos), transitivos=len(vistos),
|
||||||
imagenes=sorted(imgs), sellados_que_caen=sellados,
|
imagenes=sorted(imgs), sellados_que_caen=sellados,
|
||||||
|
solo_runtime=len(solo_runtime),
|
||||||
|
solo_runtime_nombres=[f"{q}/{n}" for q, n in solo_runtime][:12],
|
||||||
dependientes_por_cola=_por_cola(vistos))
|
dependientes_por_cola=_por_cola(vistos))
|
||||||
|
|
||||||
if como_json:
|
if como_json:
|
||||||
@@ -718,8 +754,11 @@ def radio(nombre, como_json=False):
|
|||||||
return 0
|
return 0
|
||||||
for nodo, r in resultado.items():
|
for nodo, r in resultado.items():
|
||||||
print(f"== radio de tocar {nodo}")
|
print(f"== radio de tocar {nodo}")
|
||||||
print(f" dependientes directos {r['directos']}")
|
print(f" dependientes directos {r['directos']} (por BUILD: los que un re-hash arrastra)")
|
||||||
print(f" dependientes transitivos {r['transitivos']} (el radio real de un re-hash en cascada)")
|
print(f" dependientes transitivos {r['transitivos']} (el radio real de un re-hash en cascada)")
|
||||||
|
if r["solo_runtime"]:
|
||||||
|
print(f" sólo por RUNTIME {r['solo_runtime']} NO se reconstruyen "
|
||||||
|
f"(`runtime` está fuera de hash_inputs): " + ", ".join(r["solo_runtime_nombres"]))
|
||||||
print(f" por cola " +
|
print(f" por cola " +
|
||||||
" ".join(f"{q}={n}" for q, n in sorted(r["dependientes_por_cola"].items())))
|
" ".join(f"{q}={n}" for q, n in sorted(r["dependientes_por_cola"].items())))
|
||||||
print(f" IMÁGENES afectadas {', '.join(r['imagenes']) or '(ninguna declarada)'}")
|
print(f" IMÁGENES afectadas {', '.join(r['imagenes']) or '(ninguna declarada)'}")
|
||||||
|
|||||||
Reference in New Issue
Block a user