planear: buscar la receta por el PAQUETE del origen — y el intérprete no es el programa
`origen_binario` buscaba la receta por el nombre del SERVICIO, y ése casi nunca es el nombre del paquete: `sshd` lo trae `openssh`, `crond` lo trae `cronie`. El censo ya le preguntó al gestor de paquetes quién posee cada binario, así que ese nombre también se prueba. El perfil pasó de 4 recetas a 7, y los 27 servicios quedan: receta-takana 9 · suelto 9 · paquete-ajeno 13 · interprete 4 · borrado 4. **La trampa, que es la que haría mentir al perfil.** `openclaw` lo posee el paquete `nodejs`; `fail2ban-server`, `glances` y `uvicorn` los posee `python`. Contarlos como cubiertos porque existe `recipes/nodejs.toml` haría salir el perfil N/N describiendo un servidor al que le faltan CUATRO programas. Tienen clase propia (`interprete`) y cuentan las DOS cosas a la vez, porque las dos son ciertas: su runtime entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo que pase— y el servicio sigue listado como NO cubierto. Meterlo sólo en las raíces miente; dejarlo sólo en los faltantes arma una imagen sin runtime y el programa, cuando llegue, no arranca. Y el paquete del origen no se llama igual que la receta ni para el mismo intérprete: Artix empaqueta `python` y el catálogo tiene `python3.toml`. Sin ese alias tres servicios decían «hace falta una receta takana» teniendo el runtime sellado — dos trabajos muy distintos. **Sellada y sin sellar tampoco son lo mismo**, y el perfil las listaba igual: una sellada se INSTALA del repo firmado, una sin sellar hay que CONSTRUIRLA. Salió con un caso real — `recipes/qdrant.toml` entró al catálogo desde otro frente mientras se escribía esto y no tiene artefacto. Ahora se marca en la línea y se resume al pie. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
@@ -53,7 +53,7 @@ def perfil(censo, label, repo):
|
||||
|
||||
Lo que el perfil NO puede cubrir se lista aparte, con su motivo. Un perfil que se calla lo que le
|
||||
falta sale N/N y describe un servidor incompleto — la lección de `foot` en `escritorio-sway`."""
|
||||
raices, sin_receta = {}, []
|
||||
raices, sin_receta, sin_sellar = {}, [], set()
|
||||
for s_ in censo.get("servicio", []):
|
||||
if s_.get("decision") != "muda":
|
||||
continue
|
||||
@@ -66,8 +66,25 @@ def perfil(censo, label, repo):
|
||||
# (uvicorn y python3 son los dos `python3`), y repetir una raíz en un perfil no la
|
||||
# duplica — sólo ensucia y hace creer que son dos cosas distintas.
|
||||
receta = m.group(1) if m else elegido
|
||||
# ⚠ Sellada y sin sellar no son lo mismo, y el perfil las listaba igual. Una sellada se
|
||||
# INSTALA del repo firmado; una sin sellar hay que CONSTRUIRLA antes, y eso puede ser una
|
||||
# tarde. Medido: `qdrant` entró al catálogo ayer desde otro frente y no tiene artefacto.
|
||||
if "sin sellar" in detalle:
|
||||
sin_sellar.add(receta)
|
||||
raices.setdefault(receta, []).append((s_["name"], tuple(s_.get("ports", []))))
|
||||
else:
|
||||
# ⚠ Un servicio de clase `interprete` cuenta las DOS cosas a la vez, y hay que hacer las
|
||||
# dos: su runtime SÍ entra al perfil —`openclaw` necesita `nodejs` en la imagen pase lo
|
||||
# que pase— y el servicio sigue listado como NO CUBIERTO, porque el intérprete no es el
|
||||
# programa. Meterlo sólo en las raíces haría salir el perfil N/N con la app faltando;
|
||||
# dejarlo sólo en la lista de faltantes armaría una imagen sin runtime y el programa,
|
||||
# cuando llegue, no arrancaría.
|
||||
if clase == "interprete":
|
||||
import re as _re
|
||||
m = _re.search(r'`recipes/([^`]+)\.toml`', detalle)
|
||||
if m:
|
||||
raices.setdefault(m.group(1), []).append(
|
||||
(f"{s_['name']} (sólo el runtime)", tuple(s_.get("ports", []))))
|
||||
sin_receta.append((s_["name"], clase, detalle))
|
||||
|
||||
L = [f"[perfil.{label}]",
|
||||
@@ -82,9 +99,14 @@ def perfil(censo, label, repo):
|
||||
nombres = ", ".join(sorted({q[0] for q in quienes}))
|
||||
puertos = sorted({p for q in quienes for p in q[1]})
|
||||
det = nombres + (f" · puertos {','.join(str(x) for x in puertos)}" if puertos else "")
|
||||
if receta in sin_sellar:
|
||||
det = ("⚠ SIN SELLAR: hay que construirla antes de armar la imagen — " + det).strip(" —")
|
||||
nota = f" # {det}" if det and det != receta else ""
|
||||
L.append(f' "{receta}",{nota}')
|
||||
L.append("]")
|
||||
if sin_sellar:
|
||||
L.append(f"# ⚠ {len(sin_sellar)} sin artefacto en el store: {', '.join(sorted(sin_sellar))}.")
|
||||
L.append("# El perfil es correcto, pero armar la imagen las va a CONSTRUIR, no instalar.")
|
||||
return "\n".join(L), sin_receta
|
||||
|
||||
|
||||
|
||||
@@ -121,6 +121,17 @@ def _no_aplica(nombre):
|
||||
return None
|
||||
|
||||
|
||||
# Paquetes que proveen un RUNTIME, no un programa. Que exista `recipes/nodejs.toml` no significa que
|
||||
# `openclaw` esté cubierto: el intérprete se resuelve y el programa sigue faltando.
|
||||
INTERPRETES = {"python", "python3", "nodejs", "node", "perl", "ruby", "php", "lua", "java", "jre"}
|
||||
|
||||
# El paquete del origen y la receta del catálogo no siempre se llaman igual ni siquiera para el mismo
|
||||
# intérprete: Artix empaqueta `python` y el catálogo tiene `python3.toml`. Sin esto, tres servicios
|
||||
# salían «hace falta una receta takana» cuando el runtime YA está sellado y lo que falta es el
|
||||
# programa — dos trabajos muy distintos.
|
||||
ALIAS_INTERPRETE = {"python": "python3", "node": "nodejs", "jre": "java"}
|
||||
|
||||
|
||||
# ── familias de equivalentes funcionales ────────────────────────────────────────────────────────
|
||||
# Agrupa cosas que hacen EL MISMO trabajo, para poder ofrecer alternativas. Es conocimiento de
|
||||
# dominio, no gusto: que nginx y caddy sirvan HTTP es un hecho. Lo que NO se decide acá es cuál es
|
||||
@@ -285,14 +296,33 @@ def origen_binario(e, repo=None):
|
||||
# contestar `suelto` a todo sería una respuesta falsa con forma de respuesta.
|
||||
raise SystemExit(f"!! no encuentro el catálogo de recetas en {repo}/recipes — "
|
||||
f"pasá --repo o corré desde el repo")
|
||||
for cand in (nombre, nombre.replace("_", "-"), os.path.basename(exe or "")):
|
||||
if cand and os.path.exists(os.path.join(repo, "recipes", f"{cand}.toml")):
|
||||
import glob
|
||||
# ⚠ El nombre del SERVICIO no es el nombre del paquete: `sshd` lo trae `openssh`, `crond` lo trae
|
||||
# `cronie`. Buscar sólo por el nombre del servicio daba «no hay receta» para cinco que el catálogo
|
||||
# SÍ construye y tiene selladas. El censo ya le preguntó al gestor de paquetes quién posee el
|
||||
# binario, así que ese nombre también se prueba.
|
||||
import glob
|
||||
for cand in (nombre, nombre.replace("_", "-"), os.path.basename(exe or ""),
|
||||
pkg, (pkg or "").removesuffix("-bin"), ALIAS_INTERPRETE.get(pkg or "", "")):
|
||||
if not cand or not os.path.exists(os.path.join(repo, "recipes", f"{cand}.toml")):
|
||||
continue
|
||||
# ⚠⚠ Y la trampa, que es la que haría mentir al perfil: un paquete INTÉRPRETE provee el
|
||||
# runtime, no el programa. `openclaw` lo posee `nodejs` y `fail2ban-server` lo posee
|
||||
# `python`; tener esas recetas NO te da esos servicios. Contarlos como cubiertos haría que el
|
||||
# perfil saliera N/N describiendo un servidor al que le faltan cuatro programas.
|
||||
if pkg in INTERPRETES and cand in (pkg, ALIAS_INTERPRETE.get(pkg or "", "")) and cand != nombre:
|
||||
sellado = bool(glob.glob(os.path.join(repo, "store", f"*-{cand}")))
|
||||
return ("receta-takana",
|
||||
f"takana YA lo construye: `recipes/{cand}.toml`"
|
||||
+ (" (sellado en el store)" if sellado else " (sin sellar: hay que construirlo)")
|
||||
+ f" ⇒ instalarlo del repo firmado, no copiar el binario de {exe or '?'}")
|
||||
return ("interprete",
|
||||
f"lo corre el intérprete `{pkg}`, que takana SÍ construye (`recipes/{cand}.toml`"
|
||||
+ (", sellada)" if sellado else ", sin sellar)")
|
||||
+ f" — pero el intérprete NO es el programa: `{nombre}` es código que hay que "
|
||||
f"llevar y declarar aparte. El perfil resuelve el runtime y nada más")
|
||||
sellado = bool(glob.glob(os.path.join(repo, "store", f"*-{cand}")))
|
||||
via = "" if cand in (nombre, nombre.replace("_", "-")) else f" (por su paquete `{pkg}`)" \
|
||||
if cand in (pkg, (pkg or "").removesuffix("-bin")) else f" (por su binario)"
|
||||
return ("receta-takana",
|
||||
f"takana YA lo construye{via}: `recipes/{cand}.toml`"
|
||||
+ (" (sellado en el store)" if sellado else " (sin sellar: hay que construirlo)")
|
||||
+ f" ⇒ instalarlo del repo firmado, no copiar el binario de {exe or '?'}")
|
||||
if pkg:
|
||||
return ("paquete-ajeno",
|
||||
f"lo provee el gestor del origen: paquete `{pkg}`. En el destino hace falta una "
|
||||
|
||||
Reference in New Issue
Block a user