From 55fc353e47b8c8dc7f83ef8bdcc82039df41ae8c Mon Sep 17 00:00:00 2001 From: Emir Beganovic Date: Sat, 10 Oct 2026 02:20:08 +0200 Subject: [PATCH 1/2] Enable first-run units one at a time, and test that units are shipped The Thunderbolt approval watcher is enabled by its setup command, a migration and first-run, but no package put it in a user unit folder and nothing installed it. On edge the migration failed with "Unit omarchy-thunderbolt-authorization.service does not exist", so every later migration stayed pending. First-run failed too, and enabled none of its units: given a list, systemctl enables nothing when one unit is unknown. The units are now shipped by the settings packages (omarchy-pkgs#922). Here, first-run enables its units one at a time, so a unit missing from a build costs only that unit, and config-test fails when a unit under default/systemd/user is enabled without being shipped or installed first. --- install/user/first-run/enable-user-units.sh | 11 +++++++++-- test/shell.d/config-test.sh | 13 +++++++++++++ test/shell.d/first-run-test.sh | 21 +++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/install/user/first-run/enable-user-units.sh b/install/user/first-run/enable-user-units.sh index 1cf68380..aedf5905 100755 --- a/install/user/first-run/enable-user-units.sh +++ b/install/user/first-run/enable-user-units.sh @@ -12,7 +12,11 @@ set -euo pipefail systemctl --user daemon-reload -systemctl --user enable --now \ + +# One at a time: given a list, systemctl enables none of it when it cannot find +# one unit, and that should not cost the session its bluetooth agent as well. +failed=0 +for unit in \ bt-agent.service \ owed.service \ omarchy-recover-internal-monitor.service \ @@ -21,6 +25,9 @@ systemctl --user enable --now \ omarchy-fcitx5.service \ omarchy-crash-watch.service \ omarchy-usb-authorization.service \ - omarchy-thunderbolt-authorization.service + omarchy-thunderbolt-authorization.service; do + systemctl --user enable --now "$unit" || failed=1 +done omarchy-hook-install theme-set /usr/share/owe/10-owe-sync +exit "$failed" diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 12ffd850..476b3825 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -173,6 +173,19 @@ for source, destination, legacy in package_defaults: if destination and (source not in pkgbuild or destination not in pkgbuild): errors.append(f"PKGBUILD does not explicitly install {source} -> {destination}") +# A user unit has to be on the machine before anything turns it on: shipped in +# /usr/lib/systemd/user, or copied out of default/ by the command enabling it. +scripts = [path.read_text(errors="ignore") for folder in ("bin", "install", "migrations") + for path in (root / folder).rglob("*") if path.is_file()] +for unit in sorted(path.name for path in (root / "default/systemd/user").glob("*.service")): + if f"/usr/lib/systemd/user/{unit}" in pkgbuild: + continue + named = [text for text in scripts if unit in text] + if not any("default/systemd/user/" in text for text in named): + errors.append(f"PKGBUILD does not ship default/systemd/user/{unit} and no command installs it") + elif any("systemctl --user enable" in text and "default/systemd/user/" not in text for text in named): + errors.append(f"{unit} is enabled without being shipped by PKGBUILD or installed first") + # Existing users have an absolute wants symlink to the old unit path, and the # migration that repoints it only runs for users who run an update -- the # opposite of who the notifier is for. Dropping this alias strands them. diff --git a/test/shell.d/first-run-test.sh b/test/shell.d/first-run-test.sh index 0fd59375..54b5c197 100644 --- a/test/shell.d/first-run-test.sh +++ b/test/shell.d/first-run-test.sh @@ -33,3 +33,24 @@ if grep -F 'skip-first-run-update-notification' "$ROOT/install/user/first-run/wi fi pass "first-run uses one lifecycle completion marker" + +# systemctl enables none of a list when one unit in it is unknown, so a unit +# missing from a build must cost first-run only that unit. +cat >"$mock_bin/systemctl" <<'SH' +#!/bin/bash +[[ $* != *omarchy-sleep-lock.service* ]] || exit 1 +printf '%s\n' "$*" >>"$OMARCHY_TEST_CALLS" +SH +cat >"$mock_bin/omarchy-hook-install" <<'SH' +#!/bin/bash +echo hook >>"$OMARCHY_TEST_CALLS" +SH +chmod +x "$mock_bin/systemctl" "$mock_bin/omarchy-hook-install" + +if PATH="$mock_bin:$PATH" OMARCHY_TEST_CALLS="$test_tmp/calls" bash "$ROOT/install/user/first-run/enable-user-units.sh"; then + fail "first-run reports a unit it could not enable" +fi +for expected in bt-agent.service omarchy-crash-watch.service hook; do + grep -Fq "$expected" "$test_tmp/calls" || fail "a unit missing from the build does not cost first-run $expected" +done +pass "a unit missing from the build costs first-run only that unit" From 5489418c9de9bd82d6ec403087905bc2e68e6cdd Mon Sep 17 00:00:00 2001 From: Emir Beganovic Date: Sat, 10 Oct 2026 02:27:56 +0200 Subject: [PATCH 2/2] Require the unit copy to come before the enable, and compare first-run to a clean run Naming a unit's source in default/ counted as installing it, so a command that never copied the unit, or copied it after enabling it, passed. Look for the install or cp of that source ahead of the enable instead. The first-run test accepted any script that tried three of the units. It now requires one enable --now per unit and, with each unit unknown in turn, exactly the calls of a clean run minus that unit. --- test/shell.d/config-test.sh | 18 ++++++++++++++++-- test/shell.d/first-run-test.sh | 28 +++++++++++++++++++++------- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/test/shell.d/config-test.sh b/test/shell.d/config-test.sh index 476b3825..a10f0fba 100755 --- a/test/shell.d/config-test.sh +++ b/test/shell.d/config-test.sh @@ -108,6 +108,7 @@ pass "default bar widget ids resolve to manifests and entry points" ROOT="$ROOT" python3 <<'PY' import os +import re import sys from pathlib import Path @@ -175,15 +176,28 @@ for source, destination, legacy in package_defaults: # A user unit has to be on the machine before anything turns it on: shipped in # /usr/lib/systemd/user, or copied out of default/ by the command enabling it. +def installs_first(text): + source = re.search(r'(\w+)="[^"\n]*default/systemd/user/', text) + held = rf"|\$\{{?{source[1]}\b" if source else "" + copy = re.search(rf"^[ \t]*(?:sudo[ \t]+)?(?:install|cp)\b[^\n]*(?:default/systemd/user/{held})", text, re.MULTILINE) + enable = text.find("systemctl --user enable") + return bool(copy) and (enable < 0 or copy.start() < enable) + +named = 'unit="$OMARCHY_PATH/default/systemd/user/a.service"\n' +if installs_first(named + "systemctl --user enable a.service\n"): + errors.append("unit check accepts a command that names a unit's source but never installs it") +if installs_first(named + 'systemctl --user enable a.service\ninstall -Dm644 "$unit" "$target"\n'): + errors.append("unit check accepts a command that installs a unit after enabling it") + scripts = [path.read_text(errors="ignore") for folder in ("bin", "install", "migrations") for path in (root / folder).rglob("*") if path.is_file()] for unit in sorted(path.name for path in (root / "default/systemd/user").glob("*.service")): if f"/usr/lib/systemd/user/{unit}" in pkgbuild: continue named = [text for text in scripts if unit in text] - if not any("default/systemd/user/" in text for text in named): + if not any(installs_first(text) for text in named): errors.append(f"PKGBUILD does not ship default/systemd/user/{unit} and no command installs it") - elif any("systemctl --user enable" in text and "default/systemd/user/" not in text for text in named): + elif any("systemctl --user enable" in text and not installs_first(text) for text in named): errors.append(f"{unit} is enabled without being shipped by PKGBUILD or installed first") # Existing users have an absolute wants symlink to the old unit path, and the diff --git a/test/shell.d/first-run-test.sh b/test/shell.d/first-run-test.sh index 54b5c197..16bc861b 100644 --- a/test/shell.d/first-run-test.sh +++ b/test/shell.d/first-run-test.sh @@ -38,19 +38,33 @@ pass "first-run uses one lifecycle completion marker" # missing from a build must cost first-run only that unit. cat >"$mock_bin/systemctl" <<'SH' #!/bin/bash -[[ $* != *omarchy-sleep-lock.service* ]] || exit 1 +[[ $* != *"$OMARCHY_TEST_MISSING_UNIT"* ]] || exit 1 printf '%s\n' "$*" >>"$OMARCHY_TEST_CALLS" SH cat >"$mock_bin/omarchy-hook-install" <<'SH' #!/bin/bash -echo hook >>"$OMARCHY_TEST_CALLS" +echo "hook $*" >>"$OMARCHY_TEST_CALLS" SH chmod +x "$mock_bin/systemctl" "$mock_bin/omarchy-hook-install" -if PATH="$mock_bin:$PATH" OMARCHY_TEST_CALLS="$test_tmp/calls" bash "$ROOT/install/user/first-run/enable-user-units.sh"; then - fail "first-run reports a unit it could not enable" -fi -for expected in bt-agent.service omarchy-crash-watch.service hook; do - grep -Fq "$expected" "$test_tmp/calls" || fail "a unit missing from the build does not cost first-run $expected" +enable_user_units() { + rm -f "$test_tmp/calls" + PATH="$mock_bin:$PATH" OMARCHY_TEST_CALLS="$test_tmp/calls" OMARCHY_TEST_MISSING_UNIT="$1" \ + bash "$ROOT/install/user/first-run/enable-user-units.sh" +} + +enable_user_units no-such-unit || fail "first-run enables its user units" +clean_run=$(<"$test_tmp/calls") +grep -Fq 'hook theme-set' <<<"$clean_run" || fail "first-run installs the theme hook" +mapfile -t units < <(sed -n 's/^--user enable --now //p' <<<"$clean_run") + +for unit in "${units[@]}"; do + [[ $unit != *" "* ]] || fail "first-run enables each unit on its own" "$unit" + if enable_user_units "$unit"; then + fail "first-run reports a unit it could not enable" "$unit" + fi + [[ $(<"$test_tmp/calls") == "$(grep -Fvx -- "--user enable --now $unit" <<<"$clean_run")" ]] || + fail "a unit missing from the build costs first-run only that unit" "$unit" done +(( ${#units[@]} > 1 )) || fail "first-run enables each unit on its own" pass "a unit missing from the build costs first-run only that unit"