Files
brahman/crates/init/arje-absorb/src/main.rs
T
sergio 762bf95dfd feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
Nuevo crate `crates/init/arje-absorb`: lee la configuración de un init
clásico y la traduce a una Tarjeta Semilla (Card JSON) con cada
servicio como hija genesis de arje-zero. El paso «absorber» de la
migración a arje — para no perder los servicios al cambiar de init.

- Absorbers: sysvinit (/etc/inittab), runit (runsvdir o /etc/sv),
  dinit (/etc/dinit.d), openrc (/etc/runlevels). Autodetección.
- Modelo intermedio ForeignService → Card vía brahman-card (validado).
- `--with-carmen`: agrega carmen-dm (gestor de login gráfico).
- CLI: --from/--root/--output/--label/--with-carmen. 24 tests, clippy
  limpio.

`scripts/migrate-to-arje.sh`: orquesta absorber → validar → (carmen:
compila+instala mirada dinámico) → install-arje-as-init.sh. El init
viejo queda intacto; arje se elige en GRUB. --dry-run no toca nada.

systemd no se absorbe (units no son texto trivial) — para systemd
sigue la capa de shims + seeds/arje-host.card.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 00:40:34 +00:00

175 lines
5.7 KiB
Rust

//! `arje-absorb` — traduce la configuración de otro init a una Semilla.
//!
//! Lee la configuración de un init clásico (sysvinit, runit, dinit,
//! OpenRC) y emite una Tarjeta Semilla brahman (`Card` JSON) con cada
//! servicio como hija `genesis` de `arje-zero`. Es el paso «absorber»
//! de la migración: `scripts/migrate-to-arje.sh` lo usa para no perder
//! los servicios del sistema al cambiar de init.
//!
//! La Semilla resultante NO endurece el sandbox — conserva el
//! comportamiento del init viejo. Revisala antes de instalarla.
use std::path::{Path, PathBuf};
use std::process::exit;
mod card;
mod dinit;
mod model;
mod openrc;
mod runit;
mod sysvinit;
const HELP: &str = "\
arje-absorb — absorbe la configuración de otro init en una Semilla brahman.
USO:
arje-absorb [--from <init>] [--root <dir>] [--output <archivo>] [--label <s>]
OPCIONES:
--from <init> sysvinit | runit | dinit | openrc | auto (def: auto)
--root <dir> raíz del sistema a leer (def: /)
--output <f> archivo de salida, o '-' para stdout (def: -)
--label <s> label de la Semilla raíz (def: arje.seed.absorbed)
--with-carmen agrega carmen-dm (gestor de login gráfico) a la Semilla
-h, --help esta ayuda
Emite una Tarjeta Semilla con cada servicio del init ajeno como hija
genesis de arje-zero. Revisala antes de instalarla como
/ente/seed.card.json.";
fn main() {
if let Err(e) = run() {
eprintln!("arje-absorb: error: {e:#}");
exit(1);
}
}
fn run() -> anyhow::Result<()> {
let mut from = "auto".to_string();
let mut root = PathBuf::from("/");
let mut output = "-".to_string();
let mut label = "arje.seed.absorbed".to_string();
let mut with_carmen = false;
let mut args = std::env::args().skip(1);
while let Some(arg) = args.next() {
match arg.as_str() {
"-h" | "--help" => {
println!("{HELP}");
return Ok(());
}
"--from" => {
from = args.next().ok_or_else(|| anyhow::anyhow!("--from necesita un valor"))?
}
"--root" => {
root = PathBuf::from(
args.next().ok_or_else(|| anyhow::anyhow!("--root necesita un valor"))?,
)
}
"--output" => {
output =
args.next().ok_or_else(|| anyhow::anyhow!("--output necesita un valor"))?
}
"--label" => {
label =
args.next().ok_or_else(|| anyhow::anyhow!("--label necesita un valor"))?
}
"--with-carmen" => with_carmen = true,
other => anyhow::bail!("opción desconocida «{other}» (usá --help)"),
}
}
let init = if from == "auto" {
detect(&root).ok_or_else(|| {
anyhow::anyhow!(
"no pude autodetectar el init en {} — pasá --from <init>",
root.display()
)
})?
} else {
from.clone()
};
let services = match init.as_str() {
"sysvinit" => sysvinit::absorb(&root)?,
"runit" => runit::absorb(&root)?,
"dinit" => dinit::absorb(&root)?,
"openrc" => openrc::absorb(&root)?,
other => anyhow::bail!(
"init «{other}» no soportado (sysvinit | runit | dinit | openrc | auto)"
),
};
eprintln!(
"arje-absorb: init «{init}» → {} servicio(s) absorbido(s).",
services.len()
);
if services.is_empty() {
eprintln!("arje-absorb: aviso: 0 servicios — la Semilla quedará sin hijas.");
}
let mut seed = card::build_seed(&label, &services);
if with_carmen {
seed.genesis.push(card::carmen_dm_card());
eprintln!("arje-absorb: agregado carmen-dm (gestor de login gráfico).");
}
seed.validate()
.map_err(|e| anyhow::anyhow!("la Semilla generada no valida: {e}"))?;
let json = seed.to_json_pretty()?;
if output == "-" {
println!("{json}");
} else {
std::fs::write(&output, json)
.map_err(|e| anyhow::anyhow!("escribiendo {output}: {e}"))?;
eprintln!("arje-absorb: Semilla escrita en {output}");
}
Ok(())
}
/// Autodetecta el init presente en `root`. El orden importa: lo más
/// específico primero — un sistema OpenRC suele llevar también un
/// `/etc/inittab` (para las consolas), así que sysvinit va último.
fn detect(root: &Path) -> Option<String> {
if root.join("etc/dinit.d").is_dir() {
return Some("dinit".to_string());
}
if root.join("etc/runlevels").is_dir() || root.join("sbin/openrc").exists() {
return Some("openrc".to_string());
}
if root.join("etc/runit").is_dir() || root.join("etc/sv").is_dir() {
return Some("runit".to_string());
}
if root.join("etc/inittab").exists() {
return Some("sysvinit".to_string());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_prefers_dinit() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("etc/dinit.d")).unwrap();
std::fs::create_dir_all(tmp.path().join("etc/runlevels")).unwrap();
assert_eq!(detect(tmp.path()).as_deref(), Some("dinit"));
}
#[test]
fn detect_openrc_over_sysvinit() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("etc/runlevels/default")).unwrap();
std::fs::write(tmp.path().join("etc/inittab"), "").unwrap();
assert_eq!(detect(tmp.path()).as_deref(), Some("openrc"));
}
#[test]
fn detect_none_on_empty_root() {
let tmp = tempfile::tempdir().unwrap();
assert!(detect(tmp.path()).is_none());
}
}