Last active
January 4, 2026 12:33
-
-
Save arathunku/b2ee71644f2b07f6f1117d7dbf39832a 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
| #!/bin/sh | |
| set -e | |
| script=$(readlink -f "$0") | |
| scriptname=$(basename "$script") | |
| [ "$(id -u)" -eq 0 ] || { echo "ERROR: Must be run as root."; exit 1; } | |
| mp=/mnt/root | |
| keyslot_size=32m | |
| swap_size=32G | |
| umount_retry() { | |
| umount "$1" 2>/dev/null || { sleep 2; umount "$1" 2>/dev/null || umount -l "$1" 2>/dev/null; } || true | |
| } | |
| preparation() { | |
| echo "Prepare" | |
| if [ "$efi" = true ]; then | |
| umount_retry /target/boot/efi | |
| fi | |
| umount_retry /target/boot | |
| umount_retry /target/cdrom | |
| umount_retry /target | |
| mkdir -p "$mp" | |
| } | |
| create_subvols() { | |
| echo "Create subvolumes" | |
| mount "/dev/$1" "$mp" | |
| cd "$mp" || { echo "ERROR: Failed to cd to $mp"; exit 1; } | |
| [ -z "$(ls -A home 2>/dev/null)" ] || { echo "ERROR: home directory not empty"; exit 3; } | |
| btrfs subvolume snapshot . @ | |
| # Create all subvolumes | |
| btrfs subvolume create @home | |
| btrfs subvolume create @var-cache | |
| btrfs subvolume create @var-log | |
| btrfs subvolume create @var-snap | |
| btrfs subvolume create @usr-local | |
| btrfs subvolume create @nix | |
| btrfs subvolume create @swap | |
| btrfs subvolume create @var-lib-flatpak | |
| btrfs subvolume create @var-lib-docker | |
| btrfs subvolume create @var-lib-snapd | |
| btrfs subvolume create @var-lib-libvirt | |
| btrfs subvolume create .timeline | |
| # Disable CoW for write-heavy subvolumes | |
| chattr +C @var-cache | |
| chattr +C @var-lib-docker | |
| chattr +C @var-lib-libvirt | |
| # Migrate data from @ to subvolumes (includes hidden files) | |
| # Using cp -a to preserve attributes and handle dotfiles, then rm source | |
| migrate_data() { | |
| src="${1:?}"; dst="${2:?}" | |
| if [ -d "$src" ] && [ -n "$(ls -A "$src" 2>/dev/null)" ]; then | |
| cp -a "$src"/. "$dst"/ && rm -rf "${src:?}"/* "${src:?}"/.[!.]* "${src:?}"/..?* 2>/dev/null | |
| fi | |
| } | |
| migrate_data @/home @home | |
| migrate_data @/var/cache @var-cache | |
| migrate_data @/var/log @var-log | |
| migrate_data @/var/snap @var-snap | |
| migrate_data @/usr/local @usr-local | |
| migrate_data @/var/lib/docker @var-lib-docker | |
| migrate_data @/var/lib/flatpak @var-lib-flatpak | |
| migrate_data @/var/lib/snapd @var-lib-snapd | |
| migrate_data @/var/lib/libvirt @var-lib-libvirt | |
| # Remove everything except subvolumes | |
| find . -maxdepth 1 \! -name "@*" \! -name ".*" \! -name . -exec rm -Rf {} \; | |
| btrfs filesystem mkswapfile --size "$swap_size" --uuid clear @swap/swapfile | |
| cd / | |
| umount "$mp" | |
| echo "Mount @ instead of /" | |
| mount "/dev/$1" -o subvol=@ "$mp" | |
| } | |
| encrypt_and_enlarge() { | |
| echo "Encrypt $1" | |
| btrfs filesystem resize "-$keyslot_size" "$mp" | |
| cd / | |
| umount "$mp" | |
| disk=$(lsblk --noheadings --output pkname "/dev/$1") | |
| partition_number=$(lsblk --noheadings --output partn "/dev/$1" | tr -d "[:space:]") | |
| echo 'You may ignore "Warning: keyslot operation could fail ...".' | |
| echo 'See <https://gitlab.com/cryptsetup/cryptsetup/-/issues/896>.' | |
| cryptsetup reencrypt --encrypt --type luks2 --reduce-device-size "$keyslot_size" "/dev/$1" | |
| if [ "$enlarge" = yes ]; then | |
| echo "Enlarge $1" | |
| parted --script "/dev/$disk" resizepart "$partition_number" "100%" | |
| fi | |
| echo "Enter LUKS passphrase to unlock encrypted root:" | |
| cryptsetup open "/dev/$1" root | |
| mount /dev/mapper/root -o subvol=@ "$mp" | |
| btrfs filesystem resize max "$mp" | |
| } | |
| chroot_and_mkinitramfs() { | |
| echo "Prepare $mp for chroot" | |
| mount -t proc proc "$mp/proc" | |
| mount -t sysfs sys "$mp/sys" | |
| mount --bind /dev "$mp/dev" | |
| mount --bind /run "$mp/run" | |
| mount "/dev/$2" "$mp/boot" | |
| if [ -n "$3" ]; then | |
| mount "/dev/$3" "$mp/boot/efi" | |
| fi | |
| cp "$script" "$mp/tmp/$scriptname" | |
| chmod a+x "$mp/tmp/$scriptname" | |
| echo "Chrooting and call the script in the other root" | |
| chroot "$mp" "/tmp/$scriptname" //inner "/dev/$1" | |
| } | |
| unmount_everything() { | |
| echo "Unmounting" | |
| cd / | |
| set +e # Don't exit on error during cleanup | |
| for dir in proc sys dev run; do | |
| umount_retry "$mp/$dir" | |
| done | |
| if [ "$efi" = true ]; then | |
| umount_retry "$mp/boot/efi" | |
| fi | |
| umount_retry "$mp/boot" | |
| umount_retry "$mp" | |
| cryptsetup close root | |
| set -e | |
| } | |
| if [ "$1" = "//inner" ]; then | |
| # echo GRUB_DISABLE_OS_PROBER=false >> /etc/default/grub | |
| root_uuid=$(blkid --output export "$2" | grep ^UUID=) || { echo "ERROR: Failed to get UUID for $2"; exit 1; } | |
| [ -n "$root_uuid" ] || { echo "ERROR: UUID empty for $2"; exit 1; } | |
| opts="defaults,space_cache=v2,ssd,discard=async,noatime" | |
| mkdir -p /nix /swap /var/cache /var/log /var/snap /usr/local \ | |
| /var/lib/docker /var/lib/libvirt /var/lib/flatpak /var/lib/snapd | |
| echo "Patch /etc/fstab" | |
| sed --in-place 's!^.* / btrfs defaults 0 1$!/dev/mapper/root / btrfs defaults,space_cache=v2,ssd,discard=async,noatime,subvol=@ 0 0!' /etc/fstab | |
| { | |
| echo "/dev/mapper/root /home btrfs $opts,subvol=@home 0 0" | |
| echo "/dev/mapper/root /nix btrfs $opts,subvol=@nix 0 0" | |
| echo "/dev/mapper/root /swap btrfs $opts,subvol=@swap 0 0" | |
| echo "/dev/mapper/root /var/cache btrfs $opts,subvol=@var-cache 0 0" | |
| echo "/dev/mapper/root /var/log btrfs $opts,subvol=@var-log 0 0" | |
| echo "/dev/mapper/root /var/snap btrfs $opts,subvol=@var-snap 0 0" | |
| echo "/dev/mapper/root /usr/local btrfs $opts,subvol=@usr-local 0 0" | |
| echo "/dev/mapper/root /var/lib/docker btrfs $opts,subvol=@var-lib-docker 0 0" | |
| echo "/dev/mapper/root /var/lib/flatpak btrfs $opts,subvol=@var-lib-flatpak 0 0" | |
| echo "/dev/mapper/root /var/lib/snapd btrfs $opts,subvol=@var-lib-snapd 0 0" | |
| echo "/dev/mapper/root /var/lib/libvirt btrfs $opts,subvol=@var-lib-libvirt 0 0" | |
| echo "/swap/swapfile none swap defaults 0 0" | |
| } >> /etc/fstab | |
| echo "root $root_uuid none luks,discard" > /etc/crypttab | |
| update-grub | |
| echo "Install cryptsetup and update initramfs" | |
| apt-get install -y cryptsetup-initramfs | |
| update-initramfs -u -k all | |
| exit | |
| fi | |
| show_help() { | |
| echo "Add BTRFS subvolumes to a root partition and encrypt it with LUKS2" | |
| echo | |
| echo "Usage: ubuntu-btrfs-setup [options] {root-dev} {boot-dev} [{efi-dev}]" | |
| echo | |
| echo "The devices must be given without the /dev/ prefix." | |
| echo | |
| echo " --enlarge enlarge root partition to maximum" | |
| echo | |
| echo "--enlarge resizes the root partition to 100%. This makes encryption" | |
| echo "much faster, because you make a small partition for just the" | |
| echo "installation of the base system, and then you call this script," | |
| echo "which has to encrypt only the smaller partition. The enlargement" | |
| echo "covers only empty space, so no encryption is necessary there." | |
| echo | |
| echo "Subvolumes created:" | |
| echo " @, @home, @nix, @swap, @var-cache, @var-log, @var-snap, @usr-local," | |
| echo " @var-lib-docker, @var-lib-flatpak, @var-lib-snapd, @var-lib-libvirt," | |
| echo " .timeline" | |
| } | |
| if [ $# -eq 0 ]; then | |
| show_help | |
| exit | |
| fi | |
| enlarge=no | |
| while :; do | |
| case $1 in | |
| -h|-\?|--help) | |
| show_help | |
| exit | |
| ;; | |
| --enlarge) | |
| enlarge=yes | |
| ;; | |
| --*) | |
| echo "Invalid option \"$1\". Use --help for more information." | |
| exit 2 | |
| ;; | |
| *) | |
| break | |
| esac | |
| shift | |
| done | |
| if [ -z "$3" ]; then | |
| efi=false | |
| else | |
| efi=true | |
| fi | |
| if [ -z "$1" ] || [ -z "$2" ]; then | |
| echo "You must pass the device of the root, boot, and optionally " | |
| echo "EFI partition (without the /dev/) to this script." | |
| echo | |
| echo "Use --help for more information." | |
| exit 2 | |
| fi | |
| # Validate devices exist | |
| [ -b "/dev/$1" ] || { echo "ERROR: Root device /dev/$1 not found"; exit 1; } | |
| [ -b "/dev/$2" ] || { echo "ERROR: Boot device /dev/$2 not found"; exit 1; } | |
| [ -z "$3" ] || [ -b "/dev/$3" ] || { echo "ERROR: EFI device /dev/$3 not found"; exit 1; } | |
| # Validate filesystem types | |
| root_fs=$(blkid -o value -s TYPE "/dev/$1" 2>/dev/null) | |
| [ "$root_fs" = "btrfs" ] || { echo "ERROR: Root must be BTRFS, got: ${root_fs:-none}"; exit 1; } | |
| boot_fs=$(blkid -o value -s TYPE "/dev/$2" 2>/dev/null) | |
| case "$boot_fs" in | |
| ext2|ext3|ext4|vfat) ;; | |
| *) echo "ERROR: Boot must be ext2/3/4 or vfat, got: ${boot_fs:-none}"; exit 1 ;; | |
| esac | |
| if [ -n "$3" ]; then | |
| efi_fs=$(blkid -o value -s TYPE "/dev/$3" 2>/dev/null) | |
| [ "$efi_fs" = "vfat" ] || { echo "ERROR: EFI must be vfat, got: ${efi_fs:-none}"; exit 1; } | |
| fi | |
| preparation | |
| create_subvols "$1" | |
| encrypt_and_enlarge "$1" | |
| chroot_and_mkinitramfs "$1" "$2" "$3" | |
| unmount_everything | |
| echo "Finished" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment