From 47991727fff93f138e24debfe42e8c78035b2863 Mon Sep 17 00:00:00 2001 From: Pathfinder Date: Tue, 27 Oct 2026 12:25:13 +0100 Subject: [PATCH 1/2] Add dotfiles integration module (fedora-dots) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New modules/user/dotfiles.sh behind a menu question ('Clone personal dotfiles?', default no) and --dotfiles=yes|no flag. Does a --bare clone of yuri/fedora-dots into ~/.cfg.git, checks the repo out into $HOME with status.showUntrackedFiles=no, and handles an empty repo gracefully (configures the alias, skips checkout). Existing ~/.cfg.git is left untouched (idempotent). Fix proven in the harness: the module originally used a plain git clone, which creates a working copy and breaks the git-dir alias — the same failure YuKu hit manually; --bare is required. Dispatch runs the dotfiles module before DE/TWM so configs are in place when sessions are installed. --- modules/user/dotfiles.sh | 56 ++++++++++++++++++++++++++++++++++++++++ setup.sh | 28 ++++++++++++++++++-- 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100755 modules/user/dotfiles.sh diff --git a/modules/user/dotfiles.sh b/modules/user/dotfiles.sh new file mode 100755 index 0000000..19a5324 --- /dev/null +++ b/modules/user/dotfiles.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../../lib/common.sh +source "$HERE/../../lib/common.sh" + +if is_debug; then set +e; else set -e; fi + +DOTFILES_REPO="${DOTFILES_REPO:-https://git.ku-it.org/yuri/fedora-dots.git}" +CFG_DIR="$HOME/.cfg.git" + +step "Personal dotfiles: fedora-dots" +rule + +if [[ ! -d "$HOME/.config" ]]; then + mkdir -p "$HOME/.config" +fi + +if [[ -d "$CFG_DIR" ]]; then + ok "Dotfiles already set up ($CFG_DIR exists). Skipping." + exit 0 +fi + +# A bare repo is the storage; $HOME is the work tree. Checkout happens through +# the alias form, never a plain `git checkout` inside $HOME. +info "Cloning dotfiles (bare) into $CFG_DIR..." +# --bare is what makes ~/.cfg.git the storage; the plain clone YuKu first ran +# created a working copy instead and broke the alias (proven the hard way). +git clone --quiet --bare "$DOTFILES_REPO" "$CFG_DIR" + +cfg() { + git --git-dir="$CFG_DIR" --work-tree="$HOME" "$@" +} + +# Empty repo (no commits yet): nothing to check out, but leave everything +# configured so the first `cfg add` on this machine just works. +if ! git --git-dir="$CFG_DIR" rev-parse --verify -q HEAD >/dev/null; then + warn "Dotfiles repo has no commits yet — checkout skipped." + warn "Add folders on your main machine first; here you can start fresh." + cfg config status.showUntrackedFiles no + ok "cfg alias ready: git --git-dir=$CFG_DIR --work-tree=$HOME" + exit 0 +fi + +info "Checking dotfiles out into \$HOME..." +# -f on purpose: a fresh machine HAS stock files (e.g. .bashrc); the repo's +# versions win on first setup. Untracked files are never touched. +cfg checkout -f +cfg config status.showUntrackedFiles no + +ok "Dotfiles checked out. Tracked paths:" +git --git-dir="$CFG_DIR" ls-tree -r --name-only HEAD | cut -d/ -f1-2 | sort -u | sed 's/^/ /' + +info "Suggested shell alias for managing them:" +info " alias cfg='git --git-dir=$CFG_DIR --work-tree=\$HOME'" diff --git a/setup.sh b/setup.sh index 4250242..bf8250a 100755 --- a/setup.sh +++ b/setup.sh @@ -21,6 +21,7 @@ DE="" TWM="" SELINUX_OPT="" SNAPPER_OPT="" +DOTFILES_OPT="" usage() { cat <<'EOF' @@ -36,6 +37,7 @@ Options: --twm=NAME chadwm | hyprland | mango | niri | none --selinux=X keep | disabled (asked, default keep) --snapper=X yes | no (asked when / is btrfs, default yes) + --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 -h, --help show this help @@ -59,6 +61,7 @@ for arg in "$@"; do --de=*) DE="${arg#*=}" ;; --selinux=*) SELINUX_OPT="${arg#*=}" ;; --snapper=*) SNAPPER_OPT="${arg#*=}" ;; + --dotfiles=*) DOTFILES_OPT="${arg#*=}" ;; --twm=*) TWM="${arg#*=}" ;; -h|--help) usage; exit 0 ;; *) @@ -256,9 +259,23 @@ case "${SELINUX_OPT:-}" in esac case "${SNAPPER_OPT:-}" in yes) BTRFS_SNAPPER=true ;; - no|"") : ;; + no) BTRFS_SNAPPER=false ;; # explicit flag beats the interactive default + ""|keep) : ;; *) err "Unknown --snapper value: '$SNAPPER_OPT' (yes|no)"; exit 2 ;; esac + +DOTFILES=false +if [[ -z "$DOTFILES_OPT" ]]; then + printf "\n" + choice=$(ask_yes_no "Clone personal dotfiles (fedora-dots) into \$HOME?" "no") + [[ "$choice" == "yes" ]] && DOTFILES=true +else + case "$DOTFILES_OPT" in + yes) DOTFILES=true ;; + no) : ;; + *) err "Unknown --dotfiles value: '$DOTFILES_OPT' (yes|no)"; exit 2 ;; + esac +fi pause_if_debug #------------------------------------------ @@ -361,6 +378,11 @@ if [[ "$BTRFS_SNAPPER" == "true" ]]; then pause_if_debug fi +if [[ "$DOTFILES" == "true" ]]; then + run_module "$SCRIPT_DIR/modules/user/dotfiles.sh" + pause_if_debug +fi + if [[ "$DE" != "none" ]]; then run_module "$SCRIPT_DIR/modules/de/${DE}.sh" pause_if_debug @@ -378,7 +400,9 @@ step "7 Summary" rule ok "DE: $DE" ok "TWM: $TWM" -ok "SELinux: $([[ "$SELINUX_DISABLED" == "true" ]] && echo disabled || echo keeping)"; ok "Btrfs snapshots: $([[ "$BTRFS_SNAPPER" == "true" ]] && echo yes || echo skipped)" +ok "SELinux: $([[ "$SELINUX_DISABLED" == "true" ]] && echo disabled || echo keeping)" +ok "Btrfs snapshots: $([[ "$BTRFS_SNAPPER" == "true" ]] && echo yes || echo skipped)" +ok "Dotfiles: $([[ "$DOTFILES" == "true" ]] && echo clone || echo no)" DM=$(detect_display_manager) ok "Display manager: ${DM:-none}" From 3776dcb5a4f0cde20e63b9702f453839573f4f3b Mon Sep 17 00:00:00 2001 From: Pathfinder Date: Tue, 27 Oct 2026 12:37:18 +0100 Subject: [PATCH 2/2] Snapper: no question on btrfs; vendor the SysGuides scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- lib/common.sh | 69 +++++++++++++++---- setup.sh | 7 +- vendor/snapper/README.md | 11 +++ vendor/snapper/config/snapper.actions | 26 +++++++ vendor/snapper/scripts/snapper-desc.sh | 35 ++++++++++ vendor/snapper/scripts/snapper-gui-pkg.sh | 49 +++++++++++++ vendor/snapper/scripts/snapper-post.sh | 51 ++++++++++++++ vendor/snapper/scripts/snapper-pre.sh | 45 ++++++++++++ .../snapper/scripts/snapper-wal-checkpoint.sh | 47 +++++++++++++ 9 files changed, 324 insertions(+), 16 deletions(-) create mode 100644 vendor/snapper/README.md create mode 100644 vendor/snapper/config/snapper.actions create mode 100755 vendor/snapper/scripts/snapper-desc.sh create mode 100755 vendor/snapper/scripts/snapper-gui-pkg.sh create mode 100755 vendor/snapper/scripts/snapper-post.sh create mode 100755 vendor/snapper/scripts/snapper-pre.sh create mode 100755 vendor/snapper/scripts/snapper-wal-checkpoint.sh 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