feat(arje): reemplaza systemd en máquina real con coexistencia GRUB

Flujo seguro de adopción: arje se instala como entrada GRUB
alternativa, no toca systemd ni /sbin/init. Booteás arje cuando
querés, volvés a systemd si rompe (rollback instantáneo desde el
menú).

Artefactos nuevos:

- scripts/install-arje-as-init.sh: instala binarios musl-static a
  /usr/sbin/ y /usr/bin/, copia seed a /ente/seed.card.json, agrega
  menuentry "arje" a /etc/grub.d/40_custom usando init=/sbin/ente-zero
  con kernel + initrd nativos. NO cambia GRUB_DEFAULT. Idempotente
  (regenera el bloque ARJE-MENUENTRY si existe).

- scripts/uninstall-arje.sh: revierte binarios + menuentry. Conserva
  /ente/seed.card.json por si la editaste.

- seeds/arje-host.card.json: seed para máquina real con 15 cards:
  tmpfiles + mount-fstab + swap-on + dbus-system + 11 compat shims +
  dhcpcd + sshd + agetty. Validada.

- docs/arje-replace-systemd.md: filosofía vs systemd ("no acapara
  porque no genera, sólo arranca lo declarado"), lista exhaustiva de
  servicios systemd que NO deben migrarse (ModemManager, snapd, cups,
  unattended-upgrades, etc.), tabla diferencial de UX vs systemd
  (systemctl restart → kill PID, systemctl enable → editar seed),
  checklist pre-primer-boot, instrucciones de rollback y cómo hacer
  arje default sólo cuando estés seguro.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-18 20:01:12 +00:00
parent e4b1d41b62
commit ca5dd04176
4 changed files with 693 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
# Reemplazar systemd con arje en una máquina real
> **Importante**: este flujo deja **systemd intacto** y agrega arje como
> entrada GRUB alternativa. No es "destruir y reemplazar"; es "convivir
> hasta confiar". Booteás arje cuando querés, volvés a systemd si rompe.
## 1. Estrategia
| componente | qué hacemos |
| --------------------- | -------------------------------------------------------- |
| `/sbin/init` | NO se toca (sigue siendo systemd). |
| `/lib/systemd/*` | NO se toca. |
| `kernel + initramfs` | reusamos los del host (firmware, módulos, root FS). |
| `init=` en GRUB | nueva entrada apunta a `/sbin/ente-zero`. |
| `GRUB_DEFAULT` | NO se cambia. Sigue arrancando systemd por default. |
| `/ente/seed.card.json`| nueva — define qué arranca arje. |
| `/usr/sbin/ente-*` | binarios musl-static del fractal. |
Booteo del kernel → initramfs nativo hace su trabajo (mountea root,
carga módulos) → cuando llega el `switch_root`, en lugar de exec
`/lib/systemd/systemd`, exec `/sbin/ente-zero`. Esto deja firmware y
udev fuera del scope de arje (todavía).
## 2. Instalación
```bash
# 1. (opcional) revisá la seed default que se va a copiar a /ente/
$EDITOR seeds/arje-host.card.json
# 2. instalar
sudo scripts/install-arje-as-init.sh
# 3. (opcional) bootear arje SÓLO en el próximo reboot (no cambia default)
sudo grub-reboot "arje (init=/sbin/ente-zero) — kernel $(uname -r)"
sudo reboot
```
Si algo rompe en arje: reset/poweroff → en el menú GRUB elegir la
entrada default (systemd) → volvés a un sistema funcional. `arje` no
toca configuración de systemd, así que el rollback es instantáneo.
Para **desinstalar**:
```bash
sudo scripts/uninstall-arje.sh
```
## 3. Filosofía vs systemd — y por qué arje no acapara
systemd arranca, por default, **todo lo que tiene un `.service` con
`WantedBy=multi-user.target`**, recursivamente vía dependencias. Eso es
opaco y crece con cada paquete instalado.
arje no tiene generadores, ni `WantedBy`, ni `Wants`, ni target trees.
**Arranca exactamente lo declarado en `genesis: [...]` de la
`seed.card.json`**, en el orden declarativo, con la `supervision` que
declarás vos. Si no lo declaraste, no corre. Punto.
### 3a. Servicios que systemd arranca y arje NO debería migrar
Estos son los sospechosos típicos en Debian/Ubuntu/Arch. **No agregues
ninguno a tu seed** salvo que sepas que lo usás:
| systemd unit | por qué saltearlo |
| ------------------------------------ | ---------------------------------------- |
| `ModemManager.service` | no tenés módem 3G/4G. |
| `bluetooth.service` | si no usás bluetooth. |
| `cups.service`, `cups-browsed` | si no imprimís. |
| `avahi-daemon.service` | mDNS local. Ruido si no lo necesitás. |
| `snapd.service` | si no usás snaps. |
| `apt-daily.service`, `apt-daily-upgrade` | autoupdates — no son init. |
| `unattended-upgrades.service` | corre tras boot, no init. |
| `accounts-daemon.service` | UI helpers GNOME; no init. |
| `colord.service` | calibración de color. No init. |
| `geoclue.service` | localización. No init. |
| `gdm.service`, `lightdm.service` | DM gráfico — agregalo SÓLO si querés GUI.|
| `NetworkManager-wait-online.service` | espera red. arje no necesita "esperar". |
| `systemd-timesyncd.service` | reemplaza con cron-like + ntpd one-shot. |
| `systemd-machined.service` | reemplazado por `ente-machined-compat`. |
| `systemd-logind.service` | reemplazado por `ente-logind-compat`. |
| `systemd-resolved.service` | reemplazado por `ente-resolved-compat`. |
| `systemd-hostnamed.service` | reemplazado por `ente-hostnamed-compat`. |
| `systemd-timedated.service` | reemplazado por `ente-timedated-compat`. |
| `systemd-localed.service` | reemplazado por `ente-localed-compat`. |
| `systemd-journald.service` | reemplazado por `ente-journald-compat`. |
| `systemd-tmpfiles-*.service` | reemplazado por `ente-tmpfiles-compat`. |
| `systemd-binfmt.service` | reemplazado por `ente-binfmt-compat`. |
| `polkitd.service` | reemplazado por `ente-polkit-compat`. |
Las últimas 12 ya están como Cards en `seeds/arje-host.card.json`. Las
de la mitad de arriba **no** — no las agregues.
### 3b. Servicios que SÍ necesitás declarar (y que ya están en `arje-host.card.json`)
| Card label | reemplaza | binario |
| ------------------ | -------------------------- | -------------------------------------------- |
| `tmpfiles-boot` | `systemd-tmpfiles-setup` | `/usr/sbin/ente-tmpfiles-compat --boot` |
| `mount-fstab` | `local-fs.target` | `/bin/mount -a` (one-shot) |
| `swap-on` | `swap.target` | `/sbin/swapon -a` (one-shot) |
| `dbus-system` | `dbus.service` | `/usr/bin/dbus-daemon --system --nofork` |
| `compat-*` (×11) | shims D-Bus de systemd | `/usr/sbin/ente-*-compat` |
| `network-dhcpcd` | `NetworkManager` / `networkd` | `/usr/sbin/dhcpcd -B` |
| `sshd` | `ssh.service` | `/usr/sbin/sshd -D` |
| `getty-tty1` | `getty@tty1.service` | `/sbin/agetty --noclear tty1 linux` |
15 entes. Compará: una instalación Debian fresh con GNOME desktop tiene
**>180 unidades activas** tras `systemctl list-units --state=running`.
### 3c. Cómo agregar un servicio que falta
1. Encontrá el binario y los argumentos. Mirá el `.service` original:
```
systemctl cat <unit>
```
Mirá `ExecStart=` y reproducí.
2. Agregá una Card al `genesis` de `/ente/seed.card.json`:
```json
{
"schema_version": 1,
"id": "<ULID nuevo, Crockford base32, no I L O U>",
"label": "mi-servicio",
"provides": [], "requires": [],
"permissions": { "networking": "outbound", "filesystem": "read-write", "ipc": { "allow": [] }, "processes": true },
"soma": { ... idéntico al patrón de los otros ... },
"payload": { "Native": { "exec": "/usr/sbin/mi-bin", "argv": ["--foreground"], "envp": [] } },
"supervision": { "Restart": { "initial": 500, "max": 30000 } },
"lifecycle": "daemon",
"priority": "normal",
"flow": { "input": [], "output": [] },
"genesis": []
}
```
**Importante**: `argv` tiene que tener el equivalente a `--foreground` o
`-D` o `--no-fork`. Si el binario se daemoniza solo (fork+exit), arje
no lo puede supervisar y caerá en loop de restart.
3. Validar:
```bash
seeds/validate.sh /ente/seed.card.json
```
4. Reboot a arje. Verificar:
```sh
arje# brahman-status
```
## 4. Diferencias de UX con systemd
| acción | systemd | arje |
| ---------------------------- | ------------------------------------ | ------------------------------------------------------- |
| Ver servicios activos | `systemctl list-units --state=running` | `brahman-status` + `busctl list-entes` |
| Ver logs | `journalctl -u foo` | `ente-journalctl` (TODO en CLI; por ahora `dmesg`) |
| Restart un servicio | `systemctl restart foo` | matar el PID (`brain` lo reanuda por `Supervision`) |
| Habilitar/Deshabilitar | `systemctl enable/disable` | editar `/ente/seed.card.json` + reboot |
| Ver dependencias | `systemctl list-dependencies foo` | `brahman-status` muestra references y flow matches |
| Detectar fallas reincidentes | `systemctl status foo` → "failed" | `brainctl crystals` muestra patrones `EnteDied → …` |
| socket activation | `.socket` unit | (no implementado todavía; ver flow + broker matching) |
| user services | `--user` | (no implementado; arje es system-wide por ahora) |
## 5. Cosas que arje todavía NO hace que sí hace systemd
- **udev / hotplug**: arje captura uevents (vía `spawn_uevent_stream`),
pero no hay reglas declarativas todavía. Para boot inicial, el
initramfs nativo ya hizo el cold-plug, así que no es bloqueante.
- **Timers** (cron-like): `ente-timer-compat` existe pero no está en la
seed host por default. Agregalo si querés `*.timer` units migradas.
- **Service generators**: no hay equivalente a `systemd-fstab-generator`
ni `systemd-getty-generator`. Tu seed los declara explícitamente.
- **Socket activation**: parcialmente — el broker matchea flow
consumer↔producer, pero no hay aún listen-on-fd-handoff como
`sd_listen_fds()`.
- **User instances**: no hay todavía `arje --user`. Los procesos de
usuario los lanza un getty/login estándar.
## 6. Checklist pre-primer-boot
- [ ] `seeds/arje-host.card.json` ajustada a tu hardware/red/users.
- [ ] `seeds/validate.sh` → OK.
- [ ] `/usr/bin/dbus-daemon` existe (de lo contrario los shims no se
anuncian — `apt install dbus`).
- [ ] `/usr/sbin/dhcpcd` o tu network stack preferido está instalado.
- [ ] `/usr/sbin/sshd` instalado y `/etc/ssh/sshd_config` ok si querés
poder entrar remoto si la consola falla.
- [ ] `/sbin/agetty` instalado (`util-linux`) para tty1.
- [ ] `sudo scripts/install-arje-as-init.sh` ejecutado.
- [ ] `grub-reboot` apuntado a la entrada arje **sólo para el próximo
boot** — no `grub-set-default`.
- [ ] Listo para revertir vía menú GRUB si algo falla.
## 7. Cuando arje sea estable: hacerlo default
Cuando ya hayas booteado arje N veces sin problemas y querés que sea
default:
```bash
# Listar entradas
sudo grep -E "^\s*menuentry " /boot/grub/grub.cfg | nl
# Ponerlo como default (usa el índice del menú o el title exacto)
sudo grub-set-default "arje (init=/sbin/ente-zero) — kernel $(uname -r)"
```
Mantené la entrada systemd como rescue. Si querés acortar la lista de
units que systemd arrancaría si bootea de nuevo: `systemctl disable
ModemManager bluetooth cups …` — los nombres de §3a son una guía.
## Apéndice — depurar el primer boot fallido
Si arje no levanta el primer boot:
1. Volver a GRUB → entrada systemd → bootea normal.
2. `journalctl --boot=-1` (logs del intento previo).
3. Buscar `ente-zero` o `Tarjeta Semilla`. Si nada → el ejecutable no
se lanzó (kernel panic temprano, falta `init=` correcto, etc.).
4. Si arrancó pero falló alguna Card: la próxima vez, antes de reboot:
```bash
sudo sed -i 's/console=tty1/console=tty1 ente.log=trace/' /etc/grub.d/40_custom
sudo update-grub
```
y revisá `dmesg` desde systemd tras el reintento.