Dotfiles integration: modules/user/dotfiles.sh #2

Merged
yuri merged 2 commits from feat/dotfiles-integration into main 2026-10-27 12:49:54 +01:00
10 changed files with 406 additions and 18 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"
}
+56
View File
@@ -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'"
+29 -6
View File
@@ -21,6 +21,7 @@ DE=""
TWM=""
SELINUX_OPT=""
SNAPPER_OPT=""
DOTFILES_OPT=""
usage() {
cat <<'EOF'
@@ -35,7 +36,8 @@ 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
-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 ;;
*)
@@ -241,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
@@ -256,9 +258,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 +377,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 +399,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}"
+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