#!/usr/bin/env bash # metal-desktop-image.sh — imagen de disco EFI del escritorio COSMIC para METAL REAL, con # **aceleración de hardware Intel (iris)**. Hermano de scripts/kde/metal-desktop-image-dual.sh, pero # con una diferencia de fondo que es el motivo de existir. # # ── POR QUÉ ESTA IMAGEN ES HW-ACCEL Y LA DE KDE ES SOFTWARE ───────────────────────────────────── # La de KDE va por `mesa-llvmpipe` a propósito: tenía que aguantar DOS máquinas (Intel y una NVIDIA # Pascal) y la mesa HW de nouveau sólo existe como binario Alpine, o sea rompiendo soberanía. # # Acá el objetivo es otro y por eso la decisión se invierte: **cerrar el frente de ScreenCast**. El # diagnóstico del 2026-08-05 (ver el runbook) midió que la cadena entera del portal funciona —el nodo # `cosmic-screencast` existe en el grafo de pipewire— y que lo que falla es el COMPOSITOR en una # llamada EGL, 8 ms antes de que el stream quede en `Paused`, porque en QEMU mesa es `kms_swrast`: # software, **sin exportación dmabuf**. Un stream de vídeo continuo la necesita; una captura de una # sola toma no (por eso `cosmic-screenshot` sí anda, va por shm). # # El `mesa` del corpus se construye con `-Dgallium-drivers=iris` y **ya está en el rootfs de COSMIC** # (`usr/lib/dri/iris_dri.so`). En QEMU nunca se usaba; en un TigerLake es el driver correcto y trae # dmabuf de verdad. O sea que este viaje no es «lo mismo pero en metal»: es la única forma de saber si # ScreenCast está bien. # # ⚠ POR ESO NO SE INYECTA llvmpipe ACÁ. Mezclarlos no es aditivo: llvmpipe trae su propio # `libgbm`/`libEGL`/`libgallium` y sobrescribirlos DESACTIVA iris. Si esta máquina no tuviera Intel, # la imagen correcta sería la de KDE, no ésta con un parche. # # ── LO QUE **NO** HACE FALTA INYECTAR, Y ES MEDIBLE ───────────────────────────────────────────── # La imagen de KDE inyecta `libLLVM.so.18`, `libgcc_s.so.1` y `libstdc++.so.6` porque el JIT de # llvmpipe los NEEDea. Acá NO: `iris_dri.so` pide sólo `libglapi`, `libdrm`, `libz`, `libzstd` y # `libc` —mesa va con `-Dllvm=disabled`— y `cosmic-comp` sólo `libdisplay-info`, `libgbm`, `libseat`, # `libudev`, `libinput`, `libpixman`, `libxkbcommon` y `libc`. **Cero C++ y cero libgcc**: la imagen # COSMIC de metal no arrastra un solo binario ajeno de Alpine. Verificado con clausura ELF completa # del rootfs: 42 raíces de runtime, 0 con NEEDED sin proveedor. # # work/metal-rootfs base: arje-zero PID1 + busybox + firmware i915 (estática ⇒ inyectar musl) # work/cosmic-rootfs COSMIC runtime-completo (89 recetas: compositor, apps, portal, pipewire, # wireplumber, portal-probe) — ya trae iris_dri.so # + kernel linux-metal-dual (i915 builtin) # → work/hammer-cosmic-metal.img # # Uso: scripts/cosmic/metal-desktop-image.sh set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)"; cd "$ROOT" BASE="${BASE:-work/metal-rootfs}" COSMIC="${COSMIC:-work/cosmic-rootfs}" KERNEL="${KERNEL:-$(ls -dt store/*-linux-metal-dual/boot/bzImage 2>/dev/null | head -1)}" MERGED="${MERGED:-work/cosmic-metal-rootfs}" IMG="${IMG:-work/hammer-cosmic-metal.img}" for x in "$BASE" "$COSMIC"; do [ -d "$x" ] || { echo "falta $x (corré scripts/cosmic/hydrate-cosmic.sh)" >&2; exit 1; }; done [ -n "$KERNEL" ] && [ -r "$KERNEL" ] || { echo "falta linux-metal-dual (hammer build recipes/linux-metal-dual.toml)" >&2; exit 1; } echo "==> fundiendo rootfs por hardlinks (base metal + COSMIC)" rm -rf "$MERGED" cp -al "$BASE" "$MERGED" cp -al --remove-destination "$COSMIC"/. "$MERGED"/ . "$ROOT/scripts/lib/pid1-desde-store.sh" inyectar_pid1 "$MERGED" "$BASE" # El driver HW tiene que estar SÍ O SÍ: si no está, esta imagen no tiene razón de ser y es mejor # fallar acá que descubrirlo con el USB en la mano frente a una pantalla negra. [ -r "$MERGED/usr/lib/dri/iris_dri.so" ] || { echo " ✗ falta iris_dri.so — sin GPU Intel esta imagen no sirve" >&2; exit 1; } echo "==> GL por HARDWARE: $(ls "$MERGED"/usr/lib/dri/ | tr '\n' ' ')" echo "==> inyectando musl (loader + libc) — COSMIC es dinámico, la base metal es estática" MUSL="${MUSL:-/usr/lib/musl/lib/libc.so}" [ -r "$MUSL" ] || { echo "no encuentro musl libc ($MUSL)" >&2; exit 1; } mkdir -p "$MERGED/usr/lib/musl/lib" "$MERGED/lib" cp -a "$MUSL" "$MERGED/usr/lib/musl/lib/libc.so" ln -sf /usr/lib/musl/lib/libc.so "$MERGED/lib/ld-musl-x86_64.so.1" ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.so" ln -sf /usr/lib/musl/lib/libc.so "$MERGED/usr/lib/libc.musl-x86_64.so.1" # ── LA PREPARACIÓN DE SESIÓN QUE ESTA IMAGEN NO TENÍA ─────────────────────────────────────────── # Todo lo que sigue existía sólo en qemu-desktop-image.sh. La imagen de metal se armó copiando el # arranque pero NO esta parte, y el resultado es que el escritorio no podía funcionar en metal por # razones que nada tienen que ver con la GPU: sin `video` no arranca seatd, sin `messagebus` no # arranca el bus de SISTEMA — y sin bus de sistema NO HAY PORTAL, o sea que `portal-probe screencast` # no habría tenido con quién hablar aunque el GL fuese perfecto. Se descubrió corriendo `cosmic-start` # sobre la imagen de metal por primera vez (antes sólo se validaba que llegara al prompt). echo "==> inyectando arje-logind-compat (el login1 del fractal) + su política" if ALC="${ALC:-store/$(./target/release/hammer --store store hash recipes/arje-logind-compat.toml 2>/dev/null | tail -1 | sed 's/^b3://')-arje-logind-compat}"; [ -d "$ALC" ]; then install -Dm755 "$ALC"/usr/bin/arje-logind-compat "$MERGED"/usr/bin/arje-logind-compat install -d "$MERGED/usr/share/dbus-1/system.d" cp -a --remove-destination "$ALC"/usr/share/dbus-1/system.d/. "$MERGED"/usr/share/dbus-1/system.d/ 2>/dev/null || true echo " ✓ $(basename "$ALC" | cut -c1-12)" else echo " ✗ falta arje-logind-compat en store/ — cosmic-comp no hallará sesión" >&2; exit 1 fi [ -f "$MERGED/usr/share/dbus-1/system.d/org.freedesktop.login1.conf" ] \ || { echo " ✗ falta org.freedesktop.login1.conf — re-sellá arje-logind-compat" >&2; exit 1; } echo "==> grupo video (seatd lo exige por NOMBRE) y usuario messagebus (el bus de sistema hace setuid)" if ! grep -q '^video:' "$MERGED/etc/group" 2>/dev/null; then { cat "$MERGED/etc/group" 2>/dev/null; echo "video:x:44:"; } > "$MERGED/etc/group.new" chmod 644 "$MERGED/etc/group.new"; mv "$MERGED/etc/group.new" "$MERGED/etc/group" fi for f in passwd:messagebus:x:81:81:messagebus:/dev/null:/sbin/nologin group:messagebus:x:81:; do file="${f%%:*}"; line="${f#*:}" if ! grep -q '^messagebus:' "$MERGED/etc/$file" 2>/dev/null; then { cat "$MERGED/etc/$file" 2>/dev/null; echo "$line"; } > "$MERGED/etc/$file.new" chmod 644 "$MERGED/etc/$file.new"; mv "$MERGED/etc/$file.new" "$MERGED/etc/$file" fi done for h in usr/libexec/dbus-daemon-launch-helper usr/lib/dbus-1.0/dbus-daemon-launch-helper; do [ -f "$MERGED/$h" ] && { chown 0:81 "$MERGED/$h" 2>/dev/null || true; chmod 4750 "$MERGED/$h"; } done grep -q '^video:' "$MERGED/etc/group" && grep -q '^messagebus:' "$MERGED/etc/passwd" \ || { echo " ✗ video/messagebus no quedaron en /etc — seatd y dbus fallarían" >&2; exit 1; } echo " ✓ video + messagebus" # arje-logind-compat busca el bus de sistema en la ruta LEGACY /var/run y dbus-daemon escucha en /run. # Esta base metal es tan mínima que ni siquiera tiene /var/run: sin el enlace, el daemon se va a idle # y nunca registra login1. ln -sfn /run "$MERGED/var/run" printf 'COSMIC_MODE=%s\n' "${COSMIC_MODE:-bare}" > "$MERGED/etc/cosmic-mode" # ── el arranque del escritorio ────────────────────────────────────────────────────────────────── # Se instala EL MISMO script que corre en QEMU. Eso es deseable —hace que «validado en QEMU» # signifique algo para el metal— pero SÓLO si el script no tiene supuestos del emulador escondidos. # # ⚠ ACÁ HABÍA UN COMENTARIO QUE AFIRMABA QUE NO LOS TENÍA, Y ERA FALSO. El script exportaba # `LIBGL_ALWAYS_SOFTWARE=1` y `MESA_LOADER_DRIVER_OVERRIDE=kms_swrast` incondicionalmente, que es # correcto sobre virtio-gpu y letal sobre un Intel real: la mesa del corpus es `iris` y no existe # ningún `kms_swrast_dri.so` que cargar ⇒ EGL falla y el compositor no arranca. Costó un viaje # físico el 2026-08-05. Ahora el script DETECTA el modo de GL, y esta imagen lo verifica abajo. echo "==> instalando /usr/bin/cosmic-start (detecta HW/software; ya no fuerza kms_swrast)" install -Dm755 scripts/cosmic/cosmic-start.sh "$MERGED/usr/bin/cosmic-start" grep -q "MESA_LOADER_DRIVER_OVERRIDE=kms_swrast$" "$MERGED/usr/bin/cosmic-start" && { echo " ✗ cosmic-start todavía fuerza kms_swrast incondicionalmente — esta imagen sería inútil" >&2; exit 1; } # ── firmware i915 para OTRAS generaciones Intel, no sólo la del laptop ─────────────────────────── # La base metal trae sólo `tgl_*` (TigerLake). En el Coffee Lake del OptiPlex 3060 el kernel pidió # `i915/kbl_dmc_ver1_04.bin` y no estaba. NO es fatal —el propio kernel dice «Disabling runtime power # management» y sigue haciendo modeset— pero es gratis arreglarlo y evita perseguir un fantasma. FWSRC="${FWSRC:-/usr/lib/firmware/i915}" if [ -d "$FWSRC" ]; then n=0 for f in "$FWSRC"/{kbl,skl,cfl,icl,ehl,jsl,adl,dg}*.bin*; do [ -r "$f" ] || continue b="$(basename "$f")" case "$b" in *.zst) zstd -qdf "$f" -o "$MERGED/lib/firmware/i915/${b%.zst}" 2>/dev/null && n=$((n+1)) ;; *) cp -a "$f" "$MERGED/lib/firmware/i915/" && n=$((n+1)) ;; esac done echo "==> firmware i915: +$n blobs de otras generaciones (la base sólo traía tgl_*)" else echo " ⚠ sin $FWSRC — la imagen sólo servirá plenamente en TigerLake" fi # ── SSH: la puerta trasera que convierte un viaje físico en una sesión remota ──────────────────── # sshd ya arrancaba y escuchaba en :22, pero era INALCANZABLE: `PasswordAuthentication no` y sin # /root/.ssh/authorized_keys. O sea que en metal sólo quedaba depurar a ciegas sobre la pantalla. # Con una clave horneada, el próximo fallo se mira por red y se itera sin quemar el USB de nuevo. AKEY="${AKEY:-work/metal-authorized_keys}" if [ -r "$AKEY" ]; then install -Dm600 "$AKEY" "$MERGED/root/.ssh/authorized_keys" chmod 700 "$MERGED/root/.ssh" echo "==> SSH: authorized_keys horneada ($(wc -l < "$AKEY") clave/s) — root por clave, sin password" else echo " ⚠ sin $AKEY ⇒ sshd escuchará pero será inalcanzable (depuración sólo a ciegas)" fi # ── cosmic-diag: UN comando que deja toda la evidencia en disco ───────────────────────────────── # Depurar en metal cuesta un viaje físico por iteración, así que el objetivo es que una corrida # capture TODO de una vez y sin que nadie tenga que acordarse de los pasos. Lo que importa acá y no # se estaba capturando: el `dmesg` DESPUÉS de la sesión. Si cosmic-comp muere por SIGSEGV —que es lo # que indica su log cortado a los 0,4 s, sin panic y con RUST_BACKTRACE=1— el kernel imprime # `cosmic-comp[PID]: segfault at ... in `, y esa biblioteca ES el diagnóstico. cat > "$MERGED/usr/bin/cosmic-diag" <<'DIAG' #!/bin/sh export PATH=/usr/bin:/bin:/usr/sbin:/sbin L=/var/log/cosmic mkdir -p "$L" echo "== cosmic-diag: corriendo la sesión y capturando todo en $L ==" /bin/busybox dmesg > "$L/dmesg-antes.txt" 2>&1 cosmic-start > "$L/run.txt" 2>&1 & # ⚠ NO usar un `sleep` fijo. La 1ª versión esperaba 50s y en el OptiPlex 3060 la sesión tarda ~105s # en llegar al compositor (seatd, bus, logind, pipewire, wireplumber, cada uno con su espera) ⇒ el # dmesg «después» se capturaba ANTES de que cosmic-comp arrancara, salía byte a byte idéntico al de # antes, y el viaje se gastaba sin dato. Ahora se espera al HECHO, no al reloj: que cosmic-comp # aparezca y después desaparezca. Ésa es la ventana donde el kernel diría un segfault. vivo() { for c in /proc/[0-9]*/comm; do [ "$(cat "$c" 2>/dev/null)" = cosmic-comp ] && return 0; done; return 1; } i=0; visto=0 while [ $i -lt 72 ]; do # 72 × 5s = 6 min de techo if vivo; then [ $visto = 0 ] && echo " cosmic-comp vivo (t=$((i*5))s), esperando a que muera o se estabilice" visto=1 elif [ $visto = 1 ]; then echo " cosmic-comp DESAPARECIÓ (t=$((i*5))s) — capturando dmesg" break fi i=$((i + 1)); sleep 5 done [ $visto = 0 ] && echo " ⚠ cosmic-comp nunca apareció en $((i*5))s — capturo igual" sleep 2 /bin/busybox dmesg > "$L/dmesg-despues.txt" 2>&1 /bin/busybox sync echo "== listo. Lo que importa: ==" # OJO con el patrón: `Code: ` (mayúsc/minúsc ignoradas) matchea `microcode: Current revision:` y # reporta un falso positivo que parece un hallazgo. Se ancla a lo que de verdad imprime el kernel # ante una señal, y sólo para el proceso que nos importa. grep -aE "segfault|general protection fault|traps:|Killed process" "$L/dmesg-despues.txt" | tail -5 || true echo "== (si no salió nada arriba, no fue SIGSEGV: cosmic-comp salió por su cuenta) ==" echo "== evidencia completa en $L — apagá y traé el USB ==" DIAG chmod 0755 "$MERGED/usr/bin/cosmic-diag" # ── WiFi: la QCA9377 del OptiPlex, y por qué hace falta un re-probe ────────────────────────────── # `pci 0000:02:00.0: [168c:0042]` = Qualcomm Atheros QCA9377, que va por `ath10k`. El kernel ahora lo # lleva compilado dentro (MODULES está desactivado en esta receta), y ahí aparece un problema de # ORDEN: un driver builtin prueba el dispositivo a los ~5 s y pide su firmware, pero el rootfs recién # se monta a los ~13 s ⇒ `Direct firmware load failed -2` y la tarjeta queda muda para siempre. No hay # módulo que cargar después. La salida es forzar un RE-PROBE del dispositivo PCI cuando /lib/firmware # ya existe: `remove` + `rescan`. Es el mismo tipo de carrera que la del initramfs con el USB. FWATH="${FWATH:-/usr/lib/firmware/ath10k}" if [ -d "$FWATH/QCA9377" ]; then n=0 for f in "$FWATH"/QCA9377/hw1.0/*.bin*; do [ -r "$f" ] || continue b="$(basename "$f")"; d="$MERGED/lib/firmware/ath10k/QCA9377/hw1.0" mkdir -p "$d" case "$b" in *.zst) zstd -qdf "$f" -o "$d/${b%.zst}" 2>/dev/null && n=$((n+1)) ;; *) cp -a "$f" "$d/" && n=$((n+1)) ;; esac done echo "==> firmware ath10k/QCA9377: $n blobs" else echo " ⚠ sin $FWATH/QCA9377 — el WiFi de esa máquina no va a levantar" fi cat > "$MERGED/usr/bin/wifi-up" <<'WIFI' #!/bin/sh # wifi-up — conecta la WiFi y muestra la IP para entrar por SSH. export PATH=/usr/bin:/bin:/usr/sbin:/sbin SSID="$1"; PSK="$2" [ -n "$SSID" ] || { echo "uso: wifi-up "; exit 1; } # Re-probe: ath10k pidió su firmware antes de que existiera /lib/firmware (ver el comentario de la # imagen). Sin esto la tarjeta no aparece como interfaz aunque el driver esté. for d in /sys/bus/pci/devices/*; do [ "$(cat "$d/vendor" 2>/dev/null)" = "0x168c" ] || continue echo "re-probando $(basename "$d") para que ath10k reintente el firmware…" echo 1 > "$d/remove" 2>/dev/null; sleep 2 echo 1 > /sys/bus/pci/rescan 2>/dev/null; sleep 4 done IF="" for i in /sys/class/net/*; do case "$(basename "$i")" in wlan*|wlp*) IF="$(basename "$i")"; break ;; esac done [ -n "$IF" ] || { echo "no apareció interfaz wifi. Mirá: dmesg | grep -i ath10k"; exit 1; } echo "interfaz: $IF" ip link set "$IF" up # ctrl_interface NO es opcional: sin esa línea wpa_supplicant no abre socket de control y `wpa_cli` # no tiene con qué hablar — el síntoma es «wpa_cli no conecta», que parece un fallo de red y es # simplemente que no hay puerta. Y sin wpa_cli no se puede saber si asoció, que es el único dato que # distingue «clave mal» de «DHCP mudo». mkdir -p /run/wpa_supplicant printf 'ctrl_interface=/run/wpa_supplicant\nupdate_config=1\nnetwork={\n\tssid="%s"\n\tpsk="%s"\n}\n' "$SSID" "$PSK" > /tmp/wpa.conf wpa_supplicant -B -i "$IF" -c /tmp/wpa.conf -Dnl80211 || { echo "wpa_supplicant falló"; exit 1; } # Esperar a la ASOCIACIÓN, no al reloj. Con `sleep 6` fijo, udhcpc salía a preguntar antes de que # hubiera enlace y devolvía «sin IPv4», que se lee como «clave equivocada» cuando la clave está bien. # Un 802.11ac tarda más: escaneo de dos bandas + 4-way handshake. i=0 while [ $i -lt 40 ]; do ST=$(wpa_cli -i "$IF" status 2>/dev/null | sed -n 's/^wpa_state=//p') [ "$ST" = COMPLETED ] && { echo "asociado a $SSID (${i}s)"; break; } [ $((i % 5)) -eq 0 ] && echo " esperando asociación… wpa_state=${ST:-?} (${i}s)" i=$((i + 1)); sleep 1 done [ "$ST" = COMPLETED ] || echo " ⚠ no asoció (wpa_state=${ST:-?}). 4WAY_HANDSHAKE=clave; SCANNING=no ve la red" cat > /tmp/udhcpc.script <<'US' #!/bin/sh case "$1" in bound|renew) ifconfig "$interface" "$ip" netmask "${subnet:-255.255.255.0}" [ -n "$router" ] && route add default gw "${router%% *}" dev "$interface" 2>/dev/null [ -n "$dns" ] && { : > /etc/resolv.conf; for d in $dns; do echo "nameserver $d" >> /etc/resolv.conf; done; } ;; esac exit 0 US chmod +x /tmp/udhcpc.script # Tres intentos: el AP a veces no contesta el primer DISCOVER justo tras asociar. for t in 1 2 3; do udhcpc -i "$IF" -n -q -s /tmp/udhcpc.script && break echo " DHCP intento $t sin respuesta, reintento…"; sleep 3 done ip -4 addr show "$IF" | grep -w inet \ || echo "sin IPv4. Mirá 'wpa_cli -i $IF status': si wpa_state=COMPLETED el problema es DHCP, no la clave" echo "== desde el laptop: ssh -i ~/.ssh/hammer-metal root@ ==" WIFI chmod 0755 "$MERGED/usr/bin/wifi-up" # ── el enlace WiFi se caía solo y cortaba la depuración a mitad ───────────────────────────────── # Medido el 2026-08-06 depurando la OptiPlex por SSH sobre un hotspot: la máquina desaparecía de la # red cada pocos minutos. Dos causas que se suman, y ninguna es «la WiFi anda mal»: # - `udhcpc -q` (el que usa wifi-up) pide la dirección UNA vez y termina: nadie renueva el lease. # - el AP olvida a los clientes ociosos y deja de contestar ARP ⇒ «No route to host» desde fuera, # mientras la máquina se cree perfectamente conectada. El síntoma engaña: no hay nada en dmesg. # El ping periódico resuelve las dos a la vez — detecta la caída Y mantiene viva la entrada en el AP. # Sin esto, cada sesión de depuración remota se interrumpe sola y hay que volver físicamente al # teclado, que es justo lo que la red venía a evitar. cat > "$MERGED/usr/bin/wifi-keep" <<'KEEP' #!/bin/sh # wifi-keep — mantiene el enlace vivo y reasocia si se cae. export PATH=/usr/bin:/bin:/usr/sbin:/sbin GW="$1"; SSID="$2"; PSK="$3" [ -n "$PSK" ] || { echo "uso: wifi-keep "; exit 1; } L=/var/log/wifi-keep.log echo "$(date): vigilante arriba (gw=$GW ssid=$SSID)" >> $L while :; do if ping -c1 -W3 "$GW" >/dev/null 2>&1; then sleep 10 else echo "$(date): sin respuesta del gateway — reasociando" >> $L pkill wpa_supplicant 2>/dev/null sleep 2 wifi-up "$SSID" "$PSK" >> $L 2>&1 sleep 10 fi done KEEP chmod 0755 "$MERGED/usr/bin/wifi-keep" # ── getty en tty1: el prompt TIENE que salir por la PANTALLA ──────────────────────────────────── # Lección del 1er viaje físico de KDE, que costó un USB quemado: el CMDLINE horneado es # `console=tty0 console=ttyS0,115200 …` y el kernel hace /dev/console = la ÚLTIMA `console=` ⇒ ttyS0. # La seed card de la base supervisa un solo getty, sobre `console` ⇒ en un metal sin puerto serie el # shell nace INVISIBLE: la pantalla se congela en el último printk y parece que el arranque murió, # cuando está vivo. Nunca se vio en QEMU porque siempre se validaba con `-nographic`, donde el serial # ES la pantalla. # # Se arregla acá y no en el cmdline: un getty sobre tty1 no depende del cmdline (en metal siempre hay # VT) y no obliga a re-sellar el kernel. El getty de `console` se conserva para debug por serie. echo '==> añadiendo getty en tty1 (el prompt de `console` va al SERIAL, invisible en metal)' cat > "$MERGED/usr/bin/console-login" <<'LOGIN' #!/bin/sh # arje-zero lanza los getty con envp VACÍO ⇒ sin PATH no se resuelve ni `cat` ni `cosmic-start`. export PATH=/usr/bin:/bin:/usr/sbin:/sbin export HOME=/root TERM=linux [ -r /etc/motd ] && /bin/busybox cat /etc/motd exec /bin/sh LOGIN chmod 0755 "$MERGED/usr/bin/console-login" # ── netup pedía DHCP antes de que existiera el enlace ──────────────────────────────────────────── # Medido en el dmesg del OptiPlex 3060: el r8169 levanta el enlace a los 20,2 s (con downshift a # 100 Mbps), y el ente sshd corría `netup` a los 13,7 s ⇒ DHCP sobre un cable que aún no estaba y # «sin respuesta DHCP» para siempre, sin reintento. En QEMU no se ve: el virtio-net tiene carrier # desde el instante cero. Sin red no hay SSH, y sin SSH el metal se depura a ciegas. cat > "$MERGED/usr/bin/netup-wait" <<'NETUP' #!/bin/sh export PATH=/usr/bin:/bin:/usr/sbin:/sbin i=0 while [ $i -lt 40 ]; do for c in /sys/class/net/*/carrier; do case "$c" in */lo/*) continue ;; esac # lo siempre dice 1: no es señal de nada [ -r "$c" ] || continue [ "$(cat "$c" 2>/dev/null)" = 1 ] && exec /usr/bin/netup done i=$((i + 1)); sleep 1 done echo "netup-wait: ninguna interfaz con carrier tras ${i}s — pido DHCP igual" exec /usr/bin/netup NETUP chmod 0755 "$MERGED/usr/bin/netup-wait" python3 - "$MERGED/ente/seed.card.json" <<'PY' # OJO: $MERGED es un árbol de HARDLINKS a la base metal ⇒ escribir in-place mutaría también # work/metal-rootfs (y toda otra imagen que comparta el inodo). Se escribe a un temporal y se # os.replace ⇒ se reemplaza la ENTRADA de directorio, el inodo compartido queda intacto. import json, sys, copy, os p = sys.argv[1] card = json.load(open(p)) gen = card["genesis"] gen[:] = [g for g in gen if g["label"] != "tty1-getty"] # idempotente base = next(g for g in gen if g["label"] == "console-getty") tty1 = copy.deepcopy(base) tty1["id"] = "01HQAR53D4M2NBV8KZTYXFQA04" tty1["label"] = "tty1-getty" tty1["payload"]["Native"]["argv"] = ["getty", "-n", "-l", "/usr/bin/console-login", "38400", "tty1"] gen.append(tty1) # el ente sshd espera al carrier antes de pedir DHCP (ver netup-wait); idempotente for g in gen: if g["label"] == "sshd": a = g["payload"]["Native"]["argv"] a[:] = [x.replace("/usr/bin/netup;", "/usr/bin/netup-wait;") for x in a] json.dump(card, open(p + ".new", "w"), indent=2, sort_keys=True) os.replace(p + ".new", p) print(" ✓ genesis: " + ", ".join(g["label"] for g in gen)) PY # `rm -f` antes del heredoc para ROMPER EL HARDLINK: `cat >` escribiría el inodo COMPARTIDO con # work/metal-rootfs y le cambiaría el motd a todas las demás imágenes. rm -f "$MERGED/etc/motd" cat > "$MERGED/etc/motd" <<'MOTD' #-- hammer :: escritorio COSMIC (metal, Intel i915 + iris HW) --------------- GL por HARDWARE (iris). Es la diferencia con la imagen de KDE, y el motivo de este viaje: en QEMU mesa era software y ScreenCast no podía exportar dmabuf. Arrancar el escritorio: cosmic-start ⇒ tiene que decir "GL por HARDWARE". Si dice SOFTWARE, el viaje no sirve: ScreenCast fallaría igual que en QEMU. Si NO levanta: cosmic-diag <-- UN comando, deja todo en /var/log/cosmic/ (incluido el dmesg de después, que es donde el kernel nombra el segfault) Probar el portal: portal-probe version portal-probe screencast <-- LO QUE VINIMOS A VER GPU detectada: ls /dev/dri ; dmesg | grep -iE 'i915|drm' Logs (en DISCO, sobreviven al apagón): /var/log/cosmic/ Red y SSH: netup-wait ; ip addr (root por clave, sin password) WiFi: wifi-up WiFi que no se cae: wifi-keep & (reasocia sola) --------------------------------------------------------------------------- MOTD # ── /var/log/cosmic VACÍO, y no es un detalle ─────────────────────────────────────────────────── # Los logs de la sesión van a disco (para que sobrevivan al apagón en metal), pero eso crea una # trampa: si se valida la imagen arrancándola en QEMU, esa corrida ESCRIBE DENTRO DEL FICHERO DE # IMAGEN, y al quemarla el USB se lleva el log de la prueba. Pasó: el usuario grepeó el log en metal # y leyó `drm=virtio-pci` — mi corrida en QEMU, no la suya. Un log viejo que parece nuevo es peor que # no tener log. # REGLA: validar sobre una COPIA de la imagen, o regenerarla antes de quemar. Esto es el cinturón. rm -rf "$MERGED/var/log/cosmic" install -d -m 755 "$MERGED/var/log/cosmic" mkdir -p "$MERGED/root/.config" "$MERGED/root/.cache" "$MERGED/run/user/0" "$MERGED/tmp" chmod 1777 "$MERGED/tmp" echo "==> rootfs fundido: $(du -sh "$MERGED" | cut -f1)" need_mb=$(du -sm "$MERGED" | cut -f1) ROOT_SIZE="${ROOT_SIZE:-$(( need_mb + 1536 ))}" echo "==> ROOT_SIZE=${ROOT_SIZE} MiB · kernel: $(basename "$(dirname "$(dirname "$KERNEL")")")" export ROOTFS="$MERGED" IMG ROOT_SIZE KERNEL export ESP_MB="${ESP_MB:-160}" STORE_SIZE="${STORE_SIZE:-512}" STATE_SIZE="${STATE_SIZE:-768}" exec ./scripts/install-image-efi.sh