diff --git a/bin/omarchy-pkg-aur-install b/bin/omarchy-pkg-aur-install index dcb7ba1e..3d38b4db 100755 --- a/bin/omarchy-pkg-aur-install +++ b/bin/omarchy-pkg-aur-install @@ -24,6 +24,6 @@ if [[ -n $pkg_names ]]; then source omarchy-sudo-keepalive echo "$pkg_names" | sed 's/^/aur\//' | tr '\n' ' ' | xargs yay -S --noconfirm - sudo updatedb + sudo updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots omarchy-show-done fi diff --git a/default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf b/default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf new file mode 100644 index 00000000..571a53bc --- /dev/null +++ b/default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf @@ -0,0 +1,3 @@ +[Service] +ExecStart= +ExecStart=/usr/bin/updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots diff --git a/docs/file-layout.md b/docs/file-layout.md index 4d4e0da0..d1565940 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -153,6 +153,14 @@ without a file conflict. Instead their sources (under `etc/` in the repo; Tradeoff: user edits to those files get clobbered on every `omarchy-settings` upgrade. This is documented in the PKGBUILD. +## Locate indexing + +`default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf` ships through `omarchy-settings` to `/usr/lib/systemd/system/plocate-updatedb.service.d/10-omarchy.conf`. It replaces the existing service's `ExecStart` with `updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots`, keeping Btrfs subvolume mounts searchable and excluding Snapper snapshots. The upstream service retains its timer, resource limits, and sandbox; Omarchy's existing AC-power condition still applies. + +`/etc/updatedb.conf` remains owned by plocate and is never rewritten by Omarchy. The command-line options override bind-mount pruning and add to the administrator's existing path exclusions. Installer and AUR package refreshes pass the same options directly because installation may run without systemd and an explicitly requested refresh should work on battery. + +Arch's systemd package hook reloads units when the vendor drop-in is installed or upgraded. The settings package containing the drop-in must ship alongside the runtime package that removes the old configuration helper and migration. Pacman removes those retired files; no new state migration is needed. A running indexer finishes with its original options, and subsequent service starts use the drop-in. For an immediate local test after installing the packages, restart `plocate-updatedb.service` while connected to AC power. + ## Env bootstrap (`default/bash/env-bootstrap`) Single source of truth for `OMARCHY_PATH` and dev-link-aware `PATH`. It: diff --git a/install/config/all.sh b/install/config/all.sh index d8c7d9bb..a221a398 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -7,6 +7,5 @@ run_logged "$OMARCHY_INSTALL/config/ssh-command-path.sh" run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh" run_logged "$OMARCHY_INSTALL/config/docker.sh" run_logged "$OMARCHY_INSTALL/config/snapper.sh" -run_logged "$OMARCHY_INSTALL/config/locate.sh" run_logged "$OMARCHY_INSTALL/config/enable-services.sh" run_logged "$OMARCHY_INSTALL/config/firewall.sh" diff --git a/install/config/locate.sh b/install/config/locate.sh deleted file mode 100644 index cc95e9b8..00000000 --- a/install/config/locate.sh +++ /dev/null @@ -1,32 +0,0 @@ -UPDATEDB_CONF_PATH="${OMARCHY_UPDATEDB_CONF_PATH:-/etc/updatedb.conf}" - -echo "Configuring locate to skip Btrfs snapshots and index Btrfs subvolumes" - -[[ -f $UPDATEDB_CONF_PATH ]] || exit 0 - -# updatedb refuses to run at all on a config that defines a variable twice, so -# every setting here is rewritten where it already stands and only appended -# when the file has no line for it. - -# Btrfs subvolume mounts (like /home) look like bind mounts, so pruning -# bind mounts leaves them out of the index entirely. -if grep -qE '^[[:space:]]*PRUNE_BIND_MOUNTS[[:space:]]*=' "$UPDATEDB_CONF_PATH"; then - sed -i -E 's|^[[:space:]]*PRUNE_BIND_MOUNTS[[:space:]]*=.*|PRUNE_BIND_MOUNTS = "no"|' "$UPDATEDB_CONF_PATH" -else - printf '%s\n' 'PRUNE_BIND_MOUNTS = "no"' >>"$UPDATEDB_CONF_PATH" -fi - -# Snapper snapshots are nested subvolumes reached by plain directory -# traversal, so without this updatedb indexes the system once per snapshot. -if grep -qE '^[[:space:]]*PRUNEPATHS[[:space:]]*=' "$UPDATEDB_CONF_PATH"; then - # updatedb only accepts quoted values and allows a comment after them. Read - # back what the machine already prunes and write the whole setting out again - # rather than splicing into a line of unknown shape. - pruned=$(sed -nE 's|^[[:space:]]*PRUNEPATHS[[:space:]]*=[[:space:]]*"([^"]*)".*|\1|p' "$UPDATEDB_CONF_PATH" | tail -n 1) - - if [[ " $pruned " != *" /.snapshots "* ]]; then - sed -i -E "s|^[[:space:]]*PRUNEPATHS[[:space:]]*=.*|PRUNEPATHS = \"/.snapshots${pruned:+ $pruned}\"|" "$UPDATEDB_CONF_PATH" - fi -else - printf '%s\n' 'PRUNEPATHS = "/.snapshots"' >>"$UPDATEDB_CONF_PATH" -fi diff --git a/install/post-install/localdb.sh b/install/post-install/localdb.sh index 8c3f36a8..6600d301 100644 --- a/install/post-install/localdb.sh +++ b/install/post-install/localdb.sh @@ -1,2 +1,3 @@ # Update localdb so locate can find the installed system files immediately. -updatedb +# Match the scheduled service while installation runs without a system manager. +updatedb --prune-bind-mounts=no --add-prunepaths=/.snapshots diff --git a/migrations/1784809451.sh b/migrations/1784809451.sh deleted file mode 100644 index bcd6c6de..00000000 --- a/migrations/1784809451.sh +++ /dev/null @@ -1,30 +0,0 @@ -echo "Configure locate to skip Btrfs snapshots and index Btrfs subvolumes" - -OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}" -locate_config_script="$OMARCHY_PATH/install/config/locate.sh" -UPDATEDB_CONF_PATH="${OMARCHY_UPDATEDB_CONF_PATH:-/etc/updatedb.conf}" - -as_root() { - if (( EUID == 0 )); then - "$@" - else - sudo "$@" - fi -} - -[[ -f $UPDATEDB_CONF_PATH ]] || exit 0 -[[ -f $locate_config_script ]] || exit 0 - -if grep -q '^PRUNE_BIND_MOUNTS = "no"' "$UPDATEDB_CONF_PATH" && - grep -E '^PRUNEPATHS' "$UPDATEDB_CONF_PATH" | grep -E '(^|[[:space:]"])/\.snapshots([[:space:]"]|$)' >/dev/null; then - exit 0 -fi - -as_root env OMARCHY_UPDATEDB_CONF_PATH="$UPDATEDB_CONF_PATH" bash -euo pipefail "$locate_config_script" - -# Rebuild the index with the new exclusions; pruning /.snapshots turns -# multi-hour runs on snapshot-heavy systems back into one-minute runs. Restart -# rather than start: the machines this targets are the ones with an updatedb -# already grinding through every snapshot, and a run that started before the -# rewrite keeps using the config it read at startup. -as_root systemctl restart --no-block plocate-updatedb.service >/dev/null 2>&1 || true diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 320ee0c8..355ca26b 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -151,6 +151,7 @@ package_defaults = [ ("default/systemd/user/omarchy-fcitx5.service", "/usr/lib/systemd/user/omarchy-fcitx5.service", "systemd/user/omarchy-fcitx5.service"), ("default/systemd/user/omarchy-crash-watch.service", "/usr/lib/systemd/user/omarchy-crash-watch.service", "systemd/user/omarchy-crash-watch.service"), ("default/systemd/zram-generator.conf.d/90-omarchy.conf", "/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf", "systemd/zram-generator.conf.d/90-omarchy.conf"), + ("default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf", "/usr/lib/systemd/system/plocate-updatedb.service.d/10-omarchy.conf", "systemd/system/plocate-updatedb.service.d/10-omarchy.conf"), ("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"), ("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"), ] diff --git a/test/shell.d/locate-test.sh b/test/shell.d/locate-test.sh index cab54c24..58eacbbd 100644 --- a/test/shell.d/locate-test.sh +++ b/test/shell.d/locate-test.sh @@ -4,166 +4,95 @@ set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" -config_script="$ROOT/install/config/locate.sh" +require_command python3 +require_command updatedb +require_command plocate -test_tmp=$(mktemp -d) -trap 'rm -rf "$test_tmp"' EXIT +python3 - <<'PY' +import os +from pathlib import Path +import shlex +import subprocess +import tempfile -stock_conf() { - cat >"$1" <<'CONF' -PRUNE_BIND_MOUNTS = "yes" -PRUNEFS = "9p afs autofs cifs fuse nfs nfs4 proc sysfs tmpfs" -PRUNENAMES = ".git .hg .svn" -PRUNEPATHS = "/afs /media /mnt /net /sfs /tmp /udev /var/cache /var/lib/pacman/local /var/lock /var/run /var/spool /var/tmp" -CONF -} +root = Path(os.environ["ROOT"]) -# updatedb dies on a config that defines a variable twice, so hand every -# rewritten file to the real parser rather than trusting the greps below. -empty_tree="$test_tmp/empty-tree" -mkdir -p "$empty_tree" +def check(condition, description): + if not condition: + raise SystemExit("not ok - " + description) + print("ok - " + description, flush=True) -assert_conf_parses() { - command -v updatedb >/dev/null || return 0 +drop_in = root / "default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf" +directives = [line.strip() for line in drop_in.read_text().splitlines() if line.strip() and not line.startswith("#")] +check(len(directives) == 3 and directives[:2] == ["[Service]", "ExecStart="] and directives[2].startswith("ExecStart="), + "locate drop-in replaces the command and preserves upstream service restrictions") +command = shlex.split(directives[2].removeprefix("ExecStart=")) +options = ["--prune-bind-mounts=no", "--add-prunepaths=/.snapshots"] +check(command == ["/usr/bin/updatedb", *options], + "locate service runs updatedb directly with fixed Btrfs options") +check("ConditionACPower=true" in (root / "etc/systemd/system/plocate-updatedb.service.d/ac-only.conf").read_text(), + "scheduled locate indexing keeps its AC-power condition") +check(not (root / "install/config/locate.sh").exists() and not (root / "migrations/1784809451.sh").exists(), + "the retired locate configuration helper and migration are absent") +for directory in ("bin", "install", "migrations"): + for path in (root / directory).rglob("*"): + if path.is_file(): + content = path.read_text() + if "OMARCHY_UPDATEDB_CONF_PATH" in content or "config/locate.sh" in content: + raise SystemExit("not ok - retired locate configuration path remains in " + str(path)) +check(True, "runtime and installation no longer reference the configuration rewrite") - local errors - errors=$(updatedb --config-file "$1" -U "$empty_tree" -o "$test_tmp/plocate.db" 2>&1 >/dev/null | grep -F "$1:" || true) - [[ -z $errors ]] || fail "updatedb accepts the rewritten config" "$errors" -} +with tempfile.TemporaryDirectory(prefix="omarchy-locate-") as scratch: + scratch = Path(scratch) + fake_bin = scratch / "bin" + fake_bin.mkdir() + stubs = { + "updatedb": 'printf "%s\\n" "$@" >"$TEST_CALLS"', + "sudo": 'exec "$@"', + "fzf": 'cat >/dev/null\nprintf "%s\\n" test-package', + "yay": 'if [[ ${1:-} == "-Slqa" ]]; then printf "%s\\n" test-package; fi', + "omarchy-sudo-keepalive": ':', + "omarchy-show-done": ':', + } + for name, body in stubs.items(): + path = fake_bin / name + path.write_text("#!/bin/bash\n" + body + "\n") + path.chmod(0o755) + calls = scratch / "updatedb-arguments" + env = dict(os.environ, PATH=str(fake_bin) + ":" + os.environ["PATH"], TEST_CALLS=str(calls)) + for relative in ("install/post-install/localdb.sh", "bin/omarchy-pkg-aur-install"): + subprocess.run(["bash", "-euo", "pipefail", str(root / relative)], env=env, check=True) + check(calls.read_text().splitlines() == options, + relative + " passes the scheduled service options directly") + calls.unlink() -conf="$test_tmp/updatedb.conf" -stock_conf "$conf" - -OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null - -grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config indexes Btrfs subvolume mounts like /home" -grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate config prunes /.snapshots" -assert_conf_parses "$conf" -pass "locate config skips Btrfs snapshots and indexes Btrfs subvolumes" - -OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null - -[[ $(grep -o '/\.snapshots' "$conf" | wc -l) -eq 1 ]] || fail "locate config is idempotent" -assert_conf_parses "$conf" -pass "locate config leaves an already-configured file alone" - -OMARCHY_UPDATEDB_CONF_PATH="$test_tmp/missing.conf" bash -euo pipefail "$config_script" >/dev/null -pass "locate config tolerates a missing updatedb.conf" - -# A hand-edited updatedb.conf may drop the settings entirely, or write them -# without the spaces around the "=" or the quotes that the stock Arch file uses. -conf="$test_tmp/sparse-updatedb.conf" -printf '%s\n' 'PRUNENAMES = ".git .hg .svn"' >"$conf" - -OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null - -grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config adds a missing PRUNE_BIND_MOUNTS" -grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config adds a missing PRUNEPATHS" -assert_conf_parses "$conf" -pass "locate config adds settings a hand-edited updatedb.conf is missing" - -conf="$test_tmp/unspaced-updatedb.conf" -printf '%s\n' 'PRUNE_BIND_MOUNTS="yes"' 'PRUNEPATHS="/tmp /var/tmp"' >"$conf" - -OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null - -grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an unspaced PRUNE_BIND_MOUNTS" -grep -qFx 'PRUNEPATHS = "/.snapshots /tmp /var/tmp"' "$conf" || fail "locate config prunes /.snapshots in an unspaced PRUNEPATHS" -[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config keeps a single PRUNEPATHS setting" -assert_conf_parses "$conf" -pass "locate config handles updatedb.conf written without spaces around =" - -# updatedb allows a comment after a value and indented settings, and defining -# either setting twice makes it refuse to run at all. -conf="$test_tmp/commented-updatedb.conf" -printf '%s\n' ' PRUNE_BIND_MOUNTS = "yes" # subvolumes look like bind mounts' \ - 'PRUNEPATHS = "/tmp" # scratch' >"$conf" - -OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null - -grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an indented PRUNE_BIND_MOUNTS" -grep -qFx 'PRUNEPATHS = "/.snapshots /tmp"' "$conf" || fail "locate config keeps the paths a commented PRUNEPATHS already prunes" -[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces a commented PRUNEPATHS instead of adding a second one" -assert_conf_parses "$conf" -pass "locate config handles indented settings and trailing comments" - -# A hand-edited file may have dropped the quotes updatedb requires, which -# leaves it unparseable until something writes the setting out properly. -conf="$test_tmp/unquoted-updatedb.conf" -printf '%s\n' 'PRUNEPATHS = /tmp' >"$conf" - -OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null - -grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config repairs an unquoted PRUNEPATHS" -[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces an unquoted PRUNEPATHS instead of adding a second one" -assert_conf_parses "$conf" -pass "locate config handles updatedb.conf written without quotes" - -# A path that merely ends in /.snapshots is not the root snapshot directory. -conf="$test_tmp/nested-snapshots-updatedb.conf" -printf '%s\n' 'PRUNEPATHS = "/var/lib/machines/.snapshots"' >"$conf" - -OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null - -grep -qFx 'PRUNEPATHS = "/.snapshots /var/lib/machines/.snapshots"' "$conf" || fail "locate config prunes /.snapshots alongside a path that ends in it" -assert_conf_parses "$conf" -pass "locate config tells /.snapshots apart from a path that ends in it" - -locate_migration=$(grep -rl 'Configure locate to skip Btrfs snapshots' "$ROOT/migrations" | head -n 1 || true) -[[ -n $locate_migration ]] || fail "locate migration exists" - -fake_bin="$test_tmp/bin" -mkdir -p "$fake_bin" - -cat >"$fake_bin/sudo" <<'STUB' -#!/bin/bash -exec "$@" -STUB -chmod +x "$fake_bin/sudo" - -cat >"$fake_bin/systemctl" <<'STUB' -#!/bin/bash -printf 'systemctl %s\n' "$*" >>"$TEST_LOG" -STUB -chmod +x "$fake_bin/systemctl" - -conf="$test_tmp/migration-updatedb.conf" -stock_conf "$conf" - -TEST_LOG="$test_tmp/calls.log" \ -PATH="$fake_bin:$PATH" \ -OMARCHY_PATH="$ROOT" \ -OMARCHY_UPDATEDB_CONF_PATH="$conf" \ - bash -euo pipefail "$locate_migration" >/dev/null - -grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate migration rewrites updatedb.conf" -grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate migration prunes /.snapshots" -grep -qFx 'systemctl restart --no-block plocate-updatedb.service' "$test_tmp/calls.log" || fail "locate migration replaces an in-flight run and rebuilds the index without blocking" -pass "locate migration fixes existing installs and rebuilds the index" - -: >"$test_tmp/calls.log" - -TEST_LOG="$test_tmp/calls.log" \ -PATH="$fake_bin:$PATH" \ -OMARCHY_PATH="$ROOT" \ -OMARCHY_UPDATEDB_CONF_PATH="$conf" \ - bash -euo pipefail "$locate_migration" >/dev/null - -[[ ! -s $test_tmp/calls.log ]] || fail "locate migration skips already-configured installs" -pass "locate migration is a no-op once updatedb.conf is configured" - -# A dev checkout carries migrations from a release whose install scripts the -# checked-out tree may not have yet, and omarchy-migrate runs under set -e. -: >"$test_tmp/calls.log" -conf="$test_tmp/no-config-script-updatedb.conf" -stock_conf "$conf" - -TEST_LOG="$test_tmp/calls.log" \ -PATH="$fake_bin:$PATH" \ -OMARCHY_PATH="$test_tmp/empty" \ -OMARCHY_UPDATEDB_CONF_PATH="$conf" \ - bash -euo pipefail "$locate_migration" >/dev/null || - fail "locate migration survives a tree without the locate config script" - -[[ ! -s $test_tmp/calls.log ]] || fail "locate migration touches nothing without the locate config script" -pass "locate migration is a no-op when the locate config script is missing" + tree = scratch / "tree" + visible = tree / "home/current-file" + excluded = tree / "private&pipe|directory" + hidden = excluded / "private-file" + visible.parent.mkdir(parents=True) + excluded.mkdir() + visible.touch() + hidden.touch() + conf = scratch / "updatedb.conf" + conf.write_text('PRUNE_BIND_MOUNTS = "yes"\nPRUNEPATHS = "' + str(excluded) + '"\n') + conf.chmod(0o640) + original = conf.read_bytes() + metadata = conf.stat() + database = scratch / "plocate.db" + run = [*command, "--config-file", str(conf), "--database-root", str(tree), + "--output", str(database), "--require-visibility", "no", "--debug-pruning"] + result = subprocess.run(run, capture_output=True, text=True, check=True) + debug = result.stdout + result.stderr + check("prune_bind_mounts\\000\n0\\000" in debug and "/.snapshots\\000" in debug, + "real updatedb overrides bind-mount pruning and adds root snapshots to exclusions") + entries = subprocess.check_output(["plocate", "--database", str(database), ""], text=True).splitlines() + check(str(visible) in entries and str(hidden) not in entries, + "real locate indexes current files and preserves literal administrator exclusions") + check(conf.read_bytes() == original and (conf.stat().st_mode, conf.stat().st_uid, conf.stat().st_gid, conf.stat().st_mtime_ns) + == (metadata.st_mode, metadata.st_uid, metadata.st_gid, metadata.st_mtime_ns), + "indexing preserves configuration bytes, permissions, ownership, and modification time") + subprocess.run(run, capture_output=True, check=True) + repeated = subprocess.check_output(["plocate", "--database", str(database), ""], text=True).splitlines() + check(repeated == entries, "repeated indexing retains the same results and exclusions") +PY