#!/bin/bash # Build a niri/Wayland LIVE desktop ISO (mainline-niri profile). # # Boots directly into a niri session with noctalia-shell as user 'live' # (no password). agetty autologin on tty1 → .bash_profile → dbus-run-session niri. # All themes, wallpapers, and the # void-installer are pre-baked into the squashfs. # # Requires (host): bash, git, curl, docker, and Bibata-Modern-Ice cursor # installed at /usr/share/icons/Bibata-Modern-Ice. # # Usage: # iso/build-niri-live-iso.sh # OUTPUT_ISO=/path/to/output.iso iso/build-niri-live-iso.sh set -Eeuo pipefail PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CACHE_DIR="${CACHE_DIR:-$PROJECT_DIR/cache}" OUT_DIR="${OUT_DIR:-$PROJECT_DIR/out}" BUILD_DIR="${BUILD_DIR:-$PROJECT_DIR/build}" INCLUDE_DIR="$BUILD_DIR/niri-live-includes" MKLIVE_DIR="$CACHE_DIR/void-mklive-niri" # separate clone — avoids race with Cinnamon parallel build MKLIVE_REPO="${MKLIVE_REPO:-https://github.com/void-linux/void-mklive.git}" MKLIVE_REF="${MKLIVE_REF:-master}" PATCH_DIR="$PROJECT_DIR/iso/patches" DOCKER_IMAGE="${DOCKER_IMAGE:-void-installer-builder:latest}" DOCKER="${DOCKER:-docker}" LIVE_USER="${LIVE_USER:-live}" # shellcheck disable=SC1091 source "$PROJECT_DIR/config/install.conf" # Load niri profile settings (KERNEL_PKG, GTK_THEME, CURSOR_THEME, etc.) # shellcheck disable=SC1091 source "$PROJECT_DIR/config/profiles/mainline-niri/profile.conf" command -v "$DOCKER" >/dev/null \ || { echo "ERROR: '$DOCKER' not in PATH"; exit 1; } "$DOCKER" info >/dev/null 2>&1 \ || { echo "ERROR: '$DOCKER' daemon unreachable"; exit 1; } mkdir -p "$CACHE_DIR" "$OUT_DIR" "$BUILD_DIR" # 1) clone + patch mklive (shared with Cinnamon build) if [[ ! -d "$MKLIVE_DIR/.git" ]]; then echo ">>> cloning void-mklive" git clone --depth=1 --branch "$MKLIVE_REF" "$MKLIVE_REPO" "$MKLIVE_DIR" fi if compgen -G "$PATCH_DIR/*.patch" >/dev/null; then echo ">>> resetting + applying iso/patches/" ( cd "$MKLIVE_DIR" && git checkout -- . ) for p in "$PATCH_DIR"/*.patch; do echo " $(basename "$p")" ( cd "$MKLIVE_DIR" && patch -p1 --silent < "$p" ) done fi # 2) xbps-static (shared cache) XBPS_STATIC_DIR="$CACHE_DIR/xbps-static" if [[ ! -x "$XBPS_STATIC_DIR/usr/bin/xbps-install.static" ]]; then echo ">>> downloading xbps-static" mkdir -p "$XBPS_STATIC_DIR" curl -fsSL "https://repo-default.voidlinux.org/static/xbps-static-latest.x86_64-musl.tar.xz" \ | tar xJf - -C "$XBPS_STATIC_DIR" fi # 3) build includes overlay (separate dir from Cinnamon build) echo ">>> staging niri live includes overlay at $INCLUDE_DIR" # The nix store (staged by Docker/root) uses 444/555 permissions — chmod first. chmod -R u+rwX "$INCLUDE_DIR" 2>/dev/null || true rm -rf "$INCLUDE_DIR" mkdir -p "$INCLUDE_DIR" install -d -m 0755 "$INCLUDE_DIR/etc" # ── 3a) greetd config (fallback TUI on tty2) + agetty autologin on tty1 ─ # greetd's initial_session is not used: its PAM session setup fails silently # in the live environment (pam_elogind ENOSYS, missing D-Bus session bus). # Instead we autologin via agetty on tty1 and launch niri from .bash_profile. install -d -m 0755 "$INCLUDE_DIR/etc/greetd" cat > "$INCLUDE_DIR/etc/greetd/config.toml" <<'EOF' [terminal] vt = 2 [default_session] command = "tuigreet --time --greeting 'Void Linux Live (niri)' --cmd 'niri --session'" user = "_greeter" EOF # agetty-tty1 autologin: override conf so agetty-tty1 (mklive's default sv) # automatically logs in the live user on tty1 without racing with a custom sv. install -d -m 0755 "$INCLUDE_DIR/etc/sv/agetty-tty1" cat > "$INCLUDE_DIR/etc/sv/agetty-tty1/conf" < "$INCLUDE_DIR/etc/xbps.d/10-noctalia.conf" <<'EOF' repository=https://universalrepo.r1xelelo.workers.dev/void EOF # ── 3c) live-setup.sh (runs from runit/2 before any service starts) ──── install -d -m 0755 "$INCLUDE_DIR/etc/runit" cat > "$INCLUDE_DIR/etc/runit/live-setup.sh" <<'SV_EOF' #!/bin/sh # Niri live session setup. Runs from /etc/runit/2 before services start. LIVE_USER="${USERNAME:-live}" [ -f /etc/default/live.conf ] && . /etc/default/live.conf LIVE_USER="${LIVE_USER:-live}" # Extra groups (dracut only adds audio,video,wheel) for g in plugdev input network video audio _seatd; do groupadd -f "$g" 2>/dev/null || true usermod -aG "$g" "$LIVE_USER" 2>/dev/null || true done install -d -m 0755 /etc/sudoers.d printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$LIVE_USER" > /etc/sudoers.d/live chmod 0440 /etc/sudoers.d/live # nsswitch: remove mdns (library absent on Void; causes DNS lookup hangs) if [ -f /etc/nsswitch.conf ]; then sed -i '/^hosts:/s/mdns[^ ]* *//g' /etc/nsswitch.conf fi # Fallback DNS (QEMU's 10.0.2.3 is unreliable; NM will overwrite once DHCP settles) if ! grep -q '^nameserver' /etc/resolv.conf 2>/dev/null || \ grep -q '^nameserver 10\.0\.2\.3' /etc/resolv.conf 2>/dev/null; then printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' > /etc/resolv.conf fi # Ensure XDG_RUNTIME_DIR exists for the live user (greetd + elogind handle # this normally, but set it here as a safety net). XDG_RUN="/run/user/$(id -u "$LIVE_USER" 2>/dev/null || echo 1000)" install -d -m 0700 "$XDG_RUN" 2>/dev/null || true chown "$LIVE_USER" "$XDG_RUN" 2>/dev/null || true # Start elogind here, once, before runsvdir brings up greetd. # Runit does NOT supervise it — this avoids cgroup-race restart spam. # We wait until dbus is available (dbus service starts first in runsvdir), # then start elogind as a background daemon. # NOTE: live-setup.sh runs BEFORE runsvdir, so dbus isn't up yet here. # We start elogind from a one-shot at-startup hook instead — see runit/2. echo "niri live-setup: done (user=$LIVE_USER)" SV_EOF chmod 0755 "$INCLUDE_DIR/etc/runit/live-setup.sh" # runit/2: standard Void runit stage 2 — runs live-setup then hands off to runsvdir cat > "$INCLUDE_DIR/etc/runit/2" <<'SV_EOF' #!/bin/sh PATH=/usr/bin:/usr/sbin # Live session setup: groups, sudo, DNS [ -x /etc/runit/live-setup.sh ] && /etc/runit/live-setup.sh # Select runlevel from cmdline (default: default) runlevel=default for arg in $(cat /proc/cmdline); do if [ -d /etc/runit/runsvdir/"$arg" ]; then runlevel="$arg" fi done [ -x /etc/rc.local ] && /etc/rc.local runsvchdir "${runlevel}" mkdir -p /run/runit/runsvdir ln -sf /etc/runit/runsvdir/current /run/runit/runsvdir/current exec env - PATH=$PATH \ runsvdir -P /run/runit/runsvdir/current \ 'log: ...........................................................................................................................................................................................................................................................................................................................................................................................................' SV_EOF chmod 0755 "$INCLUDE_DIR/etc/runit/2" # Enable services for the niri live session install -d -m 0755 "$INCLUDE_DIR/etc/runit/runsvdir/default" # Custom elogind sv: uses correct binary path and waits for dbus socket before # starting (prevents "elogind is already running" spam from rapid runit restarts). install -d -m 0755 "$INCLUDE_DIR/etc/sv/elogind" cat > "$INCLUDE_DIR/etc/sv/elogind/run" <<'EOF' #!/bin/sh exec 2>&1 # Wait for dbus socket — poll every second, up to 30s i=0 while [ $i -lt 30 ] && [ ! -S /run/dbus/system_bus_socket ]; do sleep 1; i=$((i+1)) done exec /usr/libexec/elogind/elogind.wrapper EOF chmod 0755 "$INCLUDE_DIR/etc/sv/elogind/run" # finish: rate-limit restarts to avoid "already running" spam cat > "$INCLUDE_DIR/etc/sv/elogind/finish" <<'EOF' #!/bin/sh sleep 3 EOF chmod 0755 "$INCLUDE_DIR/etc/sv/elogind/finish" for svc in dbus elogind NetworkManager sshd; do ln -sf "/etc/sv/$svc" "$INCLUDE_DIR/etc/runit/runsvdir/default/$svc" done # ── 3d) Wayland environment ───────────────────────────────────────────── install -d -m 0755 "$INCLUDE_DIR/etc/profile.d" cat > "$INCLUDE_DIR/etc/profile.d/wayland.sh" <<'EOF' # Wayland defaults (mainline-niri live session) export QT_QPA_PLATFORM="wayland;xcb" export GDK_BACKEND="wayland,x11" export MOZ_ENABLE_WAYLAND=1 export _JAVA_AWT_WM_NONREPARENTING=1 export XDG_CURRENT_DESKTOP=niri export XDG_SESSION_TYPE=wayland # Use elogind's logind backend — works correctly on Void/runit via audit session IDs export LIBSEAT_BACKEND=logind EOF chmod 0644 "$INCLUDE_DIR/etc/profile.d/wayland.sh" # Nix profile.d: adds ~/.nix-profile/bin to PATH for interactive shells cat > "$INCLUDE_DIR/etc/profile.d/nix-prebaked.sh" <<'EOF' # Pre-baked nix profile — expose nix package binaries if [[ -d "${HOME:-}/.nix-profile/bin" ]]; then case ":$PATH:" in *":$HOME/.nix-profile/bin:"*) ;; *) export PATH="$HOME/.nix-profile/bin:$PATH" ;; esac fi EOF chmod 0644 "$INCLUDE_DIR/etc/profile.d/nix-prebaked.sh" # Nix daemon config (trusted live user so nix commands work without root) install -d -m 0755 "$INCLUDE_DIR/etc/nix" cat > "$INCLUDE_DIR/etc/nix/nix.conf" </dev/null | head -1) && [ -n "$_HOST_PUBKEY" ]; then install -d -m 0700 "$INCLUDE_DIR/etc/skel/.ssh" echo "$_HOST_PUBKEY" > "$INCLUDE_DIR/etc/skel/.ssh/authorized_keys" chmod 0600 "$INCLUDE_DIR/etc/skel/.ssh/authorized_keys" fi KEYMAP_XKB_LAYOUT="${KEYMAP%%-*}" # ch-fr_nodeadkeys → ch KEYMAP_XKB_VARIANT="${KEYMAP#*-}" # ch-fr_nodeadkeys → fr_nodeadkeys KEYMAP_XKB_VARIANT="${KEYMAP_XKB_VARIANT//_nodeadkeys/}" # strip trailing _nodeadkeys install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/niri" cat > "$INCLUDE_DIR/etc/skel/.config/niri/config.kdl" <>> staging niri customizations overlay" OVERLAY="$INCLUDE_DIR/etc/installer-overlay" install -d -m 0755 "$OVERLAY" "$OVERLAY/wallpapers" \ "$OVERLAY/themes" "$OVERLAY/icons" # Wallpapers WP_SRC="${WALLPAPERS_SRC:-$HOME/Scaricati}" shopt -s nullglob for f in "$WP_SRC"/pxfuel*.jpg; do install -m 0644 "$f" "$OVERLAY/wallpapers/$(basename "$f")" done shopt -u nullglob echo " wallpapers: $(ls "$OVERLAY/wallpapers" 2>/dev/null | wc -l) file(s)" # Gruvbox GTK theme (for GTK apps running under niri) THEME_CACHE="$CACHE_DIR/gruvbox-gtk-theme" THEME_BUILD="$CACHE_DIR/gruvbox-gtk-built" if [[ ! -d "$THEME_CACHE/.git" ]]; then git clone --depth=1 https://github.com/Fausto-Korpsvart/Gruvbox-GTK-Theme.git "$THEME_CACHE" || true fi if [[ -x "$THEME_CACHE/themes/install.sh" && ! -d "$THEME_BUILD" ]]; then echo " building gruvbox themes" install -d -m 0755 "$THEME_BUILD" "$DOCKER" run --rm \ -v "$THEME_CACHE":/src \ -v "$THEME_BUILD":/out \ debian:stable-slim sh -c ' export DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null apt-get install -y --no-install-recommends sassc bash >/dev/null cd /src/themes && bash install.sh -d /out -t default -c dark -s standard ' || true fi if [[ -d "$THEME_BUILD" ]]; then for d in "$THEME_BUILD"/Gruvbox-Dark*; do [[ -d "$d" ]] && cp -a "$d" "$OVERLAY/themes/$(basename "$d")" done echo " themes: $(ls "$OVERLAY/themes" 2>/dev/null | wc -l) variant(s)" fi # Gruvbox Plus icons ICON_CACHE="$CACHE_DIR/gruvbox-plus-icons" if [[ ! -d "$ICON_CACHE/.git" ]]; then git clone --depth=1 https://github.com/SylEleuth/gruvbox-plus-icon-pack.git "$ICON_CACHE" || true fi if [[ -d "$ICON_CACHE/Gruvbox-Plus-Dark" ]]; then cp -a "$ICON_CACHE/Gruvbox-Plus-Dark" "$OVERLAY/icons/Gruvbox-Plus-Dark" echo " icons: Gruvbox-Plus-Dark" fi # Bibata cursor BIBATA_SRC="${BIBATA_SRC:-/usr/share/icons/Bibata-Modern-Ice}" if [[ -d "$BIBATA_SRC" ]]; then cp -a "$BIBATA_SRC" "$OVERLAY/icons/Bibata-Modern-Ice" echo " cursor: Bibata-Modern-Ice" fi # Copy wallpapers and assets into usr/share (rootfs overlay) install -d -m 0755 "$INCLUDE_DIR/usr/share/backgrounds/void-installer" cp -a "$OVERLAY/wallpapers"/. "$INCLUDE_DIR/usr/share/backgrounds/void-installer/" 2>/dev/null || true install -d -m 0755 "$INCLUDE_DIR/usr/share/themes" [[ -d "$OVERLAY/themes" ]] && cp -a "$OVERLAY/themes"/. "$INCLUDE_DIR/usr/share/themes/" 2>/dev/null || true install -d -m 0755 "$INCLUDE_DIR/usr/share/icons" [[ -d "$OVERLAY/icons" ]] && cp -a "$OVERLAY/icons"/. "$INCLUDE_DIR/usr/share/icons/" 2>/dev/null || true # ── 3g) GTK settings for Wayland apps ─────────────────────────────────── # Write GTK2/3/4 settings to skel so the live user picks them up. install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/gtk-3.0" install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/gtk-4.0" cat > "$INCLUDE_DIR/etc/skel/.config/gtk-3.0/settings.ini" < "$INCLUDE_DIR/etc/skel/.gtkrc-2.0" < "$INCLUDE_DIR/etc/environment" <<'ENVEOF' XDG_DATA_DIRS=/usr/local/share:/usr/share QT_QPA_PLATFORM=wayland;xcb GDK_BACKEND=wayland,x11 MOZ_ENABLE_WAYLAND=1 LIBSEAT_BACKEND=logind ENVEOF # ── 3h) .bash_profile: source .bashrc + launch niri on tty1 ─────────── # agetty autologin on tty1 runs a login shell; .bash_profile execs niri # via dbus-run-session so it gets a D-Bus session bus. cat > "$INCLUDE_DIR/etc/skel/.bash_profile" <<'EOF' [[ -f ~/.bashrc ]] && . ~/.bashrc if [[ "$(tty)" == /dev/tty1 ]] && [[ -z "$WAYLAND_DISPLAY" ]] && [[ -z "$NIRI_SOCKET" ]]; then exec dbus-run-session -- niri --session fi EOF # ── 3i) Void Linux installer baked in ─────────────────────────────────── install -d -m 0755 "$INCLUDE_DIR/usr/local/lib/void-installer/lib" install -d -m 0755 "$INCLUDE_DIR/usr/local/share/installer" install -m 0755 "$PROJECT_DIR/installer/install.sh" \ "$INCLUDE_DIR/usr/local/lib/void-installer/install.sh" for f in "$PROJECT_DIR/installer/lib/"*.sh; do install -m 0755 "$f" \ "$INCLUDE_DIR/usr/local/lib/void-installer/lib/$(basename "$f")" done install -m 0644 "$PROJECT_DIR/config/install.conf" \ "$INCLUDE_DIR/usr/local/share/installer/install.conf" install -m 0644 "$PROJECT_DIR/config/profiles/mainline-niri/packages.list" \ "$INCLUDE_DIR/usr/local/share/installer/packages.list" if [[ -d "$PROJECT_DIR/config/profiles" ]]; then cp -r "$PROJECT_DIR/config/profiles" \ "$INCLUDE_DIR/usr/local/share/installer/profiles" fi install -d -m 0755 "$INCLUDE_DIR/usr/local/bin" cat > "$INCLUDE_DIR/usr/local/bin/void-install" <<'WRAPEOF' #!/bin/sh exec /usr/local/lib/void-installer/install.sh "$@" WRAPEOF chmod 0755 "$INCLUDE_DIR/usr/local/bin/void-install" _SECRETS_SRC="${SECRETS_ENV:-$PROJECT_DIR/secrets.env}" if [[ -r "$_SECRETS_SRC" ]]; then install -d -m 0755 "$INCLUDE_DIR/etc" install -m 0600 "$_SECRETS_SRC" "$INCLUDE_DIR/etc/installer-secrets.env" echo " baked installer secrets from $_SECRETS_SRC" else echo " WARNING: no secrets.env found — installer will prompt at runtime" fi install -d -m 0755 "$INCLUDE_DIR/usr/share/applications" cat > "$INCLUDE_DIR/usr/share/applications/void-installer.desktop" <<'DESKEOF' [Desktop Entry] Version=1.0 Type=Application Name=Install Void Linux Comment=Install Void Linux to this machine Exec=alacritty --title "Void Linux Installer" -e sudo /usr/local/bin/void-install Icon=system-software-install Terminal=false Categories=System; StartupNotify=true DESKEOF # 4) build Docker image (reuse the same image as Cinnamon build) echo ">>> building docker image $DOCKER_IMAGE" if "$DOCKER" buildx version >/dev/null 2>&1; then "$DOCKER" build -t "$DOCKER_IMAGE" "$PROJECT_DIR/iso" else DOCKER_BUILDKIT=0 "$DOCKER" build -t "$DOCKER_IMAGE" "$PROJECT_DIR/iso" fi # 5) packages + output filename ISO_PKGS=$(grep -vE '^\s*(#|$)' \ "$PROJECT_DIR/config/profiles/mainline-niri/packages.live-desktop.list" \ | tr '\n' ' ') TS="$(date -u +%Y%m%d)" OUT_ISO="${OUTPUT_ISO:-$OUT_DIR/void-live-niri-${TS}.iso}" BOOT_CMDLINE="${BOOT_CMDLINE:-live.user=${LIVE_USER} console=tty0 console=ttyS0,115200}" echo ">>> running mklive.sh inside docker — output: $OUT_ISO" "$DOCKER" run --rm --privileged \ -v "$PROJECT_DIR:/work:rw" \ -v "$CACHE_DIR:/cache:rw" \ -e ARCH="${ARCH:-x86_64}" \ -e REPO_URL="${REPO_URL:-https://repo-default.voidlinux.org/current}" \ -e KEYMAP="${KEYMAP:-us}" \ -e LOCALE="${LOCALE:-en_US.UTF-8}" \ -e ISO_PKGS="$ISO_PKGS" \ -e ISO_TITLE="Void Live (niri / noctalia-shell)" \ -e OUT_ISO_REL="${OUT_ISO#$PROJECT_DIR/}" \ -e BOOT_CMDLINE="${BOOT_CMDLINE:-}" \ -e INCLUDE_DIR_REL="${INCLUDE_DIR#$PROJECT_DIR/}" \ -e NIX_PACKAGES_PREBAKE="${NIX_USER_PACKAGES[*]}" \ -e HOST_UID="$(id -u)" \ -e HOST_GID="$(id -g)" \ "$DOCKER_IMAGE" \ bash /work/iso/_inner-build-niri-live.sh echo echo ">>> Niri live ISO built: $OUT_ISO" sha256sum "$OUT_ISO" | tee "${OUT_ISO}.sha256" || true