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>
This commit is contained in:
Generated
+9
@@ -342,6 +342,15 @@ dependencies = [
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arje-absorb"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brahman-card",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arje-binfmt-compat"
|
||||
version = "0.0.1"
|
||||
|
||||
@@ -26,6 +26,7 @@ members = [
|
||||
"crates/init/arje-soma",
|
||||
"crates/init/arje-snapshot",
|
||||
"crates/init/arje-incarnate",
|
||||
"crates/init/arje-absorb",
|
||||
|
||||
# ============================================================
|
||||
# runtime/ — Infraestructura de ejecución (bus + cas + wasm + brain)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "arje-absorb"
|
||||
version = "0.0.1"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
description = "arje-absorb — lee la configuración de otro init (sysvinit/runit/dinit/openrc) y la traduce a una Tarjeta Semilla brahman."
|
||||
|
||||
[[bin]]
|
||||
name = "arje-absorb"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
brahman-card = { path = "../../protocol/brahman-card" }
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
@@ -0,0 +1,51 @@
|
||||
# arje-absorb
|
||||
|
||||
Absorbe la configuración de **otro init** y la traduce a una **Tarjeta
|
||||
Semilla** brahman: una `Card` JSON con cada servicio como hija `genesis`
|
||||
de `arje-zero`.
|
||||
|
||||
Es el paso «absorber» de la migración a arje — para no perder los
|
||||
servicios del sistema al cambiar de init. Lo usa `scripts/migrate-to-arje.sh`.
|
||||
|
||||
## Inits soportados
|
||||
|
||||
| init | qué lee | servicio → Card |
|
||||
| ---------- | ----------------------------------------- | --------------- |
|
||||
| `sysvinit` | `/etc/inittab` | `respawn` → daemon supervisado; `wait`/`once`/`sysinit` → one-shot |
|
||||
| `runit` | el `runsvdir` activo (o `/etc/sv`) | cada script `run` → daemon supervisado (calce 1:1) |
|
||||
| `dinit` | `/etc/dinit.d/*` | `type=process`/`bgprocess` → daemon; `scripted` → one-shot |
|
||||
| `openrc` | `/etc/runlevels/{sysinit,boot,default}` | `/etc/init.d/<svc> start` → one-shot |
|
||||
|
||||
`systemd` no se absorbe — sus units no son un formato de texto trivial.
|
||||
Para systemd ya existe la capa de shims (`crates/compat/`) y la seed
|
||||
`seeds/arje-host.card.json`.
|
||||
|
||||
## Uso
|
||||
|
||||
```sh
|
||||
# autodetecta el init y emite la Semilla a stdout
|
||||
arje-absorb
|
||||
|
||||
# explícito, a un archivo, agregando el gestor de login gráfico
|
||||
arje-absorb --from openrc --output /ente/seed.card.json --with-carmen
|
||||
|
||||
# absorber otra raíz (un chroot, una imagen montada)
|
||||
arje-absorb --root /mnt/sistema --from runit
|
||||
```
|
||||
|
||||
Opciones: `--from <init>` (def. `auto`), `--root <dir>` (def. `/`),
|
||||
`--output <f>` (def. `-` = stdout), `--label <s>`, `--with-carmen`.
|
||||
|
||||
## Lo que NO hace
|
||||
|
||||
La Semilla absorbida **conserva el comportamiento** del init viejo: los
|
||||
servicios corren sin aislar (namespaces en `false`, FS read-write, red
|
||||
`full`). No endurece el sandbox — eso se hace después, Card por Card.
|
||||
|
||||
La absorción de OpenRC es **superficial**: envuelve `/etc/init.d/<svc>
|
||||
start` como one-shot (los scripts de OpenRC son shell completo, no se
|
||||
parsean — el daemon lo deja en segundo plano OpenRC mismo, no arje).
|
||||
runit y dinit, en cambio, dan supervisión real porque su `run`/`command`
|
||||
es un proceso en primer plano.
|
||||
|
||||
Revisá la Semilla antes de instalarla como `/ente/seed.card.json`.
|
||||
@@ -0,0 +1,193 @@
|
||||
//! Traducción del modelo intermedio a Tarjetas Semilla brahman.
|
||||
//!
|
||||
//! Cada [`ForeignService`] se vuelve una Card hija; el conjunto cuelga
|
||||
//! como `genesis` de una Card raíz que `arje-zero` encarna al arrancar.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use brahman_card::{
|
||||
Capability, Card, CgroupSpec, FsPolicy, Lifecycle, NetworkingPolicy, Payload, Permissions,
|
||||
Priority, SomaSpec, Supervision,
|
||||
};
|
||||
|
||||
use crate::model::{ForeignService, ServiceKind};
|
||||
|
||||
/// Convierte un servicio absorbido en una Card hija (genesis child).
|
||||
fn service_to_card(svc: &ForeignService) -> Card {
|
||||
let (lifecycle, supervision) = match svc.kind {
|
||||
ServiceKind::Daemon => (
|
||||
Lifecycle::Daemon,
|
||||
Supervision::Restart {
|
||||
initial: Duration::from_millis(1_000),
|
||||
max: Duration::from_millis(60_000),
|
||||
},
|
||||
),
|
||||
ServiceKind::OneShot => (Lifecycle::Oneshot, Supervision::OneShot),
|
||||
};
|
||||
Card {
|
||||
payload: Payload::Native {
|
||||
exec: svc.exec.clone(),
|
||||
argv: svc.argv.clone(),
|
||||
envp: svc.env.clone(),
|
||||
},
|
||||
supervision,
|
||||
lifecycle,
|
||||
priority: Priority::Normal,
|
||||
// Servicio de sistema absorbido: sin aislar (namespaces en
|
||||
// `false` por defecto), con FS de escritura, red y subprocesos.
|
||||
// La migración conserva el comportamiento; endurecer el sandbox
|
||||
// queda como trabajo posterior, Card por Card.
|
||||
permissions: Permissions {
|
||||
filesystem: FsPolicy::ReadWrite,
|
||||
networking: NetworkingPolicy::Full,
|
||||
processes: true,
|
||||
..Permissions::default()
|
||||
},
|
||||
soma: SomaSpec {
|
||||
cgroup: CgroupSpec {
|
||||
path: "ente.slice/absorbed".to_string(),
|
||||
..CgroupSpec::default()
|
||||
},
|
||||
..SomaSpec::default()
|
||||
},
|
||||
..Card::new(svc.name.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// La Card de `carmen-dm`: el compositor `mirada` en modo greeter,
|
||||
/// como gestor de login gráfico. Para agregar a una Semilla absorbida
|
||||
/// al migrar un escritorio (flag `--with-carmen`). Idéntica a la
|
||||
/// entrada `carmen-dm` de `seeds/arje-host.card.json`.
|
||||
pub fn carmen_dm_card() -> Card {
|
||||
Card {
|
||||
payload: Payload::Native {
|
||||
exec: "/usr/bin/mirada-compositor".to_string(),
|
||||
argv: vec!["--greeter".to_string(), "--drm".to_string()],
|
||||
envp: vec![
|
||||
(
|
||||
"PATH".to_string(),
|
||||
"/usr/local/bin:/usr/bin:/usr/sbin:/sbin:/bin".to_string(),
|
||||
),
|
||||
("XDG_RUNTIME_DIR".to_string(), "/run".to_string()),
|
||||
(
|
||||
"MIRADA_GREETER_BIN".to_string(),
|
||||
"/usr/bin/mirada-greeter".to_string(),
|
||||
),
|
||||
],
|
||||
},
|
||||
supervision: Supervision::Restart {
|
||||
initial: Duration::from_millis(2_000),
|
||||
max: Duration::from_millis(60_000),
|
||||
},
|
||||
lifecycle: Lifecycle::Daemon,
|
||||
priority: Priority::High,
|
||||
permissions: Permissions {
|
||||
filesystem: FsPolicy::ReadWrite,
|
||||
networking: NetworkingPolicy::None,
|
||||
processes: true,
|
||||
..Permissions::default()
|
||||
},
|
||||
soma: SomaSpec {
|
||||
cgroup: CgroupSpec {
|
||||
path: "ente.slice/carmen".to_string(),
|
||||
..CgroupSpec::default()
|
||||
},
|
||||
..SomaSpec::default()
|
||||
},
|
||||
..Card::new("carmen-dm")
|
||||
}
|
||||
}
|
||||
|
||||
/// Arma una Tarjeta Semilla raíz que encarna todos los servicios
|
||||
/// absorbidos como hijas `genesis` de `arje-zero`.
|
||||
pub fn build_seed(label: &str, services: &[ForeignService]) -> Card {
|
||||
let genesis: Vec<Card> = services.iter().map(service_to_card).collect();
|
||||
Card {
|
||||
provides: [Capability::Spawn, Capability::Journal]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
permissions: Permissions {
|
||||
filesystem: FsPolicy::ReadWrite,
|
||||
networking: NetworkingPolicy::Full,
|
||||
processes: true,
|
||||
..Permissions::default()
|
||||
},
|
||||
genesis,
|
||||
..Card::new(label)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn svc(name: &str, kind: ServiceKind) -> ForeignService {
|
||||
ForeignService {
|
||||
name: name.to_string(),
|
||||
exec: "/usr/bin/foo".to_string(),
|
||||
argv: vec!["-x".to_string()],
|
||||
env: Vec::new(),
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_validates() {
|
||||
let seed = build_seed(
|
||||
"arje.seed.absorbed",
|
||||
&[svc("a", ServiceKind::Daemon), svc("b", ServiceKind::OneShot)],
|
||||
);
|
||||
seed.validate().expect("la Semilla absorbida debe validar");
|
||||
assert_eq!(seed.genesis.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn daemon_maps_to_restart() {
|
||||
let c = service_to_card(&svc("d", ServiceKind::Daemon));
|
||||
assert!(matches!(c.supervision, Supervision::Restart { .. }));
|
||||
assert_eq!(c.lifecycle, Lifecycle::Daemon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oneshot_maps_to_oneshot() {
|
||||
let c = service_to_card(&svc("o", ServiceKind::OneShot));
|
||||
assert!(matches!(c.supervision, Supervision::OneShot));
|
||||
assert_eq!(c.lifecycle, Lifecycle::Oneshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn children_get_distinct_ids() {
|
||||
let seed = build_seed(
|
||||
"x",
|
||||
&[svc("a", ServiceKind::Daemon), svc("b", ServiceKind::Daemon)],
|
||||
);
|
||||
assert_ne!(seed.genesis[0].id, seed.genesis[1].id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_service_list_still_validates() {
|
||||
let seed = build_seed("arje.seed.absorbed", &[]);
|
||||
seed.validate().expect("una Semilla sin hijas es válida");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn carmen_card_is_valid_greeter() {
|
||||
let c = carmen_dm_card();
|
||||
c.validate().expect("la Card de carmen-dm debe validar");
|
||||
match &c.payload {
|
||||
brahman_card::Payload::Native { exec, argv, .. } => {
|
||||
assert_eq!(exec, "/usr/bin/mirada-compositor");
|
||||
assert!(argv.contains(&"--greeter".to_string()));
|
||||
}
|
||||
_ => panic!("carmen-dm debe ser un payload Native"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn carmen_can_be_appended_to_absorbed_seed() {
|
||||
let mut seed = build_seed("arje.seed.absorbed", &[svc("a", ServiceKind::Daemon)]);
|
||||
seed.genesis.push(carmen_dm_card());
|
||||
seed.validate().expect("la Semilla con carmen debe validar");
|
||||
assert_eq!(seed.genesis.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Absorbe **dinit**: parsea los archivos de `/etc/dinit.d`.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::model::{split_command, ForeignService, ServiceKind};
|
||||
|
||||
/// Descubre los servicios dinit de `<root>/etc/dinit.d`.
|
||||
///
|
||||
/// Cada archivo es un servicio con líneas `clave = valor`. Absorbemos
|
||||
/// los de `type` = `process`/`bgprocess` (daemon) y `scripted`
|
||||
/// (one-shot) que declaren un `command`. `internal`/`triggered` no
|
||||
/// encarnan un proceso y se omiten — entre ellos el servicio `boot`.
|
||||
pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
|
||||
let dir = root.join("etc/dinit.d");
|
||||
anyhow::ensure!(
|
||||
dir.is_dir(),
|
||||
"no encontré /etc/dinit.d en {}",
|
||||
root.display()
|
||||
);
|
||||
let mut names: Vec<String> = fs::read_dir(&dir)?
|
||||
.flatten()
|
||||
.filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| !n.starts_with('.'))
|
||||
.collect();
|
||||
names.sort();
|
||||
|
||||
let mut out = Vec::new();
|
||||
for name in names {
|
||||
let text = fs::read_to_string(dir.join(&name))?;
|
||||
if let Some(svc) = parse_service(&name, &text) {
|
||||
out.push(svc);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Parsea un archivo de servicio dinit. `None` si no encarna un proceso.
|
||||
fn parse_service(name: &str, text: &str) -> Option<ForeignService> {
|
||||
let mut ty = String::new();
|
||||
let mut command = String::new();
|
||||
for raw in text.lines() {
|
||||
let line = raw.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let Some((key, val)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
match key.trim() {
|
||||
"type" => ty = val.trim().to_string(),
|
||||
"command" => command = val.trim().to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let kind = match ty.as_str() {
|
||||
"process" | "bgprocess" => ServiceKind::Daemon,
|
||||
"scripted" => ServiceKind::OneShot,
|
||||
_ => return None, // internal, triggered, o sin `type`
|
||||
};
|
||||
let (exec, argv) = split_command(&command)?;
|
||||
Some(ForeignService {
|
||||
name: format!("dinit-{name}"),
|
||||
exec,
|
||||
argv,
|
||||
env: Vec::new(),
|
||||
kind,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn absorbs_process_and_scripted_skips_internal() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let d = tmp.path().join("etc/dinit.d");
|
||||
fs::create_dir_all(&d).unwrap();
|
||||
fs::write(d.join("boot"), "type = internal\nwaits-for = sshd\n").unwrap();
|
||||
fs::write(
|
||||
d.join("sshd"),
|
||||
"# servicio ssh\ntype = process\ncommand = /usr/sbin/sshd -D\nrestart = true\n",
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(d.join("fsck"), "type = scripted\ncommand = /sbin/fsck -A\n").unwrap();
|
||||
|
||||
let svcs = absorb(tmp.path()).unwrap();
|
||||
// sshd + fsck — boot (internal) se omite.
|
||||
assert_eq!(svcs.len(), 2);
|
||||
let sshd = svcs.iter().find(|s| s.name == "dinit-sshd").unwrap();
|
||||
assert_eq!(sshd.kind, ServiceKind::Daemon);
|
||||
assert_eq!(sshd.exec, "/usr/sbin/sshd");
|
||||
assert_eq!(sshd.argv, ["-D"]);
|
||||
let fsck = svcs.iter().find(|s| s.name == "dinit-fsck").unwrap();
|
||||
assert_eq!(fsck.kind, ServiceKind::OneShot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_without_dinit_d() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
assert!(absorb(tmp.path()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//! `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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//! El modelo intermedio: un servicio descubierto en el init ajeno,
|
||||
//! independiente de su formato de origen. Cada absorber
|
||||
//! (`sysvinit`, `runit`, `dinit`, `openrc`) produce [`ForeignService`]s;
|
||||
//! `card` los traduce a Cards brahman.
|
||||
|
||||
/// Un servicio leído de la configuración de otro init.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ForeignService {
|
||||
/// Etiqueta del servicio — pasa a ser el `label` de su Card.
|
||||
pub name: String,
|
||||
/// Ejecutable (path absoluto, tal como existirá en el sistema).
|
||||
pub exec: String,
|
||||
/// Argumentos del ejecutable.
|
||||
pub argv: Vec<String>,
|
||||
/// Variables de entorno (clave, valor) a inyectarle.
|
||||
pub env: Vec<(String, String)>,
|
||||
/// Daemon supervisado, o tarea de una sola ejecución.
|
||||
pub kind: ServiceKind,
|
||||
}
|
||||
|
||||
/// Cómo trata arje a un servicio absorbido.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ServiceKind {
|
||||
/// Servicio de larga duración: arje lo supervisa y reinicia.
|
||||
Daemon,
|
||||
/// Tarea puntual: corre una vez y termina.
|
||||
OneShot,
|
||||
}
|
||||
|
||||
/// Parte una línea de comando en `(exec, argv)`, respetando comillas
|
||||
/// simples y dobles. Best-effort: no expande variables ni globs —
|
||||
/// suficiente para los campos `process`/`command` de los inits
|
||||
/// clásicos. `None` si la línea no tiene ningún token.
|
||||
pub fn split_command(line: &str) -> Option<(String, Vec<String>)> {
|
||||
let mut tokens: Vec<String> = Vec::new();
|
||||
let mut cur = String::new();
|
||||
let mut in_token = false;
|
||||
let mut chars = line.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
match c {
|
||||
'"' | '\'' => {
|
||||
in_token = true;
|
||||
let quote = c;
|
||||
for q in chars.by_ref() {
|
||||
if q == quote {
|
||||
break;
|
||||
}
|
||||
cur.push(q);
|
||||
}
|
||||
}
|
||||
c if c.is_whitespace() => {
|
||||
if in_token {
|
||||
tokens.push(std::mem::take(&mut cur));
|
||||
in_token = false;
|
||||
}
|
||||
}
|
||||
c => {
|
||||
in_token = true;
|
||||
cur.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if in_token {
|
||||
tokens.push(cur);
|
||||
}
|
||||
if tokens.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let exec = tokens.remove(0);
|
||||
Some((exec, tokens))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn splits_plain_command() {
|
||||
let (exec, argv) = split_command("/usr/sbin/sshd -D -e").unwrap();
|
||||
assert_eq!(exec, "/usr/sbin/sshd");
|
||||
assert_eq!(argv, ["-D", "-e"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_double_quotes() {
|
||||
let (exec, argv) = split_command(r#"/bin/foo "arg con espacios" bar"#).unwrap();
|
||||
assert_eq!(exec, "/bin/foo");
|
||||
assert_eq!(argv, ["arg con espacios", "bar"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_single_quotes() {
|
||||
let (exec, argv) = split_command("/bin/sh -c 'echo hola mundo'").unwrap();
|
||||
assert_eq!(exec, "/bin/sh");
|
||||
assert_eq!(argv, ["-c", "echo hola mundo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_line_yields_none() {
|
||||
assert!(split_command(" ").is_none());
|
||||
assert!(split_command("").is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//! Absorbe **OpenRC**: descubre los servicios habilitados por runlevel.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::model::{ForeignService, ServiceKind};
|
||||
|
||||
/// Descubre los servicios OpenRC habilitados en `<root>`.
|
||||
///
|
||||
/// OpenRC habilita un servicio con un symlink en `/etc/runlevels/<rl>/`.
|
||||
/// Los scripts de `/etc/init.d/` son shell completo —no se parsean—; se
|
||||
/// absorben como tarea one-shot `/etc/init.d/<svc> start`, que arranca
|
||||
/// el daemon (OpenRC mismo lo lleva a segundo plano). Recorremos los
|
||||
/// runlevels `sysinit` → `boot` → `default` y deduplicamos.
|
||||
pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
|
||||
let runlevels = ["sysinit", "boot", "default"];
|
||||
let mut found_any = false;
|
||||
let mut seen: BTreeSet<String> = BTreeSet::new();
|
||||
let mut out = Vec::new();
|
||||
for rl in runlevels {
|
||||
let dir = root.join("etc/runlevels").join(rl);
|
||||
if !dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
found_any = true;
|
||||
let mut names: Vec<String> = fs::read_dir(&dir)?
|
||||
.flatten()
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| !n.starts_with('.'))
|
||||
.collect();
|
||||
names.sort();
|
||||
for svc in names {
|
||||
if !seen.insert(svc.clone()) {
|
||||
continue; // ya absorbido en un runlevel anterior
|
||||
}
|
||||
out.push(ForeignService {
|
||||
name: format!("openrc-{svc}"),
|
||||
exec: format!("/etc/init.d/{svc}"),
|
||||
argv: vec!["start".to_string()],
|
||||
env: Vec::new(),
|
||||
kind: ServiceKind::OneShot,
|
||||
});
|
||||
}
|
||||
}
|
||||
anyhow::ensure!(
|
||||
found_any,
|
||||
"no encontré /etc/runlevels/* en {}",
|
||||
root.display()
|
||||
);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn absorbs_enabled_services_deduped() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
for (rl, svcs) in [
|
||||
("boot", ["bootmisc", "hostname"].as_slice()),
|
||||
("default", ["sshd", "hostname"].as_slice()),
|
||||
] {
|
||||
let d = tmp.path().join("etc/runlevels").join(rl);
|
||||
fs::create_dir_all(&d).unwrap();
|
||||
for s in svcs {
|
||||
// Un archivo simple basta: sólo leemos los nombres.
|
||||
fs::write(d.join(s), "").unwrap();
|
||||
}
|
||||
}
|
||||
let out = absorb(tmp.path()).unwrap();
|
||||
// bootmisc, hostname, sshd — `hostname` deduplicado entre runlevels.
|
||||
assert_eq!(out.len(), 3);
|
||||
let sshd = out.iter().find(|s| s.name == "openrc-sshd").unwrap();
|
||||
assert_eq!(sshd.exec, "/etc/init.d/sshd");
|
||||
assert_eq!(sshd.argv, ["start"]);
|
||||
assert_eq!(sshd.kind, ServiceKind::OneShot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_without_runlevels() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
assert!(absorb(tmp.path()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
//! Absorbe **runit**: descubre los servicios bajo supervisión.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::model::{ForeignService, ServiceKind};
|
||||
|
||||
/// Descubre los servicios runit de `<root>`.
|
||||
///
|
||||
/// runit supervisa un directorio de servicios activos (el `runsvdir`
|
||||
/// del runlevel). Cada entrada apunta a `/etc/sv/<nombre>`, cuyo script
|
||||
/// `run` es un daemon en primer plano — el calce con arje es 1:1: arje
|
||||
/// supervisa ese mismo `run`. Si no hay `runsvdir`, cae a `/etc/sv`
|
||||
/// (todos los servicios definidos).
|
||||
pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
|
||||
let runsvdir = [
|
||||
"etc/runit/runsvdir/default",
|
||||
"etc/runit/runsvdir/current",
|
||||
"service",
|
||||
"var/service",
|
||||
"etc/service",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|c| root.join(c))
|
||||
.find(|p| p.is_dir());
|
||||
|
||||
let scan = match runsvdir {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
let sv = root.join("etc/sv");
|
||||
anyhow::ensure!(
|
||||
sv.is_dir(),
|
||||
"no encontré servicios runit en {}",
|
||||
root.display()
|
||||
);
|
||||
sv
|
||||
}
|
||||
};
|
||||
|
||||
let mut names: Vec<String> = fs::read_dir(&scan)?
|
||||
.flatten()
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| !n.starts_with('.'))
|
||||
.collect();
|
||||
names.sort();
|
||||
|
||||
let mut out = Vec::new();
|
||||
for name in names {
|
||||
// El servicio vive en /etc/sv/<name>; su run-script es el exec.
|
||||
let run = root.join("etc/sv").join(&name).join("run");
|
||||
if !run.exists() {
|
||||
continue; // entrada sin run-script real — la saltamos
|
||||
}
|
||||
out.push(ForeignService {
|
||||
name: format!("runit-{name}"),
|
||||
exec: format!("/etc/sv/{name}/run"),
|
||||
argv: Vec::new(),
|
||||
env: Vec::new(),
|
||||
kind: ServiceKind::Daemon, // runit siempre supervisa
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_sv(root: &Path, name: &str) {
|
||||
let d = root.join("etc/sv").join(name);
|
||||
fs::create_dir_all(&d).unwrap();
|
||||
fs::write(d.join("run"), "#!/bin/sh\nexec daemon\n").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absorbs_from_etc_sv_fallback() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
make_sv(tmp.path(), "sshd");
|
||||
make_sv(tmp.path(), "dhcpcd");
|
||||
let svcs = absorb(tmp.path()).unwrap();
|
||||
assert_eq!(svcs.len(), 2);
|
||||
// Salida ordenada por nombre.
|
||||
assert_eq!(svcs[0].name, "runit-dhcpcd");
|
||||
assert_eq!(svcs[0].exec, "/etc/sv/dhcpcd/run");
|
||||
assert_eq!(svcs[0].kind, ServiceKind::Daemon);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absorbs_only_enabled_from_runsvdir() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
for s in ["sshd", "dhcpcd", "apagado"] {
|
||||
make_sv(tmp.path(), s);
|
||||
}
|
||||
let rsv = tmp.path().join("etc/runit/runsvdir/default");
|
||||
fs::create_dir_all(&rsv).unwrap();
|
||||
// Sólo sshd y dhcpcd están habilitados (symlink en el runsvdir).
|
||||
for s in ["sshd", "dhcpcd"] {
|
||||
std::os::unix::fs::symlink(tmp.path().join("etc/sv").join(s), rsv.join(s))
|
||||
.unwrap();
|
||||
}
|
||||
let svcs = absorb(tmp.path()).unwrap();
|
||||
assert_eq!(svcs.len(), 2);
|
||||
assert!(svcs.iter().all(|s| s.name != "runit-apagado"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_without_services() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
assert!(absorb(tmp.path()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//! Absorbe **sysvinit**: parsea `/etc/inittab`.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::model::{split_command, ForeignService, ServiceKind};
|
||||
|
||||
/// Lee `<root>/etc/inittab` y devuelve sus servicios.
|
||||
///
|
||||
/// Formato de cada línea: `id:runlevels:action:process`. Tomamos las
|
||||
/// que tienen un `process` real: `respawn` → daemon supervisado;
|
||||
/// `wait`/`once`/`boot`/`bootwait`/`sysinit` → one-shot. El resto de
|
||||
/// acciones (`initdefault`, `ctrlaltdel`, `power*`, `off`, …) no lanzan
|
||||
/// un servicio y se ignoran.
|
||||
pub fn absorb(root: &Path) -> anyhow::Result<Vec<ForeignService>> {
|
||||
let path = root.join("etc/inittab");
|
||||
let text =
|
||||
fs::read_to_string(&path).with_context(|| format!("leyendo {}", path.display()))?;
|
||||
let mut out = Vec::new();
|
||||
for raw in text.lines() {
|
||||
let line = raw.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let mut f = line.splitn(4, ':');
|
||||
let (id, action, process) = match (f.next(), f.next(), f.next(), f.next()) {
|
||||
(Some(id), Some(_rl), Some(action), Some(process)) => (id, action, process),
|
||||
_ => continue, // línea malformada
|
||||
};
|
||||
let kind = match action.trim() {
|
||||
"respawn" => ServiceKind::Daemon,
|
||||
"wait" | "once" | "boot" | "bootwait" | "sysinit" => ServiceKind::OneShot,
|
||||
_ => continue,
|
||||
};
|
||||
// El proceso puede empezar con `+` (sysvinit: no escribir utmp).
|
||||
let process = process.trim().trim_start_matches('+').trim();
|
||||
let Some((exec, argv)) = split_command(process) else {
|
||||
continue;
|
||||
};
|
||||
out.push(ForeignService {
|
||||
name: format!("sysv-{}", id.trim()),
|
||||
exec,
|
||||
argv,
|
||||
env: Vec::new(),
|
||||
kind,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn with_inittab(content: &str) -> tempfile::TempDir {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
fs::create_dir_all(tmp.path().join("etc")).unwrap();
|
||||
fs::write(tmp.path().join("etc/inittab"), content).unwrap();
|
||||
tmp
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_respawn_and_oneshot() {
|
||||
let tmp = with_inittab(
|
||||
"# consolas del sistema\n\
|
||||
id:3:initdefault:\n\
|
||||
si::sysinit:/etc/rc.d/rc.sysinit\n\
|
||||
1:2345:respawn:/sbin/agetty 38400 tty1 linux\n\
|
||||
rc::wait:/etc/rc.d/rc 3\n",
|
||||
);
|
||||
let svcs = absorb(tmp.path()).unwrap();
|
||||
// sysinit + respawn + wait — initdefault no cuenta.
|
||||
assert_eq!(svcs.len(), 3);
|
||||
let agetty = svcs.iter().find(|s| s.name == "sysv-1").unwrap();
|
||||
assert_eq!(agetty.kind, ServiceKind::Daemon);
|
||||
assert_eq!(agetty.exec, "/sbin/agetty");
|
||||
assert_eq!(agetty.argv, ["38400", "tty1", "linux"]);
|
||||
let si = svcs.iter().find(|s| s.name == "sysv-si").unwrap();
|
||||
assert_eq!(si.kind, ServiceKind::OneShot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_comments_and_blank_lines() {
|
||||
let tmp = with_inittab("\n \n# sólo comentarios\n");
|
||||
assert!(absorb(tmp.path()).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strips_leading_plus() {
|
||||
let tmp = with_inittab("x:2:respawn:+/sbin/getty tty2\n");
|
||||
let svcs = absorb(tmp.path()).unwrap();
|
||||
assert_eq!(svcs[0].exec, "/sbin/getty");
|
||||
}
|
||||
}
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# migrate-to-arje.sh — migra una máquina de su init actual a arje,
|
||||
# absorbiendo su configuración de servicios.
|
||||
#
|
||||
# QUÉ HACE:
|
||||
# 1. Compila arje-absorb (host).
|
||||
# 2. Autodetecta el init actual (o --from) y absorbe sus servicios en
|
||||
# una Tarjeta Semilla (out/arje-absorbed.card.json por default).
|
||||
# 3. (--with-carmen) agrega carmen-dm —el gestor de login gráfico— a
|
||||
# la Semilla y compila+instala mirada-compositor y mirada-greeter.
|
||||
# 4. Pasa la Semilla a install-arje-as-init.sh, que instala arje en
|
||||
# paralelo a tu init actual, con su propia entrada GRUB.
|
||||
#
|
||||
# Tu init actual queda INTACTO y sigue siendo el default — arje se elige
|
||||
# en GRUB. Revertir: sudo scripts/uninstall-arje.sh
|
||||
#
|
||||
# systemd NO se absorbe (sus units no son un formato de texto trivial);
|
||||
# para systemd usá install-arje-as-init.sh con seeds/arje-host.card.json.
|
||||
# Este script cubre sysvinit / runit / dinit / OpenRC.
|
||||
#
|
||||
# USO:
|
||||
# sudo scripts/migrate-to-arje.sh [opciones]
|
||||
# --from <init> sysvinit|runit|dinit|openrc|auto (def: auto)
|
||||
# --with-carmen agrega el escritorio carmen como gestor de login
|
||||
# --seed-out <f> dónde escribir la Semilla absorbida
|
||||
# (def: out/arje-absorbed.card.json)
|
||||
# --dry-run absorbe y valida, sin instalar nada (no necesita root)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
FROM="auto"
|
||||
WITH_CARMEN=0
|
||||
DRY_RUN=0
|
||||
SEED_OUT="$REPO_DIR/out/arje-absorbed.card.json"
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--from) FROM="${2:?--from necesita un valor}"; shift 2 ;;
|
||||
--with-carmen) WITH_CARMEN=1; shift ;;
|
||||
--seed-out) SEED_OUT="${2:?--seed-out necesita un valor}"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h|--help)
|
||||
cat <<'USAGE'
|
||||
migrate-to-arje.sh — migra una máquina a arje absorbiendo su init actual.
|
||||
--from <init> sysvinit|runit|dinit|openrc|auto (def: auto)
|
||||
--with-carmen agrega el escritorio carmen como gestor de login
|
||||
--seed-out <f> dónde escribir la Semilla absorbida
|
||||
--dry-run absorbe y valida sin instalar (no necesita root)
|
||||
USAGE
|
||||
exit 0 ;;
|
||||
*) echo "[migrate] opción desconocida: $1 (usá --help)" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$REPO_DIR"
|
||||
|
||||
if [ "$DRY_RUN" -eq 0 ] && [ "$(id -u)" -ne 0 ]; then
|
||||
echo "[migrate] hace falta root para instalar — o corré con --dry-run" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[migrate] paso 1/4: compilar arje-absorb"
|
||||
cargo build --quiet --release -p arje-absorb
|
||||
ABSORB="$REPO_DIR/target/release/arje-absorb"
|
||||
|
||||
echo "[migrate] paso 2/4: absorber el init actual (--from $FROM)"
|
||||
mkdir -p "$(dirname "$SEED_OUT")"
|
||||
ABSORB_ARGS=(--from "$FROM" --output "$SEED_OUT" --label "arje.seed.migrated")
|
||||
[ "$WITH_CARMEN" -eq 1 ] && ABSORB_ARGS+=(--with-carmen)
|
||||
"$ABSORB" "${ABSORB_ARGS[@]}"
|
||||
echo "[migrate] Semilla escrita en: $SEED_OUT"
|
||||
bash "$REPO_DIR/seeds/validate.sh" "$SEED_OUT"
|
||||
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
cat <<EOF
|
||||
|
||||
[migrate] --dry-run: Semilla absorbida y validada en
|
||||
$SEED_OUT
|
||||
Revisala y, cuando quieras instalar:
|
||||
sudo scripts/install-arje-as-init.sh "$SEED_OUT"
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$WITH_CARMEN" -eq 1 ]; then
|
||||
echo "[migrate] paso 3/4: compilar e instalar carmen (compositor + greeter)"
|
||||
# carmen enlaza mesa/libdrm/libinput/libseat — se compila con el
|
||||
# target del host (glibc dinámico), NO musl: esta máquina ya tiene
|
||||
# esas librerías. Sólo el PID 1 y los shims van musl-static al initrd.
|
||||
cargo build --quiet --release -p mirada-compositor -p mirada-greeter
|
||||
install -m 0755 "$REPO_DIR/target/release/mirada-compositor" /usr/bin/mirada-compositor
|
||||
install -m 0755 "$REPO_DIR/target/release/mirada-greeter" /usr/bin/mirada-greeter
|
||||
echo " mirada-compositor y mirada-greeter instalados en /usr/bin/"
|
||||
else
|
||||
echo "[migrate] paso 3/4: carmen omitido (sin --with-carmen)"
|
||||
fi
|
||||
|
||||
echo "[migrate] paso 4/4: instalar arje como init"
|
||||
"$SCRIPT_DIR/install-arje-as-init.sh" "$SEED_OUT"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
[migrate] HECHO. arje quedó instalado en paralelo a tu init actual.
|
||||
- Semilla migrada: $SEED_OUT → /ente/seed.card.json
|
||||
- tu init actual sigue siendo el default; arje se elige en GRUB.
|
||||
|
||||
Para arrancar arje una vez en el próximo boot:
|
||||
sudo grub-reboot "arje (init=/sbin/arje-zero) — kernel \$(uname -r)"
|
||||
sudo reboot
|
||||
|
||||
Revertir todo:
|
||||
sudo scripts/uninstall-arje.sh
|
||||
EOF
|
||||
Reference in New Issue
Block a user