diff --git a/crates/takana-cli/src/main.rs b/crates/takana-cli/src/main.rs index ea27e1e3..0879089f 100644 --- a/crates/takana-cli/src/main.rs +++ b/crates/takana-cli/src/main.rs @@ -49,6 +49,24 @@ enum Cmd { /// Ruta a la receta TOML. recipe: String, }, + /// [SDD 30] Emite las **Cards de arje** de los servicios que declaran estas recetas + /// (`[[service]]`), como array JSON, para que el ensamblado de una imagen las meta en el + /// `genesis` de la seed. + /// + /// Existe para que la traducción `[[service]]` → Card tenga **una sola implementación**. Los + /// scripts de imagen ya parchean la seed con python; si además compusieran el JSON de la Card, + /// habría dos generadores del mismo formato divergiendo en silencio — y el formato es contrato + /// con `card_core::Card`, que no es nuestro. + /// + /// Por defecto sólo las de `scope = "system"`: una Card de sesión en el `genesis` arrancaría + /// antes de que exista `XDG_RUNTIME_DIR` o un usuario logueado. + ServiceCards { + /// Rutas de recetas TOML. `scripts/targets.py --service-paths ` las lista. + recipes: Vec, + /// Qué ámbito emitir: `system` (default), `session` o `all`. + #[arg(long, default_value = "system")] + scope: String, + }, /// [Fase 0] Calcula el `ArtifactHash` de una receta SIN construir — puro sobre las recetas /// (source_id + compiler/target/link + patches + flags + fases + hashes de deps). No baja /// fuentes ni compila. Responde "¿cuál es el hash VIGENTE de esta receta?", que el store no @@ -947,6 +965,25 @@ fn main() -> anyhow::Result<()> { let hash = takana_build::build(&recipe, &cfg, &store)?; println!("{hash}"); } + Cmd::ServiceCards { recipes, scope } => { + use takana_core::service::Scope; + let filtro = match scope.as_str() { + "system" => Some(Scope::System), + "session" => Some(Scope::Session), + "all" => None, + otro => anyhow::bail!("--scope inválido: {otro} (system | session | all)"), + }; + let mut cards: Vec = Vec::new(); + for ruta in &recipes { + let r = takana_core::Recipe::load_from_path(ruta)?; + for s in &r.services { + if filtro.is_none_or(|f| f == s.scope) { + cards.push(s.card()); + } + } + } + println!("{}", serde_json::to_string_pretty(&cards)?); + } Cmd::Hash { recipe, check } => { let store = takana_core::Store::open(&cli.store)?; let recipe = takana_core::Recipe::load_from_path(&recipe)?; diff --git a/docs/30-servicios-de-paquete.md b/docs/30-servicios-de-paquete.md index 811d2559..5b6e62d0 100644 --- a/docs/30-servicios-de-paquete.md +++ b/docs/30-servicios-de-paquete.md @@ -1,7 +1,7 @@ # SDD 30 — Los servicios que trae un paquete > **Estado:** §3, §4a y §4b IMPLEMENTADOS y **VALIDADOS EN ARRANQUE REAL** (2026-09-12/14); -> §4c cerrado para la imagen de producto y bloqueado para las de escritorio; §5 abierto. Nace de una pregunta directa: +> §4c CERRADO también para GNOME (2026-09-14, con control); §5 abierto. Nace de una pregunta directa: > *«los paquetes que tienen sus servicios para systemd, openrc u otros, ¿ya saben empaquetarse en > takana con arje?»*. La respuesta medida era **no**, y este documento dice exactamente qué faltaba, > qué se cerró y qué queda. @@ -229,26 +229,74 @@ Dos cosas que NO son transcripción y hay que saber: a PID 1 por `RunCard`), pero mutter y kwin no tienen esa integración. `--services` avisa por cada una: el hueco queda contado, no omitido. -#### Por qué el escritorio NO se puede arrancar supervisado todavía (medido) +#### 4c.1 GNOME con los demonios supervisados por arje — medido contra un control -El paso final —que el ensamblado de la imagen ponga las cards de sistema en el `genesis` en vez de -que el script las lance con `&`— **no está bloqueado por diseño sino por corpus**, y conviene que -esté escrito para no volver a averiguarlo: +El bloqueo que este documento daba por corpus (gnome-shell en deuda) **se levantó solo**: otro +agente selló `evolution-data-server` y `gnome-shell` mientras esto se escribía. `escritorio-gnome` +quedó 309/309 y el cierre hidrata completo (319 nodos, 8,2 G). -| imagen | estado | qué falta | +**Se corrió un CONTROL primero**, y fue lo que hizo interpretable el resultado: la misma imagen sin +inyectar cards, para saber si GNOME arranca hoy en este hub. Arranca — y trae un fallo propio que +sirvió de sonda. + +| | control (el script con `&`) | con Cards en el `genesis` | |---|---|---| -| `escritorio-gnome` | 307/309 de la clausura sellada | **`gnome-shell` en deuda**, y su único bloqueante es `evolution-data-server` (que tiene `blocked_by: []` — o sea que sus deps ya están: falta construirlo, no destrabarlo) | -| `escritorio-sway` | 260/261 | **`sway` mismo sin sellar** | +| `colord` | `!! colord MURIÓ al arrancar` | `ya vive (pid 175)` — lo encarnó PID 1 | +| `org.freedesktop.ColorManager` | `NO apareció en 40s` | **`OK`** | +| `login1` / `Accounts` / `UPower` | OK | OK | +| compositor | `wayland-0`, shell vivo | `wayland-0`, shell vivo | -Sin el compositor/shell no hay sesión que arrancar, así que el arranque supervisado de escritorio no -es verificable hoy por más cards que se inyecten. Y el andamiaje de la imagen tampoco está: en este -hub `work/metal-rootfs` no existe y `work/gnome-rootfs` son 388 K de residuo, no el cierre hidratado. +**El fallo del control desapareció.** No es una mejora buscada: `colord` moría arrancado por el +script y vive arrancado por arje, con el bus ya listo porque la espera está dentro de su `argv`. Es +exactamente lo que un supervisor está para dar, y el control es lo único que permite decirlo — sin +él, «ColorManager OK» sería un dato suelto en vez de una diferencia. -**Lo que NO se hizo a propósito:** cablear los scripts de imagen a ciegas. Inyectar cards en un -`genesis` que no se puede bootear es escribir código que nadie puede contradecir — y este documento -ya tiene un ejemplo de lo que pasa entonces (el SDD 06 afirmando durante meses un lector que no -existía). El mecanismo está probado donde SÍ se puede arrancar (§4b.2); el escritorio espera a que -su corpus cierre. +Los PIDs lo confirman: `polkit-compat`=119, `colord`=175, `upowerd`=178, `accounts-daemon`=180 — +números bajos, de antes de que el lanzador de sesión existiera. + +**Qué NO se comprobó:** no hay `screendump`. QEMU salió por timeout antes de poder pedirlo, y el +control tampoco lo tuvo, así que la comparación es serial contra serial. Lo que se afirma acá es +sobre los DEMONIOS (nombres del bus, PIDs, supervisión), no sobre el pintado; para afirmar que la +imagen pinta sigue rigiendo la regla de validar escritorios con pantalla. + +#### Las piezas, y por qué cada una está donde está + +- **`takana service-cards `** — la traducción `[[service]]` → Card, con **una sola + implementación**. El script de imagen ya parchea la seed con python; si además compusiera el JSON + de la Card habría dos generadores del mismo formato divergiendo en silencio, y el formato es + contrato con `card_core::Card`, que no es nuestro. +- **`targets.py --service-paths `** — une las dos mitades: qué es el servicio (receta) y si + arranca (perfil). Sólo `scope = system`. +- **`scripts/gnome/inyectar-cards.py`** — compone la seed, idempotente por `label`. Es un fichero y + no un heredoc porque **el script de imagen ya tiene uno de python y anidar dos falló en vivo**: el + terminador del interno cierra el externo y media cosa se ejecuta como shell. +- **La espera del bus dentro del `argv`** de las cinco recetas de sistema. Sin ella un daemon puede + arrancar antes de que dbus escuche y quedarse en modo idle sin registrar su nombre — un fallo que + **no se ve**, porque el proceso vive y el bus no lo tiene. Los 5 hashes no se movieron. +- **El guardia de `gnome-start` es por «¿está corriendo?», no por una perilla de la imagen.** Así es + correcto venga de donde venga el proceso, y la misma copia sirve en una imagen sin cards. Una + condición que consulta el estado real no puede desincronizarse de él; un flag sí. + +#### Lo que este frente se encontró, no lo que se propuso + +`--service-paths` devolvió **nueve** servicios de sistema y sólo seis eran míos: otro agente declaró +`cupsd`, `bluetoothd` y `NetworkManager` en sus recetas y los habilitó en el perfil, con `-f`, +`--nodetach` y `--no-daemon` — o sea que leyó y aplicó la regla de que arje supervisa al hijo directo +y `Type=forking` no existe. El mecanismo se usó sin que nadie lo coordinara, que es la única prueba +que vale de que el sitio donde se puso la declaración era el correcto. + +#### Lo que queda de §4c + +La base de la imagen es el `product-rootfs`, no `work/metal-rootfs`: ese fichero **no existe en este +hub y ningún script del repo lo produce**. El producto sirve porque ya es arje-zero PID 1 + seed + +busybox, pero el andamiaje de imagen de escritorio sigue dependiendo de un insumo sin dueño, y eso es +su propia deuda. + +Y las tres cosas de siempre: EXDEV. `store/` es bind-mount de `/dev/sdb` y `work/` vive en +`/dev/sdc`, así que **todo lo que enlaza en vez de copiar** se rompe al cruzar — lo pegaron +`hydrate-profile.py` (que ya lo detecta y dice dónde poner el `--into`) y el staging de +`install-image-efi.sh` (configurable por `STAGE`). Hidratado, fundido y staging tienen que vivir bajo +el mismo montaje. ## 5. El hueco que no es nuestro: no hay readiness @@ -281,7 +329,8 @@ arriba en arje. | Emisor desde el sidecar (`service_cards`, falla ruidosa + test) | ✅ §4b | | Re-sellado de openssh con control → **reproduce bit a bit** | ✅ §4b.1 | | La cadena receta→sidecar→card→genesis→arje, **en arranque real** | ✅ §4b.2 (`PRODUCT_SSH_OK`) | -| Que la imagen de ESCRITORIO los arranque y no el script `&` | ⛔ §4c, bloqueado (abajo) | +| Que la imagen de ESCRITORIO los arranque y no el script `&` | ✅ §4c.1, con control (y arregló `colord`) | +| `takana service-cards` + `--service-paths` + el inyector | ✅ §4c.1 | | `escritorio-cosmic`: auditar su `cosmic-start.sh` y habilitar | ◻ hoy sale con 5 AVISO | **La primera lectura del vigía**, para que no haya que creerle a este documento: 0 errores y 18 diff --git a/recipes/arje-logind-compat.toml b/recipes/arje-logind-compat.toml index 6f2782fb..4c75c24c 100644 --- a/recipes/arje-logind-compat.toml +++ b/recipes/arje-logind-compat.toml @@ -89,9 +89,17 @@ XML # Va DESPUÉS del bus de sistema (registra `org.freedesktop.login1` ahí). Ese orden hoy lo garantiza # el script esperando el socket; en Cards lo daría el grafo de capacidades de arje, pero el # ordenamiento es por spawn y no por READINESS (SDD 30 §5) — el hueco está contado, no tapado. +# ⚠ EL `argv` ESPERA AL BUS ANTES DEL `exec`, Y NO ES UNA MAÑA DE ESTA RECETA. arje ordena el +# arranque por capacidades (orden topológico real), pero eso ordena el SPAWN, no el ESTAR LISTO: no +# existe primitiva «esperá a que aparezca este socket» (DE-SYSTEMD-A-ARJE.md §13 lo lista como hueco +# abierto). Sin la espera, este daemon puede arrancar antes de que dbus-daemon escuche y quedarse en +# modo idle sin registrar nunca su nombre — un fallo que NO se ve: el proceso vive, el bus no lo +# tiene. Mientras el hueco siga abierto la espera va DENTRO del card, a la vista en el argv y no +# escondida en un wrapper, para que se pueda contar cuántos servicios la están pagando. [[service]] label = "logind-compat" id = "01HQAR53D4M2NBV8KZTYXFQA21" -exec = "/usr/bin/arje-logind-compat" +exec = "/bin/busybox" +argv = ["sh", "-c", "i=0; while [ ! -S /run/dbus/system_bus_socket ] && [ $i -lt 100 ]; do i=$((i+1)); sleep 0.2; done; exec /usr/bin/arje-logind-compat"] envp = [["ARJE_LOGIN_STATE", "1"]] restart = { initial_ms = 500, max_ms = 20000 } diff --git a/recipes/arje-polkit-compat.toml b/recipes/arje-polkit-compat.toml index 61dcc619..d0931344 100644 --- a/recipes/arje-polkit-compat.toml +++ b/recipes/arje-polkit-compat.toml @@ -85,8 +85,16 @@ XML ''' # ── EL SERVICIO QUE ESTE PAQUETE TRAE (SDD 30) ────────────────────────────────────────────────── +# ⚠ EL `argv` ESPERA AL BUS ANTES DEL `exec`, Y NO ES UNA MAÑA DE ESTA RECETA. arje ordena el +# arranque por capacidades (orden topológico real), pero eso ordena el SPAWN, no el ESTAR LISTO: no +# existe primitiva «esperá a que aparezca este socket» (DE-SYSTEMD-A-ARJE.md §13 lo lista como hueco +# abierto). Sin la espera, este daemon puede arrancar antes de que dbus-daemon escuche y quedarse en +# modo idle sin registrar nunca su nombre — un fallo que NO se ve: el proceso vive, el bus no lo +# tiene. Mientras el hueco siga abierto la espera va DENTRO del card, a la vista en el argv y no +# escondida en un wrapper, para que se pueda contar cuántos servicios la están pagando. [[service]] label = "polkit-compat" id = "01HQAR53D4M2NBV8KZTYXFQA22" -exec = "/usr/bin/arje-polkit-compat" +exec = "/bin/busybox" +argv = ["sh", "-c", "i=0; while [ ! -S /run/dbus/system_bus_socket ] && [ $i -lt 100 ]; do i=$((i+1)); sleep 0.2; done; exec /usr/bin/arje-polkit-compat"] restart = { initial_ms = 500, max_ms = 20000 } diff --git a/recipes/incoming-gnome/accountsservice.toml b/recipes/incoming-gnome/accountsservice.toml index 7ab7825f..a3e461d8 100644 --- a/recipes/incoming-gnome/accountsservice.toml +++ b/recipes/incoming-gnome/accountsservice.toml @@ -103,8 +103,16 @@ build = ["meson", "samurai", "python3", "pkgconf", "gettext-tiny", "py3-setuptoo # ── EL SERVICIO QUE ESTE PAQUETE TRAE (SDD 30) ────────────────────────────────────────────────── # Se lanza EXPLÍCITO, no por activación de D-Bus, y el motivo está medido en el script de sesión: la # activación por bus funciona, pero deja el arranque esperando el timeout y no deja log donde mirar. +# ⚠ EL `argv` ESPERA AL BUS ANTES DEL `exec`, Y NO ES UNA MAÑA DE ESTA RECETA. arje ordena el +# arranque por capacidades (orden topológico real), pero eso ordena el SPAWN, no el ESTAR LISTO: no +# existe primitiva «esperá a que aparezca este socket» (DE-SYSTEMD-A-ARJE.md §13 lo lista como hueco +# abierto). Sin la espera, este daemon puede arrancar antes de que dbus-daemon escuche y quedarse en +# modo idle sin registrar nunca su nombre — un fallo que NO se ve: el proceso vive, el bus no lo +# tiene. Mientras el hueco siga abierto la espera va DENTRO del card, a la vista en el argv y no +# escondida en un wrapper, para que se pueda contar cuántos servicios la están pagando. [[service]] label = "accounts-daemon" id = "01HQAR53D4M2NBV8KZTYXFQA23" -exec = "/usr/libexec/accounts-daemon" +exec = "/bin/busybox" +argv = ["sh", "-c", "i=0; while [ ! -S /run/dbus/system_bus_socket ] && [ $i -lt 100 ]; do i=$((i+1)); sleep 0.2; done; exec /usr/libexec/accounts-daemon"] restart = { initial_ms = 500, max_ms = 20000 } diff --git a/recipes/incoming-gnome/colord.toml b/recipes/incoming-gnome/colord.toml index 2021ec3d..b72df4b6 100644 --- a/recipes/incoming-gnome/colord.toml +++ b/recipes/incoming-gnome/colord.toml @@ -74,8 +74,16 @@ build = ["meson", "samurai", "python3", "pkgconf", "gettext-tiny", "glib", "lcms # ── EL SERVICIO QUE ESTE PAQUETE TRAE (SDD 30) ────────────────────────────────────────────────── # Mutter lo pide al arrancar y, si no está, se come 25 s de timeout de activación D-Bus. Por eso es # un servicio declarado y no algo que el bus levante cuando alguien pregunte. +# ⚠ EL `argv` ESPERA AL BUS ANTES DEL `exec`, Y NO ES UNA MAÑA DE ESTA RECETA. arje ordena el +# arranque por capacidades (orden topológico real), pero eso ordena el SPAWN, no el ESTAR LISTO: no +# existe primitiva «esperá a que aparezca este socket» (DE-SYSTEMD-A-ARJE.md §13 lo lista como hueco +# abierto). Sin la espera, este daemon puede arrancar antes de que dbus-daemon escuche y quedarse en +# modo idle sin registrar nunca su nombre — un fallo que NO se ve: el proceso vive, el bus no lo +# tiene. Mientras el hueco siga abierto la espera va DENTRO del card, a la vista en el argv y no +# escondida en un wrapper, para que se pueda contar cuántos servicios la están pagando. [[service]] label = "colord" id = "01HQAR53D4M2NBV8KZTYXFQA25" -exec = "/usr/libexec/colord" +exec = "/bin/busybox" +argv = ["sh", "-c", "i=0; while [ ! -S /run/dbus/system_bus_socket ] && [ $i -lt 100 ]; do i=$((i+1)); sleep 0.2; done; exec /usr/libexec/colord"] restart = { initial_ms = 500, max_ms = 20000 } diff --git a/recipes/incoming-gnome/upower.toml b/recipes/incoming-gnome/upower.toml index 088ce6d0..8d0d384c 100644 --- a/recipes/incoming-gnome/upower.toml +++ b/recipes/incoming-gnome/upower.toml @@ -58,8 +58,16 @@ install = "PYTHONPATH=/usr/lib/python3.12/site-packages DESTDIR=/out meson insta build = ["meson", "samurai", "python3", "pkgconf", "gettext-tiny", "py3-setuptools", "gobject-introspection", "gi-foreign-girs", "glib", "glib-introspected", "libgudev", "udev-pc", "libudev-zero", "polkit", "libffi", "pcre2", "zlib-shared", "expat", "dbus-shared"] # ── EL SERVICIO QUE ESTE PAQUETE TRAE (SDD 30) ────────────────────────────────────────────────── +# ⚠ EL `argv` ESPERA AL BUS ANTES DEL `exec`, Y NO ES UNA MAÑA DE ESTA RECETA. arje ordena el +# arranque por capacidades (orden topológico real), pero eso ordena el SPAWN, no el ESTAR LISTO: no +# existe primitiva «esperá a que aparezca este socket» (DE-SYSTEMD-A-ARJE.md §13 lo lista como hueco +# abierto). Sin la espera, este daemon puede arrancar antes de que dbus-daemon escuche y quedarse en +# modo idle sin registrar nunca su nombre — un fallo que NO se ve: el proceso vive, el bus no lo +# tiene. Mientras el hueco siga abierto la espera va DENTRO del card, a la vista en el argv y no +# escondida en un wrapper, para que se pueda contar cuántos servicios la están pagando. [[service]] label = "upowerd" id = "01HQAR53D4M2NBV8KZTYXFQA24" -exec = "/usr/libexec/upowerd" +exec = "/bin/busybox" +argv = ["sh", "-c", "i=0; while [ ! -S /run/dbus/system_bus_socket ] && [ $i -lt 100 ]; do i=$((i+1)); sleep 0.2; done; exec /usr/libexec/upowerd"] restart = { initial_ms = 500, max_ms = 20000 } diff --git a/scripts/gnome/gnome-start-qemu.sh b/scripts/gnome/gnome-start-qemu.sh index a019ec1f..a88680f1 100755 --- a/scripts/gnome/gnome-start-qemu.sh +++ b/scripts/gnome/gnome-start-qemu.sh @@ -187,7 +187,8 @@ say "dbus sesión: ${DBUS_SESSION_BUS_ADDRESS:-NO ARRANCO}" # org.freedesktop.login1 ahí) y ANTES del shell. Mutter no sólo lo consulta por D-Bus: pregunta por # SU sesión vía la C-ABI sd-login, que libelogind resuelve LEYENDO /run/systemd/sessions. Sin este # daemon esos ficheros no existen y mutter muere en "Failed to find any matching session". -if [ -x /usr/bin/arje-logind-compat ]; then +if supervisado arje-logind-compat "logind-compat"; then : +elif [ -x /usr/bin/arje-logind-compat ]; then # ARJE_LOGIN_STATE=1: la perilla que YA traía el daemon y que nadie prendía. Su comentario en # arje-compat lo dice explícito — "en un host CON systemd real pisaría su estado; en arje (sin # systemd) el launcher de sesión lo prende. Default off" — y el launcher de sesión es ESTE script. @@ -285,6 +286,23 @@ esperar_nombre() { return 1 } +# ── ¿YA LO SUPERVISA ARJE? (SDD 30 §4c) ───────────────────────────────────────────────────────── +# Desde que los demonios de sistema son Cards del `genesis`, PID 1 ya los encarnó antes de que este +# script corra. Relanzarlos daría DOS procesos peleando por el mismo nombre del bus, y el segundo +# fallaría con un error que no nombra la causa. +# +# El guardia se hace por «¿está corriendo?» y NO por una perilla de la imagen, a propósito: así es +# correcto venga de donde venga el proceso —arje, otro script, una corrida anterior— y esta misma +# copia sigue sirviendo en una imagen donde las Cards no se inyectaron. Una condición que consulta +# el estado real no puede desincronizarse de él; un flag sí. +ya_corre() { # $1 = nombre del ejecutable tal como lo ve el kernel + pidof "$1" >/dev/null 2>&1 +} +supervisado() { # $1 = ejecutable, $2 = etiqueta para el log + if ya_corre "$1"; then say "$2 ya vive (pid $(pidof "$1" | cut -d' ' -f1)) — lo supervisa arje, no lo relanzo"; return 0; fi + return 1 +} + # arje-polkit-compat: el PolicyKit1 del fractal. **VA ANTES que accounts-daemon y upowerd**, y el # orden no es estético: los dos se bloquean en `polkit_authority_get_sync()` al arrancar, así que si # `org.freedesktop.PolicyKit1` no está en el bus se quedan conectados pero SIN adquirir su nombre —y @@ -301,7 +319,8 @@ esperar_nombre() { # tranquilo y el chequeo del final dice `PolicyKit1 OK` — o sea que el resumen final da verde y los # clientes ya están muertos. La carrera es real y la ganaba cualquiera: es la misma lección de «un pid # no es un servicio», aplicada al productor en vez de al consumidor. -if [ -x /usr/bin/arje-polkit-compat ]; then +if supervisado arje-polkit-compat "polkit-compat"; then : +elif [ -x /usr/bin/arje-polkit-compat ]; then /usr/bin/arje-polkit-compat >/tmp/polkit-compat.log 2>&1 & say "arje-polkit-compat lanzado (pid $!)" esperar_nombre org.freedesktop.PolicyKit1 /tmp/polkit-compat.log \ @@ -318,7 +337,8 @@ fi # lo es. Corriendo todo como root la activación probablemente funcionaría igual, pero depender de eso # es depender de un accidente; lanzarlo a mano es una línea y no deja ambigüedad. Si además llegara a # activarse por bus, el segundo simplemente no adquiere el nombre y se va: no hay daño. -if [ -x /usr/libexec/accounts-daemon ]; then +if supervisado accounts-daemon "accounts-daemon"; then : +elif [ -x /usr/libexec/accounts-daemon ]; then /usr/libexec/accounts-daemon >/tmp/accounts-daemon.log 2>&1 & say "accounts-daemon lanzado (pid $!)" else @@ -334,7 +354,8 @@ fi # Los directorios de estado los deriva upower de --prefix (historydir/statedir vacíos ⇒ # $prefix/var/lib/upower); si no existen, arranca y se muere sin ruido. mkdir -p /usr/var/lib/upower /var/lib/upower -if [ -x /usr/libexec/upowerd ]; then +if supervisado upowerd "upowerd"; then : +elif [ -x /usr/libexec/upowerd ]; then /usr/libexec/upowerd >/tmp/upowerd.log 2>&1 & UPOWERD_PID=$! say "upowerd lanzado (pid $UPOWERD_PID)" @@ -433,7 +454,8 @@ fi # 25 s de timeout de activación D-Bus. Se lanza explícito por el mismo motivo que accounts-daemon y # upowerd: la activación por bus funciona (el launch-helper ya es setuid) pero deja el arranque # esperando, y lanzarlo a mano además deja un log donde mirar si no adquiere el nombre. -if [ -x /usr/libexec/colord ]; then +if supervisado colord "colord"; then : +elif [ -x /usr/libexec/colord ]; then # --verbose: su log normal se corta después de abrir las tres bases de datos y no dice nada más, así # que sin esto un colord que no adquiere el nombre es indistinguible de uno que se cuelga. CD_VERBOSE=1 /usr/libexec/colord --verbose >/tmp/colord.log 2>&1 & diff --git a/scripts/gnome/inyectar-cards.py b/scripts/gnome/inyectar-cards.py new file mode 100755 index 00000000..dd43e314 --- /dev/null +++ b/scripts/gnome/inyectar-cards.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# inyectar-cards.py — mete las Cards de servicio de un perfil en el `genesis` de la seed (SDD 30 §4c). +# +# Por qué es un fichero y no un heredoc dentro del script de imagen: el script ya tiene un heredoc de +# python (el del console-getty) y anidar dos es la receta del error mudo — el terminador del interno +# cierra el externo y la mitad del programa se ejecuta como shell. Pasó al escribir esto. +# +# NO genera las Cards: las recibe ya emitidas por `takana service-cards`, que es la única +# implementación de la traducción `[[service]]` → Card. Acá sólo se COMPONE la seed. +import json, os, sys + +seed_p, cards_p = sys.argv[1], sys.argv[2] +seed = json.load(open(seed_p)) +cards = json.load(open(cards_p)) + +# Idempotente por `label`: re-armar la imagen no puede duplicar Entes. Dos Cards con el mismo label +# no son "dos servicios": para el grafo de arje son una colisión de identidad, y el segundo se +# perdería en silencio o pelearía con el primero por el mismo nombre del bus. +ya = {g.get("label") for g in seed.get("genesis", [])} +puestos = [c["label"] for c in cards if c["label"] not in ya] +seed.setdefault("genesis", []).extend(c for c in cards if c["label"] not in ya) + +tmp = seed_p + ".new" +with open(tmp, "w") as f: + json.dump(seed, f, indent=2) +os.chmod(tmp, 0o644) +# Reemplazar la ENTRADA de directorio, no escribir en sitio: la seed del árbol fundido es un hardlink +# read-only al artefacto del store, y escribirle encima mutaría el store. +os.replace(tmp, seed_p) +print(" cards en el genesis:", ", ".join(puestos) or "(ninguna nueva)") diff --git a/scripts/gnome/qemu-desktop-image.sh b/scripts/gnome/qemu-desktop-image.sh index 22d26378..459fe5af 100755 --- a/scripts/gnome/qemu-desktop-image.sh +++ b/scripts/gnome/qemu-desktop-image.sh @@ -209,6 +209,20 @@ os.chmod(tmp, 0o644) os.replace(tmp, p) PY +echo "==> inyectando las Cards de servicio en el genesis (SDD 30 4c)" +# LO QUE ESTO CAMBIA: hasta hoy los demonios de sistema los lanzaba `gnome-start` con `&` -- sin +# supervision, sin backoff y sin el `CRASHED` real, o sea sin nada de lo que arje es PID 1 para dar. +# Aca pasan a ser Cards del `genesis`, que es de donde arje los encarna y revive. +# +# Las dos mitades salen de donde viven: QUE es cada servicio, de `[[service]]` en su receta; CUALES +# arranca esta imagen, de `servicios = [...]` del perfil en targets.toml. La traduccion receta->Card +# la hace `takana service-cards` y no este script: el formato es contrato con `card_core::Card`, y +# dos generadores del mismo formato divergen en silencio. +CARDS_JSON="${CARDS_JSON:-$(mktemp)}" +./target/release/takana service-cards $(python3 scripts/targets.py --service-paths escritorio-gnome) > "$CARDS_JSON" +[ -s "$CARDS_JSON" ] || { echo " x no pude emitir las cards de servicio" >&2; exit 1; } +python3 scripts/gnome/inyectar-cards.py "$MERGED/ente/seed.card.json" "$CARDS_JSON" + # /var/run → /run: arje-logind-compat busca el bus de sistema en la ruta LEGACY # (/var/run/dbus/system_bus_socket) y dbus-daemon escucha en /run/dbus/system_bus_socket. En casi # toda distro /var/run es un symlink a /run desde hace 15 años; esta base metal es tan mínima que ni diff --git a/scripts/targets.py b/scripts/targets.py index 457b8d58..c1f99821 100755 --- a/scripts/targets.py +++ b/scripts/targets.py @@ -9,6 +9,7 @@ # scripts/targets.py --json # todo el manifiesto expandido # scripts/targets.py --services cli # los servicios que ESA imagen arranca, resueltos # scripts/targets.py --selftest # roturas a propósito + un control que TIENE que pasar +# scripts/targets.py --service-paths gnome # rutas de receta de los servicios de SISTEMA # # ⚠ La FLAG va en inglés (regla 4 del CLAUDE.md: la superficie de CLI es contrato) aunque `--lista` # sea deuda vieja del mismo fichero. La CLAVE del TOML sí es `servicios`, en castellano, porque ahí @@ -110,6 +111,19 @@ GRAFOS = ["build-state.json", "build-state-gnome.json", "build-state-kde.json", "build-state-cosmic.json", "build-state-wlr.json"] +def _ruta_de(nombre, recipes_dir=None): + """La ruta de la receta que se llama `nombre`. Se busca por el campo `name`, no por el nombre de + fichero: son casi siempre iguales, pero el que manda es el campo.""" + recipes_dir = Path(recipes_dir) if recipes_dir else ROOT / "recipes" + for f in sorted(recipes_dir.rglob("*.toml")): + try: + if tomllib.load(open(f, "rb")).get("name") == nombre: + return str(f.relative_to(ROOT)) + except tomllib.TOMLDecodeError: + continue + return None + + def _membresia(): """{receta: [perfiles]} fusionando TODOS los grafos de estado. `{}` si no hay ninguno (no es fatal: degrada a no poder comprobar la pertenencia, y se dice).""" @@ -268,6 +282,29 @@ def main(): return 0 if args[0] == "--selftest": return 0 if autotest() else 1 + if args[0] == "--service-paths": + # Las RUTAS de receta de los servicios de SISTEMA habilitados, para que el ensamblado de la + # imagen se las pase a `takana service cards`. Sólo `system`: una Card de sesión en el + # `genesis` arrancaría antes de que exista XDG_RUNTIME_DIR o un usuario (SDD 30 §4c). + if len(args) < 2 or args[1] not in perfiles: + print(f"uso: targets.py --service-paths ", file=sys.stderr) + return 1 + decl = declaraciones() + memb = _membresia() + raices = set(perfiles[args[1]]["raices"]) + vistas = [] + for lbl in perfiles[args[1]]["servicios_exp"]: + for receta, svc in decl.get(lbl, []): + if svc.get("scope", "system") != "system": + continue + if not (receta in raices or args[1] in memb.get(receta, [])): + continue + ruta = _ruta_de(receta) + if ruta and ruta not in vistas: + vistas.append(ruta) + for r in vistas: + print(r) + return 0 if args[0] in ("--services", "-s"): if len(args) < 2 or args[1] not in perfiles: print(f"uso: targets.py --services (hay: {', '.join(sorted(perfiles))})",