Skip to content

Instantly share code, notes, and snippets.

@cezarlamann
Created November 16, 2025 23:33
Show Gist options
  • Select an option

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

Select an option

Save cezarlamann/fdc51ba202fe880427a9fa80e3b65f37 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -euo pipefail
### ====== EDIT ME (DISKS & BASICS) ==========================================
# Root (NVMe) disk -> ESP + LUKS1 for root btrfs
ROOT_DISK="/dev/sda"
# Data (SATA/SSD) disk -> LUKS1 for data btrfs
DATA_DISK="/dev/sdb"
# Partition sizes
ESP_SIZE="1024MiB" # EFI System Partition size on ROOT_DISK
# Swap size (hibernation capable) - currently used as swapfile space, not a partition
SWAP_SIZE="18G"
### ====== LIVE ISO PREFLIGHT (tools needed before installation) ==============
echo "==> Ensuring required live-ISO tools are present..."
# Detect a package manager once
detect_pkg_manager() {
for pm in xbps-install apt-get dnf yum zypper pacman; do
if command -v "$pm" >/dev/null 2>&1; then
echo "$pm"
return 0
fi
done
return 1
}
PKG_MANAGER="$(detect_pkg_manager || true)"
install_pkg() {
local pkg="$1"
if [[ -z "${PKG_MANAGER:-}" ]]; then
echo "!! No supported package manager found (xbps-install/apt-get/dnf/yum/zypper/pacman)."
echo " Please install '$pkg' manually and re-run the script."
exit 1
fi
case "$PKG_MANAGER" in
xbps-install)
xbps-install -Sy "$pkg"
;;
apt-get)
apt-get update
apt-get install -y "$pkg"
;;
dnf)
dnf install -y "$pkg"
;;
yum)
yum install -y "$pkg"
;;
zypper)
zypper --non-interactive install --no-recommends "$pkg"
;;
pacman)
pacman -Sy --noconfirm "$pkg"
;;
*)
echo "!! Unsupported package manager: $PKG_MANAGER"
echo " Please install '$pkg' manually and re-run."
exit 1
;;
esac
}
need() {
local bin="$1"
local pkg="$2"
if ! command -v "$bin" >/dev/null 2>&1; then
echo "==> Installing missing tool '$bin' (package: $pkg) using $PKG_MANAGER..."
install_pkg "$pkg"
fi
}
# For partitioning
need sgdisk gptfdisk
need partprobe parted
# Also make sure these exist (common across distros)
need cryptsetup cryptsetup
need mkfs.btrfs btrfs-progs
need btrfs btrfs-progs
# 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
}
# Calculate free-at-end reservation-aware size on a disk (MiB)
# - Finds the LAST "free;" region via parted
# - free_mib = size of that region
# - ten_percent = free_mib / 10, capped at 51205 MiB
# - returns: free_mib - ten_percent (or - cap)
calc_disk_reservation() {
local disk="$1"
if [[ -z "$disk" || ! -b "$disk" ]]; then
echo "Error: '$disk' is not a valid block device" >&2
return 1
fi
if ! command -v parted >/dev/null 2>&1; then
echo "Error: 'parted' is required but not installed" >&2
return 1
fi
local free_mib
free_mib="$(
parted -m -s "$disk" unit MiB print free 2>/dev/null \
| awk -F: '
# Only lines whose last field is "free;"
$NF == "free;" {
size = $4 # e.g. "31743MiB"
sub(/MiB$/, "", size) # strip unit
last = size + 0 # overwrite: we want the last free region
}
END {
if (last == "") last = 0
printf "%.0f\n", last # integer MiB
}
'
)"
[[ -z "$free_mib" ]] && free_mib=0
local ten_percent=$(( free_mib / 10 ))
local cap=51205
local reserve=$ten_percent
if (( ten_percent > cap )); then
reserve=$cap
fi
local final=$(( free_mib - reserve ))
(( final < 0 )) && final=0
printf '%d\n' "$final"
}
### ====== 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 sized via reservation)..."
sgdisk --zap-all "$ROOT_DISK"
sgdisk -o "$ROOT_DISK" # create new GPT so parted can see it
# ESP (fixed size)
sgdisk -n 1:0:+"$ESP_SIZE" -t 1:ef00 -c 1:"EFI System Partition" "$ROOT_DISK"
reread_pt "$ROOT_DISK"
# Compute root partition size: free-at-end minus 10% (capped)
ROOT_ROOT_SIZE_MIB="$(calc_disk_reservation "$ROOT_DISK")"
if [[ -z "$ROOT_ROOT_SIZE_MIB" || "$ROOT_ROOT_SIZE_MIB" -le 0 ]]; then
echo "Error: calculated root partition size for $ROOT_DISK is invalid: '$ROOT_ROOT_SIZE_MIB' MiB" >&2
exit 1
fi
echo "==> Root partition size on $ROOT_DISK: ${ROOT_ROOT_SIZE_MIB}MiB (reservation-adjusted)"
# root LUKS (use +<MiB>M, sgdisk interprets 'M' as MiB)
sgdisk -n 2:0:+"${ROOT_ROOT_SIZE_MIB}M" -t 2:8300 -c 2:"cryptroot" "$ROOT_DISK"
reread_pt "$ROOT_DISK"
echo "==> Partitioning $DATA_DISK (GPT: data LUKS sized via reservation)..."
sgdisk --zap-all "$DATA_DISK"
sgdisk -o "$DATA_DISK" # new GPT label for data disk
reread_pt "$DATA_DISK"
# Compute data partition size on DATA_DISK
DATA_SIZE_MIB="$(calc_disk_reservation "$DATA_DISK")"
if [[ -z "$DATA_SIZE_MIB" || "$DATA_SIZE_MIB" -le 0 ]]; then
echo "Error: calculated data partition size for $DATA_DISK is invalid: '$DATA_SIZE_MIB' MiB" >&2
exit 1
fi
echo "==> Data partition size on $DATA_DISK: ${DATA_SIZE_MIB}MiB (reservation-adjusted)"
# data LUKS
sgdisk -n 1:0:+"${DATA_SIZE_MIB}M" -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"
mkdir -p "$d"
chattr +C "$d" 2>/dev/null || true
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 "==> Done. Layout prepared."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment