- first-login.sh: remove nix-env --switch-profile (caused .nix-profile-> .nix-profile circular symlink, breaking all nix profile commands and causing ELOOP on any exec via nix PATH including xz/tar/node) - first-login.sh: add circular symlink guard before nix profile add - first-login.sh: nix profile install -> nix profile add (deprecated alias) - live-setup.sh: strip mdns from nsswitch.conf hosts line at boot (no libnss_mdns/Avahi in live; caused first-login DNS hang) - docs/LIVE_ISO.md: document all three issues and their fixes
9.8 KiB
Live ISO Build — Findings & Architecture Notes
Overview
The live ISO boots directly into a Cinnamon desktop session as user live with no password prompt. It is designed for hardware testing on XPS 9700 and serves as the installer delivery vehicle.
Builder: iso/build-live-iso.sh (host) → Docker container running iso/_inner-build-live.sh → void-mklive/mklive.sh
Boot + Session Startup
Kernel Cmdline
live.user=live console=tty0 console=ttyS0,115200
The live.user=live parameter is consumed by the vmklive dracut hook (adduser.sh) which creates the user inside the initramfs and sets password voidlinux.
runit Stage 2 Override
We override /etc/runit/2 to run /etc/runit/live-setup.sh before handing off to runsvdir. The script:
- Adds extra groups (
plugdev input network docker) to the live user - Writes
/etc/sudoers.d/live(full passwordless sudo) - Configures
/etc/nix/nix.conf(daemon mode,trusted-users = root live) - Auto-detects GPU and writes
/etc/X11/xorg.conf.d/20-gpu.conf
After live-setup.sh, stage 2 mirrors the real runit-void exactly:
runsvchdir "${runlevel}"
ln -sf /etc/runit/runsvdir/current /run/runit/runsvdir/current
exec runsvdir -P /run/runit/runsvdir/current
Services (runsvdir/default symlinks in overlay)
Enabled at build time via symlinks in build/live-includes/etc/runit/runsvdir/default/:
dbusNetworkManagerlightdmnix-daemon
Note: Do NOT use mklive.sh's
-Sflag for service enable — it is not supported by the version used. Services must be wired via runsvdir symlinks in the include overlay.
LightDM Autologin
Critical: lightdm-session does not exist on Void Linux
The Void lightdm 1.32 package does not ship the lightdm-session binary. The default LightDM behaviour of spawning lightdm-session causes the session to crash immediately (exit code 1 in ~20ms) with no error message.
Fix: Set session-wrapper=/etc/lightdm/Xsession in lightdm.conf. The /etc/lightdm/Xsession wrapper is provided by the Void lightdm package and correctly sources /etc/profile → /etc/profile.d/.
greeter-env= and session-env= are not supported
These options are silently ignored in LightDM 1.32 on Void. To propagate environment variables to the session use /etc/profile.d/ scripts instead.
lightdm.conf autologin lines must be commented
The vmklive dracut hook display-manager-autologin.sh uses sed to uncomment lines. The autologin lines in lightdm.conf must be present but commented out — the hook finds them by regex and uncomments them at boot.
[Seat:*]
#autologin-user=
#autologin-user-timeout=0
#autologin-session=
#user-session=
session-wrapper=/etc/lightdm/Xsession
greeter-session=lightdm-gtk-greeter
The /etc/lightdm/.session file (content: cinnamon) is read by the hook to set the session name.
GPU Auto-Detection
live-setup.sh runs lspci at boot and writes /etc/X11/xorg.conf.d/20-gpu.conf:
| Detected | Xorg Config | Extra |
|---|---|---|
| Virtual (virtio/VMware/QEMU/VirtualBox) | modesetting, AccelMethod none |
LIBGL_ALWAYS_SOFTWARE=1 in /etc/profile.d/live-env.sh |
NVIDIA + proprietary driver (nvidia_drv.so) |
PRIME offload: Intel modesetting + NVIDIA nvidia |
No software GL |
| NVIDIA without proprietary driver | modesetting |
— |
| Intel / AMD / other | modesetting |
— |
LIBGL_ALWAYS_SOFTWARE=1 is set via /etc/profile.d/live-env.sh, not via session-env= (unsupported).
Nix Integration
Daemon mode (not single-user)
The Void nix xbps package ships nix-daemon with a runit service at /etc/sv/nix-daemon. The daemon puts its socket at:
/var/nix/daemon-socket/socket
We use daemon mode (not single-user) because /nix/store stays root-owned. The live user is granted trust via nix.conf:
experimental-features = nix-command flakes
sandbox = false
auto-optimise-store = true
trusted-users = root live
sandbox = false is required because the live system has no nixbld users and no user namespaces in the dracut initramfs environment.
Package list
/usr/local/libexec/nix-packages.list is written at ISO build time from NIX_USER_PACKAGES in config/install.conf. At first login, first-login.sh reads this file and runs nix profile install --impure with NIXPKGS_ALLOW_UNFREE=1.
Current packages:
nixpkgs#google-chromenixpkgs#spotifynixpkgs#discordnixpkgs#localsendnixpkgs#mission-center
postinstall.sh socket path (installed system)
In the installed system (not live), installer/lib/postinstall.sh polls for the nix-daemon socket. The correct path is:
/var/nix/daemon-socket/socket
Not /nix/var/nix/daemon-socket/socket (upstream Nix default) — Void's package uses /var/nix/.
dconf / Theme
The Gruvbox-Dark GTK theme and Cinnamon dconf settings are pre-applied via a system-db. The dconf binary database must be compiled at ISO build time, not at runtime.
Build-time compilation
iso/_inner-build-live.sh runs inside the Debian Docker container. The Dockerfile installs dconf-cli for this step. The correct Debian dconf-cli API is:
dconf compile <output_binary_db> <input_keyfile_dir>
# e.g.:
dconf compile build/live-includes/etc/dconf/db/local \
build/live-includes/etc/dconf/db/local.d
Note:
dconf update <path>does not work in Debian'sdconf-cli— it only updates the user's own db.dconf compileis the correct tool for building a system-db binary.
dconf profile
/etc/dconf/profile/user must point to the system-db:
user-db:user
system-db:local
Without this file, the compiled system-db is ignored and Cinnamon shows a black wallpaper with default GTK theme.
First-Login Setup (installer/first-login.sh)
Runs once via XDG autostart (~/.config/autostart/void-live-first-login.desktop) when Cinnamon first loads. Installs:
- Claude Code — official installer from
https://claude.ai/install.sh - Nix user packages — from
/usr/local/libexec/nix-packages.list - NVM + Node LTS
- VS Code extensions — from
/etc/installer-vscode-extensions.txt
Idempotent: creates ~/.first-login-done on success. Logs to ~/.first-login.log.
The script does NOT use set -u because nvm.sh references unbound variables.
Build Pipeline
iso/build-live-iso.sh (host — stages overlay, builds Docker image if needed)
└─ Docker: void-installer-builder:latest
└─ iso/_inner-build-live.sh
├─ dconf compile (pre-bakes system-db)
└─ void-mklive/mklive.sh -a x86_64 -r <repo> -I <include_dir> ...
└─ squashfs + GRUB + ISO 9660
Output: out/void-live-stable.iso (~2.9 GB)
Build artifacts that must NOT be committed
build/live-includes/— generated staging tree (hundreds of binary assets)out/— ISO outputcache/— cloned void-mklive, xbps package cache
Known Issues & Fixes
nix-env --switch-profile "$HOME/.nix-profile" creates a circular symlink
Symptom: error: filesystem error: status: Too many levels of symbolic links [/home/live/.nix-profile/manifest.json] and tar: xz: Cannot exec: Too many levels of symbolic links (all binaries fail to exec via nix PATH).
Cause: Passing $HOME/.nix-profile as the target to nix-env --switch-profile creates ~/.nix-profile -> .nix-profile — a symlink that points to itself. This corrupts the nix profile directory and causes ELOOP on any file lookup under that path.
Fix: Do not call nix-env --switch-profile at all when using nix profile add (new-style commands). Let nix profile add initialise the profile automatically. The first-login script also contains a guard that detects and removes the circular symlink before proceeding.
nix profile install is deprecated
Use nix profile add instead. nix profile install is an alias that emits a warning and will be removed in a future Nix version.
DNS hang in live environment (nsswitch mdns without Avahi)
Symptom: getent hosts github.com hangs indefinitely; first-login.sh stuck at "starting".
Cause: /etc/nsswitch.conf includes mdns in the hosts: line. On Void Linux, libnss_mdns.so.2 may not be present, and even if it is, the Avahi daemon is not running in the live session. glibc waits for Avahi's D-Bus socket before timing out.
Fix: live-setup.sh runs at boot and removes mdns from nsswitch.conf: sed -i '/^hosts:/s/mdns[^ ]* *//g' /etc/nsswitch.conf. This is safe on real hardware (NetworkManager provides proper DNS via DHCP).
QEMU internal DNS (10.0.2.3) unreliable
Symptom: Even after removing mdns, DNS queries to QEMU's built-in resolver (10.0.2.3) time out.
Cause: QEMU's user-mode DNS proxy may not forward queries correctly depending on the host network configuration.
Workaround for QEMU testing: echo nameserver 8.8.8.8 > /etc/resolv.conf. This is not needed on real hardware.
cp /usr/share/OVMF/OVMF_VARS.fd out/OVMF_VARS.live.fd
qemu-system-x86_64 -name void-live-test -machine q35,accel=kvm:tcg -cpu max \
-m 4096 -smp 4 \
-drive "if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE.fd" \
-drive "if=pflash,format=raw,file=out/OVMF_VARS.live.fd" \
-cdrom out/void-live-stable.iso -boot order=d,menu=off \
-netdev user,id=n0 -device virtio-net-pci,netdev=n0 \
-serial "unix:out/live-serial.sock,server,nowait" \
-monitor "unix:out/qemu-monitor.sock,server,nowait" \
-device virtio-vga -display gtk,gl=off &
Serial console access (root shell for diagnostics):
import socket, time
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect('out/live-serial.sock')
# send commands, read output
GPU in QEMU: virtio-vga is detected as virtual → modesetting + LIBGL_ALWAYS_SOFTWARE=1.