Skip to content

Instantly share code, notes, and snippets.

@Yaksinikos
Last active August 12, 2026 11:59
Show Gist options
  • Select an option

  • Save Yaksinikos/32542f78c7b4d015f85b8a449fe7dcda to your computer and use it in GitHub Desktop.

Select an option

Save Yaksinikos/32542f78c7b4d015f85b8a449fe7dcda to your computer and use it in GitHub Desktop.
* text=auto eol=lf
*.sh text eol=lf
#!/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
@Yaksinikos

Copy link
Copy Markdown
Author

TDOO: add /var/log and var/cache subvolumes, // remove from root snapshots

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment