diff --git a/lib/common.sh b/lib/common.sh index 0ea6874..cac492a 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -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" } diff --git a/setup.sh b/setup.sh index bf8250a..8e53ee0 100755 --- a/setup.sh +++ b/setup.sh @@ -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 diff --git a/vendor/snapper/README.md b/vendor/snapper/README.md new file mode 100644 index 0000000..87649db --- /dev/null +++ b/vendor/snapper/README.md @@ -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. diff --git a/vendor/snapper/config/snapper.actions b/vendor/snapper/config/snapper.actions new file mode 100644 index 0000000..c5ca34f --- /dev/null +++ b/vendor/snapper/config/snapper.actions @@ -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} diff --git a/vendor/snapper/scripts/snapper-desc.sh b/vendor/snapper/scripts/snapper-desc.sh new file mode 100755 index 0000000..5c6fdad --- /dev/null +++ b/vendor/snapper/scripts/snapper-desc.sh @@ -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 diff --git a/vendor/snapper/scripts/snapper-gui-pkg.sh b/vendor/snapper/scripts/snapper-gui-pkg.sh new file mode 100755 index 0000000..e4a7c98 --- /dev/null +++ b/vendor/snapper/scripts/snapper-gui-pkg.sh @@ -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 diff --git a/vendor/snapper/scripts/snapper-post.sh b/vendor/snapper/scripts/snapper-post.sh new file mode 100755 index 0000000..beed153 --- /dev/null +++ b/vendor/snapper/scripts/snapper-post.sh @@ -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" diff --git a/vendor/snapper/scripts/snapper-pre.sh b/vendor/snapper/scripts/snapper-pre.sh new file mode 100755 index 0000000..f47589e --- /dev/null +++ b/vendor/snapper/scripts/snapper-pre.sh @@ -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}" diff --git a/vendor/snapper/scripts/snapper-wal-checkpoint.sh b/vendor/snapper/scripts/snapper-wal-checkpoint.sh new file mode 100755 index 0000000..a8dcad9 --- /dev/null +++ b/vendor/snapper/scripts/snapper-wal-checkpoint.sh @@ -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