#!/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/00-void-repos.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 bluetooth; 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 # Fix sound device permissions — PCM/control nodes are created root:root during # early boot before the audio group is guaranteed to exist. Re-apply the correct # ownership now that the group is present, then trigger udev to re-evaluate the # sound subsystem so the persistent 70-sound-perms.rules rule is applied too. chown root:audio /dev/snd/pcmC* /dev/snd/controlC* /dev/snd/hwC* 2>/dev/null || true chmod 660 /dev/snd/pcmC* /dev/snd/controlC* /dev/snd/hwC* 2>/dev/null || true udevadm trigger --subsystem-match=sound 2>/dev/null || true # Timezone ln -sf /usr/share/zoneinfo/Europe/Zurich /etc/localtime 2>/dev/null || true 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 # Helper: is elogind's login1 D-Bus name already registered on the system bus? _login1_on_dbus() { dbus-send --system --print-reply --dest=org.freedesktop.DBus \ /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner \ string:org.freedesktop.login1 >/dev/null 2>&1 } # Helper: is elogind process alive via its PID file? _elogind_alive() { for _pf in /run/elogind.pid /run/elogind/elogind.pid; do [ -f "$_pf" ] && kill -0 "$(cat "$_pf" 2>/dev/null)" 2>/dev/null && return 0 done return 1 } # If elogind is already running (process alive or D-Bus name taken), yield # permanently so runit does not spam restarts. if _elogind_alive || _login1_on_dbus; then echo "elogind-sv: already running — yielding to avoid restart spam" exec tail -f /dev/null fi # 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; if the run was very short (elogind crashed or # reported "already running" before our detection), back off longer. cat > "$INCLUDE_DIR/etc/sv/elogind/finish" <<'EOF' #!/bin/sh # $1=exitcode, $2=signal (or -1) exitcode="$1" # Non-zero exit with no signal = elogind bailed out (e.g. "already running"). # Sleep longer to avoid log spam; next run will yield via the D-Bus/PID check. if [ "$exitcode" != "0" ] && [ "$2" = "-1" ]; then sleep 10 else sleep 3 fi EOF chmod 0755 "$INCLUDE_DIR/etc/sv/elogind/finish" for svc in dbus elogind NetworkManager bluetoothd 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 # Force GTK apps (and Electron/VSCode) to use XDG portal file dialogs export GTK_USE_PORTAL=1 export ELECTRON_OZONE_PLATFORM_HINT=auto 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 # Expose nix .desktop files and icons to XDG-compliant launchers/shells if [[ -d "${HOME:-}/.nix-profile/share" ]]; then case ":${XDG_DATA_DIRS:-}:" in *":$HOME/.nix-profile/share:"*) ;; *) export XDG_DATA_DIRS="$HOME/.nix-profile/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}" ;; esac fi export NIXPKGS_ALLOW_UNFREE=1 # Pre-baked nix is single-user (no daemon) — bypass daemon connection attempt export NIX_REMOTE=local # Flake commands ignore NIXPKGS_ALLOW_UNFREE unless --impure is passed. # Wrap nix so interactive installs work without extra flags. nix() { command nix "$@" --impure; } export -f nix 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" < "$INCLUDE_DIR/etc/skel/.config/nixpkgs/config.nix" # git: GUI askpass so prompts work without a controlling terminal install -d -m 0755 "$INCLUDE_DIR/usr/local/bin" cat > "$INCLUDE_DIR/usr/local/bin/git-askpass" <<'EOF' #!/bin/sh for cmd in zenity qarma; do command -v "$cmd" >/dev/null 2>&1 || continue case "$1" in *[Uu]sername*) exec "$cmd" --entry --title="Git Credentials" --text="$1" ;; *) exec "$cmd" --password --title="Git Credentials" --text="$1" ;; esac done printf '%s' "$1" >&2; read -r answer; printf '%s\n' "$answer" EOF chmod 0755 "$INCLUDE_DIR/usr/local/bin/git-askpass" cat > "$INCLUDE_DIR/etc/gitconfig" <<'EOF' [core] askPass = /usr/local/bin/git-askpass EOF # ── 3e) niri config.kdl in /etc/skel ─────────────────────────────────── # dracut's adduser.sh copies skel → /home/live, so the live user gets a # ready niri config without any first-boot setup step. # Pre-bake SSH authorized_keys from host so passwordless SSH just works in QEMU tests. if _HOST_PUBKEY=$(ssh-add -L 2>/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" </dev/null 2>&1; do sleep 1; i=\$((i+1)); done; exec quickshell -c noctalia-shell" // First-login setup: installs Claude Code (and NVM) once, then closes spawn-at-startup "sh" "-c" "[ -f ~/.first-login-done ] || alacritty -T 'Void Setup' -e /usr/local/libexec/first-login.sh" binds { Mod+T { spawn "alacritty"; } Mod+D { spawn "sh" "-c" "quickshell msg -c noctalia-shell launcher toggle"; } Mod+Q { close-window; } Mod+Shift+E { quit; } Print { screenshot; } Mod+H { focus-column-left; } Mod+L { focus-column-right; } Mod+J { focus-window-down; } Mod+K { focus-window-up; } Mod+Shift+H { move-column-left; } Mod+Shift+L { move-column-right; } Mod+1 { focus-workspace 1; } Mod+2 { focus-workspace 2; } Mod+3 { focus-workspace 3; } Mod+4 { focus-workspace 4; } Mod+Shift+1 { move-column-to-workspace 1; } Mod+Shift+2 { move-column-to-workspace 2; } XF86AudioRaiseVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%+"; } XF86AudioLowerVolume { spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%-"; } XF86AudioMute { spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; } XF86MonBrightnessUp { spawn "brightnessctl" "set" "+5%"; } XF86MonBrightnessDown { spawn "brightnessctl" "set" "5%-"; } } EOF # ── 3f) Themes / icons / wallpapers overlay ───────────────────────────── echo ">>> 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 # first-login.sh — deployed by _deploy_first_login() to the installed system. [[ -r "$PROJECT_DIR/installer/first-login.sh" ]] && \ install -m 0755 "$PROJECT_DIR/installer/first-login.sh" "$OVERLAY/first-login.sh" # Claude Code config + auth tokens from host (deployed to the installed system). CLAUDE_SRC="${CLAUDE_SRC:-$HOME/.claude}" [[ -d "$CLAUDE_SRC" ]] && { cp -a "$CLAUDE_SRC" "$OVERLAY/claude"; echo " claude: ~/.claude bundled"; } [[ -r "${HOME}/.claude.json" ]] && install -m 0600 "${HOME}/.claude.json" "$OVERLAY/claude.json" # VS Code user config + extension list from host. VSCODE_SRC="${VSCODE_USER_SRC:-$HOME/.config/Code/User}" install -d -m 0755 "$OVERLAY/vscode-user" if [[ -d "$VSCODE_SRC" ]]; then for f in settings.json keybindings.json; do [[ -r "$VSCODE_SRC/$f" ]] && install -m 0644 "$VSCODE_SRC/$f" "$OVERLAY/vscode-user/$f" done [[ -d "$VSCODE_SRC/snippets" ]] && cp -a "$VSCODE_SRC/snippets" "$OVERLAY/vscode-user/snippets" command -v code >/dev/null 2>&1 && \ code --list-extensions > "$OVERLAY/vscode-extensions.txt" 2>/dev/null || true echo " vscode-user: staged from $VSCODE_SRC" 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 + dark theme dconf ──────────────────────────────── # 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" # dconf system keyfile: ensures GTK dark theme is reported to all apps via # xdg-desktop-portal-gtk regardless of whether the user has a dconf DB yet. install -d -m 0755 "$INCLUDE_DIR/etc/dconf/db/local.d" install -d -m 0755 "$INCLUDE_DIR/etc/dconf/profile" cat > "$INCLUDE_DIR/etc/dconf/db/local.d/01-dark-theme" <<'EOF' [org/gnome/desktop/interface] color-scheme='prefer-dark' gtk-theme='${GTK_THEME}' icon-theme='${ICON_THEME}' cursor-theme='${CURSOR_THEME:-Bibata-Modern-Ice}' cursor-size=24 EOF sed -i "s/'\${GTK_THEME}'/'${GTK_THEME}'/; s/'\${ICON_THEME}'/'${ICON_THEME}'/; s/'\${CURSOR_THEME:-Bibata-Modern-Ice}'/'${CURSOR_THEME:-Bibata-Modern-Ice}'/" \ "$INCLUDE_DIR/etc/dconf/db/local.d/01-dark-theme" printf 'user-db:user\nsystem-db:local\n' > "$INCLUDE_DIR/etc/dconf/profile/user" cat > "$INCLUDE_DIR/etc/skel/.config/gtk-3.0/settings.ini" < "$INCLUDE_DIR/etc/skel/.gtkrc-2.0" < "$INCLUDE_DIR/etc/environment" < "$INCLUDE_DIR/etc/skel/.config/noctalia/settings.json" < "$INCLUDE_DIR/etc/xdg/mimeapps.list" <<'EOF' [Default Applications] text/html=google-chrome.desktop x-scheme-handler/http=google-chrome.desktop x-scheme-handler/https=google-chrome.desktop x-scheme-handler/about=google-chrome.desktop x-scheme-handler/unknown=google-chrome.desktop application/pdf=google-chrome.desktop application/xhtml+xml=google-chrome.desktop application/xml=google-chrome.desktop EOF install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config" cp "$INCLUDE_DIR/etc/xdg/mimeapps.list" "$INCLUDE_DIR/etc/skel/.config/mimeapps.list" # ── Portal backend configuration for niri ───────────────────────────────── # Without this, xdg-desktop-portal doesn't know which backend to use for # XDG_CURRENT_DESKTOP=niri, causing file-picker / portal calls to fail. install -d -m 0755 "$INCLUDE_DIR/etc/xdg/xdg-desktop-portal" cat > "$INCLUDE_DIR/etc/xdg/xdg-desktop-portal/niri-portals.conf" <<'EOF' [preferred] default=gtk org.freedesktop.impl.portal.FileChooser=gtk org.freedesktop.impl.portal.AppChooser=gtk org.freedesktop.impl.portal.OpenURI=gtk org.freedesktop.impl.portal.Print=gtk org.freedesktop.impl.portal.Screenshot=gtk org.freedesktop.impl.portal.Inhibit=gtk org.freedesktop.impl.portal.Notification=gtk org.freedesktop.impl.portal.Settings=gtk EOF # ── Sound device udev rules ──────────────────────────────────────── # PCM/control nodes are created root:root at early boot before the audio # group is provisioned; this rule ensures correct ownership on every boot. install -d -m 0755 "$INCLUDE_DIR/etc/udev/rules.d" cat > "$INCLUDE_DIR/etc/udev/rules.d/70-sound-perms.rules" <<'EOF' # Allow the audio group to access ALSA PCM and control devices. SUBSYSTEM=="sound", GROUP="audio", MODE="0660" EOF # ── Timezone ────────────────────────────────────────────────────────── # Create the /etc/localtime symlink and rc.conf TIMEZONE setting so the live # session starts in the correct timezone without requiring a user step. ln -sf /usr/share/zoneinfo/Europe/Zurich "$INCLUDE_DIR/etc/localtime" cat > "$INCLUDE_DIR/etc/rc.conf" <<'RCEOF' KEYMAP="ch" HARDWARECLOCK="UTC" TIMEZONE="Europe/Zurich" RCEOF # ── NVIDIA PRIME overlay ──────────────────────────────────────────────── echo ">>> staging NVIDIA PRIME overlay" # Blacklist nouveau — prevents the open-source driver from grabbing the GPU # before the proprietary nvidia driver. install -d -m 0755 "$INCLUDE_DIR/etc/modprobe.d" cat > "$INCLUDE_DIR/etc/modprobe.d/blacklist-nouveau.conf" <<'EOF' blacklist nouveau options nouveau modeset=0 EOF # Intel BT on this platform suffers firmware download failures when USB # autosuspend is active for the btusb adapter. cat > "$INCLUDE_DIR/etc/modprobe.d/btusb-quirks.conf" <<'EOF' options btusb enable_autosuspend=0 EOF # Load nvidia modules early so the DRM device node exists before niri starts. install -d -m 0755 "$INCLUDE_DIR/etc/modules-load.d" cat > "$INCLUDE_DIR/etc/modules-load.d/nvidia.conf" <<'EOF' nvidia nvidia_modeset nvidia_uvm nvidia_drm EOF # dracut: include nvidia modules in initramfs; omit nouveau. install -d -m 0755 "$INCLUDE_DIR/etc/dracut.conf.d" cat > "$INCLUDE_DIR/etc/dracut.conf.d/10-nvidia.conf" <<'EOF' add_drivers+=" nvidia nvidia_modeset nvidia_uvm nvidia_drm " omit_drivers+=" nouveau " EOF # Xorg output-class configs: intel=modesetting (primary), nvidia=PRIME offload. # Needed by xwayland-satellite for X11 clients running under niri. install -d -m 0755 "$INCLUDE_DIR/etc/X11/xorg.conf.d" cat > "$INCLUDE_DIR/etc/X11/xorg.conf.d/10-intel.conf" <<'EOF' Section "OutputClass" Identifier "intel" MatchDriver "i915" Driver "modesetting" EndSection EOF cat > "$INCLUDE_DIR/etc/X11/xorg.conf.d/20-nvidia.conf" <<'EOF' Section "OutputClass" Identifier "nvidia" MatchDriver "nvidia-drm" Driver "nvidia" Option "AllowEmptyInitialConfiguration" Option "PrimaryGPU" "no" ModulePath "/usr/lib/nvidia/xorg" ModulePath "/usr/lib/xorg/modules" EndSection EOF # prime-run: launch any app on the NVIDIA dGPU via PRIME render-offload. cat > "$INCLUDE_DIR/usr/local/bin/prime-run" <<'EOF' #!/bin/sh # Run a program on the NVIDIA dGPU via PRIME render offload. exec env __NV_PRIME_RENDER_OFFLOAD=1 \ __VK_LAYER_NV_optimus=NVIDIA_only \ __GLX_VENDOR_LIBRARY_NAME=nvidia \ "$@" EOF chmod 0755 "$INCLUDE_DIR/usr/local/bin/prime-run" # ── 3g2) niri-session wrapper ───────────────────────────────────────────── # greetd/tuigreet starts niri-session (not niri --session directly) so that # /etc/profile is sourced first, ensuring /etc/profile.d/* scripts run and # XDG_DATA_DIRS gets ~/.nix-profile/share prepended for the compositor and # all apps it spawns (noctalia-shell, fuzzel, etc.). install -d -m 0755 "$INCLUDE_DIR/usr/local/bin" cat > "$INCLUDE_DIR/usr/local/bin/niri-session" <<'EOF' #!/bin/bash # niri-session — wrapper started by greetd/tuigreet. # Sources /etc/profile so that all /etc/profile.d/* scripts run # (nix paths, wayland env, XDG_DATA_DIRS with ~/.nix-profile/share, etc.) # before handing off to the compositor. [ -f /etc/profile ] && . /etc/profile exec niri --session "$@" EOF chmod 0755 "$INCLUDE_DIR/usr/local/bin/niri-session" # ── 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 # Wrap niri startup: start GNOME Keyring daemon first (inside the D-Bus session) # so Chrome/VSCode/apps can store secrets via org.freedesktop.secrets. exec dbus-run-session -- sh -c ' if command -v gnome-keyring-daemon >/dev/null 2>&1; then eval "$(gnome-keyring-daemon --start --components=secrets,pkcs11 2>/dev/null)" || true export GNOME_KEYRING_CONTROL GNOME_KEYRING_PID SSH_AUTH_SOCK fi exec 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" # first-login.sh at /usr/local/libexec: available in the live session and # deployed by _deploy_first_login() to the installed system via the overlay. install -d -m 0755 "$INCLUDE_DIR/usr/local/libexec" install -m 0755 "$PROJECT_DIR/installer/first-login.sh" \ "$INCLUDE_DIR/usr/local/libexec/first-login.sh" # nix-packages.list: tells first-login.sh which nix packages to install. # In the live session these are already prebaked; in the installed system the # first-boot-nix runit service handles them, so this is informational only. { printf '# Nix user packages\n' printf '%s\n' "${NIX_USER_PACKAGES[@]}" } > "$INCLUDE_DIR/usr/local/libexec/nix-packages.list" _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 notes (kernel audio, GPU, Bluetooth, etc.): # • NO forced audio kernel parameters: Removed snd-intel-dspcfg.dsp_driver=1 and snd_hda_intel.dmic_detect=0 # because they forced legacy HDA mode, blocking SOF/SoundWire on hardware that needs it (e.g., XPS 9700). # The kernel auto-selects the correct driver (0=auto, 3=SOF, 4=AVS) based on hardware IDs. # Users can override at boot with "snd-intel-dspcfg.dsp_driver=3" if needed. # See docs/KERNEL7_AUDIO_XPS9700.md for diagnostics. # • nvidia-drm.modeset=1: Required for nvidia GPU to use atomic mode-setting on Wayland (Niri). # • blacklist nouveau: nvidia GPU needs proprietary driver, not nouveau. # • btusb.enable_autosuspend=0: Prevent Bluetooth headset dropouts when idle. BOOT_CMDLINE="${BOOT_CMDLINE:-live.user=${LIVE_USER} console=tty0 console=ttyS0,115200 nvidia-drm.modeset=1 rd.driver.blacklist=nouveau modprobe.blacklist=nouveau btusb.enable_autosuspend=0}" 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