Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Belphemur/8176d4dc1a259d736067a31b90931047 to your computer and use it in GitHub Desktop.

Select an option

Save Belphemur/8176d4dc1a259d736067a31b90931047 to your computer and use it in GitHub Desktop.
setup-zswap.fish
#!/usr/bin/env fish
# =============================================================================
# setup-zswap.fish
# Disables zram and switches to zswap on CachyOS (Limine + LUKS btrfs)
# - Backs up files before changes
# - Verifies each step
# - Rolls back everything on failure
# =============================================================================
# ── Configuration ─────────────────────────────────────────────────────────────
set -g LUKS_DEVICE /dev/mapper/sda1_crypt
set -g BTRFS_TMP_MOUNT /mnt/btrfs-root-zswap-setup
set -g SWAP_MOUNT /swap
set -g SWAP_SUBVOL @swap
set -g SWAP_FILE /swap/swapfile
set -g SWAP_SIZE 32g
set -g ZSWAP_COMPRESSOR zstd
set -g ZSWAP_MAX_POOL 30
set -g SWAPPINESS 10
set -g SYSCTL_CONF /etc/sysctl.d/99-zswap-vm.conf
set -g LIMINE_CONF /etc/default/limine
set -g MKINITCPIO_CONF /etc/mkinitcpio.conf
set -g FSTAB /etc/fstab
set -g UDEV_RULE /etc/udev/rules.d/30-zram.rules
set -g BACKUP_SUFFIX ".bak-zswap-"(date +%Y%m%d-%H%M%S)
# ── Rollback stack ────────────────────────────────────────────────────────────
set -g ROLLBACK_STACK
# ── Helpers ───────────────────────────────────────────────────────────────────
function info
echo (set_color --bold cyan)"[INFO]"(set_color normal)" $argv"
end
function ok
echo (set_color --bold green)"[ OK ]"(set_color normal)" $argv"
end
function warn
echo (set_color --bold yellow)"[WARN]"(set_color normal)" $argv"
end
function fail
echo (set_color --bold red)"[FAIL]"(set_color normal)" $argv" >&2
end
function die
fail $argv
rollback
exit 1
end
function section
echo ""
echo (set_color --bold cyan)"════════════════════════════════════════"(set_color normal)
echo (set_color --bold cyan)" $argv"(set_color normal)
echo (set_color --bold cyan)"════════════════════════════════════════"(set_color normal)
end
function section_red
echo ""
echo (set_color --bold red)"════════════════════════════════════════"(set_color normal)
echo (set_color --bold red)" $argv"(set_color normal)
echo (set_color --bold red)"════════════════════════════════════════"(set_color normal)
end
function section_green
echo ""
echo (set_color --bold green)"════════════════════════════════════════"(set_color normal)
echo (set_color --bold green)" $argv"(set_color normal)
echo (set_color --bold green)"════════════════════════════════════════"(set_color normal)
end
# Push a rollback action onto the stack.
# Usage: push_rollback "description" "cmd1" ["cmd2" ...]
function push_rollback
set -l entry (string join "|" $argv)
set -g ROLLBACK_STACK $ROLLBACK_STACK $entry
end
# Execute all rollback actions in reverse order.
function rollback
section_red "ROLLING BACK ALL CHANGES"
if test (count $ROLLBACK_STACK) -eq 0
warn "Nothing to roll back."
return
end
# Reverse the stack
set -l reversed
for entry in $ROLLBACK_STACK
set reversed $entry $reversed
end
for entry in $reversed
set -l parts (string split "|" $entry)
set -l description $parts[1]
info "Undoing: $description"
for i in (seq 2 (count $parts))
set -l cmd $parts[$i]
eval $cmd
if test $status -ne 0
warn "Rollback command failed (continuing): $cmd"
end
end
end
echo ""
ok "Rollback complete. System should be in its original state."
end
# Backup a file and register its restoration in the rollback stack.
function backup_file
set -l file $argv[1]
if test -f $file
set -l backup $file$BACKUP_SUFFIX
cp $file $backup
or die "Failed to back up $file"
ok "Backed up $file → $backup"
push_rollback "Restore $file" "cp '$backup' '$file'" "rm -f '$backup'"
else
warn "$file does not exist yet — no backup needed."
end
end
# ── Preflight checks ──────────────────────────────────────────────────────────
function preflight
info "Running preflight checks..."
if test (id -u) -ne 0
die "This script must be run as root (use: sudo fish setup-zswap.fish)"
end
if not test -b $LUKS_DEVICE
die "LUKS device $LUKS_DEVICE not found. Is sda1_crypt unlocked?"
end
for cmd in btrfs swapon swapoff mount umount mkinitcpio limine-mkinitcpio udevadm sysctl findmnt
if not command -q $cmd
die "Required command not found: $cmd"
end
end
if findmnt --source $LUKS_DEVICE --target / -n &>/dev/null
ok "LUKS device is the root filesystem — good."
else
warn "LUKS device is not mounted at /. Proceeding anyway."
end
ok "Preflight checks passed."
end
# ── Step 1: Create @swap btrfs subvolume ─────────────────────────────────────
function step_create_subvolume
info "Step 1: Creating @swap btrfs subvolume on $LUKS_DEVICE..."
if test -d $BTRFS_TMP_MOUNT
die "$BTRFS_TMP_MOUNT already exists. Please remove it first."
end
mkdir -p $BTRFS_TMP_MOUNT
or die "Failed to create temp mount point $BTRFS_TMP_MOUNT"
mount -o subvolid=5 $LUKS_DEVICE $BTRFS_TMP_MOUNT
or die "Failed to mount btrfs root of $LUKS_DEVICE at $BTRFS_TMP_MOUNT"
if btrfs subvolume show $BTRFS_TMP_MOUNT/$SWAP_SUBVOL &>/dev/null
warn "@swap subvolume already exists — skipping creation."
umount $BTRFS_TMP_MOUNT
rmdir $BTRFS_TMP_MOUNT
else
btrfs subvolume create $BTRFS_TMP_MOUNT/$SWAP_SUBVOL
or begin
umount $BTRFS_TMP_MOUNT
rmdir $BTRFS_TMP_MOUNT
die "Failed to create @swap subvolume"
end
if not btrfs subvolume show $BTRFS_TMP_MOUNT/$SWAP_SUBVOL &>/dev/null
umount $BTRFS_TMP_MOUNT
rmdir $BTRFS_TMP_MOUNT
die "Verification failed: @swap subvolume not found after creation."
end
umount $BTRFS_TMP_MOUNT
or die "Failed to unmount temp btrfs mount after subvolume creation"
rmdir $BTRFS_TMP_MOUNT
# Only register rollback if WE created it
push_rollback "Delete @swap subvolume" \
"mkdir -p '$BTRFS_TMP_MOUNT'" \
"mount -o subvolid=5 '$LUKS_DEVICE' '$BTRFS_TMP_MOUNT'" \
"btrfs subvolume delete '$BTRFS_TMP_MOUNT/$SWAP_SUBVOL'" \
"umount '$BTRFS_TMP_MOUNT'" \
"rmdir '$BTRFS_TMP_MOUNT'"
end
ok "Step 1: @swap subvolume ready."
end
# ── Step 2: Mount @swap subvolume at /swap ────────────────────────────────────
function step_mount_swap_subvolume
info "Step 2: Mounting @swap subvolume at $SWAP_MOUNT..."
if test -d $SWAP_MOUNT
die "$SWAP_MOUNT already exists. Please remove it first."
end
mkdir -p $SWAP_MOUNT
or die "Failed to create $SWAP_MOUNT"
push_rollback "Remove $SWAP_MOUNT mountpoint" "rmdir '$SWAP_MOUNT' 2>/dev/null; true"
mount -o subvol=$SWAP_SUBVOL,noatime,nodatacow $LUKS_DEVICE $SWAP_MOUNT
or die "Failed to mount @swap subvolume at $SWAP_MOUNT"
push_rollback "Unmount $SWAP_MOUNT" "umount '$SWAP_MOUNT' 2>/dev/null; true"
if not findmnt --target $SWAP_MOUNT -n &>/dev/null
die "Verification failed: $SWAP_MOUNT is not mounted."
end
ok "Step 2: @swap subvolume mounted at $SWAP_MOUNT and verified."
end
# ── Step 3: Create and activate swapfile ─────────────────────────────────────
function step_create_swapfile
info "Step 3: Creating swapfile ($SWAP_SIZE) at $SWAP_FILE..."
if test -f $SWAP_FILE
warn "$SWAP_FILE already exists — skipping creation."
else
btrfs filesystem mkswapfile --size $SWAP_SIZE --uuid clear $SWAP_FILE
or die "Failed to create swapfile at $SWAP_FILE"
if not test -f $SWAP_FILE
die "Verification failed: $SWAP_FILE not found after creation."
end
set -l actual_size (stat -c %s $SWAP_FILE)
if test $actual_size -le 0
die "Verification failed: $SWAP_FILE has zero size."
end
ok "Swapfile created ($actual_size bytes)."
# Only register file removal rollback if WE created it
push_rollback "Remove swapfile" "swapoff '$SWAP_FILE' 2>/dev/null; true" "rm -f '$SWAP_FILE'"
end
# Activate only if not already active
if swapon --show | grep -q $SWAP_FILE
warn "$SWAP_FILE already active as swap — skipping swapon."
else
swapon $SWAP_FILE
or die "Failed to activate swapfile with swapon"
push_rollback "Deactivate swapfile" "swapoff '$SWAP_FILE' 2>/dev/null; true"
if not swapon --show | grep -q $SWAP_FILE
die "Verification failed: $SWAP_FILE not showing in swapon --show"
end
end
ok "Step 3: Swapfile ready and active."
end
# ── Step 4: Update /etc/fstab ─────────────────────────────────────────────────
function step_update_fstab
info "Step 4: Updating $FSTAB..."
backup_file $FSTAB
if grep -q "$SWAP_SUBVOL" $FSTAB
die "fstab already contains a @swap entry. Aborting."
end
echo "" >> $FSTAB
echo "# @swap subvolume (zswap backing store — added by setup-zswap.fish)" >> $FSTAB
echo "$LUKS_DEVICE $SWAP_MOUNT btrfs subvol=$SWAP_SUBVOL,noatime,nodatacow 0 0" >> $FSTAB
echo "" >> $FSTAB
echo "# Swapfile" >> $FSTAB
echo "$SWAP_FILE none swap defaults 0 0" >> $FSTAB
or die "Failed to write fstab entries"
if not grep -q "$SWAP_SUBVOL" $FSTAB
die "Verification failed: @swap entry not found in $FSTAB after writing."
end
if not grep -q "$SWAP_FILE" $FSTAB
die "Verification failed: swapfile entry not found in $FSTAB after writing."
end
ok "Step 4: $FSTAB updated and verified."
end
# ── Step 5: Add zstd to mkinitcpio MODULES ────────────────────────────────────
function step_update_mkinitcpio
info "Step 5: Adding zstd to MODULES in $MKINITCPIO_CONF..."
backup_file $MKINITCPIO_CONF
if grep -qP 'MODULES=\(.*\bzstd\b.*\)' $MKINITCPIO_CONF
ok "zstd already present in MODULES — skipping modification."
return
end
sed -i 's/^MODULES=(\(.*\))/MODULES=(\1 zstd)/' $MKINITCPIO_CONF
sed -i 's/MODULES=( */MODULES=(/' $MKINITCPIO_CONF
or die "Failed to update MODULES in $MKINITCPIO_CONF"
if not grep -qP 'MODULES=\(.*\bzstd\b.*\)' $MKINITCPIO_CONF
die "Verification failed: zstd not found in MODULES after editing $MKINITCPIO_CONF"
end
ok "Step 5: $MKINITCPIO_CONF updated and verified."
end
# ── Step 6: Create udev rule override ────────────────────────────────────────
function step_create_udev_rule
info "Step 6: Creating empty udev override at $UDEV_RULE..."
set -l udev_existed false
if test -f $UDEV_RULE
warn "$UDEV_RULE already exists — backing it up."
backup_file $UDEV_RULE
set udev_existed true
end
touch $UDEV_RULE
or die "Failed to create $UDEV_RULE"
if not $udev_existed
push_rollback "Remove udev override" "rm -f '$UDEV_RULE'"
end
if not test -f $UDEV_RULE
die "Verification failed: $UDEV_RULE not found after creation."
end
udevadm control --reload-rules
or warn "udevadm reload failed — rules will apply on next boot."
ok "Step 6: Udev rule created and verified."
end
# ── Step 7: Update /etc/default/limine ───────────────────────────────────────
function step_update_limine
info "Step 7: Updating $LIMINE_CONF with zswap kernel parameters..."
set -l new_params "systemd.zram=0 zswap.enabled=1 zswap.shrinker_enabled=1 zswap.compressor=$ZSWAP_COMPRESSOR zswap.max_pool_percent=$ZSWAP_MAX_POOL"
backup_file $LIMINE_CONF
if not test -f $LIMINE_CONF
echo "# Created by setup-zswap.fish" > $LIMINE_CONF
echo "KERNEL_CMDLINE[default]+=\"$new_params\"" >> $LIMINE_CONF
push_rollback "Remove created limine config" "rm -f '$LIMINE_CONF'"
else
if grep -q "zswap.enabled" $LIMINE_CONF
die "$LIMINE_CONF already contains zswap parameters. Aborting to avoid duplication."
end
# Detect which operator is used on the default cmdline ("+=" or "=")
if grep -qP 'KERNEL_CMDLINE\[default\]\+=' $LIMINE_CONF
# Append-style: KERNEL_CMDLINE[default]+="... existing ..."
# Strategy: add a new += line directly after the existing one
sed -i '/KERNEL_CMDLINE\[default\]+=/a KERNEL_CMDLINE[default]+="'"$new_params"'"' $LIMINE_CONF
or die "Failed to insert new KERNEL_CMDLINE[default]+= line in $LIMINE_CONF"
else if grep -qP 'KERNEL_CMDLINE\[default\]=' $LIMINE_CONF
# Plain assignment: append inside the existing quotes
sed -i "s|KERNEL_CMDLINE\[default\]=\"\(.*\)\"|KERNEL_CMDLINE[default]=\"\1 $new_params\"|" $LIMINE_CONF
or die "Failed to update KERNEL_CMDLINE[default]= in $LIMINE_CONF"
else
# No default entry at all — add a new += line at the end
echo "" >> $LIMINE_CONF
echo "KERNEL_CMDLINE[default]+=\"$new_params\"" >> $LIMINE_CONF
or die "Failed to append KERNEL_CMDLINE[default] to $LIMINE_CONF"
end
end
# Verify all params are present
for param in systemd.zram=0 zswap.enabled=1 zswap.compressor=$ZSWAP_COMPRESSOR
if not grep -q $param $LIMINE_CONF
die "Verification failed: '$param' not found in $LIMINE_CONF after editing."
end
end
ok "Step 7: $LIMINE_CONF updated and verified."
end
# ── Step 8: Configure swappiness ─────────────────────────────────────────────
function step_set_swappiness
info "Step 8: Configuring vm.swappiness=$SWAPPINESS..."
set -l old_swappiness (sysctl -n vm.swappiness 2>/dev/null)
if test -z "$old_swappiness"
set old_swappiness 60
end
info "Current vm.swappiness: $old_swappiness → will be set to $SWAPPINESS"
sysctl -w vm.swappiness=$SWAPPINESS
or die "Failed to set vm.swappiness=$SWAPPINESS via sysctl"
push_rollback "Restore vm.swappiness to $old_swappiness" \
"sysctl -w vm.swappiness=$old_swappiness 2>/dev/null; true"
set -l current (sysctl -n vm.swappiness)
if test "$current" != "$SWAPPINESS"
die "Verification failed: vm.swappiness is '$current', expected '$SWAPPINESS'"
end
ok "Runtime vm.swappiness set to $current."
backup_file $SYSCTL_CONF
if test -f $SYSCTL_CONF
if grep -q "vm.swappiness" $SYSCTL_CONF
die "$SYSCTL_CONF already contains vm.swappiness. Aborting to avoid duplication."
end
echo "" >> $SYSCTL_CONF
echo "vm.swappiness = $SWAPPINESS" >> $SYSCTL_CONF
or die "Failed to append vm.swappiness to $SYSCTL_CONF"
else
echo "# zswap tuning — written by setup-zswap.fish" > $SYSCTL_CONF
echo "# Lower swappiness is appropriate for zswap (vs 150 used by zram)" >> $SYSCTL_CONF
echo "vm.swappiness = $SWAPPINESS" >> $SYSCTL_CONF
or die "Failed to create $SYSCTL_CONF"
push_rollback "Remove sysctl config" "rm -f '$SYSCTL_CONF'"
end
if not grep -q "vm.swappiness = $SWAPPINESS" $SYSCTL_CONF
die "Verification failed: vm.swappiness not found in $SYSCTL_CONF after writing."
end
sysctl --system &>/dev/null
or warn "sysctl --system reload had warnings — check $SYSCTL_CONF manually."
ok "Step 8: vm.swappiness=$SWAPPINESS applied (runtime + persistent via $SYSCTL_CONF)."
end
# ── Step 9: Rebuild initramfs ─────────────────────────────────────────────────
function step_rebuild_initramfs
info "Step 9: Rebuilding initramfs with limine-mkinitcpio..."
info "(This may take a minute...)"
push_rollback "Note: initramfs rebuilt — re-run 'limine-mkinitcpio' manually if rollback caused issues"
limine-mkinitcpio
or die "limine-mkinitcpio failed. Check output above for errors."
# CachyOS/Limine stores initramfs under /boot/<machine-id>/<kernel>/
# e.g. /boot/4bc226dd53f7440e8a8aa50d32838302/linux-cachyos/initramfs-linux-cachyos
set -l found (find /boot -name 'initramfs-*.img' -o -name 'initramfs-linux-*' 2>/dev/null | grep -v '\.bak' | head -1)
if test -z "$found"
die "Verification failed: No initramfs images found anywhere under /boot after rebuild."
end
ok "Initramfs verified: $found"
ok "Step 9: Initramfs rebuilt and verified."
end
# ── Step 10: Runtime zswap activation (no reboot) ────────────────────────────
function step_activate_zswap_now
info "Step 10: Activating zswap in the running kernel (no reboot)..."
echo 1 > /sys/module/zswap/parameters/enabled
or die "Failed to enable zswap via sysfs"
echo $ZSWAP_COMPRESSOR > /sys/module/zswap/parameters/compressor
or warn "Could not set zswap compressor via sysfs (may need reboot to take effect)"
echo $ZSWAP_MAX_POOL > /sys/module/zswap/parameters/max_pool_percent
or warn "Could not set zswap max_pool_percent via sysfs"
set -l zswap_enabled (cat /sys/module/zswap/parameters/enabled 2>/dev/null)
if test "$zswap_enabled" != "Y"
warn "zswap enabled sysfs shows '$zswap_enabled' — may require reboot to fully activate."
else
ok "zswap is active in the running kernel."
end
if swapon --show | grep -q zram
warn "zram swap is still active in this session. Disabling it now..."
for zram_dev in (swapon --show --noheadings | awk '{print $1}' | grep zram)
swapoff $zram_dev
and ok "Disabled zram swap: $zram_dev"
or warn "Could not swapoff $zram_dev — may need reboot"
end
end
ok "Step 10: Runtime zswap activation complete."
end
# ── Final verification ────────────────────────────────────────────────────────
function final_verify
section_green "FINAL VERIFICATION"
set -l all_ok true
set -l zswap_en (cat /sys/module/zswap/parameters/enabled 2>/dev/null)
if test "$zswap_en" = "Y"
ok "zswap enabled: Y ✓"
else
warn "zswap enabled: $zswap_en (will be Y after reboot)"
end
set -l zswap_comp (cat /sys/module/zswap/parameters/compressor 2>/dev/null)
ok "zswap compressor: $zswap_comp"
set -l zswap_pool (cat /sys/module/zswap/parameters/max_pool_percent 2>/dev/null)
ok "zswap max_pool_percent: $zswap_pool%"
if swapon --show | grep -q $SWAP_FILE
ok "Swapfile active: $SWAP_FILE ✓"
else
fail "Swapfile NOT active: $SWAP_FILE"
set all_ok false
end
if swapon --show | grep -q zram
warn "zram still active in this session (will be gone after reboot)"
else
ok "zram swap: not active ✓"
end
if grep -q $SWAP_SUBVOL $FSTAB; and grep -q $SWAP_FILE $FSTAB
ok "fstab entries: present ✓"
else
fail "fstab entries: MISSING"
set all_ok false
end
if grep -q "zswap.enabled=1" $LIMINE_CONF
ok "Limine config: zswap params present ✓"
else
fail "Limine config: zswap params MISSING"
set all_ok false
end
if grep -qP 'MODULES=\(.*\bzstd\b.*\)' $MKINITCPIO_CONF
ok "mkinitcpio MODULES: zstd present ✓"
else
fail "mkinitcpio MODULES: zstd MISSING"
set all_ok false
end
if test -f $UDEV_RULE
ok "udev override: $UDEV_RULE present ✓"
else
fail "udev override: $UDEV_RULE MISSING"
set all_ok false
end
set -l current_swappiness (sysctl -n vm.swappiness 2>/dev/null)
if test "$current_swappiness" = "$SWAPPINESS"
ok "vm.swappiness (runtime): $current_swappiness ✓"
else
fail "vm.swappiness (runtime): $current_swappiness (expected $SWAPPINESS)"
set all_ok false
end
if grep -q "vm.swappiness = $SWAPPINESS" $SYSCTL_CONF
ok "vm.swappiness (persist): $SYSCTL_CONF ✓"
else
fail "vm.swappiness (persist): not found in $SYSCTL_CONF"
set all_ok false
end
echo ""
if $all_ok
echo (set_color --bold green)"✔ All checks passed! Reboot to fully apply kernel parameters."(set_color normal)
echo ""
echo " Post-reboot, confirm with:"
echo " cat /sys/module/zswap/parameters/enabled # → Y"
echo " cat /sys/module/zswap/parameters/compressor # → zstd"
echo " swapon --show # → $SWAP_FILE only"
echo " sysctl vm.swappiness # → $SWAPPINESS"
else
echo (set_color --bold red)"✘ Some checks failed. Review output above."(set_color normal)
rollback
exit 1
end
end
# ── Main ──────────────────────────────────────────────────────────────────────
function main
section "CachyOS: zram → zswap Setup Script"
echo ""
echo " LUKS device: $LUKS_DEVICE"
echo " Swap mount: $SWAP_MOUNT"
echo " Swap size: $SWAP_SIZE"
echo " Compressor: $ZSWAP_COMPRESSOR"
echo " Pool max: $ZSWAP_MAX_POOL%"
echo " Swappiness: $SWAPPINESS"
echo " Sysctl conf: $SYSCTL_CONF"
echo " Backup suffix: $BACKUP_SUFFIX"
echo ""
preflight
step_create_subvolume
step_mount_swap_subvolume
step_create_swapfile
step_update_fstab
step_update_mkinitcpio
step_create_udev_rule
step_update_limine
step_set_swappiness
step_rebuild_initramfs
step_activate_zswap_now
final_verify
end
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment