Created
November 16, 2025 23:33
-
-
Save cezarlamann/fdc51ba202fe880427a9fa80e3b65f37 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
| #!/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