//! `[[service]]` — **el servicio que un paquete TRAE**, traducido a una Card de arje. //! //! ## Por qué existe (el hueco, medido) //! //! Hasta hoy una receta no tenía dónde decir «yo traigo un demonio». La consecuencia no era //! teórica: los dos únicos servicios del producto (`hammerd` y `sshd`) vivían **hardcodeados en //! constantes de Rust** (`takana_bootstrap::SSHD_SERVICE_CARD`), y los ocho demonios de una sesión //! GNOME (dbus, logind-compat, accounts-daemon, upowerd, pipewire, pipewire-pulse, wireplumber, //! colord) se lanzaban a mano con `&` desde `scripts/gnome/gnome-start-qemu.sh` — **sin supervisión, //! sin backoff y sin el `CRASHED` real**, que es justo lo que arje sabe hacer y para lo que se lo //! adoptó como PID 1 (SDD 12). //! //! ## Por qué NO alcanza con `arje-absorb` //! //! arje ya trae el traductor de inits ajenos (`init/arje-absorb`, con lectores de systemd, OpenRC, //! runit, dinit y sysvinit). No sirve acá, y la razón es exacta: **`arje-absorb` sólo absorbe lo //! que está HABILITADO** — los symlinks de `.wants/` — porque su trabajo es *retratar* un //! sistema vivo, no inventarle uno. Un rootfs de takana no tiene ese estado: medido el 2026-09-12 //! sobre el store, hay **62 ficheros `.service` sellados (61 de usuario, 1 de sistema) y CERO //! directorios `.wants`**. Absorber un rootfs nuestro devuelve la lista vacía, y es la respuesta //! correcta a la pregunta que `arje-absorb` contesta. //! //! O sea que el «enable» de una distro construida desde fuente **no puede leerse del árbol: hay que //! declararlo**. Esa declaración se parte en dos, igual que systemd parte `[Service]` de //! `[Install]`: //! //! - **qué es el servicio** (exec, argv, supervisión) — propiedad del PAQUETE ⇒ este bloque; //! - **si arranca en esta imagen** — propiedad del PERFIL ⇒ `docs/state/targets.toml` (pendiente). //! //! ## Fuera de `hash_inputs` //! //! Igual que `license`, `slots` y `evidence`: describe **cómo se supervisa** el artefacto, no qué //! bytes tiene. `Recipe::hash_inputs` es una lista explícita de entradas, así que declarar servicios //! en recetas YA SELLADAS no mueve un solo `ArtifactHash` — que es lo único que hace la deuda //! pagable sin reconstruir el corpus. //! //! ## Límite conocido: no hay readiness //! //! arje ordena el arranque por **capacidades** (`resolve::plan_spawn`, orden topológico), 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 — el `READY=1` de `Type=notify` se //! registra y no ordena). Mientras no exista, un servicio que necesita esperar a otro lo hace //! DENTRO de su propio card, con un `sh -c '…; exec …'` — que es exactamente lo que ya hace el card //! de `sshd` (levanta la red y genera las host keys antes del `exec`). Este módulo no lo esconde: //! lo deja explícito en `argv`. use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; /// Un servicio declarado por una receta. Se traduce 1:1 a una Card de arje ([`Service::card`]). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Service { /// Nombre de la unidad (`sshd`). Es el `label` de la Card y la clave con la que `arjectl` la /// nombra. Sin `/`: también es nombre de fichero en `cards.d/`. pub label: String, /// ULID de la Card. **Se declara, no se genera**: un id aleatorio haría que la seed —y con ella /// el `product_rootfs_hash`— cambiara en cada corrida, rompiendo la reproducibilidad del /// producto. Que sea estable es un requisito, no un detalle. pub id: String, /// El ELF a encarnar (`Payload::Native.exec`). Absoluto: lo ejecuta PID 1, que no tiene `PATH`. pub exec: String, #[serde(default)] pub argv: Vec, /// Entorno explícito. arje inyecta además `ENTE_BUS_SOCK` y `ENTE_ID` por su cuenta. #[serde(default)] pub envp: Vec<(String, String)>, /// `daemon` (default) u `oneshot` — el `lifecycle` de la Card. #[serde(default)] pub kind: Kind, /// Política de supervisión. **Ausente ⇒ `OneShot`** (no revive). Presente ⇒ `Restart` con /// backoff exponencial entre `initial_ms` y `max_ms`, y su muerte es el `CRASHED` real. #[serde(default, skip_serializing_if = "Option::is_none")] pub restart: Option, /// Permiso de red del Ente. Default `none`: un demonio que no lo declara no toca la red. #[serde(default)] pub networking: Networking, /// Slice de cgroup v2. Default `arje.slice/