From c048b2c74505807ac8cf5b74eaf77d4b7f399fd5 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sun, 21 Jun 2026 02:25:11 -0400 Subject: [PATCH] Normalize Snapper snapshot setup --- bin/omarchy-setup-system | 1 + bin/omarchy-upgrade-to-4 | 13 ++++ default/snapper/root | 2 + install/config/all.sh | 1 + install/config/snapper.sh | 24 +++++++ migrations/1781984677.sh | 39 +++++++++++ test/shell.d/config-test.sh | 8 ++- test/shell.d/snapper-test.sh | 111 ++++++++++++++++++++++++++++++ test/shell.d/upgrade-to-4-test.sh | 5 ++ 9 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 install/config/snapper.sh create mode 100644 migrations/1781984677.sh create mode 100644 test/shell.d/snapper-test.sh diff --git a/bin/omarchy-setup-system b/bin/omarchy-setup-system index 0d4e7df7..d66cbf80 100755 --- a/bin/omarchy-setup-system +++ b/bin/omarchy-setup-system @@ -78,6 +78,7 @@ run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh" run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh" run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh" run_logged "$OMARCHY_INSTALL/config/docker.sh" +run_logged "$OMARCHY_INSTALL/config/snapper.sh" run_logged "$OMARCHY_INSTALL/config/enable-services.sh" run_logged "$OMARCHY_INSTALL/config/firewall.sh" diff --git a/bin/omarchy-upgrade-to-4 b/bin/omarchy-upgrade-to-4 index 8e439654..59b3bb7d 100755 --- a/bin/omarchy-upgrade-to-4 +++ b/bin/omarchy-upgrade-to-4 @@ -456,6 +456,18 @@ normalize_limine_config() { fi } +configure_snapper_policy() { + local snapper_config_script=/usr/share/omarchy/install/config/snapper.sh + + if ! as_root test -f "$snapper_config_script"; then + warn "$snapper_config_script is unavailable; Snapper snapshot retention was not normalized." + return 0 + fi + + log "Configuring Omarchy snapshot retention" + as_root env OMARCHY_PATH=/usr/share/omarchy bash -euo pipefail "$snapper_config_script" +} + remove_conflicting_legacy_packages() { # Remove legacy packages that are known to conflict with Omarchy 4 defaults # before the main package transaction. Broader retired-package cleanup runs @@ -2226,6 +2238,7 @@ remove_legacy_limine_configs remove_conflicting_legacy_packages install_omarchy_4_packages normalize_limine_config +configure_snapper_policy migrate_1password_beta_package cleanup_legacy_user_paths apply_system_transition diff --git a/default/snapper/root b/default/snapper/root index 96d0ef23..af941c16 100644 --- a/default/snapper/root +++ b/default/snapper/root @@ -2,6 +2,8 @@ SUBVOLUME="/" FSTYPE="btrfs" +NUMBER_CLEANUP="yes" +NUMBER_MIN_AGE="0" NUMBER_LIMIT="5" NUMBER_LIMIT_IMPORTANT="5" diff --git a/install/config/all.sh b/install/config/all.sh index f5a70796..e416b7bc 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -3,5 +3,6 @@ run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh" run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh" run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh" run_logged "$OMARCHY_INSTALL/config/docker.sh" +run_logged "$OMARCHY_INSTALL/config/snapper.sh" run_logged "$OMARCHY_INSTALL/config/enable-services.sh" run_logged "$OMARCHY_INSTALL/config/firewall.sh" diff --git a/install/config/snapper.sh b/install/config/snapper.sh new file mode 100644 index 00000000..3e936ec1 --- /dev/null +++ b/install/config/snapper.sh @@ -0,0 +1,24 @@ +SNAPPER_CONFIG_PATH="${OMARCHY_SNAPPER_CONFIG_PATH:-/etc/snapper/configs/root}" +SNAPPER_CONF_PATH="${OMARCHY_SNAPPER_CONF_PATH:-/etc/conf.d/snapper}" +template="${OMARCHY_SNAPPER_TEMPLATE:-${OMARCHY_PATH:-/usr/share/omarchy}/default/snapper/root}" + +echo "Configuring Omarchy Snapper snapshot retention" + +if [[ ! -f $SNAPPER_CONFIG_PATH ]]; then + mkdir -p "$(dirname "$SNAPPER_CONFIG_PATH")" + + if [[ ${OMARCHY_SNAPPER_CONFIGURE_TEST:-0} == "1" ]]; then + : >"$SNAPPER_CONFIG_PATH" + else + snapper --no-dbus -c root create-config / >/dev/null 2>&1 || snapper -c root create-config / >/dev/null + fi +fi + +install -m 0644 "$template" "$SNAPPER_CONFIG_PATH" + +mkdir -p "$(dirname "$SNAPPER_CONF_PATH")" +printf '%s\n' 'SNAPPER_CONFIGS="root"' >"$SNAPPER_CONF_PATH" +chmod 0644 "$SNAPPER_CONF_PATH" + +systemctl disable --now snapper-timeline.timer >/dev/null 2>&1 || true +systemctl enable --now snapper-cleanup.timer limine-snapper-sync.service >/dev/null 2>&1 || true diff --git a/migrations/1781984677.sh b/migrations/1781984677.sh new file mode 100644 index 00000000..8cfa5023 --- /dev/null +++ b/migrations/1781984677.sh @@ -0,0 +1,39 @@ +echo "Normalize Snapper snapshot services" + +OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}" +snapper_config_script=/usr/share/omarchy/install/config/snapper.sh +if [[ ! -f $snapper_config_script ]]; then + snapper_config_script="$OMARCHY_PATH/install/config/snapper.sh" +fi + +as_root() { + if (( EUID == 0 )); then + "$@" + else + sudo "$@" + fi +} + +unit_enabled() { + systemctl is-enabled --quiet "$1" >/dev/null 2>&1 +} + +unit_active() { + systemctl is-active --quiet "$1" >/dev/null 2>&1 +} + +needs_repair=0 + +[[ -f /etc/snapper/configs/root ]] || needs_repair=1 + +if ! unit_enabled snapper-cleanup.timer || ! unit_active snapper-cleanup.timer; then + needs_repair=1 +fi + +if ! unit_enabled limine-snapper-sync.service || ! unit_active limine-snapper-sync.service; then + needs_repair=1 +fi + +(( needs_repair )) || exit 0 + +as_root env OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$snapper_config_script" diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 9eade041..7143a205 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -94,7 +94,12 @@ import sys from pathlib import Path root = Path(os.environ["ROOT"]) -pkgs_root = root.parent / "omarchy-pkgs/pkgbuilds" +pkgs_candidates = [ + root.parent / "omarchy-pkgs/pkgbuilds", + root.parent / "omarchy/omarchy-pkgs/pkgbuilds", + root.parent.parent / "omarchy-pkgs/pkgbuilds", +] +pkgs_root = next((path for path in pkgs_candidates if path.exists()), pkgs_candidates[0]) settings_pkgbuild_path = pkgs_root / "omarchy-settings/PKGBUILD" omarchy_pkgbuild_path = pkgs_root / "omarchy/PKGBUILD" if not settings_pkgbuild_path.exists(): @@ -118,6 +123,7 @@ package_defaults = [ ("default/systemd/user/omarchy-update-user-notify.service", "/usr/lib/systemd/user/omarchy-update-user-notify.service", "systemd/user/omarchy-update-user-notify.service"), ("default/systemd/user/omarchy-update-user-notify.path", "/usr/lib/systemd/user/omarchy-update-user-notify.path", "systemd/user/omarchy-update-user-notify.path"), ("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"), + ("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"), ] for source, destination, legacy in package_defaults: diff --git a/test/shell.d/snapper-test.sh b/test/shell.d/snapper-test.sh new file mode 100644 index 00000000..5df4bbfd --- /dev/null +++ b/test/shell.d/snapper-test.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +template="$ROOT/default/snapper/root" + +grep -Fx 'NUMBER_CLEANUP="yes"' "$template" >/dev/null +grep -Fx 'NUMBER_LIMIT="5"' "$template" >/dev/null +grep -Fx 'TIMELINE_CREATE="no"' "$template" >/dev/null +! grep -Eq '^TIMELINE_(CLEANUP|LIMIT_)' "$template" || fail "Snapper template keeps timeline cleanup details out of the default config" +pass "Snapper template keeps update snapshots and disables timeline snapshots" + +test_tmp=$(mktemp -d) +trap 'rm -rf "$test_tmp"' EXIT + +fake_bin="$test_tmp/bin" +mkdir -p "$fake_bin" + +cat >"$fake_bin/snapper" <<'STUB' +#!/bin/bash +printf 'snapper %s\n' "$*" >>"$TEST_LOG" +STUB +chmod +x "$fake_bin/snapper" + +cat >"$fake_bin/systemctl" <<'STUB' +#!/bin/bash +printf 'systemctl %s\n' "$*" >>"$TEST_LOG" +STUB +chmod +x "$fake_bin/systemctl" + +TEST_LOG="$test_tmp/calls.log" \ +PATH="$fake_bin:$PATH" \ +OMARCHY_SNAPPER_CONFIGURE_TEST=1 \ +OMARCHY_PATH="$ROOT" \ +OMARCHY_SNAPPER_CONFIG_PATH="$test_tmp/etc/snapper/configs/root" \ +OMARCHY_SNAPPER_CONF_PATH="$test_tmp/etc/conf.d/snapper" \ + bash -euo pipefail "$ROOT/install/config/snapper.sh" >/dev/null + +cmp -s "$template" "$test_tmp/etc/snapper/configs/root" || fail "snapshot configure installs the Omarchy Snapper template" +grep -Fx 'SNAPPER_CONFIGS="root"' "$test_tmp/etc/conf.d/snapper" >/dev/null || fail "snapshot configure writes /etc/conf.d/snapper" +grep -Fx 'systemctl disable --now snapper-timeline.timer' "$test_tmp/calls.log" >/dev/null || fail "snapshot configure disables timeline snapshots" +grep -Fx 'systemctl enable --now snapper-cleanup.timer limine-snapper-sync.service' "$test_tmp/calls.log" >/dev/null || fail "snapshot configure enables cleanup and Limine snapshot sync" +pass "snapshot configure normalizes Snapper policy and services" + +setup_system="$ROOT/bin/omarchy-setup-system" +grep -F 'config/snapper.sh' "$setup_system" >/dev/null +pass "system setup normalizes Snapper during fresh installs" + +migration=$(grep -rl 'Normalize Snapper snapshot services' "$ROOT/migrations" | head -n 1 || true) +[[ -n $migration ]] || fail "Snapper service migration exists" +grep -F 'unit_active snapper-cleanup.timer' "$migration" >/dev/null +grep -F 'unit_active limine-snapper-sync.service' "$migration" >/dev/null +grep -F 'sudo "$@"' "$migration" >/dev/null +grep -F 'as_root env OMARCHY_PATH="$OMARCHY_PATH" bash -euo pipefail "$snapper_config_script"' "$migration" >/dev/null +! grep -F 'NUMBER_LIMIT="5"' "$migration" >/dev/null || fail "Snapper service migration does not overwrite working custom retention" +pass "Snapper service migration only repairs broken services idempotently" + +find_omarchy_pks_root() { + local candidate + for candidate in \ + "$ROOT/../omarchy-pkgs/pkgbuilds" \ + "$ROOT/../omarchy/omarchy-pkgs/pkgbuilds" \ + "$ROOT/../../omarchy-pkgs/pkgbuilds"; do + if [[ -d $candidate ]]; then + cd "$candidate" && pwd + return 0 + fi + done + return 1 +} + +pkgs_root=$(find_omarchy_pks_root) || fail "omarchy-pkgs checkout is available for packaging coverage" +settings_pkgbuild="$pkgs_root/omarchy-settings-dev/PKGBUILD" +omarchy_pkgbuild="$pkgs_root/omarchy-dev/PKGBUILD" + +grep -F 'cp -a default/. "$pkgdir/usr/share/omarchy/default/"' "$settings_pkgbuild" >/dev/null || fail "omarchy-settings package bundles default/" +grep -F 'install -Dm644 default/snapper/root \' "$settings_pkgbuild" >/dev/null || fail "omarchy-settings package installs Snapper template source" +grep -F '"$pkgdir/etc/snapper/config-templates/omarchy"' "$settings_pkgbuild" >/dev/null || fail "omarchy-settings package installs Snapper template destination" +grep -F "'snapper'" "$omarchy_pkgbuild" >/dev/null || fail "omarchy package depends on snapper" +grep -F "'limine-snapper-sync'" "$omarchy_pkgbuild" >/dev/null || fail "omarchy package depends on limine-snapper-sync" +grep -F 'cp -a install "$pkgdir/usr/share/omarchy/"' "$omarchy_pkgbuild" >/dev/null || fail "omarchy package bundles install scripts" +grep -F 'cp -a migrations "$pkgdir/usr/share/omarchy/"' "$omarchy_pkgbuild" >/dev/null || fail "omarchy package bundles migrations" +pass "omarchy-pkgs packages Snapper template, setup, and migration coverage" + +find_omarchy_iso_root() { + local candidate + for candidate in \ + "$ROOT/../omarchy-iso" \ + "$ROOT/../omarchy/omarchy-iso" \ + "$ROOT/../../omarchy-iso"; do + if [[ -d $candidate ]]; then + cd "$candidate" && pwd + return 0 + fi + done + return 1 +} + +iso_root=$(find_omarchy_iso_root) || fail "omarchy-iso checkout is available for installer coverage" +configurator="$iso_root/configs/airootfs/root/configurator" +phases="$iso_root/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py" +manifest="$iso_root/manifests/fresh-4-semantic.json" + +! grep -F 'snapshot_config' "$configurator" >/dev/null || fail "ISO does not ask archinstall to create Snapper timeline config" +! grep -F '_configure_snapper_root' "$phases" >/dev/null || fail "ISO does not duplicate Omarchy Snapper setup" +grep -F 'run_system_finalizer' "$phases" >/dev/null || fail "ISO runs packaged system setup" +grep -F '/etc/systemd/system/timers.target.wants/snapper-cleanup.timer' "$manifest" >/dev/null || fail "fresh ISO manifest has snapper-cleanup timer enabled" +! grep -F '/etc/systemd/system/timers.target.wants/snapper-timeline.timer' "$manifest" >/dev/null || fail "fresh ISO manifest does not enable snapper timeline timer" +pass "omarchy-iso delegates Snapper setup to packaged system setup" diff --git a/test/shell.d/upgrade-to-4-test.sh b/test/shell.d/upgrade-to-4-test.sh index 754773f0..b3210f91 100644 --- a/test/shell.d/upgrade-to-4-test.sh +++ b/test/shell.d/upgrade-to-4-test.sh @@ -25,6 +25,11 @@ grep -F 'skip-first-run-update-notification' "$first_run_wifi" >/dev/null grep -F '(( skip_update_notification )) && return 0' "$first_run_wifi" >/dev/null pass "Omarchy 4 upgrade suppresses the fresh-install update toast" +grep -F 'configure_snapper_policy' "$upgrade_to_4" >/dev/null +grep -F '/usr/share/omarchy/install/config/snapper.sh' "$upgrade_to_4" >/dev/null +grep -F 'bash -euo pipefail "$snapper_config_script"' "$upgrade_to_4" >/dev/null +pass "Omarchy 4 upgrade normalizes Snapper retention" + grep -F 'OMARCHY_UPGRADE_TO_4_LIVE=1' "$upgrade_to_4" >/dev/null grep -F 'systemd-networkd.service' "$upgrade_to_4" >/dev/null grep -F 'systemd-networkd.socket' "$upgrade_to_4" >/dev/null