diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0cf82f1a..75f09b63 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/pkgbuilds/omarchy-dev/PKGBUILD b/pkgbuilds/omarchy-dev/PKGBUILD index 0e2528b1..361178cf 100644 --- a/pkgbuilds/omarchy-dev/PKGBUILD +++ b/pkgbuilds/omarchy-dev/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Ryan Hughes 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" diff --git a/pkgbuilds/omarchy-settings-dev/PKGBUILD b/pkgbuilds/omarchy-settings-dev/PKGBUILD index 008bf182..382f5765 100644 --- a/pkgbuilds/omarchy-settings-dev/PKGBUILD +++ b/pkgbuilds/omarchy-settings-dev/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Ryan Hughes 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" diff --git a/pkgbuilds/omarchy-settings-dev/omarchy-settings-dev.install b/pkgbuilds/omarchy-settings-dev/omarchy-settings-dev.install index 19f54e70..03745233 100644 --- a/pkgbuilds/omarchy-settings-dev/omarchy-settings-dev.install +++ b/pkgbuilds/omarchy-settings-dev/omarchy-settings-dev.install @@ -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 + } } diff --git a/tests/fixtures/settings-source-files b/tests/fixtures/settings-source-files new file mode 100644 index 00000000..51056989 --- /dev/null +++ b/tests/fixtures/settings-source-files @@ -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 diff --git a/tests/passwordless-package-files.sh b/tests/passwordless-package-files.sh new file mode 100755 index 00000000..e082d655 --- /dev/null +++ b/tests/passwordless-package-files.sh @@ -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 diff --git a/tests/passwordless-package-lifecycle.py b/tests/passwordless-package-lifecycle.py new file mode 100755 index 00000000..ed08f2b1 --- /dev/null +++ b/tests/passwordless-package-lifecycle.py @@ -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"(?/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()) diff --git a/tests/settings-boot-config.sh b/tests/settings-boot-config.sh index 8c003a36..0d6a66b7 100755 --- a/tests/settings-boot-config.sh +++ b/tests/settings-boot-config.sh @@ -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" diff --git a/tests/settings-runtime-profile.sh b/tests/settings-runtime-profile.sh index e55c6918..ea1b87d5 100755 --- a/tests/settings-runtime-profile.sh +++ b/tests/settings-runtime-profile.sh @@ -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