Skip to content

Instantly share code, notes, and snippets.

@cezarlamann
Last active October 12, 2025 19:04
Show Gist options
  • Select an option

  • Save cezarlamann/aa739d907cbd7424f184cb6cbee62dd0 to your computer and use it in GitHub Desktop.

Select an option

Save cezarlamann/aa739d907cbd7424f184cb6cbee62dd0 to your computer and use it in GitHub Desktop.
Void installation script for two drives + LUKS + BTRFS + Snapshots without LVM
#!/usr/bin/env bash
set -euo pipefail
### ====== EDIT ME (DISKS & BASICS) ==========================================
# Root (NVMe) disk -> ESP + LUKS1 for root btrfs
ROOT_DISK="/dev/nvme0n1"
# Data (SATA/SSD) disk -> LUKS1 for data btrfs
DATA_DISK="/dev/sda"
# Partition sizes
ESP_SIZE="1024MiB" # EFI System Partition size on ROOT_DISK
# System identity
HOSTNAME="void-hp"
USERNAME="cezar"
USER_SHELL="/bin/bash"
TZ="Europe/Madrid" # timezone
KEYMAP="br-abnt2" # console keymap
LOCALE="pt_BR.UTF-8 UTF-8" # glibc locale
# Swap size (hibernation capable)
SWAP_SIZE="40G"
# XBPS repo & arch
REPO="https://repo-fastly.voidlinux.org/current/"
ARCH="x86_64"
### ====== LIVE ISO PREFLIGHT (tools needed before installation) ==============
echo "==> Ensuring required live-ISO tools are present..."
need() { command -v "$1" >/dev/null 2>&1 || xbps-install -Sy "$2"; }
# For downloading and partitioning
need curl curl
need sgdisk gptfdisk
need partprobe parted
# Helper: re-read partition table safely
reread_pt() {
local disk="$1"
if command -v partprobe >/dev/null 2>&1; then
partprobe "$disk" || true
else
blockdev --rereadpt "$disk" || true
command -v udevadm >/dev/null 2>&1 && udevadm settle || true
sleep 1
fi
}
### ====== NO EDITS BELOW UNLESS YOU KNOW WHAT YOU'RE DOING ===================
echo "==> Validating disks..."
[[ -b "$ROOT_DISK" ]] || { echo "Root disk not found: $ROOT_DISK"; exit 1; }
[[ -b "$DATA_DISK" ]] || { echo "Data disk not found: $DATA_DISK"; exit 1; }
echo "==> Wiping old signatures..."
wipefs -a "$ROOT_DISK"
wipefs -a "$DATA_DISK"
echo "==> Partitioning $ROOT_DISK (GPT: ESP + root LUKS)..."
sgdisk --zap-all "$ROOT_DISK"
# ESP
sgdisk -n 1:0:+"$ESP_SIZE" -t 1:ef00 -c 1:"EFI System Partition" "$ROOT_DISK"
# root LUKS
sgdisk -n 2:0:0 -t 2:8300 -c 2:"cryptroot" "$ROOT_DISK"
reread_pt "$ROOT_DISK"
echo "==> Partitioning $DATA_DISK (GPT: data LUKS)..."
sgdisk --zap-all "$DATA_DISK"
sgdisk -n 1:0:0 -t 1:8300 -c 1:"crypthome" "$DATA_DISK"
reread_pt "$DATA_DISK"
ESP_PART="${ROOT_DISK}p1"
ROOT_PART="${ROOT_DISK}p2"
DATA_PART="${DATA_DISK}1"
# Handle non-NVMe naming (e.g., /dev/sda1 already ends with 1)
[[ -b "$ESP_PART" ]] || ESP_PART="${ROOT_DISK}1"
[[ -b "$ROOT_PART" ]] || ROOT_PART="${ROOT_DISK}2"
echo "==> Creating LUKS1 containers..."
cryptsetup luksFormat --type luks1 -y "$ROOT_PART"
cryptsetup luksFormat --type luks1 -y "$DATA_PART"
echo "==> Opening LUKS containers..."
cryptsetup open "$ROOT_PART" cryptroot
cryptsetup open "$DATA_PART" cryptdata
echo "==> Creating filesystems..."
mkfs.vfat -F32 -n EFI "$ESP_PART"
mkfs.btrfs -L void_root -m single /dev/mapper/cryptroot
mkfs.btrfs -L void_data -m single /dev/mapper/cryptdata
echo "==> Creating Btrfs subvolumes..."
# Root FS: mount top-level, create subvols, unmount
mount /dev/mapper/cryptroot /mnt
btrfs subvolume create /mnt/@
btrfs subvolume create /mnt/@opt
btrfs subvolume create /mnt/@usr_local
btrfs subvolume create /mnt/@root
umount /mnt
# Data FS: mount top-level, create subvols, unmount
mount /dev/mapper/cryptdata /mnt
btrfs subvolume create /mnt/@home
btrfs subvolume create /mnt/@snapshots
btrfs subvolume create /mnt/@var_lib
btrfs subvolume create /mnt/@var_log
btrfs subvolume create /mnt/@var_cache
btrfs subvolume create /mnt/@var_tmp
btrfs subvolume create /mnt/@var_swap
umount /mnt
echo "==> Mounting subvolumes..."
BTRFS_OPTS="rw,noatime,ssd,compress=zstd,space_cache=v2"
DATA_MAPPER="/dev/mapper/cryptdata" # <- ensure this matches your 'cryptsetup open' name
# Root (drive #1)
mount -o "$BTRFS_OPTS",subvol=@ /dev/mapper/cryptroot /mnt
mkdir -p /mnt/{opt,usr/local,root,home,snapshots}
mkdir -p /mnt/var /mnt/var/{lib,log,cache,tmp,swap}
mount -o "$BTRFS_OPTS",subvol=@opt /dev/mapper/cryptroot /mnt/opt
mount -o "$BTRFS_OPTS",subvol=@usr_local /dev/mapper/cryptroot /mnt/usr/local
mount -o "$BTRFS_OPTS",subvol=@root /dev/mapper/cryptroot /mnt/root
# Data (drive #2)
mount -o "$BTRFS_OPTS",subvol=@home "$DATA_MAPPER" /mnt/home
mount -o "$BTRFS_OPTS",subvol=@snapshots "$DATA_MAPPER" /mnt/snapshots
# /var subtree mounts (order matters)
mount -o "$BTRFS_OPTS",subvol=@var_lib "$DATA_MAPPER" /mnt/var/lib
mount -o "$BTRFS_OPTS",subvol=@var_log "$DATA_MAPPER" /mnt/var/log
mount -o "$BTRFS_OPTS",subvol=@var_cache "$DATA_MAPPER" /mnt/var/cache
mount -o "$BTRFS_OPTS",subvol=@var_tmp "$DATA_MAPPER" /mnt/var/tmp
mount -o "$BTRFS_OPTS",subvol=@var_swap "$DATA_MAPPER" /mnt/var/swap
# ESP
mkdir -p /mnt/boot/efi
mount -o rw,noatime "$ESP_PART" /mnt/boot/efi
echo "==> Disabling CoW where needed (var heavy dirs + swap dir)..."
# NOCOW helper: apply to the directory and any existing regular files
set_nocow_dir() {
local d="$1"
# make sure it exists
mkdir -p "$d"
# set +C on the directory
chattr +C "$d" 2>/dev/null || true
# also set +C on existing files so they won’t be COW-compressed until rewritten
find "$d" -xdev -type f -print0 2>/dev/null | xargs -0r chattr +C 2>/dev/null || true
}
set_nocow_dir /mnt/var/lib
set_nocow_dir /mnt/var/log
set_nocow_dir /mnt/var/cache
set_nocow_dir /mnt/var/tmp
set_nocow_dir /mnt/var/swap
echo "==> Bootstrapping base system..."
REPO="${REPO}" ARCH="${ARCH}"
XBPS_ARCH="$ARCH" xbps-install -Sy -R "$REPO" -r /mnt \
base-system linux-mainline linux-mainline-headers \
btrfs-progs cryptsetup grub-x86_64-efi efibootmgr dracut \
dbus-elogind dbus-elogind-libs dbus-elogind-x11 elogind \
NetworkManager iwd openresolv cronie util-linux \
glibc-locales linux-firmware-amd \
nano bash-completion
echo "==> Preparing chroot..."
for d in dev proc sys run; do
mount --rbind "/$d" "/mnt/$d"
mount --make-rslave "/mnt/$d"
done
cp /etc/resolv.conf /mnt/etc/
echo "==> Entering chroot to configure system..."
cat > /mnt/root/inside-chroot.sh <<'CHROOT_EOF'
set -euo pipefail
# Basic system config
echo "==> Setting timezone, hostname, keymap, locales..."
ln -sf "/usr/share/zoneinfo/__TZ__" /etc/localtime
echo "__HOSTNAME__" > /etc/hostname
# rc.conf (minimal essentials)
sed -i 's|^#\?KEYMAP=.*|KEYMAP="__KEYMAP__"|' /etc/rc.conf || echo 'KEYMAP="__KEYMAP__"' >> /etc/rc.conf
sed -i 's|^#\?HARDWARECLOCK=.*|HARDWARECLOCK="UTC"|' /etc/rc.conf || echo 'HARDWARECLOCK="UTC"' >> /etc/rc.conf
# glibc locales (ensure presence, then generate)
if ! grep -q '^__LOCALE__$' /etc/default/libc-locales 2>/dev/null; then
printf '%s\n' '__LOCALE__' >> /etc/default/libc-locales
fi
xbps-reconfigure -f glibc-locales || true
# Users & sudo
echo "==> Setting root password (prompt)..."
passwd
chsh -s __USER_SHELL__ root
echo "==> Creating user..."
useradd -m -G wheel,audio,video,kvm,storage,dialout,plugdev,xbuilder,lp,scanner __USERNAME__
passwd __USERNAME__
chsh -s __USER_SHELL__ __USERNAME__
# sudoers (wheel)
if command -v visudo >/dev/null 2>&1; then
EDITOR=sed visudo -f /etc/sudoers -c >/dev/null 2>&1 || true
sed -i 's|^# %wheel ALL=(ALL) ALL|%wheel ALL=(ALL) ALL|' /etc/sudoers
fi
# NetworkManager: let it own resolv.conf and use iwd
rm -f /etc/resolv.conf
ln -s /run/NetworkManager/resolv.conf /etc/resolv.conf
mkdir -p /etc/NetworkManager/conf.d
cat >/etc/NetworkManager/conf.d/wifi_backend.conf <<'EOF'
[device]
wifi.backend=iwd
wifi.iwd.autoconnect=yes
EOF
echo "==> Enabling runit services..."
ln -sf /etc/sv/dbus /var/service/
ln -sf /etc/sv/elogind /var/service/
ln -sf /etc/sv/iwd /var/service/
ln -sf /etc/sv/NetworkManager /var/service/
ln -sf /etc/sv/cronie /var/service/
echo "==> Writing fstab..."
EFI_UUID=$(blkid -s UUID -o value __ESP_PART__)
ROOT_UUID=$(blkid -s UUID -o value /dev/mapper/cryptroot)
DATA_UUID=$(blkid -s UUID -o value /dev/mapper/cryptdata)
cat > /etc/fstab <<EOF
# Root and ESP
UUID=${ROOT_UUID} / btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@ 0 1
UUID=${EFI_UUID} /boot/efi vfat defaults,noatime 0 2
# Excluded-from-root-snapshots (on drive #1)
UUID=${ROOT_UUID} /opt btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@opt 0 2
UUID=${ROOT_UUID} /usr/local btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@usr_local 0 2
UUID=${ROOT_UUID} /root btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@root 0 2
# Second (bigger) disk mounts
UUID=${DATA_UUID} /home btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@home 0 2
UUID=${DATA_UUID} /snapshots btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@snapshots 0 2
# Heavy /var subtrees on second disk
UUID=${DATA_UUID} /var/lib btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@var_lib 0 2
UUID=${DATA_UUID} /var/log btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@var_log 0 2
UUID=${DATA_UUID} /var/cache btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@var_cache 0 2
UUID=${DATA_UUID} /var/tmp btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@var_tmp 0 2
UUID=${DATA_UUID} /var/swap btrfs rw,noatime,ssd,compress=zstd,space_cache=v2,subvol=@var_swap 0 2
# tmp as tmpfs
tmpfs /tmp tmpfs defaults,nosuid,nodev,mode=1777 0 0
# Swapfile (added below after creation)
/var/swap/swapfile none swap sw 0 0
EOF
echo "==> Creating 40G swapfile with NOCOW..."
# Ensure swap dir has NOCOW & no compression
chattr +C /var/swap || true
truncate -s 0 /var/swap/swapfile
chattr +C /var/swap/swapfile || true
fallocate -l __SWAP_SIZE__ /var/swap/swapfile
chmod 600 /var/swap/swapfile
mkswap /var/swap/swapfile
swapon /var/swap/swapfile
echo "==> Creating LUKS keyfile (for single-entry passphrase at boot)..."
dd if=/dev/urandom of=/boot/keyfile.bin bs=512 count=4 status=none
chmod 000 /boot/keyfile.bin
chmod -R go-rwx /boot
echo "==> Adding keyfile to LUKS keyslots..."
cryptsetup -v luksAddKey __ROOT_PART__ /boot/keyfile.bin
cryptsetup -v luksAddKey __DATA_PART__ /boot/keyfile.bin
echo "==> Writing /etc/crypttab with discard for weekly fstrim..."
LUKS_ROOT_UUID=$(blkid -s UUID -o value __ROOT_PART__)
LUKS_DATA_UUID=$(blkid -s UUID -o value __DATA_PART__)
cat > /etc/crypttab <<EOF
cryptroot UUID=${LUKS_ROOT_UUID} /boot/keyfile.bin luks,discard
cryptdata UUID=${LUKS_DATA_UUID} /boot/keyfile.bin luks,discard
EOF
echo '==> Dracut: include keyfile and crypttab in initramfs...'
mkdir -p /etc/dracut.conf.d
cat > /etc/dracut.conf.d/10-crypt.conf <<EOF
install_items+=" /boot/keyfile.bin /etc/crypttab "
EOF
echo "==> Hibernation: computing resume offset..."
RESUME_OFFSET=$(btrfs inspect-internal map-swapfile -r /var/swap/swapfile)
echo "==> Configuring GRUB..."
# - resume points to FS that holds swapfile (DATA_UUID) + resume_offset
# - allow LUKS discards
# - add rd.auto=1 for good measure; page_poison=1 and loglevel=4 as requested
echo 'GRUB_ENABLE_CRYPTODISK=y' >> /etc/default/grub
sed -i '/^GRUB_CMDLINE_LINUX=/d' /etc/default/grub
echo "GRUB_CMDLINE_LINUX=\"resume=UUID=${DATA_UUID} resume_offset=${RESUME_OFFSET} rd.luks.allow-discards rd.auto=1 page_poison=1 loglevel=4\"" >> /etc/default/grub
echo "==> Installing GRUB..."
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id="Void"
echo "==> Rebuilding initramfs and GRUB config..."
xbps-reconfigure -fa
grub-mkconfig -o /boot/grub/grub.cfg
echo '==> Weekly fstrim via cron...'
cat >/etc/cron.weekly/fstrim <<'EOF'
#!/bin/sh
/usr/sbin/fstrim -av
EOF
chmod +x /etc/cron.weekly/fstrim
echo "==> Done inside chroot."
CHROOT_EOF
# Fill placeholders
sed -i "s|__TZ__|$TZ|g" /mnt/root/inside-chroot.sh
sed -i "s|__HOSTNAME__|$HOSTNAME|g" /mnt/root/inside-chroot.sh
sed -i "s|__KEYMAP__|$KEYMAP|g" /mnt/root/inside-chroot.sh
sed -i "s|__LOCALE__|$LOCALE|g" /mnt/root/inside-chroot.sh
sed -i "s|__USERNAME__|$USERNAME|g" /mnt/root/inside-chroot.sh
sed -i "s|__USER_SHELL__|$USER_SHELL|g" /mnt/root/inside-chroot.sh
sed -i "s|__SWAP_SIZE__|$SWAP_SIZE|g" /mnt/root/inside-chroot.sh
sed -i "s|__ESP_PART__|$ESP_PART|g" /mnt/root/inside-chroot.sh
sed -i "s|__ROOT_PART__|$ROOT_PART|g" /mnt/root/inside-chroot.sh
sed -i "s|__DATA_PART__|$DATA_PART|g" /mnt/root/inside-chroot.sh
chmod +x /mnt/root/inside-chroot.sh
echo "==> Chrooting to run configuration..."
chroot /mnt /usr/bin/env bash -c "/root/inside-chroot.sh"
echo "==> Unmounting filesystems..."
swapoff /mnt/var/swap/swapfile || true
umount -R /mnt/boot/efi || true
umount -R /mnt
echo "==> Closing LUKS mappings..."
cryptsetup close cryptdata || true
cryptsetup close cryptroot || true
echo "==> Installation complete. Reboot when ready."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment