Snapper: no question on btrfs; vendor the SysGuides scripts

- On btrfs, snapshots + zstd compression now just happen (no prompt);
  --snapper=no remains the explicit opt-out.
- Vendor scripts/snapper-*.sh + config/snapper.actions from
  SysGuides/sysguides-snapper-fedora (MIT, upstream 0a28eb4) into
  vendor/snapper/ and install from there — no runtime clone of an
  external repo, and no dependency on exec bits (fixes the
  './install.sh: Permission denied' failure seen on the test VM).
- grub-btrfs still cloned at runtime from Antynea/grub-btrfs with the
  Fedora config applied; everything else is local now.
This commit is contained in:
Pathfinder
2026-10-27 12:37:18 +01:00
parent 47991727ff
commit 3776dcb5a4
9 changed files with 324 additions and 16 deletions
+57 -12
View File
@@ -258,9 +258,14 @@ ensure_btrfs_compression() {
info " sudo mount -o remount / && sudo mount -o remount /home"
}
# Full snapshot setup via the SysGuides installer (snapper + grub-btrfs +
# DNF5 pre/post snapshot actions). The installer re-validates btrfs itself.
# Snapshot setup using the vendored SysGuides scripts (vendor/snapper/).
# Original: https://github.com/SysGuides/sysguides-snapper-fedora (MIT).
# Vendored so we do not depend on that repo at runtime; upstream changes can
# be pulled deliberately, not accidentally.
# Exec bits are not relied on: scripts are invoked via `bash`.
setup_snapper() {
local vendor="$SCRIPT_DIR/vendor/snapper"
if pkg_installed snapper && [[ -d /.snapshots ]]; then
ok "Snapper already configured (config for / exists)."
return 0
@@ -269,17 +274,57 @@ setup_snapper() {
warn "Root filesystem is not btrfs; skipping snapshot setup."
return 0
fi
info "Setting up snapper + grub-btrfs (SysGuides installer)..."
if [[ ! -f "$vendor/scripts/snapper-pre.sh" ]]; then
err "Vendored snapper scripts missing at $vendor — repo checkout incomplete?"
return 1
fi
info "Installing snapper + grub-btrfs packages..."
install_packages snapper libdnf5-plugin-actions btrfs-assistant inotify-tools make git
info "Creating snapper configs (root + home)..."
[[ -d /.snapshots ]] || sudo snapper -c root create-config /
[[ -d /home/.snapshots ]] || sudo snapper -c home create-config /home
sudo restorecon -RF /.snapshots /home/.snapshots 2>/dev/null || true
local real_user="${SUDO_USER:-$USER}"
sudo snapper -c root set-config ALLOW_USERS="$real_user" SYNC_ACL=yes
sudo snapper -c home set-config ALLOW_USERS="$real_user" SYNC_ACL=yes
# no home timeline: snapshots of your own files on every dnf run are noise
sudo snapper -c home set-config TIMELINE_CREATE=no
info "Excluding .snapshots from updatedb..."
if grep -q '^PRUNENAMES' /etc/updatedb.conf 2>/dev/null; then
grep -q '\.snapshots' /etc/updatedb.conf || \
sudo sed -i 's|^PRUNENAMES *= *"|PRUNENAMES = ".snapshots |' /etc/updatedb.conf
else
printf 'PRUNENAMES = ".snapshots"\n' | sudo tee -a /etc/updatedb.conf >/dev/null
fi
info "Installing grub-btrfs (from source, Antynea/grub-btrfs)..."
local tmp
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/SysGuides/sysguides-snapper-fedora "$tmp/sysguides" \
|| { err "Could not clone the SysGuides installer."; rm -rf "$tmp"; return 1; }
( cd "$tmp/sysguides" && ./install.sh )
local rc=$?
git clone --quiet --depth 1 https://github.com/Antynea/grub-btrfs "$tmp/grub-btrfs"
sed -i \
-e 's|^#GRUB_BTRFS_SNAPSHOT_KERNEL_PARAMETERS=.*|GRUB_BTRFS_SNAPSHOT_KERNEL_PARAMETERS="rd.live.overlay.overlayfs=1"|' \
-e 's|^#GRUB_BTRFS_GRUB_DIRNAME=.*|GRUB_BTRFS_GRUB_DIRNAME="/boot/grub2"|' \
-e 's|^#GRUB_BTRFS_MKCONFIG=.*|GRUB_BTRFS_MKCONFIG=/usr/bin/grub2-mkconfig|' \
-e 's|^#GRUB_BTRFS_SCRIPT_CHECK=.*|GRUB_BTRFS_SCRIPT_CHECK=grub2-script-check|' \
"$tmp/grub-btrfs/config"
sudo make -C "$tmp/grub-btrfs" install
sudo systemctl enable --now grub-btrfsd.service
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
rm -rf "$tmp"
if (( rc == 0 )); then
ok "Snapper + grub-btrfs installed. Every dnf transaction now gets a pre/post snapshot."
info "Try it: snapper ls | snapper status 1..2 | sudo snapper undochange 1..2"
fi
return $rc
info "Installing DNF5 snapper integration (vendored scripts)..."
sudo install -m 755 "$vendor"/scripts/* /usr/local/bin/
sudo restorecon -v /usr/local/bin/snapper-*.sh 2>/dev/null || true
sudo mkdir -p /etc/dnf/libdnf5-plugins/actions.d/
sudo install -m 644 "$vendor/config/snapper.actions" /etc/dnf/libdnf5-plugins/actions.d/
info "Enabling snapper timers..."
sudo systemctl enable --now snapper-timeline.timer snapper-cleanup.timer
ok "Snapper + grub-btrfs installed. Every dnf transaction now gets a pre/post snapshot."
info "Try it: snapper ls | snapper status 1..2 | sudo snapper undochange 1..2"
}
+3 -4
View File
@@ -36,7 +36,7 @@ Options:
--de=NAME gnome | plasma | xfce | none
--twm=NAME chadwm | hyprland | mango | niri | none
--selinux=X keep | disabled (asked, default keep)
--snapper=X yes | no (asked when / is btrfs, default yes)
--snapper=X yes | no (automatic on btrfs; use 'no' to opt out)
--dotfiles=X yes | no (clone personal dotfiles, default no)
-y, --yes never prompt (requires --de and --twm)
-d, --debug pause after each section instead of aborting on error
@@ -244,9 +244,8 @@ if [[ -z "$SELINUX_OPT" ]]; then
[[ "$choice" == "yes" ]] && SELINUX_DISABLED=true
printf "\n"
if findmnt -n -o FSTYPE / 2>/dev/null | grep -q btrfs; then
ok "Root filesystem is btrfs — snapshot support is available."
choice=$(ask_yes_no "Set up snapper + grub-btrfs snapshots and zstd compression?" "yes")
[[ "$choice" == "yes" ]] && BTRFS_SNAPPER=true
ok "Root filesystem is btrfs — snapshots + zstd compression will be set up."
BTRFS_SNAPPER=true
else
info "Root filesystem is not btrfs; skipping snapshot setup."
fi
+11
View File
@@ -0,0 +1,11 @@
# Vendored: SysGuides snapper scripts for Fedora
Source: https://github.com/SysGuides/sysguides-snapper-fedora (MIT, Madhu Desai / SysGuides)
Vendored (2026-10-27, upstream commit 0a28eb4) so setup does not depend on an
external repo at runtime. `lib/common.sh:setup_snapper` installs:
- scripts/snapper-*.sh -> /usr/local/bin/ (DNF5 pre/post snapshot actions)
- config/snapper.actions -> /etc/dnf/libdnf5-plugins/actions.d/
To update: re-fetch from upstream, diff, commit here.
+26
View File
@@ -0,0 +1,26 @@
# snapper.actions
# ----------------
# Snapper integration with libdnf5 (DNF5 transactions)
#
# This file defines hooks that:
# - Create PRE snapshots before transactions
# - Capture GUI package details
# - Create POST snapshots after transactions
# - Apply WAL checkpoint fix for consistency
#
# Project: sysguides-snapper-fedora
# Author: Madhu Desai (SysGuides)
# Website: https://sysguides.com
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
# Create PRE snapshot
pre_transaction::::/usr/local/bin/snapper-pre.sh ${pid}
# Capture GUI package info (incoming packages)
pre_transaction:*:in::/usr/local/bin/snapper-gui-pkg.sh ${pid} ${pkg.action} ${pkg.name}
# Capture GUI package info (outgoing packages)
pre_transaction:*:out::/usr/local/bin/snapper-gui-pkg.sh ${pid} ${pkg.action} ${pkg.name}
# Create POST snapshot (with WAL fix)
post_transaction::::/usr/local/bin/snapper-post.sh ${pid}
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# snapper-desc.sh
# ----------------
# Determines a human-readable description for the transaction.
#
# This script inspects the process that triggered the DNF5 transaction.
#
# Behavior:
# - If the transaction originates from GUI tools (PackageKit / dnf5daemon),
# it returns "GUI"
# - Otherwise, it returns the actual command (e.g., "dnf install vim")
#
# This description is later used by Snapper for snapshot naming.
#
# Project: sysguides-snapper-fedora
# Author: Madhu Desai (SysGuides)
# Website: https://sysguides.com
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
PID="$1"
# Get the full command of the originating process
cmd=$(ps -o command --no-headers -p "$PID" 2>/dev/null || echo "Unknown Task")
case "$cmd" in
*/dnf5daemon* | */packagekitd*)
# GUI-based transaction
echo "GUI"
;;
*)
# CLI or unknown source
echo "$cmd"
;;
esac
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# snapper-gui-pkg.sh
# ------------------
# Captures the first package involved in a GUI-based transaction.
#
# This script:
# - Runs during DNF5 transaction events (pre_transaction hooks)
# - Detects if the transaction originated from a GUI
# - Records the first package name and action (install/remove)
#
# This allows snapshots to have meaningful descriptions like:
# "GUI install firefox"
#
# Only the first package is recorded to keep descriptions concise.
#
# Project: sysguides-snapper-fedora
# Author: Madhu Desai (SysGuides)
# Website: https://sysguides.com
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
PID="$1"
ACTION="$2"
NAME="$3"
STATE_DIR="/run/snapper-actions"
DESC_FILE="$STATE_DIR/snapper_desc_${PID}"
PKG_FILE="$STATE_DIR/snapper_gui_${PID}"
# Read previously stored description (set in PRE phase)
desc=$(cat "$DESC_FILE" 2>/dev/null || echo "")
# Only proceed if this is a GUI transaction
[[ "$desc" != "GUI" ]] && exit 0
# Only capture the first package to avoid overwriting
[[ -f "$PKG_FILE" ]] && exit 0
# Determine action type and store description
case "$ACTION" in
I|U|D|R)
# Install / upgrade / downgrade / reinstall
echo "GUI install ${NAME}" > "$PKG_FILE"
;;
E|O)
# Erase / obsolete
echo "GUI remove ${NAME}" > "$PKG_FILE"
;;
esac
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# snapper-post.sh
# ----------------
# Creates a POST snapshot after a DNF5 transaction completes.
#
# This script:
# - Retrieves stored PRE snapshot and description
# - Improves description using GUI package info (if available)
# - Applies SQLite WAL checkpoint fix for libdnf5
# - Creates the POST snapshot linked to the PRE snapshot
# - Cleans up temporary state files
#
# The WAL checkpoint is necessary on Fedora 44+ to reduce
# inconsistencies between filesystem state and RPM database.
#
# Project: sysguides-snapper-fedora
# Author: Madhu Desai (SysGuides)
# Website: https://sysguides.com
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
PID="$1"
STATE_DIR="/run/snapper-actions"
DESC_FILE="$STATE_DIR/snapper_desc_${PID}"
PRE_FILE="$STATE_DIR/snapper_pre_${PID}"
GUI_FILE="$STATE_DIR/snapper_gui_${PID}"
# Load stored state (if available)
desc=$(cat "$DESC_FILE" 2>/dev/null || echo "")
pre=$(cat "$PRE_FILE" 2>/dev/null || echo "")
gui_pkg=$(cat "$GUI_FILE" 2>/dev/null || echo "")
# If no PRE snapshot exists, nothing to do
[[ -z "$pre" ]] && exit 0
# If GUI package info exists, improve snapshot description
if [[ -n "$gui_pkg" ]]; then
desc="$gui_pkg"
snapper -c root modify -d "$desc" "$pre" || true
fi
# Apply WAL checkpoint (best-effort; do not fail if it doesn't succeed)
/usr/local/bin/snapper-wal-checkpoint.sh || true
# Create POST snapshot linked to PRE snapshot
snapper -c root create -c number -t post \
--pre-number "$pre" -d "$desc"
# Clean up temporary state files
rm -f "$DESC_FILE" "$PRE_FILE" "$GUI_FILE"
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -e
# snapper-pre.sh
# ----------------
# Creates a PRE snapshot before a DNF5 transaction begins.
#
# This script:
# - Generates a description using snapper-desc.sh
# - Stores the description and snapshot number in /run for later use
#
# The stored data is used by the POST script to:
# - Pair snapshots correctly
# - Improve snapshot descriptions (especially for GUI installs)
#
# Project: sysguides-snapper-fedora
# Author: Madhu Desai (SysGuides)
# Website: https://sysguides.com
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
PID="$1"
STATE_DIR="/run/snapper-actions"
# Ensure runtime directory exists with secure permissions
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
# On a fresh setup, DNF5 may fail with "filesystem error: cannot copy: packages.toml"
# if this directory is missing, leaving an orphaned PRE snapshot with no POST.
# This block ensures the directory exists before the transaction proceeds.
if [[ ! -d /usr/lib/sysimage/libdnf5 ]]; then
mkdir -p /usr/lib/sysimage/libdnf5
restorecon -q /usr/lib/sysimage/libdnf5 2>/dev/null || true
fi
# Get transaction description (GUI or CLI command)
desc=$(/usr/local/bin/snapper-desc.sh "$PID")
# Store description for use in POST phase
echo "$desc" > "$STATE_DIR/snapper_desc_${PID}"
# Create PRE snapshot and store snapshot number
pre=$(snapper -c root create -c number -t pre -p -d "$desc") || exit 1
echo "$pre" > "$STATE_DIR/snapper_pre_${PID}"
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# snapper-wal-checkpoint.sh
# --------------------------
# Forces an SQLite WAL checkpoint for the RPM database used by libdnf5.
#
# Fedora 44 introduced DNF5 (libdnf5), which uses SQLite with WAL mode.
# This can lead to inconsistencies during snapper undochange, where:
# - Files are reverted correctly
# - But the RPM database still reports packages as installed
#
# This script attempts to flush WAL data to the main database file
# before the POST snapshot is created.
#
# The operation is best-effort:
# - Retries multiple times if the database is busy
# - Fails silently if unable to complete
#
# Project: sysguides-snapper-fedora
# Author: Madhu Desai (SysGuides)
# Website: https://sysguides.com
# GitHub: https://github.com/SysGuides/sysguides-snapper-fedora
python3 - <<'EOF'
import sqlite3
import time
import sys
DB = "/usr/lib/sysimage/rpm/rpmdb.sqlite"
# Try up to 10 times to checkpoint WAL (single process, no respawn overhead)
for i in range(10):
try:
conn = sqlite3.connect(DB, timeout=3)
conn.execute("PRAGMA busy_timeout=3000")
result = conn.execute("PRAGMA wal_checkpoint(TRUNCATE)").fetchone()
conn.close()
# result[1] = number of remaining frames in WAL (0 = fully flushed)
if result and result[1] == 0:
sys.exit(0)
except sqlite3.OperationalError:
pass
time.sleep(0.5)
# Best-effort fallback
sys.exit(1)
EOF