Last active
August 12, 2026 11:59
-
-
Save Yaksinikos/32542f78c7b4d015f85b8a449fe7dcda to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| * text=auto eol=lf | |
| *.sh text eol=lf |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # ============================================================================= | |
| # Void Linux Installation Script -- runit (no systemd), LUKS2, btrfs + ext4 | |
| # ============================================================================= | |
| # | |
| # LAYOUT (partition numbers are the same for UEFI and BIOS): | |
| # p1 ESP 1 GiB vfat (UEFI) / 1 MiB BIOS-boot (BIOS) | |
| # p2 BOOT 1 GiB ext4 unencrypted | |
| # p3 ROOT 90 GiB LUKS2 -> btrfs subvolumes @, @var-log, @var-cache | |
| # p4 HOME remainder LUKS2 -> ext4 | |
| # p5 SWAP 32 GiB LUKS2 -> swap | |
| # | |
| # WHY /boot IS UNENCRYPTED: | |
| # GRUB cannot read LUKS2 headers that use argon2id. /boot therefore lives on | |
| # its own plain ext4 partition. The direct consequence is that the initramfs | |
| # is stored in the clear, so the keyfile must NEVER be embedded in it. | |
| # | |
| # UNLOCK MODEL (one passphrase prompt per boot): | |
| # * ONE passphrase is enrolled in keyslot 0 of ROOT, HOME and SWAP. | |
| # Losing the root filesystem therefore never locks you out of HOME. | |
| # * A random 4096-byte keyfile is additionally enrolled in HOME and SWAP and | |
| # stored on the encrypted ROOT at /etc/cryptsetup-keys.d/data.key. | |
| # * dracut unlocks ROOT only -> exactly one passphrase prompt. | |
| # * /etc/runit/core-services/02-luks.sh then unlocks HOME and SWAP from the | |
| # keyfile, after / is mounted and before fsck / mount -a / swapon -a run. | |
| # | |
| # DESKTOP: | |
| # Wayland only. The installer asks which desktop to install; greetd + tuigreet | |
| # is the display manager in every case and offers whatever Wayland sessions | |
| # ended up installed. No X11 session is ever offered (XWayland is installed so | |
| # that X11 *applications* still run). | |
| # | |
| # INTERACTIVE PROMPTS, in order: | |
| # 1. target disk [1] | |
| # 2. hostname [1a] | |
| # 3. desktop choice [1a] | |
| # 4. LUKS passphrase [2] | |
| # 5. username [10] | |
| # 6. root + user password [10] | |
| # Everything before the first prompt is read-only; nothing is written to the | |
| # disk until after prompts 1-3, so aborting up to that point is harmless. | |
| # | |
| # USAGE: | |
| # ./vm5.sh fully interactive | |
| # DISK=/dev/nvme0n1 ./vm5.sh skip the disk selector | |
| # DESKTOP=niri ./vm5.sh skip the desktop prompt (kde|niri|both|none) | |
| # HOSTNAME_NEW=box ./vm5.sh skip the hostname prompt | |
| # GREETD_VT=7 ./vm5.sh greeter on a spare VT, tty1 keeps its console | |
| # EXTRA_REPOS=no ./vm5.sh official Void repositories only (see [15d]) | |
| # ALLOW_DISCARDS=yes ./vm5.sh enable TRIM through LUKS (see note below) | |
| # NO_CLEANUP=1 ./vm5.sh leave mounts/mappers in place if it fails | |
| # ============================================================================= | |
| set -euEo pipefail | |
| # ---------------------------------------------------------------- settings -- | |
| ROOT_MOUNT="${ROOT_MOUNT:-/mnt/void}" | |
| REPO="${REPO:-https://repo-de.voidlinux.org/current}" | |
| LOGFILE="${LOGFILE:-/tmp/vm5-log.log}" | |
| # Hostname. Left empty so that [1a] asks for it; HOSTNAME_DEFAULT is only the | |
| # value offered at that prompt. Set HOSTNAME_NEW in the environment to skip it. | |
| HOSTNAME_DEFAULT="${HOSTNAME_DEFAULT:-vm5}" | |
| HOSTNAME_NEW="${HOSTNAME_NEW:-}" | |
| TIMEZONE="${TIMEZONE:-Europe/Belgrade}" | |
| KEYMAP="${KEYMAP:-de}" | |
| # Which VT greetd/tuigreet runs on. | |
| # 1 (default) -- what upstream greetd/tuigreet document, and the only value | |
| # that makes the greeter appear by itself at the end of boot. | |
| # agetty-tty1 is disabled so the two do not share vt 1; | |
| # agetty-tty2..tty6 stay enabled as the recovery path | |
| # (Ctrl+Alt+F2) if the greeter ever fails to come up. | |
| # 7 -- greeter on a spare VT, tty1 keeps its console login. Nothing | |
| # starts automatically; reach it with Ctrl+Alt+F7. Useful for | |
| # validating the greeter on new hardware before committing. | |
| GREETD_VT="${GREETD_VT:-1}" | |
| # Desktop to install: kde | niri | both | none. Empty means ask at [1a]. | |
| DESKTOP="${DESKTOP:-}" | |
| # Target architecture. Also substituted into the Blackhole-VL mirror URL, | |
| # whose paths are per-arch (x86_64, x86_64-musl, aarch64, aarch64-musl). | |
| TARGET_ARCH="${TARGET_ARCH:-x86_64}" | |
| # Add the third-party repositories configured at [15d]. Set to "no" to skip. | |
| # These are UNOFFICIAL: see the warning in that step before enabling. | |
| EXTRA_REPOS="${EXTRA_REPOS:-yes}" | |
| # Partition sizes in MiB. HOME takes whatever is left over. | |
| ESP_MIB=1024 | |
| BOOT_MIB=1024 | |
| ROOT_MIB=$((90 * 1024)) | |
| SWAP_MIB=$((32 * 1024)) | |
| MIN_HOME_MIB=8192 | |
| SLACK_MIB=8 # alignment + backup GPT | |
| # argon2id memory cost in KiB. The default (~1 GiB) can OOM the initramfs on | |
| # low-memory machines, which looks exactly like a wrong passphrase. | |
| LUKS_PBKDF_MEM="${LUKS_PBKDF_MEM:-524288}" | |
| # TRIM/discard passthrough. Enabling it lets the SSD reclaim freed blocks but | |
| # leaks which blocks are in use (i.e. roughly how full the disk is) to anyone | |
| # who can read the raw device. Off by default; fstab and crypttab are kept | |
| # consistent with this setting either way. | |
| ALLOW_DISCARDS="${ALLOW_DISCARDS:-no}" | |
| KEYDIR_TMP="/tmp/vm5-keys" | |
| KEYFILE_TMP="$KEYDIR_TMP/data.key" | |
| PASSFILE_TMP="$KEYDIR_TMP/passphrase" | |
| KEYFILE_SYS="/etc/cryptsetup-keys.d/data.key" | |
| FINISHED=0 | |
| # ----------------------------------------------------------------- logging -- | |
| : > "$LOGFILE" || { echo "Cannot write $LOGFILE" >&2; exit 1; } | |
| exec > >(tee -a "$LOGFILE") 2>&1 | |
| # Interactive prompts always go straight to the terminal so that they are never | |
| # swallowed or reordered by the tee buffer. (The old script re-exec'd stdout | |
| # through a second `tee` without -a, which truncated the log halfway through.) | |
| [ -e /dev/tty ] || { echo "ERROR: no controlling terminal; this installer is interactive."; exit 1; } | |
| tty_out() { printf '%s\n' "$*" > /dev/tty; } | |
| die() { echo "ERROR: $*" >&2; exit 1; } | |
| # --------------------------------------------------------------- teardown --- | |
| cleanup() { | |
| swapoff /dev/mapper/cryptswap 2>/dev/null || true | |
| umount "$ROOT_MOUNT/dev/pts" 2>/dev/null || true | |
| umount "$ROOT_MOUNT/dev" 2>/dev/null || true | |
| umount "$ROOT_MOUNT/proc" 2>/dev/null || true | |
| umount "$ROOT_MOUNT/sys" 2>/dev/null || true | |
| umount "$ROOT_MOUNT/run" 2>/dev/null || true | |
| umount -R "$ROOT_MOUNT" 2>/dev/null || true | |
| for _m in cryptswap crypthome cryptroot; do | |
| [ -e "/dev/mapper/$_m" ] && cryptsetup close "$_m" 2>/dev/null || true | |
| done | |
| rm -rf "$KEYDIR_TMP" 2>/dev/null || true | |
| } | |
| on_exit() { | |
| local rc=$? | |
| if [ "$rc" -ne 0 ] && [ "$FINISHED" -eq 0 ]; then | |
| echo "" | |
| echo "=== INSTALLATION ABORTED (exit $rc) ===" | |
| echo "Log: $LOGFILE" | |
| if [ "${NO_CLEANUP:-0}" = "1" ]; then | |
| echo "NO_CLEANUP=1 set: leaving mounts and LUKS mappings in place." | |
| else | |
| echo "Tearing down mounts and LUKS mappings so the next run starts clean..." | |
| cleanup | |
| fi | |
| fi | |
| } | |
| trap on_exit EXIT | |
| trap 'echo "ERROR: command failed at ${BASH_SOURCE[0]}:${LINENO}" >&2' ERR | |
| # ============================================================================= | |
| echo "[0] Detecting live system boot mode..." | |
| # ============================================================================= | |
| if [ -d /sys/firmware/efi ]; then | |
| BOOT_MODE="uefi" | |
| echo "Live system booted in UEFI mode. EFI variables are accessible." | |
| else | |
| BOOT_MODE="bios" | |
| echo "Live system booted in Legacy BIOS mode." | |
| echo "Installing GRUB for BIOS (i386-pc) with a GPT BIOS-boot partition." | |
| fi | |
| echo "" | |
| # ============================================================================= | |
| echo "[1] Selecting target disk..." | |
| # ============================================================================= | |
| # nvme0n1 / mmcblk0 / loop0 need a 'p' between device and partition number. | |
| part() { | |
| case "$DISK" in | |
| *[0-9]) printf '%sp%s\n' "$DISK" "$1" ;; | |
| *) printf '%s%s\n' "$DISK" "$1" ;; | |
| esac | |
| } | |
| select_disk() { | |
| local -a devs=() | |
| local name size type model sel | |
| tty_out "" | |
| tty_out "Available disks:" | |
| while read -r name size type model; do | |
| [ "$type" = "disk" ] || continue | |
| case "$name" in | |
| /dev/loop*|/dev/ram*|/dev/zram*|/dev/sr*|/dev/fd*) continue ;; | |
| esac | |
| devs+=("$name") | |
| printf " %2d) %-16s %8s %s\n" "${#devs[@]}" "$name" "$size" "$model" > /dev/tty | |
| done < <(lsblk -dnpo NAME,SIZE,TYPE,MODEL) | |
| [ "${#devs[@]}" -gt 0 ] || die "no suitable disks found" | |
| while :; do | |
| printf "Select target disk [1-%d]: " "${#devs[@]}" > /dev/tty | |
| IFS= read -r sel < /dev/tty || sel="" | |
| case "$sel" in | |
| ''|*[!0-9]*) tty_out "Enter a number." ; continue ;; | |
| esac | |
| if [ "$sel" -ge 1 ] && [ "$sel" -le "${#devs[@]}" ]; then | |
| DISK="${devs[$((sel - 1))]}" | |
| return 0 | |
| fi | |
| tty_out "Out of range." | |
| done | |
| } | |
| if [ -n "${DISK:-}" ]; then | |
| [ -b "$DISK" ] || die "DISK=$DISK is not a block device" | |
| echo "Using disk from environment: $DISK" | |
| else | |
| select_disk | |
| fi | |
| DISK_NAME="$(basename "$DISK")" | |
| tty_out "" | |
| tty_out "Target: $DISK" | |
| lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT "$DISK" > /dev/tty 2>/dev/null || true | |
| tty_out "" | |
| tty_out "!!! EVERYTHING ON $DISK WILL BE DESTROYED !!!" | |
| printf "Type '%s' to confirm: " "$DISK_NAME" > /dev/tty | |
| IFS= read -r CONFIRM < /dev/tty || CONFIRM="" | |
| [ "$CONFIRM" = "$DISK_NAME" ] || die "not confirmed, aborting" | |
| echo "Confirmed target disk: $DISK" | |
| # ============================================================================= | |
| echo "[1a] Selecting hostname and desktop..." | |
| # ============================================================================= | |
| # Asked here, before the disk is touched, so that the long unattended stretch | |
| # after partitioning has no further questions about what to install. | |
| # | |
| # Package sets are Wayland-only in every case. | |
| # | |
| # NEITHER desktop ships applications with its shell, so both sets list them. | |
| # niri is a bare compositor -- no terminal, launcher or locker -- so its set is | |
| # exactly what its own default config.kdl expects, otherwise you land in a | |
| # working compositor with no way to start anything: | |
| # Mod+T -> alacritty, Mod+D -> fuzzel, Super+Alt+L -> swaylock, | |
| # spawn-at-startup "waybar" | |
| # kde-plasma is likewise shell-only; see the KDE_PKGS comment below. | |
| # RFC 1123: letters, digits and hyphens; may not start or end with a hyphen; | |
| # 63 characters max. Anything else and the name silently misbehaves in DNS and | |
| # in anything that resolves it. | |
| select_hostname() { | |
| local name | |
| tty_out "" | |
| while :; do | |
| printf "Hostname [%s]: " "$HOSTNAME_DEFAULT" > /dev/tty | |
| IFS= read -r name < /dev/tty || name="" | |
| name=${name:-$HOSTNAME_DEFAULT} | |
| if [ "${#name}" -le 63 ] && | |
| [[ "$name" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$ ]]; then | |
| HOSTNAME_NEW="$name" | |
| return 0 | |
| fi | |
| tty_out "ERROR: '$name' is not a valid hostname (letters, digits and" | |
| tty_out " hyphens only, no leading/trailing hyphen, max 63 chars)." | |
| done | |
| } | |
| if [ -n "${HOSTNAME_NEW:-}" ]; then | |
| echo "Using hostname from environment: $HOSTNAME_NEW" | |
| else | |
| select_hostname | |
| fi | |
| echo "Hostname: $HOSTNAME_NEW" | |
| select_desktop() { | |
| local sel | |
| tty_out "" | |
| tty_out "Which desktop should be installed? (Wayland only)" | |
| tty_out " 1) KDE Plasma (konsole, dolphin, kate, kcalc, ark," | |
| tty_out " gwenview, okular, spectacle)" | |
| tty_out " 2) niri + Waybar (alacritty, fuzzel, swaylock)" | |
| tty_out " 3) Both (pick per login in the greeter)" | |
| tty_out " 4) None (console only; no greeter)" | |
| while :; do | |
| printf "Select [1-4, default 1]: " > /dev/tty | |
| IFS= read -r sel < /dev/tty || sel="" | |
| case "${sel:-1}" in | |
| 1) DESKTOP=kde ; return 0 ;; | |
| 2) DESKTOP=niri ; return 0 ;; | |
| 3) DESKTOP=both ; return 0 ;; | |
| 4) DESKTOP=none ; return 0 ;; | |
| *) tty_out "Enter 1, 2, 3 or 4." ;; | |
| esac | |
| done | |
| } | |
| if [ -n "$DESKTOP" ]; then | |
| echo "Using desktop from environment: $DESKTOP" | |
| else | |
| select_desktop | |
| fi | |
| # kde-plasma is the desktop SHELL only. Its dependency list -- and that of | |
| # plasma-desktop underneath it -- contains no terminal emulator, no file manager | |
| # and no calculator; it stops at systemsettings, powerdevil, kmenuedit and the | |
| # like. Applications have to be asked for separately, exactly as they do for | |
| # niri. kde-baseapps is Void's curated base set and is only three programs | |
| # (dolphin, kate, konsole, plus khelpcenter on 64-bit), so the rest of the | |
| # everyday tools are listed explicitly. | |
| KDE_PKGS=( | |
| kde-plasma # Plasma 6 desktop shell | |
| kde-baseapps # dolphin (files), kate (editor), konsole (terminal) | |
| kcalc # calculator | |
| ark # archiver -- Dolphin's compress/extract actions need it | |
| gwenview # image viewer | |
| okular # document/PDF viewer | |
| spectacle # screenshots; Plasma binds the Print key to it | |
| kio-admin # admin:// KIO protocol -- edit root files from Dolphin/Kate | |
| kdeconnect # phone integration (kdeconnectd runs per session) | |
| ) | |
| NIRI_PKGS=( | |
| niri Waybar alacritty fuzzel swaylock xwayland-satellite | |
| # niri installs /usr/share/xdg-desktop-portal/niri-portals.conf, which asks | |
| # for "default=gnome;gtk;" -- but the niri package depends only on wayland, | |
| # so the backends have to be requested explicitly or screencast, file | |
| # pickers and notifications silently do nothing. | |
| xdg-desktop-portal xdg-desktop-portal-gnome xdg-desktop-portal-gtk | |
| ) | |
| ADDITIONAL_PKGS=( | |
| seahorse | |
| filelight | |
| 7zip | |
| partitionmanager | |
| gamemode | |
| i2pd | |
| libreoffice | |
| telegram-desktop | |
| syncthing | |
| ) | |
| DESKTOP_PKGS=() | |
| case "$DESKTOP" in | |
| kde) DESKTOP_PKGS=("${KDE_PKGS[@]}") ;; | |
| niri) DESKTOP_PKGS=("${NIRI_PKGS[@]}") ;; | |
| both) DESKTOP_PKGS=("${KDE_PKGS[@]}" "${NIRI_PKGS[@]}" "${ADDITIONAL_PKGS[@]}") ;; | |
| none) DESKTOP_PKGS=() ;; | |
| *) die "invalid DESKTOP='$DESKTOP' (expected kde, niri, both or none)" ;; | |
| esac | |
| echo "Desktop: $DESKTOP" | |
| if [ "${#DESKTOP_PKGS[@]}" -gt 0 ]; then | |
| echo "Desktop packages: ${DESKTOP_PKGS[*]}" | |
| else | |
| echo "No desktop packages; greetd will not be installed or enabled." | |
| fi | |
| # ============================================================================= | |
| echo "[1b] Computing partition sizes..." | |
| # ============================================================================= | |
| DISK_BYTES=$(blockdev --getsize64 "$DISK") | |
| DISK_MIB=$((DISK_BYTES / 1024 / 1024)) | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| P1_MIB="$ESP_MIB" | |
| else | |
| P1_MIB=1 | |
| fi | |
| HOME_MIB=$((DISK_MIB - P1_MIB - BOOT_MIB - ROOT_MIB - SWAP_MIB - SLACK_MIB)) | |
| if [ "$HOME_MIB" -lt "$MIN_HOME_MIB" ]; then | |
| die "disk is too small: ${DISK_MIB} MiB total leaves only ${HOME_MIB} MiB for /home (need >= ${MIN_HOME_MIB} MiB)" | |
| fi | |
| echo "Disk size: ${DISK_MIB} MiB" | |
| echo " p1 boot: ${P1_MIB} MiB" | |
| echo " p2 /boot: ${BOOT_MIB} MiB" | |
| echo " p3 root: ${ROOT_MIB} MiB" | |
| echo " p4 home: ${HOME_MIB} MiB" | |
| echo " p5 swap: ${SWAP_MIB} MiB" | |
| # ============================================================================= | |
| echo "[1c] Releasing anything still held on $DISK..." | |
| # ============================================================================= | |
| # Stale mounts and dm mappings from a previous (failed) run are the reason | |
| # cryptsetup reports "already mapped or mounted" on a second attempt. | |
| umount -R "$ROOT_MOUNT" 2>/dev/null || true | |
| while read -r _n _t; do | |
| case "$_t" in | |
| crypt) swapoff "/dev/mapper/$_n" 2>/dev/null || true | |
| cryptsetup close "$_n" 2>/dev/null || true ;; | |
| part) umount "/dev/$_n" 2>/dev/null || true ;; | |
| esac | |
| done < <(lsblk -lno NAME,TYPE "$DISK" 2>/dev/null | tac) | |
| # ============================================================================= | |
| echo "[1d] Partitioning $DISK..." | |
| # ============================================================================= | |
| wipefs -a "$DISK" >/dev/null | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| sfdisk --wipe always --wipe-partitions always "$DISK" << SFDISK | |
| label: gpt | |
| 1: size=${P1_MIB}M, type=uefi | |
| 2: size=${BOOT_MIB}M, type=linux | |
| 3: size=${ROOT_MIB}M, type=linux | |
| 4: size=${HOME_MIB}M, type=linux | |
| 5: size=${SWAP_MIB}M, type=swap | |
| SFDISK | |
| else | |
| sfdisk --wipe always --wipe-partitions always "$DISK" << SFDISK | |
| label: gpt | |
| 1: size=${P1_MIB}M, type=biosboot | |
| 2: size=${BOOT_MIB}M, type=linux | |
| 3: size=${ROOT_MIB}M, type=linux | |
| 4: size=${HOME_MIB}M, type=linux | |
| 5: size=${SWAP_MIB}M, type=swap | |
| SFDISK | |
| fi | |
| # Make sure the kernel and /dev actually caught up before we touch the nodes. | |
| partprobe "$DISK" 2>/dev/null || partx -u "$DISK" 2>/dev/null || true | |
| udevadm settle --timeout=30 2>/dev/null || true | |
| wait_for_block() { | |
| local dev="$1" i | |
| for i in $(seq 1 50); do | |
| if [ -b "$dev" ]; then return 0; fi | |
| sleep 0.2 | |
| done | |
| return 1 | |
| } | |
| for i in 1 2 3 4 5; do | |
| wait_for_block "$(part "$i")" || die "partition $(part "$i") never appeared" | |
| done | |
| # Remove any filesystem signature the new partitions may have inherited. | |
| for i in 2 3 4 5; do | |
| wipefs -a "$(part "$i")" >/dev/null 2>&1 || true | |
| done | |
| echo "Partitions created: $(part 1) $(part 2) $(part 3) $(part 4) $(part 5)" | |
| # ============================================================================= | |
| echo "[2] Creating LUKS containers..." | |
| # ============================================================================= | |
| # One passphrase, enrolled in keyslot 0 of all three volumes. IFS= is required | |
| # so that leading/trailing spaces in the passphrase survive `read` -- without it | |
| # the stored key silently differs from what the user typed. | |
| get_luks_passphrase() { | |
| local outfile="$1" pw1 pw2 | |
| while :; do | |
| printf "Disk encryption passphrase (min 16 chars): " > /dev/tty | |
| IFS= read -rs pw1 < /dev/tty || pw1="" | |
| printf "\n" > /dev/tty | |
| if [ "${#pw1}" -lt 16 ]; then | |
| tty_out "ERROR: passphrase too short (${#pw1} chars); minimum 16 required." | |
| continue | |
| fi | |
| printf "Repeat passphrase: " > /dev/tty | |
| IFS= read -rs pw2 < /dev/tty || pw2="" | |
| printf "\n" > /dev/tty | |
| if [ "$pw1" = "$pw2" ]; then | |
| break | |
| fi | |
| tty_out "ERROR: passphrases do not match." | |
| done | |
| printf '%s' "$pw1" > "$outfile" | |
| chmod 600 "$outfile" | |
| } | |
| rm -rf "$KEYDIR_TMP" | |
| mkdir -p -m 700 "$KEYDIR_TMP" | |
| tty_out "" | |
| tty_out "=== Disk Encryption ===" | |
| tty_out "This single passphrase unlocks ROOT, HOME and SWAP." | |
| tty_out "You will only be asked for it once per boot; HOME and SWAP are then" | |
| tty_out "unlocked automatically from a keyfile stored on the encrypted root." | |
| get_luks_passphrase "$PASSFILE_TMP" | |
| # -q: without it, luksFormat stops to ask for a typed "YES" confirmation and | |
| # reads that answer from the script's own stdin. | |
| CS_FMT=(--type luks2 --cipher aes-xts-plain64 --key-size 512 --pbkdf argon2id --pbkdf-memory "$LUKS_PBKDF_MEM" -q) | |
| cryptsetup luksFormat "${CS_FMT[@]}" --key-file "$PASSFILE_TMP" --label ROOT "$(part 3)" | |
| cryptsetup luksFormat "${CS_FMT[@]}" --key-file "$PASSFILE_TMP" --label HOME "$(part 4)" | |
| cryptsetup luksFormat "${CS_FMT[@]}" --key-file "$PASSFILE_TMP" --label SWAP "$(part 5)" | |
| echo "[2a] Adding keyfile slot to HOME and SWAP for automatic unlock..." | |
| dd if=/dev/urandom of="$KEYFILE_TMP" bs=4096 count=1 status=none | |
| chmod 600 "$KEYFILE_TMP" | |
| cryptsetup luksAddKey --pbkdf argon2id --pbkdf-memory "$LUKS_PBKDF_MEM" \ | |
| --key-file "$PASSFILE_TMP" "$(part 4)" "$KEYFILE_TMP" | |
| cryptsetup luksAddKey --pbkdf argon2id --pbkdf-memory "$LUKS_PBKDF_MEM" \ | |
| --key-file "$PASSFILE_TMP" "$(part 5)" "$KEYFILE_TMP" | |
| # ROOT deliberately gets no keyfile slot: its key would have to live somewhere | |
| # readable before root is unlocked, i.e. on the unencrypted /boot. | |
| CS_OPEN=() | |
| if [ "$ALLOW_DISCARDS" = "yes" ]; then CS_OPEN=(--allow-discards); fi | |
| cryptsetup open "${CS_OPEN[@]}" --key-file "$PASSFILE_TMP" "$(part 3)" cryptroot | |
| cryptsetup open "${CS_OPEN[@]}" --key-file "$PASSFILE_TMP" "$(part 4)" crypthome | |
| cryptsetup open "${CS_OPEN[@]}" --key-file "$PASSFILE_TMP" "$(part 5)" cryptswap | |
| shred -u "$PASSFILE_TMP" 2>/dev/null || rm -f "$PASSFILE_TMP" | |
| # ============================================================================= | |
| echo "[3] Formatting partitions..." | |
| # ============================================================================= | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| mkfs.vfat -F32 -n ESP "$(part 1)" | |
| fi | |
| mkfs.ext4 -F -L BOOT "$(part 2)" | |
| mkfs.btrfs -f -L ROOT /dev/mapper/cryptroot | |
| mkfs.ext4 -F -L HOME /dev/mapper/crypthome | |
| mkswap -L SWAP /dev/mapper/cryptswap | |
| echo "[3a] Capturing UUIDs..." | |
| BOOT_UUID=$(blkid -s UUID -o value "$(part 2)") | |
| CRYPTROOT_UUID=$(cryptsetup luksUUID "$(part 3)") | |
| CRYPTHOME_UUID=$(cryptsetup luksUUID "$(part 4)") | |
| CRYPTSWAP_UUID=$(cryptsetup luksUUID "$(part 5)") | |
| ROOT_UUID=$(blkid -s UUID -o value /dev/mapper/cryptroot) | |
| HOME_UUID=$(blkid -s UUID -o value /dev/mapper/crypthome) | |
| SWAP_UUID=$(blkid -s UUID -o value /dev/mapper/cryptswap) | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| ESP_UUID=$(blkid -s UUID -o value "$(part 1)") | |
| echo "ESP UUID: $ESP_UUID" | |
| fi | |
| echo "BOOT UUID: $BOOT_UUID" | |
| echo "ROOT UUID: $ROOT_UUID (LUKS $CRYPTROOT_UUID)" | |
| echo "HOME UUID: $HOME_UUID (LUKS $CRYPTHOME_UUID)" | |
| echo "SWAP UUID: $SWAP_UUID (LUKS $CRYPTSWAP_UUID)" | |
| # ============================================================================= | |
| echo "[4] Creating btrfs subvolumes..." | |
| # ============================================================================= | |
| mkdir -p "$ROOT_MOUNT" | |
| mount /dev/mapper/cryptroot "$ROOT_MOUNT" | |
| btrfs subvolume create "$ROOT_MOUNT/@" | |
| btrfs subvolume create "$ROOT_MOUNT/@var-log" | |
| btrfs subvolume create "$ROOT_MOUNT/@var-cache" | |
| umount "$ROOT_MOUNT" | |
| # ============================================================================= | |
| echo "[5] Mounting target filesystems..." | |
| # ============================================================================= | |
| DISCARD_OPT="" | |
| if [ "$ALLOW_DISCARDS" = "yes" ]; then DISCARD_OPT=",discard=async"; fi | |
| BTRFS_ROOT_OPTS="noatime,compress=zstd:6${DISCARD_OPT}" | |
| mount -o "${BTRFS_ROOT_OPTS},subvol=@" /dev/mapper/cryptroot "$ROOT_MOUNT" | |
| mkdir -p "$ROOT_MOUNT/boot" | |
| mount "$(part 2)" "$ROOT_MOUNT/boot" | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| mkdir -p "$ROOT_MOUNT/boot/efi" | |
| mount "$(part 1)" "$ROOT_MOUNT/boot/efi" | |
| fi | |
| mkdir -p "$ROOT_MOUNT/var/log" "$ROOT_MOUNT/var/cache" "$ROOT_MOUNT/home" | |
| mount -o "${BTRFS_ROOT_OPTS},nosuid,nodev,noexec,subvol=@var-log" /dev/mapper/cryptroot "$ROOT_MOUNT/var/log" | |
| mount -o "${BTRFS_ROOT_OPTS},nosuid,nodev,noexec,subvol=@var-cache" /dev/mapper/cryptroot "$ROOT_MOUNT/var/cache" | |
| mount /dev/mapper/crypthome "$ROOT_MOUNT/home" | |
| # btrfs applies datacow/compression settings filesystem-wide from the FIRST | |
| # mount, so a per-subvolume `nodatacow` mount option is silently ignored. The | |
| # per-directory attribute is the mechanism that actually works, and it only | |
| # affects files created afterwards -- hence: now, while these are empty. | |
| chattr +C "$ROOT_MOUNT/var/log" "$ROOT_MOUNT/var/cache" 2>/dev/null \ | |
| || echo "WARNING: could not set nodatacow (+C) on /var/log and /var/cache" | |
| # No swapon during installation: it buys nothing and only complicates teardown. | |
| echo "[5a] Installing keyfile on the encrypted root..." | |
| install -d -m 700 "$ROOT_MOUNT/etc/cryptsetup-keys.d" | |
| install -m 600 "$KEYFILE_TMP" "$ROOT_MOUNT$KEYFILE_SYS" | |
| # ============================================================================= | |
| echo "[6] Copying XBPS keys..." | |
| # ============================================================================= | |
| mkdir -p "$ROOT_MOUNT/var/db/xbps/keys" | |
| cp /var/db/xbps/keys/* "$ROOT_MOUNT/var/db/xbps/keys/" | |
| echo "[6a] Syncing repositories..." | |
| XBPS_ARCH="$TARGET_ARCH" xbps-install -S -y xbps --rootdir "$ROOT_MOUNT" --repository "${REPO}" | |
| # ============================================================================= | |
| echo "[7] Installing base-system..." | |
| # ============================================================================= | |
| mkdir -p "$ROOT_MOUNT/etc/xbps.d" | |
| cat > "$ROOT_MOUNT/etc/xbps.d/00-repository-main.conf" << EOF | |
| repository=${REPO} | |
| EOF | |
| XBPS_ARCH="$TARGET_ARCH" xbps-install --sync -y --repository "${REPO}" --rootdir "$ROOT_MOUNT" base-system | |
| echo "[7a] Mounting virtual filesystems for chroot..." | |
| mkdir -p "$ROOT_MOUNT/dev" "$ROOT_MOUNT/proc" "$ROOT_MOUNT/sys" "$ROOT_MOUNT/run" "$ROOT_MOUNT/dev/pts" | |
| mount --bind /dev "$ROOT_MOUNT/dev" | |
| mount --bind /proc "$ROOT_MOUNT/proc" | |
| mount --bind /sys "$ROOT_MOUNT/sys" | |
| mount --bind /run "$ROOT_MOUNT/run" | |
| mount --bind /dev/pts "$ROOT_MOUNT/dev/pts" 2>/dev/null || true | |
| echo "[7b] Installing repository and crypto packages..." | |
| XBPS_ARCH="$TARGET_ARCH" xbps-install -y --rootdir "$ROOT_MOUNT" \ | |
| void-repo-nonfree \ | |
| void-repo-multilib \ | |
| void-repo-multilib-nonfree \ | |
| cryptsetup | |
| # ============================================================================= | |
| echo "[8] Generating /etc/fstab..." | |
| # ============================================================================= | |
| # Order matters: mount -a walks fstab top to bottom, so /boot/efi MUST come | |
| # after /boot -- otherwise its mount point does not exist yet and the mount | |
| # fails with "mount point does not exist" on every boot. | |
| # | |
| # /home is fsck pass 0 because the device does not exist when `fsck -A` runs in | |
| # 03-filesystems.sh; it is checked by 02-luks.sh instead, right after unlocking. | |
| { | |
| cat << EOF | |
| # /etc/fstab: static file system information | |
| # | |
| # <file system> <mount point> <type> <options> <dump> <pass> | |
| # | |
| # /boot (p2) is a separate unencrypted ext4 partition because GRUB cannot read | |
| # LUKS2/argon2id headers. root, home and swap are all LUKS2 + argon2id. | |
| # | |
| # @var-log and @var-cache are separate btrfs subvolumes of ROOT so that they can | |
| # be excluded from snapshots of @. They are marked nodatacow via chattr +C on | |
| # the directories (a per-subvolume nodatacow MOUNT option would be ignored: | |
| # btrfs applies those filesystem-wide from the first mount). | |
| UUID=$ROOT_UUID / btrfs ${BTRFS_ROOT_OPTS},subvol=@ 0 0 | |
| UUID=$BOOT_UUID /boot ext4 noatime 0 2 | |
| EOF | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| cat << EOF | |
| UUID=$ESP_UUID /boot/efi vfat defaults,noatime 0 0 | |
| EOF | |
| fi | |
| cat << EOF | |
| UUID=$ROOT_UUID /var/log btrfs ${BTRFS_ROOT_OPTS},nosuid,nodev,noexec,subvol=@var-log 0 0 | |
| UUID=$ROOT_UUID /var/cache btrfs ${BTRFS_ROOT_OPTS},nosuid,nodev,noexec,subvol=@var-cache 0 0 | |
| UUID=$HOME_UUID /home ext4 noatime 0 0 | |
| UUID=$SWAP_UUID none swap sw 0 0 | |
| EOF | |
| } > "$ROOT_MOUNT/etc/fstab" | |
| # ============================================================================= | |
| echo "[9] Base system configuration..." | |
| # ============================================================================= | |
| echo "$HOSTNAME_NEW" > "$ROOT_MOUNT/etc/hostname" | |
| ln -sf "/usr/share/zoneinfo/$TIMEZONE" "$ROOT_MOUNT/etc/localtime" | |
| # Make the hostname resolvable locally. Without a 127.0.1.1 entry, anything | |
| # that looks itself up -- sudo most visibly -- waits for a DNS timeout first. | |
| if ! grep -q "[[:space:]]$HOSTNAME_NEW\$" "$ROOT_MOUNT/etc/hosts" 2>/dev/null; then | |
| printf '127.0.1.1\t%s\n' "$HOSTNAME_NEW" >> "$ROOT_MOUNT/etc/hosts" | |
| fi | |
| echo "[9a] Configuring locales..." | |
| sed -i 's/^#de_AT.UTF-8/de_AT.UTF-8/' "$ROOT_MOUNT/etc/default/libc-locales" | |
| sed -i 's/^#ru_RU.UTF-8/ru_RU.UTF-8/' "$ROOT_MOUNT/etc/default/libc-locales" | |
| chroot "$ROOT_MOUNT" xbps-reconfigure -f glibc-locales | |
| cat > "$ROOT_MOUNT/etc/locale.conf" << 'EOF' | |
| LANG=de_AT.UTF-8 | |
| LC_COLLATE=C | |
| EOF | |
| echo "[9b] Configuring console (runit reads /etc/rc.conf, not vconsole.conf)..." | |
| # /etc/vconsole.conf is a systemd file and is ignored on Void; the keymap has to | |
| # go into /etc/rc.conf, which is what core-services/03-console-setup.sh reads. | |
| rm -f "$ROOT_MOUNT/etc/vconsole.conf" | |
| cat >> "$ROOT_MOUNT/etc/rc.conf" << EOF | |
| # --- set by vm5 installer --- | |
| KEYMAP="$KEYMAP" | |
| HARDWARECLOCK="UTC" | |
| TIMEZONE="$TIMEZONE" | |
| EOF | |
| echo "[9c] Copying DNS configuration into the chroot..." | |
| # Must be re-planted after every package batch that can touch it. NetworkManager | |
| # depends on openresolv, and installing openresolv replaces /etc/resolv.conf with | |
| # a symlink into /run/resolvconf -- which is empty inside the chroot. DNS then | |
| # dies for every later xbps operation, usually mid-install with a name-resolution | |
| # error that looks like a network outage. | |
| sync_resolv_conf() { | |
| rm -f "$ROOT_MOUNT/etc/resolv.conf" | |
| cp -L /etc/resolv.conf "$ROOT_MOUNT/etc/resolv.conf" | |
| chmod 644 "$ROOT_MOUNT/etc/resolv.conf" | |
| } | |
| sync_resolv_conf | |
| # ============================================================================= | |
| echo "[10] Setting up users..." | |
| # ============================================================================= | |
| tty_out "" | |
| tty_out "=== User Account ===" | |
| if [ -z "${USERNAME:-}" ]; then | |
| while :; do | |
| printf "Primary user username [vu5]: " > /dev/tty | |
| IFS= read -r USERNAME < /dev/tty || USERNAME="" | |
| USERNAME=${USERNAME:-vu5} | |
| if [[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]*$ ]] && [ "${#USERNAME}" -le 32 ]; then | |
| break | |
| fi | |
| tty_out "ERROR: invalid username '$USERNAME'." | |
| done | |
| fi | |
| echo "Using username: $USERNAME" | |
| # Filter the group list against groups that actually exist. `xbuilder` in | |
| # particular is not present on a stock base-system, and useradd failing there | |
| # used to abort the whole install right after the root password was set. | |
| USER_GROUPS="" | |
| for g in wheel floppy audio video cdrom optical kvm users xbuilder input plugdev; do | |
| if chroot "$ROOT_MOUNT" getent group "$g" >/dev/null 2>&1; then | |
| USER_GROUPS="${USER_GROUPS:+$USER_GROUPS,}$g" | |
| fi | |
| done | |
| echo "Supplementary groups: $USER_GROUPS" | |
| if ! chroot "$ROOT_MOUNT" id "$USERNAME" >/dev/null 2>&1; then | |
| chroot "$ROOT_MOUNT" useradd -m -G "$USER_GROUPS" -c "$USERNAME" -s /bin/bash "$USERNAME" | |
| fi | |
| tty_out "" | |
| tty_out "=== Password Setup ===" | |
| tty_out "Root password:" | |
| chroot "$ROOT_MOUNT" passwd root < /dev/tty > /dev/tty 2>&1 | |
| tty_out "$USERNAME password:" | |
| chroot "$ROOT_MOUNT" passwd "$USERNAME" < /dev/tty > /dev/tty 2>&1 | |
| ROOT_PW_STATUS=$(chroot "$ROOT_MOUNT" awk -F: '/^root:/ {print $2}' /etc/shadow) | |
| case "$ROOT_PW_STATUS" in | |
| ''|'!'|'*'|'!!') die "root password not set correctly" ;; | |
| *) echo "Root password set successfully." ;; | |
| esac | |
| USER_PW_STATUS=$(chroot "$ROOT_MOUNT" awk -F: -v u="$USERNAME" '$1==u {print $2}' /etc/shadow) | |
| case "$USER_PW_STATUS" in | |
| ''|'!'|'*'|'!!') die "$USERNAME password not set correctly" ;; | |
| *) echo "$USERNAME password set successfully." ;; | |
| esac | |
| chroot "$ROOT_MOUNT" grep -q "^root:.*:/bin/bash$" /etc/passwd || echo "WARNING: root shell is not /bin/bash" | |
| chroot "$ROOT_MOUNT" grep -q "^$USERNAME:.*:/bin/bash$" /etc/passwd || echo "WARNING: $USERNAME shell is not /bin/bash" | |
| echo "[10a] Configuring default editor..." | |
| cat >> "$ROOT_MOUNT/etc/profile" << 'EOF' | |
| export EDITOR=nano | |
| export VISUAL=nano | |
| EOF | |
| for target in "$ROOT_MOUNT/root" "$ROOT_MOUNT/home/$USERNAME"; do | |
| cat > "$target/.bashrc" << 'EOF' | |
| export EDITOR=nano | |
| export VISUAL=nano | |
| EOF | |
| echo nano > "$target/.selected_editor" | |
| done | |
| # These were written by root; without this the user cannot edit their own dotfiles. | |
| # "$USERNAME:" means "the user's login group", whatever useradd actually created. | |
| chroot "$ROOT_MOUNT" chown -R "$USERNAME:" "/home/$USERNAME" | |
| echo "[10b] Configuring sudo..." | |
| mkdir -p "$ROOT_MOUNT/etc/sudoers.d" | |
| cat > "$ROOT_MOUNT/etc/sudoers.d/wheel" << 'EOF' | |
| %wheel ALL=(ALL) ALL | |
| EOF | |
| cat > "$ROOT_MOUNT/etc/sudoers.d/editor" << 'EOF' | |
| Defaults editor=/usr/bin/nano | |
| EOF | |
| chmod 440 "$ROOT_MOUNT/etc/sudoers.d/wheel" "$ROOT_MOUNT/etc/sudoers.d/editor" | |
| chroot "$ROOT_MOUNT" visudo -c | |
| # ============================================================================= | |
| echo "[11] Verifying mounts before bootloader installation..." | |
| # ============================================================================= | |
| check_mount() { | |
| local mp="$1" want="$2" got | |
| got=$(findmnt -rno SOURCE "$mp" 2>/dev/null || true) | |
| [ -n "$got" ] || die "$mp is not mounted" | |
| [ "$got" = "$want" ] || die "$mp is backed by '$got', expected '$want'" | |
| echo " OK $mp <- $got" | |
| } | |
| check_mount "$ROOT_MOUNT/boot" "$(part 2)" | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| check_mount "$ROOT_MOUNT/boot/efi" "$(part 1)" | |
| fi | |
| check_mount "$ROOT_MOUNT/home" "/dev/mapper/crypthome" | |
| # ============================================================================= | |
| echo "[12] Configuring the unlock chain..." | |
| # ============================================================================= | |
| # The initramfs unlocks ROOT and nothing else: | |
| # | |
| # rd.luks.uuid=<root> the one device dracut must open | |
| # rd.luks.crypttab=0 do NOT also process /etc/crypttab. Without this, | |
| # dracut has two independent sources of unlock | |
| # instructions for the same devices, opens each of them | |
| # twice ("already mapped or mounted") and, because the | |
| # keyfile referenced by crypttab does not exist inside | |
| # the initramfs, falls back to an interactive prompt. | |
| # rd.auto=0 no auto-assembly of anything we did not ask for. | |
| # root=/rootflags= stated explicitly, because grub-mkconfig runs inside a | |
| # chroot where /proc/mounts shows host paths and its own | |
| # root detection cannot be trusted. | |
| # | |
| # Deliberately NOT used: | |
| # rd.luks.uuid=keysource:<uuid> not a user-facing parameter (dracut emits | |
| # this form internally); writing it by hand | |
| # just registers the same device a second time. | |
| # rd.luks.key=<path>:<dev> dracut mounts the key device at the btrfs | |
| # DEFAULT subvolume, so the key would have to | |
| # be at /@/etc/... and the whole thing depends | |
| # on initqueue retry ordering. | |
| KCMDLINE="rd.luks.uuid=$CRYPTROOT_UUID rd.luks.crypttab=0 rd.auto=0" | |
| KCMDLINE="$KCMDLINE root=UUID=$ROOT_UUID rootfstype=btrfs rootflags=subvol=@" | |
| if [ "$ALLOW_DISCARDS" = "yes" ]; then KCMDLINE="$KCMDLINE rd.luks.allow-discards"; fi | |
| echo "[12a] Writing /etc/crypttab (secondary volumes only)..." | |
| CRYPTTAB_OPTS="luks" | |
| if [ "$ALLOW_DISCARDS" = "yes" ]; then CRYPTTAB_OPTS="luks,discard"; fi | |
| cat > "$ROOT_MOUNT/etc/crypttab" << EOF | |
| # ROOT is unlocked by the initramfs (see GRUB_CMDLINE_LINUX) and is intentionally | |
| # absent here. These two entries are processed by | |
| # /etc/runit/core-services/02-luks.sh at boot, NOT by dracut | |
| # (the kernel cmdline carries rd.luks.crypttab=0). | |
| crypthome UUID=$CRYPTHOME_UUID $KEYFILE_SYS $CRYPTTAB_OPTS | |
| cryptswap UUID=$CRYPTSWAP_UUID $KEYFILE_SYS $CRYPTTAB_OPTS | |
| EOF | |
| chmod 600 "$ROOT_MOUNT/etc/crypttab" | |
| echo "[12b] Writing dracut configuration..." | |
| mkdir -p "$ROOT_MOUNT/etc/dracut.conf.d" | |
| cat > "$ROOT_MOUNT/etc/dracut.conf.d/10-void-crypt.conf" << 'EOF' | |
| # hostonly=no on purpose. The initramfs is generated inside a chroot whose | |
| # /proc/mounts is the host's, so hostonly device detection is unreliable exactly | |
| # here -- the classic "installed fine, will not boot" failure. A generic | |
| # initramfs costs some space on /boot and is worth it. | |
| hostonly=no | |
| add_dracutmodules+=" crypt btrfs " | |
| # No hibernation: swap is unlocked from a keyfile that only exists once root is | |
| # up, so the resume module can never succeed -- it would only add a spurious | |
| # passphrase prompt for the swap device at every boot. | |
| omit_dracutmodules+=" resume " | |
| # The keyfile is deliberately NOT installed into the initramfs: /boot is | |
| # unencrypted, so anything in there is readable by anyone with physical access. | |
| EOF | |
| echo "[12c] Installing runit core-service for secondary LUKS volumes..." | |
| # Sourced by /etc/runit/1 after 02-kmods.sh and before 03-filesystems.sh, i.e. | |
| # after / is mounted (so the keyfile is readable) and before fsck / mount -a / | |
| # swapon -a need the devices to exist. | |
| cat > "$ROOT_MOUNT/etc/runit/core-services/02-luks.sh" << 'CORESVC' | |
| # 02-luks.sh - unlock non-root LUKS volumes using the keyfile on the encrypted | |
| # root. ROOT itself has already been unlocked by the initramfs. | |
| # | |
| # This runs before 03-filesystems.sh, so /home and swap exist by the time | |
| # `fsck -A`, `mount -a` and `swapon -a` run. | |
| # | |
| # Idempotent by design: a volume that is already open is skipped rather than | |
| # re-opened, which is what produces "already mapped or mounted". | |
| command -v msg >/dev/null 2>&1 || msg() { printf '%s\n' "$*"; } | |
| __luks_keyfile=/etc/cryptsetup-keys.d/data.key | |
| if [ -r /etc/crypttab ] && [ -r "$__luks_keyfile" ]; then | |
| msg "Unlocking secondary LUKS volumes..." | |
| __luks_resolve() { | |
| case "$1" in | |
| UUID=*) blkid -U "${1#UUID=}" 2>/dev/null ;; | |
| LABEL=*) blkid -L "${1#LABEL=}" 2>/dev/null ;; | |
| *) printf '%s\n' "$1" ;; | |
| esac | |
| } | |
| while read -r __luks_name __luks_dev __luks_key __luks_opts; do | |
| case "$__luks_name" in | |
| ''|\#*) continue ;; | |
| esac | |
| [ -n "$__luks_dev" ] || continue | |
| # Already open (e.g. after a re-entry into stage 1): nothing to do. | |
| if [ -e "/dev/mapper/$__luks_name" ]; then | |
| continue | |
| fi | |
| case "$__luks_key" in | |
| ''|none|-) __luks_key="$__luks_keyfile" ;; | |
| esac | |
| if [ ! -r "$__luks_key" ]; then | |
| msg "WARNING: keyfile $__luks_key is not readable, skipping $__luks_name" | |
| continue | |
| fi | |
| # Resolve UUID= without relying on /dev/disk/by-uuid: udev has not | |
| # necessarily populated those symlinks this early. blkid scans the | |
| # devtmpfs nodes directly. | |
| __luks_res=$(__luks_resolve "$__luks_dev") | |
| __luks_try=0 | |
| while [ -z "$__luks_res" ] && [ "$__luks_try" -lt 20 ]; do | |
| sleep 0.25 | |
| __luks_try=$((__luks_try + 1)) | |
| __luks_res=$(__luks_resolve "$__luks_dev") | |
| done | |
| if [ -z "$__luks_res" ] || [ ! -b "$__luks_res" ]; then | |
| msg "WARNING: could not resolve $__luks_dev for $__luks_name" | |
| continue | |
| fi | |
| __luks_extra="" | |
| case ",$__luks_opts," in | |
| *,discard,*) __luks_extra="--allow-discards" ;; | |
| esac | |
| cryptsetup open $__luks_extra --key-file "$__luks_key" "$__luks_res" "$__luks_name" \ | |
| || msg "WARNING: failed to unlock $__luks_name from $__luks_res" | |
| done < /etc/crypttab | |
| # /home is fsck pass 0 in fstab because its device does not exist yet when | |
| # 03-filesystems.sh runs `fsck -A`. Check it here instead, while it is | |
| # unlocked but still unmounted. | |
| if [ -b /dev/mapper/crypthome ]; then | |
| fsck -p /dev/mapper/crypthome >/dev/null 2>&1 \ | |
| || msg "WARNING: fsck reported issues on /home" | |
| fi | |
| unset __luks_name __luks_dev __luks_key __luks_opts __luks_res __luks_try __luks_extra | |
| unset -f __luks_resolve | |
| fi | |
| unset __luks_keyfile | |
| CORESVC | |
| chmod 644 "$ROOT_MOUNT/etc/runit/core-services/02-luks.sh" | |
| # ============================================================================= | |
| echo "[13] Installing bootloader..." | |
| # ============================================================================= | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| chroot "$ROOT_MOUNT" xbps-install -y grub-x86_64-efi efibootmgr | |
| else | |
| chroot "$ROOT_MOUNT" xbps-install -y grub | |
| fi | |
| echo "[13a] Setting kernel command line..." | |
| # Replace rather than append, so re-running the installer cannot leave two | |
| # competing GRUB_CMDLINE_LINUX assignments behind. | |
| sed -i '/^GRUB_CMDLINE_LINUX=/d' "$ROOT_MOUNT/etc/default/grub" | |
| printf '\n# --- set by vm5 installer ---\nGRUB_CMDLINE_LINUX="%s"\n' "$KCMDLINE" \ | |
| >> "$ROOT_MOUNT/etc/default/grub" | |
| echo "GRUB_CMDLINE_LINUX=\"$KCMDLINE\"" | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| echo "[13b] Installing GRUB EFI binary to the ESP..." | |
| chroot "$ROOT_MOUNT" grub-install --no-nvram --target=x86_64-efi \ | |
| --efi-directory=/boot/efi --bootloader-id=Void | |
| echo "[13c] Setting up the UEFI fallback boot path..." | |
| mkdir -p "$ROOT_MOUNT/boot/efi/EFI/BOOT" | |
| cp "$ROOT_MOUNT/boot/efi/EFI/Void/grubx64.efi" "$ROOT_MOUNT/boot/efi/EFI/BOOT/BOOTX64.EFI" | |
| [ -f "$ROOT_MOUNT/boot/efi/EFI/Void/grubx64.efi" ] || die "GRUB EFI binary missing" | |
| [ -f "$ROOT_MOUNT/boot/efi/EFI/BOOT/BOOTX64.EFI" ] || die "fallback boot binary missing" | |
| else | |
| echo "[13b] Installing GRUB (i386-pc) to the MBR of $DISK..." | |
| chroot "$ROOT_MOUNT" grub-install --target=i386-pc "$DISK" | |
| fi | |
| echo "[13d] Generating initramfs and reconfiguring all packages..." | |
| # Before grub-mkconfig, so that grub.cfg is generated against an initramfs that | |
| # already exists. | |
| chroot "$ROOT_MOUNT" xbps-reconfigure -fa | |
| echo "[13e] Generating GRUB configuration..." | |
| chroot "$ROOT_MOUNT" grub-mkconfig -o /boot/grub/grub.cfg | |
| if [ "$BOOT_MODE" = "uefi" ]; then | |
| echo "[13f] Registering UEFI NVRAM entry (best effort)..." | |
| chroot "$ROOT_MOUNT" efibootmgr --create --disk "$DISK" --part 1 \ | |
| --loader '\EFI\Void\grubx64.efi' --label "Void Linux" 2>/dev/null \ | |
| || echo "WARNING: NVRAM entry not created (EFI variables unavailable); the fallback path \\EFI\\BOOT\\BOOTX64.EFI will be used" | |
| fi | |
| echo "[13g] Verifying the generated boot configuration..." | |
| grep -q "rd.luks.uuid=$CRYPTROOT_UUID" "$ROOT_MOUNT/boot/grub/grub.cfg" \ | |
| || echo "WARNING: rd.luks.uuid is missing from grub.cfg" | |
| grep -q "rd.luks.crypttab=0" "$ROOT_MOUNT/boot/grub/grub.cfg" \ | |
| || echo "WARNING: rd.luks.crypttab=0 is missing from grub.cfg -- expect duplicate passphrase prompts" | |
| ls "$ROOT_MOUNT"/boot/initramfs-*.img >/dev/null 2>&1 \ | |
| || echo "WARNING: no initramfs found in /boot" | |
| # ============================================================================= | |
| echo "[14] Updating and installing packages..." | |
| # ============================================================================= | |
| chroot "$ROOT_MOUNT" xbps-install -Syu # xbps itself first | |
| chroot "$ROOT_MOUNT" xbps-install -Syu | |
| # Packages every install gets, desktop or not. | |
| # | |
| # xorg-server-xwayland is NOT an X11 session -- it is the nested X server that | |
| # lets X11 applications run inside a Wayland compositor. Installed explicitly | |
| # so app compatibility does not depend on what a metapackage happens to pull in. | |
| # | |
| # mesa-dri is NOT optional and nothing else pulls it in: base-system does not, | |
| # and kde-plasma/niri only depend on the Mesa *libraries* (libgbm, libEGL), not | |
| # on the hardware drivers in /usr/lib/dri. Without it every Wayland compositor | |
| # dies at startup with "failed to open dri ... No such file or directory" and | |
| # greetd drops you straight back to the greeter. | |
| BASE_PKGS=( | |
| nano | |
| base-devel | |
| elogind | |
| dbus | |
| chrony | |
| socklog-void | |
| NetworkManager | |
| wayland | |
| mesa-dri | |
| xorg-server-xwayland | |
| pipewire | |
| wireplumber | |
| # Optional PipeWire deps per the package's README.voidlinux. alsa-pipewire | |
| # routes ALSA clients through PipeWire (without it, anything using ALSA | |
| # directly grabs the device and blocks everything else); libspa-bluetooth is | |
| # what makes Bluetooth audio devices appear at all. | |
| alsa-pipewire | |
| libspa-bluetooth | |
| # Not installed on purpose: rtkit (realtime priority for audio). It ships | |
| # BOTH a runit service and a D-Bus activation file, which is the same shape | |
| # as elogind -- and enabling both of those is what flooded the console | |
| # earlier. Add it deliberately if you need low-latency audio, after | |
| # checking which of the two mechanisms actually starts it. | |
| rustup | |
| git | |
| ) | |
| # greetd + tuigreet only make sense if there is a session to log into. | |
| GREETER_PKGS=() | |
| if [ "${#DESKTOP_PKGS[@]}" -gt 0 ]; then | |
| GREETER_PKGS=(greetd tuigreet) | |
| fi | |
| # On kde: `kde5` is a transitional dummy these days (it pulls in kde-plasma plus | |
| # two Qt5 leftovers), so kde-plasma is what gets installed -- that is Plasma 6. | |
| # It depends on sddm-kcm (the KDE settings module for SDDM) but NOT on sddm | |
| # itself; the display manager here is greetd. | |
| # | |
| # WAYLAND NOTE: kde-plasma depends on plasma-workspace-x11, so the X11 Plasma | |
| # session is installed on the kde/both paths whether you want it or not. It is | |
| # small -- plasma-workspace-x11 is literally two files (/usr/bin/startplasma-x11 | |
| # and /usr/share/xsessions/plasmax11.desktop) plus a dependency on kwin-x11 | |
| # (~9 MB) -- and it is unreachable, because tuigreet is never given --xsessions. | |
| # | |
| # DO NOT try to remove it afterwards with `xbps-remove plasma-workspace-x11`. | |
| # kde-plasma is what depends on it, so xbps drops the metapackage too, and every | |
| # other Plasma component was installed automatically *as a dependency of that | |
| # metapackage*. The moment the meta is gone they are all orphans, and the next | |
| # `xbps-remove -o` on the system deletes the entire desktop. | |
| # | |
| # The only safe way to get a strictly Wayland KDE is to never install the meta: | |
| # install kde-plasma's dependency list explicitly, minus plasma-workspace-x11. | |
| # That list then has to be maintained by hand as Plasma evolves, which is why it | |
| # is not the default here. The niri path has no X11 session at all. | |
| echo "[14a] Detecting graphics hardware and microcode..." | |
| # Read straight out of sysfs rather than shelling out to lspci, which is not | |
| # guaranteed to be on the live image. PCI class 0x03xxxx is "display | |
| # controller"; the vendor IDs are AMD/ATI 0x1002, Intel 0x8086, NVIDIA 0x10de. | |
| # | |
| # Missing GPU firmware is not a cosmetic problem: the KMS driver fails to bind, | |
| # /dev/dri/card0 never appears, and every Wayland session exits immediately. | |
| # Override the detection with GPU=amd|intel|nvidia|all|none if it guesses wrong. | |
| detect_gpu_vendors() { | |
| local d class vendor out="" | |
| for d in /sys/bus/pci/devices/*; do | |
| [ -r "$d/class" ] && [ -r "$d/vendor" ] || continue | |
| class=$(cat "$d/class") | |
| case "$class" in 0x03*) ;; *) continue ;; esac | |
| vendor=$(cat "$d/vendor") | |
| case "$vendor" in | |
| 0x1002) case " $out " in *" amd "*) ;; *) out="$out amd" ;; esac ;; | |
| 0x8086) case " $out " in *" intel "*) ;; *) out="$out intel" ;; esac ;; | |
| 0x10de) case " $out " in *" nvidia "*) ;; *) out="$out nvidia" ;; esac ;; | |
| esac | |
| done | |
| printf '%s' "$out" | |
| } | |
| HW_PKGS=() | |
| case "${GPU:-auto}" in | |
| auto) GPU_VENDORS="$(detect_gpu_vendors)" ;; | |
| all) GPU_VENDORS="amd intel nvidia" ;; | |
| none) GPU_VENDORS="" ;; | |
| *) GPU_VENDORS="${GPU}" ;; | |
| esac | |
| for v in $GPU_VENDORS; do | |
| case "$v" in | |
| amd) HW_PKGS+=(linux-firmware-amd) ;; | |
| intel) HW_PKGS+=(linux-firmware-intel) ;; | |
| nvidia) HW_PKGS+=(linux-firmware-nvidia) ;; | |
| *) echo " WARNING: unknown GPU vendor '$v', ignoring" ;; | |
| esac | |
| done | |
| if [ -z "$GPU_VENDORS" ] && [ "${GPU:-auto}" = "auto" ]; then | |
| echo " No PCI display controller recognised (VM or exotic hardware?)." | |
| echo " Installing the full linux-firmware set to be safe." | |
| HW_PKGS+=(linux-firmware) | |
| else | |
| echo " GPU vendors: ${GPU_VENDORS:-none}" | |
| fi | |
| # CPU microcode. linux-firmware-amd already carries AMD CPU microcode; Intel | |
| # ships separately as intel-ucode. | |
| if grep -qi 'GenuineIntel' /proc/cpuinfo 2>/dev/null; then | |
| HW_PKGS+=(intel-ucode) | |
| echo " CPU: Intel -> intel-ucode" | |
| elif grep -qi 'AuthenticAMD' /proc/cpuinfo 2>/dev/null; then | |
| case " ${HW_PKGS[*]} " in | |
| *" linux-firmware-amd "*) ;; | |
| *) HW_PKGS+=(linux-firmware-amd) ;; | |
| esac | |
| echo " CPU: AMD -> linux-firmware-amd" | |
| fi | |
| echo " Hardware packages: ${HW_PKGS[*]:-none}" | |
| chroot "$ROOT_MOUNT" xbps-install -Sy \ | |
| "${BASE_PKGS[@]}" "${HW_PKGS[@]}" "${GREETER_PKGS[@]}" "${DESKTOP_PKGS[@]}" | |
| # openresolv came in with NetworkManager and will have taken over | |
| # /etc/resolv.conf; restore a usable one for the rest of the install. | |
| sync_resolv_conf | |
| echo "[14b] Verifying graphics drivers..." | |
| # /usr/lib/dri holds both the hardware drivers and Mesa's GBM backend | |
| # (dri_gbm.so). If it is empty, every Wayland compositor dies at startup with | |
| # "MESA-LOADER: failed to open ... dri_gbm.so: cannot open shared object file" | |
| # and greetd bounces you back to the greeter -- so fail loudly here instead. | |
| if ls "$ROOT_MOUNT"/usr/lib/dri/*.so >/dev/null 2>&1; then | |
| echo " OK: $(ls "$ROOT_MOUNT"/usr/lib/dri/*.so | wc -l) Mesa DRI objects present" | |
| else | |
| echo " WARNING: /usr/lib/dri is empty -- mesa-dri did not install." | |
| echo " Wayland sessions WILL fail. Fix before rebooting." | |
| fi | |
| # Firmware presence is checkable; whether the KMS driver actually binds and | |
| # creates /dev/dri/card0 is not -- that only happens on the installed kernel. | |
| for f in "${HW_PKGS[@]}"; do | |
| case "$f" in | |
| linux-firmware*) | |
| if [ -d "$ROOT_MOUNT/usr/lib/firmware" ]; then | |
| echo " OK: firmware tree present for $f" | |
| else | |
| echo " WARNING: $f installed but /usr/lib/firmware is missing" | |
| fi | |
| break ;; | |
| esac | |
| done | |
| # ============================================================================= | |
| echo "[15] Enabling runit services..." | |
| # ============================================================================= | |
| # Deliberately after package installation: symlinking a service before its | |
| # package exists leaves a dangling link that runsvdir complains about forever. | |
| # | |
| # socklog-unix + nanoklogd (from socklog-void) give runit somewhere to put | |
| # service and kernel output. Without them a misbehaving service writes straight | |
| # to the console, which is why a restart loop can render tty1 unusable rather | |
| # than just being a line in a log. Logs then live under /var/log/socklog/. | |
| enable_service() { | |
| local svc="$1" | |
| if [ -d "$ROOT_MOUNT/etc/sv/$svc" ]; then | |
| ln -sfn "/etc/sv/$svc" "$ROOT_MOUNT/etc/runit/runsvdir/default/$svc" | |
| echo " enabled: $svc" | |
| else | |
| echo " skipped (not installed): $svc" | |
| fi | |
| } | |
| # NOTE: `elogind` is deliberately NOT in this list. elogind is D-Bus activated: | |
| # dbus-daemon starts it on the first org.freedesktop.login1 call. Enabling the | |
| # runit service as well is a race -- whichever loses finds the lock already held, | |
| # exits with "elogind is already running under pid NNN", and runsv restarts it a | |
| # second later, forever, flooding the console. The Void handbook only suggests | |
| # enabling the service if D-Bus activation actually gives you trouble; if you | |
| # ever need it, `ln -s /etc/sv/elogind /var/service/` -- but then it is the only | |
| # one of the two that may be active. | |
| # | |
| # NetworkManager replaces dhcpcd -- they must NOT both be enabled. Both want to | |
| # own the interface and write /etc/resolv.conf, and the result is an address that | |
| # comes and goes. base-system ships dhcpcd enabled by default, so it has to be | |
| # explicitly disabled below, not merely left out of this list. | |
| # | |
| # wpa_supplicant likewise stays disabled: NetworkManager starts and manages its | |
| # own instance, and a second supervised one fights it for the wireless device. | |
| # | |
| # greetd is in this list unconditionally, but enable_service skips it when it is | |
| # not installed -- which is the case for DESKTOP=none. | |
| for svc in NetworkManager dbus chronyd greetd socklog-unix nanoklogd; do | |
| enable_service "$svc" | |
| done | |
| for svc in dhcpcd wpa_supplicant; do | |
| if [ -e "$ROOT_MOUNT/etc/runit/runsvdir/default/$svc" ]; then | |
| rm -f "$ROOT_MOUNT/etc/runit/runsvdir/default/$svc" | |
| echo " disabled: $svc (superseded by NetworkManager)" | |
| fi | |
| done | |
| echo "[15a] Configuring PipeWire / WirePlumber..." | |
| # PipeWire is deliberately NOT enabled as a runit service. Void's own | |
| # README.voidlinux for the package says of /usr/share/examples/pipewire/sv: | |
| # "experimental and only needed in rare cases, so using it should be avoided | |
| # in most setups" | |
| # PipeWire is a per-session daemon; one system-wide instance cannot serve | |
| # per-user sessions correctly. | |
| # | |
| # The documented arrangement instead: | |
| # 1. drop-ins in /etc/pipewire/pipewire.conf.d make the pipewire daemon launch | |
| # wireplumber (session manager) and pipewire-pulse (PulseAudio API) itself, | |
| # so only ONE thing needs autostarting; | |
| # 2. pipewire.desktop symlinked into /etc/xdg/autostart starts that one thing. | |
| # This covers Plasma and niri alike -- niri's own default config notes that | |
| # "running niri as a session supports xdg-desktop-autostart". | |
| # Do not also autostart wireplumber.desktop: the drop-in already starts it, and | |
| # a second instance just fights the first for the audio devices. | |
| install -d -m 755 "$ROOT_MOUNT/etc/pipewire/pipewire.conf.d" | |
| for conf in /usr/share/examples/wireplumber/10-wireplumber.conf \ | |
| /usr/share/examples/pipewire/20-pipewire-pulse.conf; do | |
| if [ -e "$ROOT_MOUNT$conf" ]; then | |
| ln -sfn "$conf" "$ROOT_MOUNT/etc/pipewire/pipewire.conf.d/$(basename "$conf")" | |
| echo " linked $(basename "$conf")" | |
| else | |
| echo " WARNING: $conf not found in the target root" | |
| fi | |
| done | |
| install -d -m 755 "$ROOT_MOUNT/etc/xdg/autostart" | |
| if [ -e "$ROOT_MOUNT/usr/share/applications/pipewire.desktop" ]; then | |
| ln -sfn /usr/share/applications/pipewire.desktop \ | |
| "$ROOT_MOUNT/etc/xdg/autostart/pipewire.desktop" | |
| echo " pipewire will autostart in every desktop session" | |
| else | |
| echo " WARNING: pipewire.desktop not found; audio will not autostart" | |
| fi | |
| if [ -d "$ROOT_MOUNT/etc/greetd" ]; then | |
| echo "[15b] Configuring greetd to use tuigreet..." | |
| # The greetd package ships /etc/greetd/config.toml as a conf_file, running | |
| # `agreety --cmd /bin/sh`. It is therefore always present and non-empty, so | |
| # this must overwrite it -- a "only write if missing" guard silently leaves | |
| # the stock agreety greeter in place and tuigreet never appears. | |
| if [ -s "$ROOT_MOUNT/etc/greetd/config.toml" ] \ | |
| && ! grep -q 'set by vm5 installer' "$ROOT_MOUNT/etc/greetd/config.toml"; then | |
| cp -a "$ROOT_MOUNT/etc/greetd/config.toml" "$ROOT_MOUNT/etc/greetd/config.toml.stock" | |
| echo " stock config saved as /etc/greetd/config.toml.stock" | |
| fi | |
| # Flags per the upstream tuigreet usage (apognu/tuigreet, the source Void | |
| # packages). --sessions takes WAYLAND session paths only. | |
| # | |
| # This is a Wayland-only setup, so --xsessions is deliberately NOT passed: | |
| # the kde-plasma metapackage drags in plasma-workspace-x11, which drops an | |
| # X11 session file into /usr/share/xsessions. Leaving --xsessions off keeps | |
| # that out of the greeter's session list. XWayland is still installed, so | |
| # X11 *applications* run fine inside the Wayland sessions. | |
| # | |
| # Upstream's example uses user = "greeter"; on Void the greetd package | |
| # creates "_greeter" instead. Upstream also requires a cache directory | |
| # owned by the greeter for the --remember* flags -- Void's tuigreet package | |
| # already ships /var/cache/tuigreet 0755 _greeter:_greeter, so nothing to do. | |
| # | |
| # --session-wrapper runs the session through a LOGIN shell. greetd does not | |
| # give sessions one, so without this /etc/profile is never sourced and the | |
| # session starts with a minimal PATH and XDG_DATA_DIRS. Upstream niri hides | |
| # this by re-exec'ing itself through `exec -l "$SHELL" -c ...` inside its | |
| # niri-session script -- which is exactly the script we have to bypass on | |
| # runit (see [15c]), so the wrapper has to put it back. | |
| cat > "$ROOT_MOUNT/etc/greetd/config.toml" << EOF | |
| # set by vm5 installer | |
| [terminal] | |
| vt = $GREETD_VT | |
| [default_session] | |
| command = "/usr/bin/tuigreet --time --remember --remember-session --sessions /usr/share/wayland-sessions --session-wrapper '/bin/bash --login -c'" | |
| user = "_greeter" | |
| EOF | |
| chmod 644 "$ROOT_MOUNT/etc/greetd/config.toml" | |
| echo " greetd configured on vt $GREETD_VT" | |
| # Nothing may share a VT with greetd: Void enables agetty-tty1..tty6, so | |
| # vt 1 has to have its agetty removed. tty2..tty6 are deliberately left | |
| # alone -- they are the way back in if the greeter ever fails to start. | |
| if [ "$GREETD_VT" = "1" ]; then | |
| rm -f "$ROOT_MOUNT/etc/runit/runsvdir/default/agetty-tty1" | |
| echo " disabled agetty-tty1 (greetd owns vt 1; tty2-tty6 unchanged)" | |
| echo " NOTE: if the greeter fails, recover from tty2 (Ctrl+Alt+F2)." | |
| else | |
| echo " agetty-tty1..tty6 left enabled; reach the greeter with Ctrl+Alt+F$GREETD_VT" | |
| fi | |
| if ls "$ROOT_MOUNT"/usr/share/wayland-sessions/*.desktop >/dev/null 2>&1; then | |
| echo " Wayland sessions offered by the greeter:" | |
| for s in "$ROOT_MOUNT"/usr/share/wayland-sessions/*.desktop; do | |
| echo " - $(basename "$s" .desktop)" | |
| done | |
| else | |
| echo " WARNING: no Wayland session .desktop files found; tuigreet will" | |
| echo " start but offer nothing to log into." | |
| fi | |
| fi | |
| NIRI_SESSION="$ROOT_MOUNT/usr/share/wayland-sessions/niri.desktop" | |
| if [ -e "$NIRI_SESSION" ]; then | |
| echo "[15c] Fixing the niri session entry for runit..." | |
| # Upstream ships niri.desktop with `Exec=niri-session`. niri-session is a | |
| # shell script that drives a *user service manager*: it handles systemd and | |
| # dinit, and its final branch is | |
| # echo "No systemd or dinit detected, please use niri --session instead." | |
| # Void is runit, so on this system it either exits immediately with that | |
| # message, or is not found at all -- Void's niri template installs | |
| # niri.desktop, niri-portals.conf and the sample config, but not the script. | |
| # Selecting niri in the greeter therefore dies instantly and looks like a | |
| # hang or an instant bounce back to the greeter. | |
| # | |
| # The fix is the one niri itself prescribes in that message, and that its | |
| # wiki confirms: "run niri-session (systemd/dinit) or niri --session | |
| # (others)". | |
| # | |
| # dbus-run-session is prepended because --session, per the same wiki, will | |
| # "import its environment variables globally into the system manager and | |
| # D-Bus, and start its D-Bus services" -- so niri needs a D-Bus SESSION bus. | |
| # systemd systems always have a per-user one; runit has none, and greetd | |
| # does not create one either. A D-Bus client with no DBUS_SESSION_BUS_ADDRESS | |
| # falls back to autolaunch, which blocks: niri prints "loaded config from | |
| # ..." and then hangs forever with no error. Plasma is unaffected because | |
| # startplasma-wayland starts its own bus before doing anything else. | |
| sed -i 's|^Exec=niri-session[[:space:]]*$|Exec=dbus-run-session niri --session|' "$NIRI_SESSION" | |
| if grep -q '^Exec=dbus-run-session niri --session$' "$NIRI_SESSION"; then | |
| echo " Exec set to 'dbus-run-session niri --session'" | |
| else | |
| echo " WARNING: could not patch niri.desktop -- check its Exec= line:" | |
| grep '^Exec=' "$NIRI_SESSION" | sed 's/^/ /' | |
| fi | |
| # niri.desktop is a regular package file, not a conf_file, so the next niri | |
| # update would restore the broken Exec= and silently break the session | |
| # again. Tell xbps to leave our copy alone. | |
| cat > "$ROOT_MOUNT/etc/xbps.d/10-niri-session.conf" << 'EOF' | |
| # niri.desktop upstream uses Exec=niri-session, which only supports systemd and | |
| # dinit user service managers and refuses to run under runit. The installer | |
| # rewrote it to `Exec=dbus-run-session niri --session`: niri --session is what | |
| # niri prescribes for other init systems, and dbus-run-session supplies the | |
| # D-Bus session bus that runit does not provide (without it niri loads its | |
| # config and then hangs). Without this noextract the next niri update would put | |
| # the broken version back. | |
| # Remove this file if upstream/Void ever ships a runit-aware niri-session. | |
| noextract=/usr/share/wayland-sessions/niri.desktop | |
| EOF | |
| echo " pinned against package updates via /etc/xbps.d/10-niri-session.conf" | |
| fi | |
| if [ "$EXTRA_REPOS" = "yes" ]; then | |
| # ========================================================================= | |
| echo "[15d] Adding third-party XBPS repositories..." | |
| # ========================================================================= | |
| # UNOFFICIAL REPOSITORIES. Everything installed from these is built and | |
| # signed by their maintainers, not by Void, and xbps installs it as root. | |
| # The installer runs unattended, so `xbps-install -y` accepts their signing | |
| # keys without a fingerprint prompt -- the keys actually trusted are printed | |
| # below, and live in /var/db/xbps/keys. Set EXTRA_REPOS=no to skip all of | |
| # this and keep the system on official repositories only. | |
| # | |
| # Declared AFTER the official repository on purpose. Blackhole-VL's own | |
| # README tells you to prepend its mirror to 00-repository-main.conf, which | |
| # lets it take precedence for any package name it shares with Void. Using | |
| # a 30-* filename keeps the official repository first, so these two only | |
| # supply what Void does not ship. Reverse it only if you specifically want | |
| # their builds of common packages. | |
| cat > "$ROOT_MOUNT/etc/xbps.d/30-blackhole-vl.conf" << EOF | |
| # Blackhole-VL -- unofficial Void repository (Hyprland and other extra packages) | |
| # https://github.com/Event-Horizon-VL/blackhole-vl | |
| repository=https://mirror.black-hole.dev/${TARGET_ARCH}/ | |
| EOF | |
| cat > "$ROOT_MOUNT/etc/xbps.d/30-librewolf.conf" << 'EOF' | |
| # LibreWolf for Void -- unofficial, served from GitHub release assets | |
| # https://github.com/index-0/librewolf-void | |
| # Provides: librewolf (install with: xbps-install -S librewolf) | |
| repository=https://github.com/index-0/librewolf-void/releases/latest/download/ | |
| EOF | |
| cat > "$ROOT_MOUNT/etc/xbps.d/30-oco.conf" << EOF | |
| # oco -- "vOid Community repOsitory", unofficial, ~400 community packages | |
| # https://codeberg.org/oSoWoSo/oco (default branch is OCO, not main) | |
| repository=https://repo.osowoso.org/${TARGET_ARCH} | |
| EOF | |
| echo " 30-blackhole-vl.conf -> https://mirror.black-hole.dev/${TARGET_ARCH}/" | |
| echo " 30-librewolf.conf -> github.com/index-0/librewolf-void releases" | |
| echo " 30-oco.conf -> https://repo.osowoso.org/${TARGET_ARCH}" | |
| # oco is the one repository here that publishes its signing key as a file | |
| # rather than relying on fingerprint-on-first-sync, so install it the way | |
| # its README documents. Not fatal if unreachable: xbps then falls back to | |
| # prompting/accepting the key on sync like the other two. | |
| OCO_KEY_URL="https://codeberg.org/oSoWoSo/oco/raw/branch/OCO/keys/oco-repo-key.plist" | |
| if command -v curl >/dev/null 2>&1 && | |
| curl -fsSL "$OCO_KEY_URL" -o "$ROOT_MOUNT/var/db/xbps/keys/oco-repo-key.plist"; then | |
| chmod 644 "$ROOT_MOUNT/var/db/xbps/keys/oco-repo-key.plist" | |
| echo " installed oco signing key from codeberg" | |
| else | |
| rm -f "$ROOT_MOUNT/var/db/xbps/keys/oco-repo-key.plist" | |
| echo " WARNING: could not fetch the oco signing key; it will be accepted" | |
| echo " on first sync instead. Key URL: $OCO_KEY_URL" | |
| fi | |
| # Sync so the indexes and signing keys are in place before first boot. A | |
| # third-party mirror being down must not abort an otherwise good install. | |
| KEYS_BEFORE=$(ls "$ROOT_MOUNT/var/db/xbps/keys/" 2>/dev/null | wc -l) | |
| if chroot "$ROOT_MOUNT" xbps-install -Sy; then | |
| KEYS_AFTER=$(ls "$ROOT_MOUNT/var/db/xbps/keys/" 2>/dev/null | wc -l) | |
| echo " Signing keys trusted: $KEYS_BEFORE -> $KEYS_AFTER" | |
| echo " Keys in /var/db/xbps/keys (filename = fingerprint):" | |
| ls "$ROOT_MOUNT/var/db/xbps/keys/" 2>/dev/null | sed 's/^/ /' | |
| else | |
| echo " WARNING: could not sync the third-party repositories." | |
| echo " The .conf files are in place; run 'sudo xbps-install -S'" | |
| echo " after first boot and accept the fingerprints yourself." | |
| fi | |
| fi | |
| # ============================================================================= | |
| echo "[16] Backing up LUKS headers..." | |
| # ============================================================================= | |
| # A damaged header means the data is gone for good, passphrase or not. These | |
| # backups are encrypted with the same passphrase as the volumes themselves. | |
| install -d -m 700 "$ROOT_MOUNT/root/luks-headers" | |
| cryptsetup luksHeaderBackup "$(part 3)" --header-backup-file "$ROOT_MOUNT/root/luks-headers/root.header" | |
| cryptsetup luksHeaderBackup "$(part 4)" --header-backup-file "$ROOT_MOUNT/root/luks-headers/home.header" | |
| cryptsetup luksHeaderBackup "$(part 5)" --header-backup-file "$ROOT_MOUNT/root/luks-headers/swap.header" | |
| chmod 600 "$ROOT_MOUNT"/root/luks-headers/*.header | |
| echo "Headers written to /root/luks-headers/ on the installed system." | |
| echo "IMPORTANT: copy them to external media. The root header backup stored" | |
| echo " inside the root volume cannot help you if that header is lost." | |
| echo "[16a] Wiping temporary key material..." | |
| shred -u "$KEYFILE_TMP" 2>/dev/null || rm -f "$KEYFILE_TMP" | |
| rm -rf "$KEYDIR_TMP" | |
| FINISHED=1 | |
| # ============================================================================= | |
| echo "" | |
| echo "=== Installation complete ===" | |
| echo "" | |
| echo "Summary:" | |
| echo " Hostname: $HOSTNAME_NEW" | |
| echo " Disk: $DISK ($BOOT_MODE)" | |
| echo " Root: $(part 3) LUKS2 -> btrfs (@, @var-log, @var-cache)" | |
| echo " Home: $(part 4) LUKS2 -> ext4" | |
| echo " Swap: $(part 5) LUKS2 -> swap" | |
| echo " Unlock: one passphrase for root at boot; home and swap follow" | |
| echo " automatically from $KEYFILE_SYS" | |
| echo " Recovery: the same passphrase also opens home and swap directly," | |
| echo " e.g. from a live ISO: cryptsetup open $(part 4) home" | |
| echo " Desktop: $DESKTOP (Wayland only)" | |
| if [ "${#DESKTOP_PKGS[@]}" -gt 0 ]; then | |
| echo " Greeter: greetd + tuigreet on vt $GREETD_VT" | |
| if [ "$GREETD_VT" = "1" ]; then | |
| echo " appears automatically at the end of boot;" | |
| echo " if it does not, recover from tty2 (Ctrl+Alt+F2)" | |
| else | |
| echo " does NOT start automatically -- Ctrl+Alt+F$GREETD_VT" | |
| fi | |
| if [ "$DESKTOP" = "niri" ] || [ "$DESKTOP" = "both" ]; then | |
| echo " niri keys: Mod+T alacritty, Mod+D fuzzel, Super+Alt+L swaylock" | |
| echo " Waybar autostarts and uses the defaults the package" | |
| echo " ships in /etc/xdg/waybar/; copy them to" | |
| echo " ~/.config/waybar/ to customise" | |
| echo " Session entry patched to" | |
| echo " 'dbus-run-session niri --session' (runit has no" | |
| echo " per-user D-Bus session bus)" | |
| fi | |
| else | |
| echo " Greeter: none (console login on tty1-tty6)" | |
| fi | |
| echo " Log: $LOGFILE" | |
| echo "" | |
| # ============================================================================= | |
| printf "Unmount and close everything now? (Y/n): " > /dev/tty | |
| IFS= read -r UNMOUNT_CHOICE < /dev/tty || UNMOUNT_CHOICE="" | |
| UNMOUNT_CHOICE=${UNMOUNT_CHOICE:-Y} | |
| if [[ "$UNMOUNT_CHOICE" =~ ^[Yy]$ ]]; then | |
| echo "Unmounting and closing LUKS mappings..." | |
| cleanup | |
| echo "Done." | |
| echo "" | |
| printf "Reboot now? (y/N): " > /dev/tty | |
| IFS= read -r REBOOT_CHOICE < /dev/tty || REBOOT_CHOICE="" | |
| REBOOT_CHOICE=${REBOOT_CHOICE:-N} | |
| if [[ "$REBOOT_CHOICE" =~ ^[Yy]$ ]]; then | |
| echo "Rebooting..." | |
| reboot | |
| else | |
| echo "Reboot manually when ready." | |
| fi | |
| else | |
| echo "Left mounted. Manual teardown:" | |
| echo " umount $ROOT_MOUNT/dev/pts $ROOT_MOUNT/dev $ROOT_MOUNT/proc $ROOT_MOUNT/sys $ROOT_MOUNT/run" | |
| echo " umount -R $ROOT_MOUNT" | |
| echo " cryptsetup close cryptswap; cryptsetup close crypthome; cryptsetup close cryptroot" | |
| fi |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
TDOO: add /var/log and var/cache subvolumes, // remove from root snapshots