#!/bin/bash # Unified ISO build script. # # Usage: # iso/build.sh [--profile PROFILE] [--type installer|live] # iso/build.sh -p PROFILE -t TYPE # # PROFILE: stable-cinnamon | stable-niri | mainline-cinnamon | mainline-niri # TYPE: installer — small auto-installing ISO (~600 MB), boots directly to # the installer; used for unattended / scripted deploys. # live — full desktop ISO with the installer also embedded; # boots to the selected DE so you can test before installing. # # The live ISO pre-configures the embedded installer to default to the SAME # PROFILE it was built with, so running 'void-install' from the live session # will install the exact same configuration you are running. # # Requires (host): bash, git, curl, docker. set -Eeuo pipefail # ── argument parsing ────────────────────────────────────────────────────── PROFILE="${PROFILE:-stable-cinnamon}" BUILD_TYPE="${BUILD_TYPE:-live}" while [[ $# -gt 0 ]]; do case $1 in --profile|-p) PROFILE="$2"; shift 2 ;; --type|-t) BUILD_TYPE="$2"; shift 2 ;; *) echo "Unknown argument: $1"; exit 1 ;; esac done case "$PROFILE" in stable-cinnamon|stable-niri|mainline-cinnamon|mainline-niri) ;; *) echo "Unknown profile: '$PROFILE'" echo "Valid values: stable-cinnamon stable-niri mainline-cinnamon mainline-niri" exit 1 ;; esac case "$BUILD_TYPE" in installer|live) ;; *) echo "Unknown build type: '$BUILD_TYPE'" echo "Valid values: installer live" exit 1 ;; esac # ── paths ───────────────────────────────────────────────────────────────── 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}" PATCH_DIR="$PROJECT_DIR/iso/patches" DOCKER_IMAGE="${DOCKER_IMAGE:-void-installer-builder:latest}" DOCKER="${DOCKER:-docker}" LIVE_USER="${LIVE_USER:-live}" # Niri profiles use a separate mklive clone directory to avoid races when # building cinnamon and niri ISOs in parallel on the same host. case "$PROFILE" in *-niri) MKLIVE_DIR="$CACHE_DIR/void-mklive-niri" ;; *) MKLIVE_DIR="$CACHE_DIR/void-mklive" ;; esac MKLIVE_REPO="${MKLIVE_REPO:-https://github.com/void-linux/void-mklive.git}" MKLIVE_REF="${MKLIVE_REF:-master}" # Per-profile, per-type build directory — parallel builds don't clobber each other. INCLUDE_DIR="$BUILD_DIR/${PROFILE}-${BUILD_TYPE}-includes" # ── load configuration ──────────────────────────────────────────────────── PROFILE_DIR="$PROJECT_DIR/config/profiles/$PROFILE" [[ -d "$PROFILE_DIR" ]] || { echo "Profile directory not found: $PROFILE_DIR"; exit 1; } # shellcheck disable=SC1091 source "$PROJECT_DIR/config/install.conf" # shellcheck disable=SC1090 source "$PROFILE_DIR/profile.conf" SECRETS_FILE="${SECRETS_FILE:-$PROJECT_DIR/secrets.env}" [[ -r "$SECRETS_FILE" ]] || { echo "missing $SECRETS_FILE — copy from template"; exit 1; } # shellcheck disable=SC1090 source "$SECRETS_FILE" : "${USER_PASSWORD:?}"; : "${ROOT_PASSWORD:?}" SSH_SRC_DIR="${SSH_SRC_DIR:-$HOME/.ssh}" [[ -d "$SSH_SRC_DIR" ]] || { echo "no SSH dir at $SSH_SRC_DIR"; exit 1; } # ── validate docker ──────────────────────────────────────────────────────── 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" # ═════════════════════════════════════════════════════════════════════════════ # HELPER FUNCTIONS # (all defined here so main execution at the bottom can call them) # ═════════════════════════════════════════════════════════════════════════════ _stage_installer_overlay() { # Installer ISO: auto-login root on tty1, run install-void, drop to shell. install -d -m 0755 "$INCLUDE_DIR/etc/sv/installer" cat > "$INCLUDE_DIR/etc/sv/installer/run" <<'SV_EOF' #!/bin/sh exec /sbin/agetty --autologin root --noclear tty1 linux SV_EOF chmod 0755 "$INCLUDE_DIR/etc/sv/installer/run" install -d -m 0755 "$INCLUDE_DIR/etc/sv/agetty-tty1" : > "$INCLUDE_DIR/etc/sv/agetty-tty1/down" install -d -m 0755 "$INCLUDE_DIR/etc/runit/runsvdir/default" ln -sf /etc/sv/installer "$INCLUDE_DIR/etc/runit/runsvdir/default/installer" install -d -m 0700 "$INCLUDE_DIR/root" cat > "$INCLUDE_DIR/root/.bash_profile" <<'PROFILE_EOF' case "$(tty)" in /dev/tty1) if [ ! -f /tmp/.installer-done ]; then touch /tmp/.installer-done clear echo echo " Void Linux Installer" echo " Press ENTER to start, or Ctrl-C within 5s for a shell." sleep 5 || true /usr/local/sbin/install-void || { echo "Installer exited with $?. Dropping to shell." exec /bin/bash } echo "Install complete. Type 'reboot' or 'poweroff'." exec /bin/bash fi ;; esac PROFILE_EOF chmod 0644 "$INCLUDE_DIR/root/.bash_profile" # shellcheck disable=SC2154 # PROFILE and HOSTNAME are from sourced config cat > "$INCLUDE_DIR/etc/motd" < "$INCLUDE_DIR/etc/xbps.d/00-void-repos.conf" < "$INCLUDE_DIR/etc/runit/2" <<'SV_EOF' #!/bin/sh PATH=/usr/bin:/usr/sbin [ -x /etc/runit/live-setup.sh ] && /etc/runit/live-setup.sh runlevel=default for arg in $(cat /proc/cmdline); do [ -d /etc/runit/runsvdir/"$arg" ] && runlevel="$arg" 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" # /etc/environment (XDG_DATA_DIRS includes nix profile share for app menus) cat > "$INCLUDE_DIR/etc/environment" <<'ENVEOF' XDG_DATA_DIRS=/home/live/.nix-profile/share:/usr/local/share:/usr/share ENVEOF # Timezone and rc.conf # shellcheck disable=SC2154 ln -sf "/usr/share/zoneinfo/${TIMEZONE:-Europe/Zurich}" "$INCLUDE_DIR/etc/localtime" cat > "$INCLUDE_DIR/etc/rc.conf" < "$INCLUDE_DIR/etc/udev/rules.d/70-sound-perms.rules" <<'EOF' SUBSYSTEM=="sound", GROUP="audio", MODE="0660" EOF # NVIDIA PRIME overlay (blacklist nouveau, prime-run wrapper, dracut config) 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 cat > "$INCLUDE_DIR/etc/modprobe.d/btusb-quirks.conf" <<'EOF' options btusb enable_autosuspend=0 EOF 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 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 install -d -m 0755 "$INCLUDE_DIR/usr/local/bin" cat > "$INCLUDE_DIR/usr/local/bin/prime-run" <<'EOF' #!/bin/sh 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" # nix prebake profile.d cat > "$INCLUDE_DIR/etc/profile.d/nix-prebaked.sh" <<'NIXEOF' if [[ -d "${HOME:-}/.nix-profile/bin" ]]; then case ":$PATH:" in *":$HOME/.nix-profile/bin:"*) ;; *) export PATH="$HOME/.nix-profile/bin:$PATH" ;; esac fi 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 export NIX_REMOTE=local nix() { command nix "$@" --impure; } export -f nix NIXEOF chmod 0644 "$INCLUDE_DIR/etc/profile.d/nix-prebaked.sh" # nix daemon config install -d -m 0755 "$INCLUDE_DIR/etc/nix" # shellcheck disable=SC2154 cat > "$INCLUDE_DIR/etc/nix/nix.conf" < "$INCLUDE_DIR/etc/skel/.config/nixpkgs/config.nix" # git askpass (works without 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 # skel .bash_profile install -d -m 0755 "$INCLUDE_DIR/etc/skel" cat > "$INCLUDE_DIR/etc/skel/.bash_profile" <<'EOF' [[ -f ~/.bashrc ]] && . ~/.bashrc EOF # themes / icons / wallpapers in usr/share 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 # first-login.sh available for manual use from live session install -d -m 0755 "$INCLUDE_DIR/usr/local/libexec" if [[ -r "$OVERLAY/first-login.sh" ]]; then install -m 0755 "$OVERLAY/first-login.sh" "$INCLUDE_DIR/usr/local/libexec/first-login.sh" fi # installer desktop entry (double-click to launch in a terminal) 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/sbin/install-void Icon=system-software-install Terminal=false Categories=System; StartupNotify=true DESKEOF } _stage_live_cinnamon() { # live-setup.sh: extra groups, sudo, nix, DNS, timezone, Xorg GPU detection cat > "$INCLUDE_DIR/etc/runit/live-setup.sh" <<'SV_EOF' #!/bin/sh LIVE_USER="${USERNAME:-live}" [ -f /etc/default/live.conf ] && . /etc/default/live.conf LIVE_USER="${LIVE_USER:-live}" for g in plugdev input network docker 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 if [ -x /usr/bin/nix ]; then install -d -m 0755 /etc/nix cat > /etc/nix/nix.conf < /etc/profile.d/nix-live.sh <<'NIXSH' if [ -d "$HOME/.nix-profile/bin" ]; then export PATH="$HOME/.nix-profile/bin:$PATH"; fi NIXSH chmod 0644 /etc/profile.d/nix-live.sh fi [ -f /etc/nsswitch.conf ] && sed -i '/^hosts:/s/mdns[^ ]* *//g' /etc/nsswitch.conf 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 ln -sf /usr/share/zoneinfo/Europe/Zurich /etc/localtime 2>/dev/null || true 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 mkdir -p /etc/X11/xorg.conf.d HAS_NVIDIA=0; IS_VIRT=0 if command -v lspci >/dev/null 2>&1; then lspci_out=$(lspci 2>/dev/null) echo "$lspci_out" | grep -qi 'NVIDIA' && HAS_NVIDIA=1 echo "$lspci_out" | grep -qiE 'virtio|VMware|QEMU|VirtualBox|bochs' && IS_VIRT=1 fi if [ $IS_VIRT -eq 1 ]; then printf 'Section "Device"\n Identifier "GPU"\n Driver "modesetting"\n Option "AccelMethod" "none"\nEndSection\n' \ > /etc/X11/xorg.conf.d/20-gpu.conf echo 'export LIBGL_ALWAYS_SOFTWARE=1' > /etc/profile.d/live-env.sh chmod 0644 /etc/profile.d/live-env.sh elif [ $HAS_NVIDIA -eq 1 ] && [ -e /usr/lib/xorg/modules/drivers/nvidia_drv.so ]; then rm -f /etc/profile.d/live-env.sh INTEL_BUSID=$(lspci | grep -iE 'VGA.*Intel|Intel.*VGA' | head -1 | \ awk '{print $1}' | awk -F'[.:]' '{printf "PCI:%d:%d:%d", strtonum("0x"$1), strtonum("0x"$2), strtonum("0x"$3)}') NVIDIA_BUSID=$(lspci | grep -i NVIDIA | head -1 | \ awk '{print $1}' | awk -F'[.:]' '{printf "PCI:%d:%d:%d", strtonum("0x"$1), strtonum("0x"$2), strtonum("0x"$3)}') cat > /etc/X11/xorg.conf.d/20-gpu.conf < /etc/X11/xorg.conf.d/20-gpu.conf fi echo "live-setup (cinnamon): done" SV_EOF chmod 0755 "$INCLUDE_DIR/etc/runit/live-setup.sh" # LightDM autologin — session file read by vmklive dracut hook install -d -m 0755 "$INCLUDE_DIR/etc/lightdm" echo 'cinnamon' > "$INCLUDE_DIR/etc/lightdm/.session" # Autologin lines commented so display-manager-autologin.sh hook uncomments them cat > "$INCLUDE_DIR/etc/lightdm/lightdm.conf" <<'EOF' [Seat:*] #autologin-user= #autologin-user-timeout=0 #autologin-session= #user-session= session-wrapper=/etc/lightdm/Xsession greeter-session=lightdm-gtk-greeter EOF # NOTE: session-wrapper=/etc/lightdm/Xsession required on Void — 'lightdm-session' # binary is NOT packaged; Xsession sources /etc/profile so profile.d/ vars reach # the Cinnamon session (LIBGL_ALWAYS_SOFTWARE=1 from live-setup.sh, etc.) # runit services install -d -m 0755 "$INCLUDE_DIR/etc/runit/runsvdir/default" for svc in dbus NetworkManager lightdm bluetoothd nix-daemon; do ln -sf "/etc/sv/$svc" "$INCLUDE_DIR/etc/runit/runsvdir/default/$svc" 2>/dev/null || true done # dconf keyfile for Cinnamon theme / keyboard install -d -m 0755 "$INCLUDE_DIR/etc/dconf/db/local.d" \ "$INCLUDE_DIR/etc/dconf/profile" cat > "$INCLUDE_DIR/etc/dconf/profile/user" <<'EOF' user-db:user system-db:local EOF WALLPAPER_FILE=$(ls "$OVERLAY/wallpapers/"*.jpg 2>/dev/null | head -1 | \ xargs basename 2>/dev/null || echo "pxfuel.jpg") # shellcheck disable=SC2154 cat > "$INCLUDE_DIR/etc/dconf/db/local.d/00-cinnamon" <t'] EOF install -d -m 0755 "$INCLUDE_DIR/etc/dconf/db/local.d/locks" cat > "$INCLUDE_DIR/etc/dconf/db/local.d/locks/keyboard" <<'EOF' /org/gnome/desktop/input-sources/sources /org/cinnamon/desktop/default-applications/terminal/exec /org/cinnamon/desktop/default-applications/terminal/exec-arg /org/gnome/desktop/default-applications/terminal/exec /org/gnome/desktop/default-applications/terminal/exec-arg EOF # Nemo: "Open in VS Code" action install -d -m 0755 "$INCLUDE_DIR/usr/share/nemo/actions" install -d -m 0755 "$INCLUDE_DIR/usr/local/bin" cat > "$INCLUDE_DIR/usr/local/bin/code-open" <<'EOF' #!/bin/sh for d in "/home/live/.nix-profile/bin" /nix/var/nix/profiles/per-user/live/profile/bin \ /usr/local/bin /usr/bin; do if [ -x "$d/code" ]; then exec "$d/code" "$@"; fi done exec code "$@" EOF chmod 0755 "$INCLUDE_DIR/usr/local/bin/code-open" cat > "$INCLUDE_DIR/usr/share/nemo/actions/open-in-vscode.nemo_action" <<'EOF' [Nemo Action] Name=Open in VS _Code Comment=Open in Visual Studio Code Exec=/usr/local/bin/code-open %F Icon-Name=code Selection=Any Extensions=any; Dependencies=/usr/local/bin/code-open; EOF # apply-live-settings.sh: runs once at first Cinnamon login install -d -m 0755 "$INCLUDE_DIR/usr/local/libexec" \ "$INCLUDE_DIR/etc/xdg/autostart" # shellcheck disable=SC2154 cat > "$INCLUDE_DIR/usr/local/libexec/apply-live-settings.sh" </dev/null)" || true; sleep 0.3 done gsettings set org.cinnamon.desktop.interface gtk-theme '${GTK_THEME}' gsettings set org.cinnamon.desktop.interface icon-theme '${ICON_THEME}' gsettings set org.gnome.desktop.interface cursor-theme '${CURSOR_THEME}' gsettings set org.cinnamon.theme name '${GTK_THEME}' _WP=\$(ls /usr/share/backgrounds/void-installer/*.jpg 2>/dev/null | head -1 || true) [[ -n "\$_WP" ]] && gsettings set org.cinnamon.desktop.background picture-uri "file://\$_WP" [[ -n "\$_WP" ]] && gsettings set org.gnome.desktop.background picture-uri "file://\$_WP" gsettings set org.cinnamon.desktop.default-applications.terminal exec '${DEFAULT_TERMINAL:-alacritty}' gsettings set org.cinnamon.desktop.default-applications.terminal exec-arg '-e' gsettings set org.gnome.desktop.default-applications.terminal exec '${DEFAULT_TERMINAL:-alacritty}' gsettings set org.gnome.desktop.default-applications.terminal exec-arg '-e' gsettings set org.gnome.desktop.input-sources sources "[('xkb', '${KEYMAP//-/+}')]" touch "\$DONE" APPLYEOF chmod 0755 "$INCLUDE_DIR/usr/local/libexec/apply-live-settings.sh" cat > "$INCLUDE_DIR/etc/xdg/autostart/void-live-settings.desktop" <<'DESK' [Desktop Entry] Type=Application Name=Void Live Apply Settings Exec=/usr/local/libexec/apply-live-settings.sh NoDisplay=true X-GNOME-Autostart-enabled=true OnlyShowIn=X-Cinnamon; DESK } _stage_live_niri() { # live-setup.sh: groups, sudo, DNS, sound, XDG_RUNTIME_DIR cat > "$INCLUDE_DIR/etc/runit/live-setup.sh" </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 [ -f /etc/nsswitch.conf ] && sed -i '/^hosts:/s/mdns[^ ]* *//g' /etc/nsswitch.conf 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 _UID=\$(id -u "\$LIVE_USER" 2>/dev/null || echo 1000) XDG_RUN="/run/user/\$_UID" install -d -m 0700 "\$XDG_RUN" 2>/dev/null || true chown "\$LIVE_USER" "\$XDG_RUN" 2>/dev/null || true 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 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" # greetd config (tty2 TUI fallback greeter) 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 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" <<'EOF' repository=https://universalrepo.r1xelelo.workers.dev/void EOF # Wayland environment cat > "$INCLUDE_DIR/etc/profile.d/wayland.sh" <<'EOF' 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 export LIBSEAT_BACKEND=logind export GTK_USE_PORTAL=1 export ELECTRON_OZONE_PLATFORM_HINT=auto EOF chmod 0644 "$INCLUDE_DIR/etc/profile.d/wayland.sh" # elogind custom sv (waits for dbus socket, avoids restart spam) install -d -m 0755 "$INCLUDE_DIR/etc/sv/elogind" cat > "$INCLUDE_DIR/etc/sv/elogind/run" <<'EOF' #!/bin/sh exec 2>&1 _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 } _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_alive || _login1_on_dbus; then echo "elogind-sv: already running — yielding" exec tail -f /dev/null fi 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" cat > "$INCLUDE_DIR/etc/sv/elogind/finish" <<'EOF' #!/bin/sh if [ "$1" != "0" ] && [ "$2" = "-1" ]; then sleep 10; else sleep 3; fi EOF chmod 0755 "$INCLUDE_DIR/etc/sv/elogind/finish" # runit services install -d -m 0755 "$INCLUDE_DIR/etc/runit/runsvdir/default" for svc in dbus elogind NetworkManager bluetoothd sshd; do ln -sf "/etc/sv/$svc" "$INCLUDE_DIR/etc/runit/runsvdir/default/$svc" done # niri skel config (dracut copies skel → /home/live at boot) local KEYMAP_XKB_LAYOUT="${KEYMAP%%-*}" local KEYMAP_XKB_VARIANT="${KEYMAP#*-}" KEYMAP_XKB_VARIANT="${KEYMAP_XKB_VARIANT//_nodeadkeys/}" install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/niri" # shellcheck disable=SC2154 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" 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 # dconf dark theme (GTK apps under niri still read dconf) install -d -m 0755 "$INCLUDE_DIR/etc/dconf/db/local.d" \ "$INCLUDE_DIR/etc/dconf/profile" cat > "$INCLUDE_DIR/etc/dconf/profile/user" <<'EOF' user-db:user system-db:local EOF cat > "$INCLUDE_DIR/etc/dconf/db/local.d/01-dark-theme" <<'EOF' [org/gnome/desktop/interface] color-scheme='prefer-dark' gtk-theme='Gruvbox-Dark' icon-theme='Gruvbox-Plus-Dark' cursor-theme='Bibata-Modern-Ice' cursor-size=24 EOF # noctalia defaults in skel install -d -m 0755 "$INCLUDE_DIR/etc/skel/.config/noctalia" cat > "$INCLUDE_DIR/etc/skel/.config/noctalia/settings.json" <<'NOCEOF' { "colorSchemes": { "darkMode": true, "predefinedScheme": "Gruvbox" }, "wallpaper": { "enabled": true, "directory": "/usr/share/backgrounds/void-installer", "fillMode": "crop" } } NOCEOF } # ═════════════════════════════════════════════════════════════════════════════ # MAIN EXECUTION # ═════════════════════════════════════════════════════════════════════════════ echo ">>> building ${BUILD_TYPE} ISO" echo " profile : ${PROFILE} — ${PROFILE_DESC:-}" echo " kernel : ${KERNEL_PKG}" echo " desktop : ${DESKTOP} (${DISPLAY_SERVER})" # 1) clone + patch mklive 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 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) stage overlay echo ">>> staging includes overlay at $INCLUDE_DIR" chmod -R u+rwX "$INCLUDE_DIR" 2>/dev/null || true rm -rf "$INCLUDE_DIR" mkdir -p "$INCLUDE_DIR" # 3a) Installer scripts + profiles (always present in both ISO types) install -d -m 0755 "$INCLUDE_DIR/usr/local/share/installer/lib" install -m 0644 "$PROJECT_DIR/config/install.conf" \ "$INCLUDE_DIR/usr/local/share/installer/install.conf" install -m 0644 "$PROJECT_DIR/config/profiles/stable-cinnamon/packages.list" \ "$INCLUDE_DIR/usr/local/share/installer/packages.list" install -m 0755 "$PROJECT_DIR/installer/install.sh" \ "$INCLUDE_DIR/usr/local/share/installer/install.sh" for f in "$PROJECT_DIR"/installer/lib/*.sh; do install -m 0755 "$f" "$INCLUDE_DIR/usr/local/share/installer/lib/$(basename "$f")" done install -d -m 0755 "$INCLUDE_DIR/usr/local/share/installer/profiles" cp -a "$PROJECT_DIR/config/profiles/." "$INCLUDE_DIR/usr/local/share/installer/profiles/" install -d -m 0755 "$INCLUDE_DIR/usr/local/sbin" ln -sf /usr/local/share/installer/install.sh "$INCLUDE_DIR/usr/local/sbin/install-void" # 3b) DEFAULT_PROFILE env marker — the embedded installer defaults to this profile install -d -m 0755 "$INCLUDE_DIR/etc/profile.d" cat > "$INCLUDE_DIR/etc/profile.d/00-void-installer.sh" < "$INCLUDE_DIR/etc/installer-secrets.env" chmod 0600 "$INCLUDE_DIR/etc/installer-secrets.env" install -d -m 0700 "$INCLUDE_DIR/etc/installer-ssh" cp -a "$SSH_SRC_DIR"/. "$INCLUDE_DIR/etc/installer-ssh/" find "$INCLUDE_DIR/etc/installer-ssh" -type f -exec chmod 0600 {} + find "$INCLUDE_DIR/etc/installer-ssh" -type d -exec chmod 0700 {} + # 3d) Customizations overlay (themes / icons / wallpapers / dotfiles / vscode) OVERLAY="$INCLUDE_DIR/etc/installer-overlay" install -d -m 0755 "$OVERLAY" "$OVERLAY/wallpapers" "$OVERLAY/themes" \ "$OVERLAY/icons" "$OVERLAY/skel" "$OVERLAY/vscode-user" 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)" 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" || echo " (warning: could not clone theme repo)" 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 ' || echo " (warning: theme build failed)" 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 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" || echo " (warning: could not clone icon repo)" 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_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" else echo " (warning: $BIBATA_SRC missing — cursor theme skipped)" fi DOTFILES_SRC="${DOTFILES_SRC:-$HOME}" for f in .bashrc .bash_aliases .gitconfig; do [[ -r "$DOTFILES_SRC/$f" ]] && install -m 0644 "$DOTFILES_SRC/$f" "$OVERLAY/skel/$f" done VSCODE_SRC="${VSCODE_USER_SRC:-$HOME/.config/Code/User}" if [[ -d "$VSCODE_SRC" ]]; then for f in settings.json keybindings.json mcp.json tasks.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" [[ -d "$VSCODE_SRC/globalStorage" ]] && cp -a "$VSCODE_SRC/globalStorage" "$OVERLAY/vscode-user/globalStorage" fi command -v code >/dev/null 2>&1 && \ code --list-extensions > "$OVERLAY/vscode-extensions.txt" 2>/dev/null || true CLAUDE_SRC="${CLAUDE_SRC:-$HOME/.claude}" [[ -d "$CLAUDE_SRC" ]] && cp -a "$CLAUDE_SRC" "$OVERLAY/claude" [[ -r "${HOME}/.claude.json" ]] && install -m 0600 "${HOME}/.claude.json" "$OVERLAY/claude.json" [[ -r "$PROJECT_DIR/installer/first-login.sh" ]] && \ install -m 0755 "$PROJECT_DIR/installer/first-login.sh" "$OVERLAY/first-login.sh" install -d -m 0755 "$INCLUDE_DIR/usr/local/libexec" { printf '# Nix user packages\n' printf '%s\n' "${NIX_USER_PACKAGES[@]}" } > "$INCLUDE_DIR/usr/local/libexec/nix-packages.list" # 3e) Type-specific + DE-specific overlay (uses functions defined above) if [[ "$BUILD_TYPE" == "installer" ]]; then _stage_installer_overlay else _stage_live_overlay fi # 4) package list for ISO if [[ "$BUILD_TYPE" == "installer" ]]; then ISO_PKGS=$(grep -vE '^\s*(#|$)' "$PROJECT_DIR/config/packages.live.list" | tr '\n' ' ') else # Prefer new canonical name, fall back to old name for existing profiles _PKG_LIST="$PROFILE_DIR/packages.live.list" [[ -f "$_PKG_LIST" ]] || _PKG_LIST="$PROFILE_DIR/packages.live-desktop.list" [[ -f "$_PKG_LIST" ]] || { echo "ERROR: no packages.live.list found in $PROFILE_DIR"; exit 1; } ISO_PKGS=$(grep -vE '^\s*(#|$)' "$_PKG_LIST" | tr '\n' ' ') fi # 5) output filename TS="$(date -u +%Y%m%d)" case "$BUILD_TYPE" in installer) OUT_ISO="${OUTPUT_ISO:-$OUT_DIR/void-install-${PROFILE}-${TS}.iso}" ;; live) OUT_ISO="${OUTPUT_ISO:-$OUT_DIR/void-live-${PROFILE}-${TS}.iso}" ;; esac # 6) boot cmdline _BOOT_BASE="nvidia-drm.modeset=1 rd.driver.blacklist=nouveau modprobe.blacklist=nouveau btusb.enable_autosuspend=0" if [[ "$BUILD_TYPE" == "live" ]]; then BOOT_CMDLINE="${BOOT_CMDLINE:-live.user=${LIVE_USER} console=tty0 console=ttyS0,115200 ${_BOOT_BASE}}" else BOOT_CMDLINE="${BOOT_CMDLINE:-${_BOOT_BASE}}" fi # 7) ISO title case "${BUILD_TYPE}-${DESKTOP}" in installer-*) ISO_TITLE="Void Installer (${PROFILE})" ;; live-cinnamon) ISO_TITLE="Void Live — Cinnamon (${KERNEL_PKG})" ;; live-niri) ISO_TITLE="Void Live — niri / noctalia-shell (${KERNEL_PKG})" ;; *) ISO_TITLE="Void Linux (${PROFILE})" ;; esac # 8) build docker image 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 # 9) noctalia repo (niri profiles only) NOCTALIA_REPO="" [[ "$DESKTOP" == "niri" ]] && \ NOCTALIA_REPO="${NOCTALIA_REPO:-https://universalrepo.r1xelelo.workers.dev/void}" # 10) nix prebake packages (live ISOs only) NIX_PREBAKE_PKGS="" [[ "$BUILD_TYPE" == "live" ]] && NIX_PREBAKE_PKGS="${NIX_USER_PACKAGES[*]}" 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="$ISO_TITLE" \ -e OUT_ISO_REL="${OUT_ISO#$PROJECT_DIR/}" \ -e INCLUDE_DIR_REL="${INCLUDE_DIR#$PROJECT_DIR/}" \ -e BUILD_TYPE="$BUILD_TYPE" \ -e DESKTOP="$DESKTOP" \ -e KERNEL_PKG="${KERNEL_PKG:-linux}" \ -e NOCTALIA_REPO="$NOCTALIA_REPO" \ -e NIX_PACKAGES_PREBAKE="$NIX_PREBAKE_PKGS" \ -e BOOT_CMDLINE="${BOOT_CMDLINE:-}" \ -e HOST_UID="$(id -u)" \ -e HOST_GID="$(id -g)" \ "$DOCKER_IMAGE" \ bash /work/iso/_inner-build-unified.sh echo echo ">>> ISO built: $OUT_ISO" sha256sum "$OUT_ISO" | tee "${OUT_ISO}.sha256" || true