⎇ 762bf95dfd770cc8bf51f21605bc2cc6cc88ea76
crates/init/arje-absorb/src/main.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(arje): arje-absorb — absorbe otros inits a una Semilla brahman
1//! `arje-absorb` — traduce la configuración de otro init a una Semilla.
2//!
3//! Lee la configuración de un init clásico (sysvinit, runit, dinit,
4//! OpenRC) y emite una Tarjeta Semilla brahman (`Card` JSON) con cada
5//! servicio como hija `genesis` de `arje-zero`. Es el paso «absorber»
6//! de la migración: `scripts/migrate-to-arje.sh` lo usa para no perder
7//! los servicios del sistema al cambiar de init.
8//!
9//! La Semilla resultante NO endurece el sandbox — conserva el
10//! comportamiento del init viejo. Revisala antes de instalarla.
11
12use std::path::{Path, PathBuf};
13use std::process::exit;
14
15mod card;
16mod dinit;
17mod model;
18mod openrc;
19mod runit;
20mod sysvinit;
21
22const HELP: &str = "\
23arje-absorb — absorbe la configuración de otro init en una Semilla brahman.
24
25USO:
26 arje-absorb [--from <init>] [--root <dir>] [--output <archivo>] [--label <s>]
27
28OPCIONES:
29 --from <init> sysvinit | runit | dinit | openrc | auto (def: auto)
30 --root <dir> raíz del sistema a leer (def: /)
31 --output <f> archivo de salida, o '-' para stdout (def: -)
32 --label <s> label de la Semilla raíz (def: arje.seed.absorbed)
33 --with-carmen agrega carmen-dm (gestor de login gráfico) a la Semilla
34 -h, --help esta ayuda
35
36Emite una Tarjeta Semilla con cada servicio del init ajeno como hija
37genesis de arje-zero. Revisala antes de instalarla como
38/ente/seed.card.json.";
39
40fn main() {
41 if let Err(e) = run() {
42 eprintln!("arje-absorb: error: {e:#}");
43 exit(1);
44 }
45}
46
47fn run() -> anyhow::Result<()> {
48 let mut from = "auto".to_string();
49 let mut root = PathBuf::from("/");
50 let mut output = "-".to_string();
51 let mut label = "arje.seed.absorbed".to_string();
52 let mut with_carmen = false;
53
54 let mut args = std::env::args().skip(1);
55 while let Some(arg) = args.next() {
56 match arg.as_str() {
57 "-h" | "--help" => {
58 println!("{HELP}");
59 return Ok(());
60 }
61 "--from" => {
62 from = args.next().ok_or_else(|| anyhow::anyhow!("--from necesita un valor"))?
63 }
64 "--root" => {
65 root = PathBuf::from(
66 args.next().ok_or_else(|| anyhow::anyhow!("--root necesita un valor"))?,
67 )
68 }
69 "--output" => {
70 output =
71 args.next().ok_or_else(|| anyhow::anyhow!("--output necesita un valor"))?
72 }
73 "--label" => {
74 label =
75 args.next().ok_or_else(|| anyhow::anyhow!("--label necesita un valor"))?
76 }
77 "--with-carmen" => with_carmen = true,
78 other => anyhow::bail!("opción desconocida «{other}» (usá --help)"),
79 }
80 }
81
82 let init = if from == "auto" {
83 detect(&root).ok_or_else(|| {
84 anyhow::anyhow!(
85 "no pude autodetectar el init en {} — pasá --from <init>",
86 root.display()
87 )
88 })?
89 } else {
90 from.clone()
91 };
92
93 let services = match init.as_str() {
94 "sysvinit" => sysvinit::absorb(&root)?,
95 "runit" => runit::absorb(&root)?,
96 "dinit" => dinit::absorb(&root)?,
97 "openrc" => openrc::absorb(&root)?,
98 other => anyhow::bail!(
99 "init «{other}» no soportado (sysvinit | runit | dinit | openrc | auto)"
100 ),
101 };
102
103 eprintln!(
104 "arje-absorb: init «{init}» → {} servicio(s) absorbido(s).",
105 services.len()
106 );
107 if services.is_empty() {
108 eprintln!("arje-absorb: aviso: 0 servicios — la Semilla quedará sin hijas.");
109 }
110
111 let mut seed = card::build_seed(&label, &services);
112 if with_carmen {
113 seed.genesis.push(card::carmen_dm_card());
114 eprintln!("arje-absorb: agregado carmen-dm (gestor de login gráfico).");
115 }
116 seed.validate()
117 .map_err(|e| anyhow::anyhow!("la Semilla generada no valida: {e}"))?;
118 let json = seed.to_json_pretty()?;
119
120 if output == "-" {
121 println!("{json}");
122 } else {
123 std::fs::write(&output, json)
124 .map_err(|e| anyhow::anyhow!("escribiendo {output}: {e}"))?;
125 eprintln!("arje-absorb: Semilla escrita en {output}");
126 }
127 Ok(())
128}
129
130/// Autodetecta el init presente en `root`. El orden importa: lo más
131/// específico primero — un sistema OpenRC suele llevar también un
132/// `/etc/inittab` (para las consolas), así que sysvinit va último.
133fn detect(root: &Path) -> Option<String> {
134 if root.join("etc/dinit.d").is_dir() {
135 return Some("dinit".to_string());
136 }
137 if root.join("etc/runlevels").is_dir() || root.join("sbin/openrc").exists() {
138 return Some("openrc".to_string());
139 }
140 if root.join("etc/runit").is_dir() || root.join("etc/sv").is_dir() {
141 return Some("runit".to_string());
142 }
143 if root.join("etc/inittab").exists() {
144 return Some("sysvinit".to_string());
145 }
146 None
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn detect_prefers_dinit() {
155 let tmp = tempfile::tempdir().unwrap();
156 std::fs::create_dir_all(tmp.path().join("etc/dinit.d")).unwrap();
157 std::fs::create_dir_all(tmp.path().join("etc/runlevels")).unwrap();
158 assert_eq!(detect(tmp.path()).as_deref(), Some("dinit"));
159 }
160
161 #[test]
162 fn detect_openrc_over_sysvinit() {
163 let tmp = tempfile::tempdir().unwrap();
164 std::fs::create_dir_all(tmp.path().join("etc/runlevels/default")).unwrap();
165 std::fs::write(tmp.path().join("etc/inittab"), "").unwrap();
166 assert_eq!(detect(tmp.path()).as_deref(), Some("openrc"));
167 }
168
169 #[test]
170 fn detect_none_on_empty_root() {
171 let tmp = tempfile::tempdir().unwrap();
172 assert!(detect(tmp.path()).is_none());
173 }
174}