sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
catálogo objetivo P1: manifiesto de perfiles — el set de la distro deja de ser un string de shell
1#!/usr/bin/env python3
2# targets.py — lee `docs/state/targets.toml` (el manifiesto de objetivo) y expande un perfil a su
3# lista de RAÍCES. Un solo parser para dos consumidores: los scripts de imagen (shell) y el grafo
4# de estado (`build-state.py`), para que el set de paquetes de la distro no vuelva a vivir en tres
5# strings de shell distintos.
6#
7# Uso: scripts/targets.py cli # raíces expandidas, una línea separada por espacios
8# scripts/targets.py --lista # perfiles disponibles, con su descripción
9# scripts/targets.py --json # todo el manifiesto expandido
10# Como módulo: from targets import load → {perfil: {descripcion, cola, hereda, paquetes, raices}}
11#
12# `hereda` es transitivo y el orden se PRESERVA (primero las raíces de lo heredado, en el orden
13# declarado, después las propias), porque hay imágenes donde el orden de hidratación importa
14# (mirada: libs antes que binarios). Duplicados: se queda la primera aparición.
15import json, sys, tomllib
16from pathlib import Path
17
18ROOT = Path(__file__).resolve().parent.parent
19DEFAULT = ROOT / "docs/state/targets.toml"
20
21
22def load(path=DEFAULT):
23 """Devuelve {perfil: {descripcion, cola, hereda, paquetes, raices}} con `raices` ya expandido."""
24 doc = tomllib.load(open(path, "rb"))
25 crudos = doc.get("perfil", {})
26 perfiles = {}
27 for n, p in crudos.items():
28 perfiles[n] = dict(
29 descripcion=p.get("descripcion", ""),
30 cola=p.get("cola", "corpus"),
31 hereda=list(p.get("hereda", [])),
32 paquetes=list(p.get("paquetes", [])),
33 )
34
35 # Expansión de `hereda` con detección de ciclo (un manifiesto con un ciclo es un error del
36 # humano, no un caso a tolerar: sale ruidoso).
37 def expandir(n, camino):
38 if n in camino:
39 raise ValueError(f"ciclo en `hereda`: {' → '.join(camino + [n])}")
40 if n not in perfiles:
41 raise KeyError(f"perfil desconocido en `hereda`: {n}")
42 p = perfiles[n]
43 if "raices" in p:
44 return p["raices"]
45 vistos, raices = set(), []
46 for h in p["hereda"] + [None]:
47 for pkg in (expandir(h, camino + [n]) if h else p["paquetes"]):
48 if pkg not in vistos:
49 vistos.add(pkg); raices.append(pkg)
50 p["raices"] = raices
51 return raices
52
53 for n in perfiles:
54 expandir(n, [])
55 return perfiles
56
57
58def main():
59 args = sys.argv[1:]
60 try:
61 perfiles = load()
62 except FileNotFoundError:
63 print(f"no existe {DEFAULT.relative_to(ROOT)}", file=sys.stderr); return 1
64 except (ValueError, KeyError) as e:
65 print(f"manifiesto inválido: {e}", file=sys.stderr); return 1
66
67 if not args or args[0] in ("--lista", "-l"):
68 for n, p in sorted(perfiles.items()):
69 print(f"{n:20} {len(p['raices']):3} raíces [{p['cola']}] {p['descripcion']}")
70 return 0
71 if args[0] == "--json":
72 print(json.dumps(perfiles, indent=1, ensure_ascii=False)); return 0
73
74 n = args[0]
75 if n not in perfiles:
76 print(f"perfil desconocido: {n} (hay: {', '.join(sorted(perfiles))})", file=sys.stderr)
77 return 1
78 print(" ".join(perfiles[n]["raices"]))
79 return 0
80
81
82if __name__ == "__main__":
83 sys.exit(main())