diff --git a/docs/state/targets.toml b/docs/state/targets.toml new file mode 100644 index 00000000..598befe4 --- /dev/null +++ b/docs/state/targets.toml @@ -0,0 +1,61 @@ +# targets.toml — el MANIFIESTO DE OBJETIVO: qué paquetes debe tener la distro. +# +# POR QUÉ EXISTE. `build-state.json` es el grafo de lo que EXISTE (768 recetas, cierra sin huérfanas). +# Lo que la distro DEBE tener no estaba en ningún lado: vivía en dos strings de shell de +# `product-userland-from-repo.sh`, otro de `mirada-usb.sh`, 55 listas planas en `tandas/` y una cola +# de 206 recetas KDE. Por eso "¿cuánto falta?" no tenía respuesta: el mapa cerraba perfecto, pero +# sólo cubría el territorio ya conquistado. Este fichero es el destino. (Plan: docs/plan-catalogo-objetivo.md) +# +# LA INVERSIÓN. `paquetes` lista sólo las RAÍCES — lo que un usuario pide POR NOMBRE. La clausura +# (las libs de las que cuelgan) la calcula el grafo siguiendo las aristas de deps, no la escribe un +# humano. Al revés de `tandas/base-system-3-libs.txt`, que enumera a mano miembros de la clausura. +# +# Un `perfil` = una imagen enviable. `hereda` compone (transitivo, sin ciclos). `cola` dice en qué +# árbol de recetas viven sus raíces: `corpus` = `recipes/`; otra cosa = `recipes/incoming-/`, que +# `build-state.py` sólo carga con su flag (`--kde`) ⇒ los perfiles de una cola no cargada se saltan +# en vez de reportar raíces fantasma. +# +# Consumidores: `scripts/targets.py ` (expande a lista de nombres) y `scripts/build-state.py` +# (marca los nodos `wanted` y calcula la membresía de perfil de cada receta). +schema = "hammer-targets/1" + +[perfil.base] +descripcion = "userland foundational C: la distro arranca y se usa (shell, VCS, privilegios, red, disco, TLS, proceso, editor, docs)" +# Lift verbatim de BASE_SYSTEM_PKGS (scripts/product-userland-from-repo.sh). De-Alpinizado y firmado, +# tandas base-system-1..3. +paquetes = [ + "bash", "git", "sudo", "doas", "util-linux", "shadow", "kbd", "procps-ng", + "iproute2", "iputils", "dhcpcd", "e2fsprogs", "dosfstools", "parted", "rsync", + "sed", "tzdata", "vim", "dbus", "ca-certificates", "gnupg", "lsof", "strace", + "pciutils", "usbutils", "mandoc", +] + +[perfil.cli] +descripcion = "la capa CLI moderna (Rust) encima de base — el userland real del producto" +hereda = ["base"] +# Lift verbatim de CLI_RUST_PKGS (scripts/product-userland-from-repo.sh). +paquetes = [ + "bat", "fd", "ripgrep", "jq", "zoxide", "dust", "procs", "htop", "less", "tree", + "hyperfine", "delta", "eza", "sd", "tokei", "fzf", "skim", "git-cliff", "gitui", + "starship", +] + +[perfil.escritorio-mirada] +descripcion = "rootfs slim del USB mirada: compositor Wayland propio sobre mesa software" +# Lift verbatim de PKGS (scripts/mirada-usb.sh). NO hereda base: es una imagen slim a propósito. +# El ORDEN importa (libs primero, binarios mirada al final) y se preserva. +paquetes = [ + "mesa-swrast", "wayland", "libdrm", "seatd", "libinput", "libudev-zero", + "libxkbcommon", "pixman", "linux-pam", "mtdev", "libevdev", + "mirada-compositor", "mirada-greeter", "mirada-ctl", +] + +[perfil.escritorio-kde] +descripcion = "escritorio KDE Plasma 6 desde fuente" +cola = "incoming-kde" +# Raíces del rootfs de metal (docs/HANDOFF-noche-kde-2026-07-14.md:135). El resto del escritorio +# —137 artefactos hidratados— es CLAUSURA de estas 7, no lista a mano. +paquetes = [ + "plasma-workspace", "kwin", "kscreenlocker", "breeze", "frameworkintegration", + "qqc2-desktop-style", "plasma-integration", +] diff --git a/scripts/mirada-usb.sh b/scripts/mirada-usb.sh index 0312d2d3..52227a95 100644 --- a/scripts/mirada-usb.sh +++ b/scripts/mirada-usb.sh @@ -34,8 +34,9 @@ declare -A PIN=( [mesa-swrast]=410e4fb9806598921048f492508725f55ee8475a1e9ef8ea2dc64f357154a0d9 [libxkbcommon]=cef7db4a82882ee942fef5e758344da67c9f3fce6b221610a7db9cb05bfb3b53 ) -# Orden: libs primero, binarios mirada al final. Todos aportan .so o /usr/bin. -PKGS="mesa-swrast wayland libdrm seatd libinput libudev-zero libxkbcommon pixman linux-pam mtdev libevdev mirada-compositor mirada-greeter mirada-ctl" +# Orden: libs primero, binarios mirada al final. Todos aportan .so o /usr/bin. El set sale del +# MANIFIESTO DE OBJETIVO (docs/state/targets.toml, perfil `escritorio-mirada`), que preserva el orden. +PKGS="$("$(dirname "$0")/targets.py" escritorio-mirada)" resolve() { # nombre -> hash del store (pin si existe, si no el más reciente por mtime) local n="$1" diff --git a/scripts/product-userland-from-repo.sh b/scripts/product-userland-from-repo.sh index ff650d96..685a4210 100755 --- a/scripts/product-userland-from-repo.sh +++ b/scripts/product-userland-from-repo.sh @@ -18,11 +18,12 @@ DB="$RFS/var/lib/hammer/installed.json" # Set curado por defecto: el userland "real" que hace usable a la distro. Todos deben estar en el # índice del repo firmado. Editá/pasá args para cambiarlo. -# base-system: userland foundational C (shell/VCS/privilegios/red/disco/TLS/proceso/editor/docs), -# de-Alpinizado y firmado (tandas base-system-1..3). CLI Rust: la capa "moderna" curada encima. -BASE_SYSTEM_PKGS="bash git sudo doas util-linux shadow kbd procps-ng iproute2 iputils dhcpcd e2fsprogs dosfstools parted rsync sed tzdata vim dbus ca-certificates gnupg lsof strace pciutils usbutils mandoc" -CLI_RUST_PKGS="bat fd ripgrep jq zoxide dust procs htop less tree hyperfine delta eza sd tokei fzf skim git-cliff gitui starship" -DEFAULT_PKGS="$BASE_SYSTEM_PKGS $CLI_RUST_PKGS" +# El set ya NO vive acá: sale del MANIFIESTO DE OBJETIVO (docs/state/targets.toml), perfil `cli` = +# `base` (userland foundational C, de-Alpinizado, tandas base-system-1..3) + la capa CLI Rust encima. +# Cambiar el userland del producto = editar el manifiesto, que es también lo que mide el grafo de +# estado. Env PERFIL para armar otra imagen (`scripts/targets.py --lista` los muestra). +PERFIL="${PERFIL:-cli}" +DEFAULT_PKGS="$("$ROOT/scripts/targets.py" "$PERFIL")" PKGS_LIST="${PKGS:-${*:-$DEFAULT_PKGS}}" [ -n "${OVER:-}" ] || { rm -rf "$RFS"; mkdir -p "$RFS"; } diff --git a/scripts/targets.py b/scripts/targets.py new file mode 100755 index 00000000..2139e130 --- /dev/null +++ b/scripts/targets.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# targets.py — lee `docs/state/targets.toml` (el manifiesto de objetivo) y expande un perfil a su +# lista de RAÍCES. Un solo parser para dos consumidores: los scripts de imagen (shell) y el grafo +# de estado (`build-state.py`), para que el set de paquetes de la distro no vuelva a vivir en tres +# strings de shell distintos. +# +# Uso: scripts/targets.py cli # raíces expandidas, una línea separada por espacios +# scripts/targets.py --lista # perfiles disponibles, con su descripción +# scripts/targets.py --json # todo el manifiesto expandido +# Como módulo: from targets import load → {perfil: {descripcion, cola, hereda, paquetes, raices}} +# +# `hereda` es transitivo y el orden se PRESERVA (primero las raíces de lo heredado, en el orden +# declarado, después las propias), porque hay imágenes donde el orden de hidratación importa +# (mirada: libs antes que binarios). Duplicados: se queda la primera aparición. +import json, sys, tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT = ROOT / "docs/state/targets.toml" + + +def load(path=DEFAULT): + """Devuelve {perfil: {descripcion, cola, hereda, paquetes, raices}} con `raices` ya expandido.""" + doc = tomllib.load(open(path, "rb")) + crudos = doc.get("perfil", {}) + perfiles = {} + for n, p in crudos.items(): + perfiles[n] = dict( + descripcion=p.get("descripcion", ""), + cola=p.get("cola", "corpus"), + hereda=list(p.get("hereda", [])), + paquetes=list(p.get("paquetes", [])), + ) + + # Expansión de `hereda` con detección de ciclo (un manifiesto con un ciclo es un error del + # humano, no un caso a tolerar: sale ruidoso). + def expandir(n, camino): + if n in camino: + raise ValueError(f"ciclo en `hereda`: {' → '.join(camino + [n])}") + if n not in perfiles: + raise KeyError(f"perfil desconocido en `hereda`: {n}") + p = perfiles[n] + if "raices" in p: + return p["raices"] + vistos, raices = set(), [] + for h in p["hereda"] + [None]: + for pkg in (expandir(h, camino + [n]) if h else p["paquetes"]): + if pkg not in vistos: + vistos.add(pkg); raices.append(pkg) + p["raices"] = raices + return raices + + for n in perfiles: + expandir(n, []) + return perfiles + + +def main(): + args = sys.argv[1:] + try: + perfiles = load() + except FileNotFoundError: + print(f"no existe {DEFAULT.relative_to(ROOT)}", file=sys.stderr); return 1 + except (ValueError, KeyError) as e: + print(f"manifiesto inválido: {e}", file=sys.stderr); return 1 + + if not args or args[0] in ("--lista", "-l"): + for n, p in sorted(perfiles.items()): + print(f"{n:20} {len(p['raices']):3} raíces [{p['cola']}] {p['descripcion']}") + return 0 + if args[0] == "--json": + print(json.dumps(perfiles, indent=1, ensure_ascii=False)); return 0 + + n = args[0] + if n not in perfiles: + print(f"perfil desconocido: {n} (hay: {', '.join(sorted(perfiles))})", file=sys.stderr) + return 1 + print(" ".join(perfiles[n]["raices"])) + return 0 + + +if __name__ == "__main__": + sys.exit(main())