Ship the complete passwordless-sudo lifecycle in settings-dev (#645)

* Move the dev sudo grant lifecycle into omarchy-settings-dev

The hardened omarchy-sudo-passwordless publishes a grant only when its revocation hook is installed and no package transaction has left /run/omarchy-sudo-passwordless-package-removing behind. The hook sets that marker before every settings upgrade, and the settings scriptlets are what clear it, so shipping the hook alone let the first grant work and then refused every grant after the next update until a reboot.

omarchy-settings-dev now ships the command, omarchy-security-functions, the boot cleanup rule and the hook together, with the scriptlets from #257 that revoke before the package changes and clear the marker once no grant remains. omarchy-dev stops shipping the two helpers and requires the settings release that does: without it omarchy-update has no library to source.

Co-authored-by: Afonso Oliveira <afonso.oliveira707@gmail.com>

* Check that settings-dev registers the scriptlet the lifecycle test exercises

The lifecycle test reads the .install file directly, so it kept passing with the recipe's install= line gone, which is the one change that would strand the hook's marker again.

Co-Authored-By: Codex XHigh <noreply@openai.com>

* Keep omarchy-dev's settings dependency unversioned

The dev pair builds each package on its own against the published edge repository, in the PR build and in publish alike, so omarchy-dev cannot require a settings-dev release that only this change produces: the floor failed both omarchy-dev builds on this PR, and an exact pkgver match would fail the same way on every upstream pin bump. Nothing needs it. The upstream watch moves both pins together, publish stops the run before anything is published when one package fails, and a new settings-dev over an old runtime is refused on the files they would both own.

* Refuse settings-dev releases that predate the grant helper move

With the dependency unversioned, a new omarchy-dev could still install over a settings-dev that is held back or not yet published, removing omarchy-security-functions from both, and omarchy-update cannot start without it. A versioned conflict refuses that pairing at install time without constraining the per-package builds, which resolve only depends.

Co-Authored-By: Codex XHigh <noreply@openai.com>

* Require the security library as a capability instead of a version floor

The conflict named the last settings-dev release without the helpers, a version that moves with every upstream pin bump and settings-only pkgrel while this change waits to merge, and a rebase that kept a stale floor still passed the tests while the published release slipped past it. omarchy-settings-dev now provides omarchy-security-functions and omarchy-dev's package() requires it. makepkg checks only the top-level depends before building, so the per-package builds still resolve against the published repository, while pacman refuses a runtime without a settings release that ships the library, and pulls that release in when only the runtime is asked for.

Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Spencer Bull <spencer@omarchy.org>
Co-authored-by: Afonso Oliveira <afonso.oliveira707@gmail.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
This commit is contained in:
authored and GitHub committed 2026-10-11 09:56:47 -05:00
1 parent 60551aaddc
commit 40ea1555f2
9 files changed
+455 -59

No files matched your search

+2
View File
@@ -50,6 +50,7 @@ jobs:
pacman -Syu --noconfirm git jq python libarchive neovim tmux
python tests/owe-package-restart.py
python tests/oma-service-removal.py
python tests/passwordless-package-lifecycle.py
python tests/upstream-watch.py
bash tests/ipu7-install.sh
bash tests/ipu7-headers.sh
@@ -63,6 +64,7 @@ jobs:
./tests/published-build-plan.sh
./tests/settings-boot-config.sh
./tests/settings-runtime-profile.sh
./tests/passwordless-package-files.sh
./tests/pinned-sources.sh
./tests/controller.sh
./tests/artifact-helpers.sh
+8 -4
View File
@@ -1,7 +1,7 @@
# Maintainer: Ryan Hughes <ryan@omarchy.org>
pkgname='omarchy-dev'
pkgver=4.0.0.r6897.g4df5459
pkgrel=1
pkgrel=2
# Pinned by the upstream watch in .omarchy/package.json (bin/sync-upstream):
# every quattro tip becomes a commit pin here, so the package is versioned,
# checksummed and built exactly like a release, just more often. The r-number
@@ -96,18 +96,23 @@ prepare() {
}
package() {
# omarchy-update cannot start without the library settings now ships. Set
# here, not above: makepkg checks only the top-level depends before
# building, against a repository that may not publish that settings yet.
depends+=('omarchy-security-functions')
cd "$srcdir/omarchy"
# Runtime binaries — ship everything in bin/ except the debug helpers
# that ship from omarchy-settings (needed before omarchy is installed,
# e.g. on the ISO live env).
# e.g. on the ISO live env), and the sudo grant helpers settings owns.
install -d "$pkgdir/usr/bin" "$pkgdir/usr/share/omarchy/bin"
for bin in bin/*; do
[[ -f $bin ]] || continue
local base
base=$(basename "$bin")
case "$base" in
omarchy-debug|omarchy-debug-idle|omarchy-upload-log)
omarchy-debug|omarchy-debug-idle|omarchy-upload-log|omarchy-sudo-passwordless|omarchy-security-functions)
continue
;;
esac
@@ -122,7 +127,6 @@ package() {
# Ship hooks with their Exec targets so package skew cannot leave a broken
# hook pointing at a missing binary.
install -Dm644 default/libalpm/hooks/00-omarchy-update-guard.hook "$pkgdir/usr/share/libalpm/hooks/00-omarchy-update-guard.hook"
install -Dm644 default/libalpm/hooks/05-omarchy-passwordless-revoke.hook "$pkgdir/usr/share/libalpm/hooks/05-omarchy-passwordless-revoke.hook"
install -Dm644 default/libalpm/hooks/10-omarchy-hyprland-reload-pause.hook "$pkgdir/usr/share/libalpm/hooks/10-omarchy-hyprland-reload-pause.hook"
install -Dm644 default/libalpm/hooks/90-omarchy-hyprland-reload-resume.hook "$pkgdir/usr/share/libalpm/hooks/90-omarchy-hyprland-reload-resume.hook"
+15 -2
View File
@@ -1,7 +1,7 @@
# Maintainer: Ryan Hughes <ryan@omarchy.org>
pkgname='omarchy-settings-dev'
pkgver=4.0.0.r6897.g4df5459
pkgrel=1
pkgrel=2
# Pinned by the upstream watch in .omarchy/package.json (bin/sync-upstream):
# every quattro tip becomes a commit pin here, so the package is versioned,
# checksummed and built exactly like a release, just more often. The r-number
@@ -21,7 +21,7 @@ pkgdesc='Omarchy user defaults, /etc/skel content, fonts, plymouth theme, and su
arch=('x86_64' 'aarch64')
url='https://github.com/basecamp/omarchy'
license=('MIT')
provides=('omarchy-settings')
provides=('omarchy-settings' 'omarchy-security-functions')
conflicts=('omarchy-settings')
# `/etc/default/limine` is intentionally NOT owned by this package. The ISO
@@ -39,6 +39,7 @@ depends=(
'bash'
'curl'
'gum'
'sudo'
'hicolor-icon-theme'
'plymouth'
)
@@ -450,6 +451,18 @@ EOF
"$pkgdir/usr/share/libalpm/scripts/omarchy-hw-platform"
fi
# Temporary passwordless sudo: the grant command, its library, the boot
# cleanup rule from etc/ and the hook that revokes grants before this
# package changes all live here, so the hook never outlives its Exec target
# and removing the runtime alone keeps expiry working.
for helper in omarchy-sudo-passwordless omarchy-security-functions; do
install -Dm755 "bin/$helper" "$pkgdir/usr/bin/$helper"
install -d "$pkgdir/usr/share/omarchy/bin"
ln -s "/usr/bin/$helper" "$pkgdir/usr/share/omarchy/bin/$helper"
done
install -Dm644 default/libalpm/hooks/05-omarchy-passwordless-revoke.hook \
"$pkgdir/usr/share/libalpm/hooks/05-omarchy-passwordless-revoke.hook"
# Branding assets (logos, icons).
install -Dm644 logo.txt "$pkgdir/usr/share/omarchy/logo.txt"
install -Dm644 logo.svg "$pkgdir/usr/share/omarchy/logo.svg"
@@ -1,3 +1,68 @@
# This lock and removal marker are shared with omarchy-sudo-passwordless.
# Keep the marker through removal so no grant can be published after its boot
# cleanup prerequisite disappears. A completed installation clears it again.
_passwordless_package_transition() (
local action=$1 lock_fd directory rule
# Scriptlets run under ordinary Bash with pacman's environment. The sweep
# below relies on pathname expansion, so it must not inherit glob state.
unset GLOBIGNORE
set +f
shopt -u failglob
[[ -d /run && ! -L /run ]] || return 1
[[ $(/usr/bin/stat -Lc '%u' /run) == "0" ]] || return 1
! (( 8#$(/usr/bin/stat -Lc '%a' /run) & 022 )) || return 1
umask 077
case "$action" in
begin|installed)
# Refuse publication before waiting for the lock: a timeout, a failed
# lock directory, or an interrupted transaction must not leave the
# namespace unguarded. The publisher rechecks this marker after it
# publishes, so it needs no lock to take effect.
: >/run/omarchy-sudo-passwordless-package-removing || return 1
;;
esac
[[ -d /run/lock && ! -L /run/lock ]] || return 1
[[ $(/usr/bin/stat -Lc '%u' /run/lock) == "0" ]] || return 1
! (( 8#$(/usr/bin/stat -Lc '%a' /run/lock) & 022 )) || return 1
exec {lock_fd}>/run/lock/omarchy-sudo-passwordless.lock || return 1
# Grant operations are short; a stalled holder must not hang pacman's
# transaction indefinitely.
/usr/bin/flock -x -w 60 "$lock_fd" || return 1
case "$action" in
begin)
/usr/bin/rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-* || return 1
;;
removed)
/usr/bin/rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-* || return 1
;;
installed)
# pacman does not stop a transaction on a failed scriptlet, so a failed
# or interrupted pre-transaction cleanup can be followed by this step,
# and a fresh install has no earlier step at all. The blocker written
# above holds while sweeping, so a failure here leaves publication
# refused; certify completion only with the reserved namespace empty.
/usr/bin/rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-* || return 1
for rule in /etc/sudoers.d/99-omarchy-nopasswd-*; do
[[ ! -e $rule && ! -L $rule ]] || return 1
done
[[ -f /etc/tmpfiles.d/omarchy-nopasswd-sudo.conf ]] || return 1
/usr/bin/rm -f -- /run/omarchy-sudo-passwordless-package-removing || return 1
;;
*) return 1 ;;
esac
)
pre_remove() {
_passwordless_package_transition begin || {
echo "ERROR: Could not revoke temporary Omarchy sudo grants before removing their boot cleanup. Administrator cleanup is required." >&2
return 1
}
}
pre_upgrade() {
pre_remove
}
_apple_silicon() {
[[ $(uname -m) == aarch64 ]] && grep -aq 'apple,' /proc/device-tree/compatible 2>/dev/null
}
@@ -56,9 +121,24 @@ _etc_overrides_apply() {
}
post_install() {
_etc_overrides_apply
local status=0
# Recover the grant lifecycle before optional configuration work, so a
# later override failure cannot strand the machine-wide blocker.
_passwordless_package_transition installed || {
echo "ERROR: Temporary Omarchy sudo grants remain or their boot cleanup is missing; passwordless sudo stays disabled until this package installs cleanly." >&2
status=1
}
_etc_overrides_apply || status=1
return "$status"
}
post_upgrade() {
_etc_overrides_apply
post_install
}
post_remove() {
_passwordless_package_transition removed || {
echo "ERROR: Temporary Omarchy sudo grants could not be removed. Administrator cleanup is required." >&2
return 1
}
}
+53
View File
@@ -0,0 +1,53 @@
config/autostart/limine-snapper-notify.desktop
etc/fastfetch/config.jsonc
etc/mkinitcpio.conf.d/omarchy_hooks.conf
etc/mkinitcpio.conf.d/thunderbolt_module.conf
etc/limine-entry-tool.d/omarchy-defaults.conf
etc/limine-entry-tool.d/omarchy-uki.conf
etc/security/faillock.conf
etc/nsswitch.conf
etc/cups/cups-browsed.conf
etc/cups/cups-files.conf
etc/plymouth/plymouthd.conf
etc/sysctl.d/99-omarchy-sysctl.conf
etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
default/uwsm/env.d/10-omarchy
default/environment.d/10-omarchy-fcitx.conf
default/fontconfig/conf.avail/50-omarchy.conf
default/xdg-terminal-exec/hyprland-xdg-terminals.list
default/applications/mimeapps.list
default/systemd/user/bt-agent.service
default/systemd/user/omarchy-sleep-lock.service
default/systemd/user/omarchy-recover-internal-monitor.service
default/systemd/user/omarchy-migrate-notify.service
default/systemd/user/omarchy-tailscale-receive.service
default/systemd/user/omarchy-fcitx5.service
default/systemd/user/omarchy-crash-watch.service
default/systemd/user/app.slice.d/10-oomd.conf
default/systemd/zram-generator.conf.d/90-omarchy.conf
default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf
default/systemd/system-sleep/unmount-fuse
default/bashrc
default/limine/default.conf
default/limine/limine.conf
default/snapper/root
default/sddm/omarchy/Main.qml
default/sddm/hyprland.lua
default/wayland-sessions/omarchy.desktop
default/plymouth/omarchy.plymouth
default/fonts/omarchy/omarchy.ttf
default/hypr/toggles/flags.lua
default/nautilus-python/extensions/localsend.py
default/nautilus-python/extensions/transcode.py
default/tensaku/state.toml
default/libalpm/hooks/05-omarchy-passwordless-revoke.hook
applications/example.desktop
bin/omarchy-upload-log
bin/omarchy-debug
bin/omarchy-debug-idle
bin/omarchy-sudo-passwordless
bin/omarchy-security-functions
logo.txt
logo.svg
icon.txt
icon.png
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
# Build the dev pair from a synthetic source tree: the sudo grant command, its
# library, boot cleanup and revocation hook ship from settings, and no path
# ships from both packages, which pacman would refuse to install together.
set -euo pipefail
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
fixture=$scratch/src/omarchy
mapfile -t files < "$BUILD_ROOT/tests/fixtures/settings-source-files"
files+=(
bin/omarchy-update
default/libalpm/hooks/00-omarchy-update-guard.hook
default/libalpm/hooks/10-omarchy-hyprland-reload-pause.hook
default/libalpm/hooks/90-omarchy-hyprland-reload-resume.hook
install/omarchy-base.packages
themes/fixture/colors.toml
migrations/1788163635.sh
shell/shell.qml
version
)
for path in "${files[@]}"; do
mkdir -p "$(dirname "$fixture/$path")"
printf 'fixture for %s\n' "$path" > "$fixture/$path"
done
# The aarch64 recipe parses the HOOKS line.
cp "$BUILD_ROOT/tests/fixtures/settings-boot/omarchy_hooks-v4.0.4.conf" "$fixture/etc/mkinitcpio.conf.d/omarchy_hooks.conf"
fail() {
echo "FAIL: $*" >&2
exit 1
}
hook=usr/share/libalpm/hooks/05-omarchy-passwordless-revoke.hook
boot_cleanup=etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
# The scriptlets are what clear the hook's marker, and
# passwordless-package-lifecycle.py only tests the file, not that it ships.
registered=$(CARCH=x86_64 && source "$BUILD_ROOT/pkgbuilds/omarchy-settings-dev/PKGBUILD" && echo "${install:-}")
[[ $registered == omarchy-settings-dev.install ]] ||
fail "omarchy-settings-dev does not register omarchy-settings-dev.install"
# The runtime needs the library settings provides, but only once installed:
# the per-package builds resolve the top-level depends against what the
# repository already publishes.
build_depends=$(CARCH=x86_64 && source "$BUILD_ROOT/pkgbuilds/omarchy-dev/PKGBUILD" && printf '%s\n' "${depends[@]}")
! grep -Fxq omarchy-security-functions <<<"$build_depends" ||
fail "omarchy-dev asks for omarchy-security-functions before building, which a published settings-dev may not provide"
for target_arch in x86_64 aarch64; do
for recipe in omarchy-dev omarchy-settings-dev; do
(
export CARCH=$target_arch OMARCHY_SRC=$fixture
export srcdir=$scratch/src pkgdir=$scratch/$recipe-$target_arch
backup=()
# shellcheck disable=SC1090 # Exercise each recipe's actual package function.
source "$BUILD_ROOT/pkgbuilds/$recipe/PKGBUILD"
package
printf '%s\n' "${depends[@]}" >"$scratch/$recipe-$target_arch.depends"
printf '%s\n' "${provides[@]}" >"$scratch/$recipe-$target_arch.provides"
) >/dev/null
done
runtime=$scratch/omarchy-dev-$target_arch
settings=$scratch/omarchy-settings-dev-$target_arch
for helper in omarchy-sudo-passwordless omarchy-security-functions; do
cmp -s "$fixture/bin/$helper" "$settings/usr/bin/$helper" && [[ -x $settings/usr/bin/$helper ]] ||
fail "$target_arch: omarchy-settings-dev does not ship /usr/bin/$helper"
[[ $(readlink "$settings/usr/share/omarchy/bin/$helper") == "/usr/bin/$helper" ]] ||
fail "$target_arch: omarchy-settings-dev does not link /usr/share/omarchy/bin/$helper"
done
cmp -s "$fixture/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$settings/$hook" &&
[[ $(stat -c %a "$settings/$hook") == 644 ]] ||
fail "$target_arch: omarchy-settings-dev does not ship /$hook"
cmp -s "$fixture/$boot_cleanup" "$settings/$boot_cleanup" ||
fail "$target_arch: omarchy-settings-dev does not ship /$boot_cleanup"
[[ -x $runtime/usr/bin/omarchy-update && -f $runtime/usr/share/libalpm/hooks/00-omarchy-update-guard.hook ]] ||
fail "$target_arch: omarchy-dev lost its own commands or hooks"
grep -Fxq omarchy-security-functions "$scratch/omarchy-dev-$target_arch.depends" ||
fail "$target_arch: the omarchy-dev package does not require omarchy-security-functions"
grep -Fxq omarchy-security-functions "$scratch/omarchy-settings-dev-$target_arch.provides" ||
fail "$target_arch: the omarchy-settings-dev package does not provide omarchy-security-functions"
overlap=$(comm -12 <(cd "$runtime" && find . ! -type d | sort) <(cd "$settings" && find . ! -type d | sort))
[[ -z $overlap ]] || fail "$target_arch: shipped by both omarchy-dev and omarchy-settings-dev:"$'\n'"$overlap"
echo "PASS: $target_arch: omarchy-settings-dev owns the sudo grant lifecycle and shares no file with omarchy-dev"
done
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/python3
"""Exercise settings scriptlet callbacks against private, inert filesystem state."""
import re
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CALLBACKS = (
"_passwordless_package_transition",
"pre_remove",
"pre_upgrade",
"post_install",
"post_upgrade",
"post_remove",
)
START = re.compile(r"^([a-z_]+)\(\) ([({])$")
def isolated_callbacks(scriptlet):
"""Copy definitions only; reject any host path left in the executable copy."""
lines = scriptlet.read_text().splitlines(keepends=True)
selected = []
for index, line in enumerate(lines):
match = START.match(line.rstrip("\n"))
if not match or match.group(1) not in CALLBACKS:
continue
end = "}\n" if match.group(2) == "{" else ")\n"
for last in range(index + 1, len(lines)):
if lines[last] == end:
selected.extend(lines[index : last + 1])
break
else:
raise AssertionError(f"unclosed callback in {scriptlet}: {match.group(1)}")
source = "".join(selected)
redirects = {
"/usr/bin/stat": "fixture_stat",
"/usr/bin/flock": "flock",
"/usr/bin/rm": "fixture_rm",
"/etc/sudoers.d/": '"$FIXTURE_ROOT/etc/sudoers.d"/',
"/etc/tmpfiles.d/": '"$FIXTURE_ROOT/etc/tmpfiles.d"/',
"/run/lock/": '"$FIXTURE_ROOT/run/lock"/',
"/run/": '"$FIXTURE_ROOT/run"/',
"/run": '"$FIXTURE_ROOT/run"',
}
source = re.sub(
"|".join(re.escape(path) for path in redirects),
lambda match: redirects[match.group()],
source,
)
# The extracted callbacks may only operate in the fixture. Refuse new
# absolute system paths until the test explicitly models them.
executable = "\n".join(line for line in source.splitlines() if not line.lstrip().startswith("#"))
executable = re.sub(r'"\$FIXTURE_ROOT(?:/[^\"]*)?"', "FIXTURE_PATH", executable)
host_path = re.search(r"(?<![\w$])/[A-Za-z][A-Za-z0-9_./-]*", executable)
if host_path:
raise AssertionError(f"unredirected host path in {scriptlet}: {host_path.group()}")
return source
HARNESS = r'''#!/bin/bash
set -u
fixture_stat() {
[[ $# == 3 && $1 == -Lc ]] || return 2
[[ $3 == "$FIXTURE_ROOT/run" || $3 == "$FIXTURE_ROOT/run/lock" ]] || return 2
[[ -d $3 && ! -L $3 ]] || return 2
case "$2" in
%u) printf '0\n' ;;
%a) /usr/bin/stat -Lc '%a' "$3" ;;
*) return 2 ;;
esac
}
fixture_rm() {
[[ $# -ge 3 && $1 == -f && $2 == -- ]] || return 2
shift 2
local path
for path in "$@"; do
case "$path" in
"$FIXTURE_ROOT/etc/sudoers.d/99-omarchy-nopasswd-"*|"$FIXTURE_ROOT/run/omarchy-sudo-passwordless-package-removing") ;;
*) return 2 ;;
esac
/usr/bin/rm -f -- "$path" || return 1
done
}
_etc_overrides_apply() { :; }
'''
def run_callback(source, root, callback, failglob=False):
script = root / "callbacks.sh"
script.write_text(
HARNESS
+ source
+ '\n# pacman skips callbacks absent from an older installed scriptlet.\n'
+ 'if [[ ${FIXTURE_FAILGLOB:-} == 1 ]]; then shopt -s failglob; fi\n'
+ 'if declare -F "$1" >/dev/null; then "$1"; fi\n'
)
result = subprocess.run(
["/bin/bash", "-p", str(script), callback],
env={"PATH": "/usr/bin:/bin", "FIXTURE_ROOT": str(root), "FIXTURE_FAILGLOB": "1" if failglob else "0"},
text=True,
capture_output=True,
timeout=5,
check=False,
)
return result.returncode, result.stderr.strip()
def fixture(root, rule=True, cleanup=True):
for directory in ("run/lock", "etc/sudoers.d", "etc/tmpfiles.d"):
path = root / directory
path.mkdir(parents=True, exist_ok=True)
path.chmod(0o755)
if rule:
(root / "etc/sudoers.d/99-omarchy-nopasswd-alice").write_text("synthetic rule\n")
if cleanup:
(root / "etc/tmpfiles.d/omarchy-nopasswd-sudo.conf").write_text("synthetic cleanup\n")
def run_suite(scriptlet, channel):
source = isolated_callbacks(scriptlet)
failures = []
count = 0
def check(name, actual, expected):
nonlocal count
count += 1
if actual != expected:
failures.append(f"{name}: expected {expected!r}, got {actual!r}")
else:
print(f"PASS {channel}: {name}")
with tempfile.TemporaryDirectory(prefix="omarchy-settings-lifecycle-") as temporary:
base = Path(temporary)
root = base / "upgrade"
fixture(root)
status, _ = run_callback(source, root, "pre_upgrade")
check("upgrade revokes rule and blocks publication", (status, (root / "etc/sudoers.d/99-omarchy-nopasswd-alice").exists(), (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (0, False, True))
(root / "etc/sudoers.d/99-omarchy-nopasswd-late").write_text("synthetic late rule\n")
status, _ = run_callback(source, root, "post_upgrade")
check("upgrade sweeps late rule before clearing blocker", (status, (root / "etc/sudoers.d/99-omarchy-nopasswd-late").exists(), (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (0, False, False))
root = base / "remove"
fixture(root)
status, _ = run_callback(source, root, "pre_remove")
check("removal revokes rule and keeps blocker", (status, (root / "etc/sudoers.d/99-omarchy-nopasswd-alice").exists(), (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (0, False, True))
(root / "etc/sudoers.d/99-omarchy-nopasswd-late").write_text("synthetic late rule\n")
status, _ = run_callback(source, root, "post_remove")
check("removal re-sweeps and leaves publication blocked", (status, (root / "etc/sudoers.d/99-omarchy-nopasswd-late").exists(), (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (0, False, True))
root = base / "failed-revocation"
fixture(root, rule=False)
# A directory in the reserved namespace makes rm -f fail safely.
(root / "etc/sudoers.d/99-omarchy-nopasswd-blocked").mkdir()
status, _ = run_callback(source, root, "pre_upgrade")
check("failed revocation reports failure and retains blocker", (status != 0, (root / "etc/sudoers.d/99-omarchy-nopasswd-blocked").is_dir(), (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (True, True, True))
status, _ = run_callback(source, root, "post_upgrade")
check("failed install cannot certify nonempty namespace", (status != 0, (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (True, True))
(root / "etc/sudoers.d/99-omarchy-nopasswd-blocked").rmdir()
status, _ = run_callback(source, root, "post_upgrade")
check("retry recovers after namespace clears", (status, (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (0, False))
root = base / "missing-cleanup"
fixture(root, rule=False, cleanup=False)
status, _ = run_callback(source, root, "post_install")
check("fresh install without boot cleanup stays blocked", (status != 0, (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (True, True))
(root / "etc/tmpfiles.d/omarchy-nopasswd-sudo.conf").write_text("synthetic cleanup\n")
status, _ = run_callback(source, root, "post_install")
check("fresh install retries normally", (status, (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (0, False))
root = base / "ordinary"
fixture(root, rule=False)
status, _ = run_callback(source, root, "post_install")
check("ordinary install without grant succeeds", (status, (root / "run/omarchy-sudo-passwordless-package-removing").exists()), (0, False))
root = base / "inherited-failglob"
fixture(root, rule=False)
status, _ = run_callback(source, root, "post_install", failglob=True)
check("no-rule install clears blocker with failglob enabled", (status, (root / "run/omarchy-sudo-passwordless-package-removing").exists(), list((root / "etc/sudoers.d").glob("99-omarchy-nopasswd-*"))), (0, False, []))
for failure in failures:
print(f"FAIL {channel}: {failure}", file=sys.stderr)
print(f"{channel}: {count - len(failures)}/{count} assertions passed")
return not failures
def main():
if len(sys.argv) == 1:
# Stable settings joins once its pinned source ships the grant helpers.
scriptlets = [ROOT / "pkgbuilds" / name / f"{name}.install" for name in ("omarchy-settings-dev",)]
else:
scriptlets = [Path(path).resolve() for path in sys.argv[1:]]
results = [run_suite(path, path.parent.name) for path in scriptlets]
return 0 if all(results) else 1
if __name__ == "__main__":
sys.exit(main())
+2 -51
View File
@@ -7,57 +7,8 @@ scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
fixture=$scratch/src/omarchy
files=(
config/autostart/limine-snapper-notify.desktop
etc/fastfetch/config.jsonc
etc/mkinitcpio.conf.d/omarchy_hooks.conf
etc/mkinitcpio.conf.d/thunderbolt_module.conf
etc/limine-entry-tool.d/omarchy-defaults.conf
etc/limine-entry-tool.d/omarchy-uki.conf
etc/security/faillock.conf
etc/nsswitch.conf
etc/cups/cups-browsed.conf
etc/cups/cups-files.conf
etc/plymouth/plymouthd.conf
etc/sysctl.d/99-omarchy-sysctl.conf
default/uwsm/env.d/10-omarchy
default/environment.d/10-omarchy-fcitx.conf
default/fontconfig/conf.avail/50-omarchy.conf
default/xdg-terminal-exec/hyprland-xdg-terminals.list
default/applications/mimeapps.list
default/systemd/user/bt-agent.service
default/systemd/user/omarchy-sleep-lock.service
default/systemd/user/omarchy-recover-internal-monitor.service
default/systemd/user/omarchy-migrate-notify.service
default/systemd/user/omarchy-tailscale-receive.service
default/systemd/user/omarchy-fcitx5.service
default/systemd/user/omarchy-crash-watch.service
default/systemd/user/app.slice.d/10-oomd.conf
default/systemd/zram-generator.conf.d/90-omarchy.conf
default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf
default/systemd/system-sleep/unmount-fuse
default/bashrc
default/limine/default.conf
default/limine/limine.conf
default/snapper/root
default/sddm/omarchy/Main.qml
default/sddm/hyprland.lua
default/wayland-sessions/omarchy.desktop
default/plymouth/omarchy.plymouth
default/fonts/omarchy/omarchy.ttf
default/hypr/toggles/flags.lua
default/nautilus-python/extensions/localsend.py
default/nautilus-python/extensions/transcode.py
default/tensaku/state.toml
applications/example.desktop
bin/omarchy-upload-log
bin/omarchy-debug
bin/omarchy-debug-idle
logo.txt
logo.svg
icon.txt
icon.png
)
# Every source path the settings recipes install by name.
mapfile -t files < "$BUILD_ROOT/tests/fixtures/settings-source-files"
for path in "${files[@]}"; do
mkdir -p "$(dirname "$fixture/$path")"
printf 'fixture for %s\n' "$path" > "$fixture/$path"
+3
View File
@@ -20,6 +20,8 @@ files=(
bin/omarchy-debug
bin/omarchy-debug-idle
bin/omarchy-hw-platform
bin/omarchy-security-functions
bin/omarchy-sudo-passwordless
bin/omarchy-upload-log
config/autostart/limine-snapper-notify.desktop
config/hypr/hyprland.lua
@@ -28,6 +30,7 @@ files=(
default/environment.d/10-omarchy-fcitx.conf
default/fontconfig/conf.avail/50-omarchy.conf
default/fonts/omarchy/omarchy.ttf
default/libalpm/hooks/05-omarchy-passwordless-revoke.hook
default/hypr/toggles/flags.lua
default/libalpm/hooks/00-omarchy-platform-guard.hook
default/libalpm/scripts/omarchy-platform-guard