From dca7c2c11a40abb4f2b2eaff09bdbdd09a2d9440 Mon Sep 17 00:00:00 2001 From: Fail-Safe <10307870+Fail-Safe@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:06:25 -0400 Subject: [PATCH 1/2] Fix false kernel reboot prompts on ARM guests --- guest/README.md | 8 ++ .../omarchy/update-restart-arm-kernel.patch | 20 ++++ guest/spec.json | 14 +++ guest/tests/fixtures/omarchy-update-restart | 51 ++++++++ guest/tests/test_update_restart_arm_kernel.py | 111 ++++++++++++++++++ guest/tests/verify.py | 1 + 6 files changed, 205 insertions(+) create mode 100644 guest/patches/omarchy/update-restart-arm-kernel.patch create mode 100644 guest/tests/fixtures/omarchy-update-restart create mode 100644 guest/tests/test_update_restart_arm_kernel.py diff --git a/guest/README.md b/guest/README.md index c903fc5..29935f6 100644 --- a/guest/README.md +++ b/guest/README.md @@ -62,6 +62,14 @@ direct-boot kernel and matching headers are held, while the packaged repository. A separate migration channel is required before those Try-Omarchy-specific revisions can advance on an existing disk without reset. +The kernel reboot check recognizes package-owned `modules.builtin` metadata as +well as `vmlinuz` under `/usr/lib/modules//`. Arch Linux ARM does not +place `vmlinuz` there, so requiring that file alone produces a false kernel-update +prompt after every no-op update. A matching release suppresses that prompt; +unrecognized layouts do not claim an update. Other reboot and service-restart +reasons still apply. This check ships as the `update-restart-arm-kernel` reviewed +backport, with a fixture from the pinned upstream command for regression tests. + The factory includes the pinned upstream `omarchy-dns` and `omarchy-theme-browser` sudoers drop-ins, owned by `try-omarchy-runtime` with root ownership and mode `0440`. These grant wheel users passwordless access diff --git a/guest/patches/omarchy/update-restart-arm-kernel.patch b/guest/patches/omarchy/update-restart-arm-kernel.patch new file mode 100644 index 0000000..e0a2a3c --- /dev/null +++ b/guest/patches/omarchy/update-restart-arm-kernel.patch @@ -0,0 +1,20 @@ +diff --git a/bin/omarchy-update-restart b/bin/omarchy-update-restart +--- a/bin/omarchy-update-restart ++++ b/bin/omarchy-update-restart +@@ -9,10 +9,14 @@ confirm_reboot() { + } + + running_kernel=$(uname -r) +-kernel_updated=true ++kernel_updated=false + +-for kernel in /usr/lib/modules/*/vmlinuz; do ++# Arch Linux ARM installs its image outside the modules directory. Its ++# package-owned modules.builtin identifies the installed kernel release too. ++# No recognized package-owned marker means unknown, not an updated kernel. ++for kernel in /usr/lib/modules/*/vmlinuz /usr/lib/modules/*/modules.builtin; do + if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then ++ kernel_updated=true + installed_kernel=$(basename "$(dirname "$kernel")") + + if [[ $installed_kernel == $running_kernel ]]; then diff --git a/guest/spec.json b/guest/spec.json index 29f4785..aef58a7 100644 --- a/guest/spec.json +++ b/guest/spec.json @@ -380,6 +380,20 @@ } ] }, + { + "id": "update-restart-arm-kernel", + "description": "Recognize package-owned ARM kernel module metadata when checking for a required reboot, without treating unknown layouts as updates.", + "reference": "https://github.com/basecamp/omarchy/blob/346e69e1cec6c4e8924531874af6ba010a1bc99e/bin/omarchy-update-restart", + "patch": "patches/omarchy/update-restart-arm-kernel.patch", + "patchSha256": "632a32ae673824a1de094156e0fd81d1c82a72f6aef25dc45ac53617002ab1a4", + "targets": [ + { + "path": "bin/omarchy-update-restart", + "beforeSha256": "7bc95f8a0a3ae35cfd81453e08194f5660c27cf59528c7d95b4345f362030b60", + "afterSha256": "d509ec49d59f7b74fd02bfabdda281cd8d94f952861b1de18700ba08098f1725" + } + ] + }, { "id": "pkg-add-aarch64-unavailable", "description": "Refuse known aarch64-unavailable packages from omarchy-pkg-add with the shared message.", diff --git a/guest/tests/fixtures/omarchy-update-restart b/guest/tests/fixtures/omarchy-update-restart new file mode 100644 index 0000000..05f7ba0 --- /dev/null +++ b/guest/tests/fixtures/omarchy-update-restart @@ -0,0 +1,51 @@ +#!/bin/bash + +# omarchy:summary=Prompt for required reboot or service restarts after updates + +echo + +confirm_reboot() { + gum confirm "$1" && { omarchy-system-reboot; exit 0; } +} + +running_kernel=$(uname -r) +kernel_updated=true + +for kernel in /usr/lib/modules/*/vmlinuz; do + if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then + installed_kernel=$(basename "$(dirname "$kernel")") + + if [[ $installed_kernel == $running_kernel ]]; then + kernel_updated=false + break + fi + fi +done + +if [[ $kernel_updated == "true" ]]; then + confirm_reboot "Linux kernel has been updated. Reboot?" +elif [[ -f $HOME/.local/state/omarchy/reboot-required ]]; then + confirm_reboot "Updates require reboot. Ready?" +fi + +running_hyprland=$(readlink /proc/$(pgrep -x Hyprland)/exe 2>/dev/null) +if [[ $running_hyprland == *"(deleted)"* ]]; then + confirm_reboot "Hyprland has been updated. Reboot?" +fi + +for file in "$HOME"/.local/state/omarchy/restart-*-required; do + if [[ -f $file ]]; then + filename=$(basename "$file") + service=$(echo "$filename" | sed 's/restart-\(.*\)-required/\1/') + echo "Restarting $service" + omarchy-state clear "$filename" + omarchy-restart-"$service" + fi +done + +# Updates routinely replace the shell's QML, and a stale process can lazy-load +# new files into old code. A restart failure (locked session, ssh, TTY) only +# prints its reason: the next update or login gets a fresh shell anyway. +echo -e "\e[32m\nRestarting shell\e[0m" +echo "All plugins have been reloaded" +omarchy-restart-shell || true diff --git a/guest/tests/test_update_restart_arm_kernel.py b/guest/tests/test_update_restart_arm_kernel.py new file mode 100644 index 0000000..7d82f24 --- /dev/null +++ b/guest/tests/test_update_restart_arm_kernel.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import hashlib +import json +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +GUEST = Path(__file__).resolve().parents[1] + + +class UpdateRestartARMKernelTests(unittest.TestCase): + def run_restart(self, markers=(), *, reboot_required=False, deleted_hyprland=False): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = GUEST / "tests/fixtures/omarchy-update-restart" + spec = json.loads((GUEST / "spec.json").read_text()) + backport = next(b for b in spec["authenticity"]["backports"] + if b["id"] == "update-restart-arm-kernel") + self.assertEqual(hashlib.sha256(source.read_bytes()).hexdigest(), + backport["targets"][0]["beforeSha256"]) + patch = GUEST / backport["patch"] + self.assertEqual(hashlib.sha256(patch.read_bytes()).hexdigest(), + backport["patchSha256"]) + result = subprocess.run( + ["patch", "--silent", "-o", "-", str(source), str(patch)], + capture_output=True, check=True, + ) + self.assertEqual(hashlib.sha256(result.stdout).hexdigest(), + backport["targets"][0]["afterSha256"]) + # Redirect only the filesystem boundary; execute the complete patched + # command with stubbed external effects, including restart fallthrough. + script = result.stdout.decode().replace("/usr/lib/modules/", f"{root}/modules/") + for release, name, owned in markers: + marker = root / "modules" / release / name + marker.parent.mkdir(parents=True, exist_ok=True) + marker.touch() + if owned: + marker.with_name(name + ".owned").touch() + state = root / ".local/state/omarchy" + state.mkdir(parents=True) + (state / "restart-test-required").touch() + if reboot_required: + (state / "reboot-required").touch() + stubs = ''' +uname() { echo 7.2.5-1-aarch64-ARCH; } +pacman() { [[ $1 == -Qo && -f $2.owned ]]; } +gum() { echo "PROMPT: $2"; return 1; } +omarchy-system-reboot() { echo UNEXPECTED_REBOOT; } +pgrep() { echo 123; } +readlink() { [[ $DELETED_HYPRLAND == 1 ]] && echo '/usr/bin/Hyprland (deleted)'; } +omarchy-state() { :; } +omarchy-restart-test() { echo SERVICE_RESTARTED; } +omarchy-restart-shell() { echo SHELL_RESTARTED; } +''' + completed = subprocess.run( + ["bash", "-c", stubs + script], capture_output=True, text=True, + env={**os.environ, "HOME": str(root), + "DELETED_HYPRLAND": str(int(deleted_hyprland))}, + check=True, + ) + self.assertIn("SERVICE_RESTARTED", completed.stdout) + self.assertIn("SHELL_RESTARTED", completed.stdout) + self.assertNotIn("UNEXPECTED_REBOOT", completed.stdout) + return completed.stdout + + def test_matching_arm_modules_without_vmlinuz_do_not_prompt(self): + output = self.run_restart([("7.2.5-1-aarch64-ARCH", "modules.builtin", True)]) + self.assertNotIn("PROMPT:", output) + + def test_newer_installed_arm_kernel_prompts(self): + output = self.run_restart([("7.2.6-1-aarch64-ARCH", "modules.builtin", True)]) + self.assertIn("PROMPT: Linux kernel has been updated.", output) + + def test_vmlinuz_layout_still_detects_matches_and_changes(self): + for release, updated in [("7.2.5-1-aarch64-ARCH", False), ("7.2.6-1-aarch64-ARCH", True)]: + with self.subTest(release=release): + output = self.run_restart([(release, "vmlinuz", True)]) + self.assertEqual("PROMPT: Linux kernel" in output, updated) + + def test_matching_arm_kernel_wins_over_other_installed_kernel(self): + output = self.run_restart([ + ("7.2.6-1-aarch64-ARCH", "vmlinuz", True), + ("7.2.5-1-aarch64-ARCH", "modules.builtin", True), + ]) + self.assertNotIn("PROMPT:", output) + + def test_unknown_and_unowned_layouts_do_not_claim_an_update(self): + for markers in [[], [("7.2.6-1-aarch64-ARCH", "modules.builtin", False)]]: + with self.subTest(markers=markers): + self.assertNotIn("PROMPT:", self.run_restart(markers)) + + def test_stale_unowned_matching_modules_do_not_hide_a_change(self): + output = self.run_restart([ + ("7.2.5-1-aarch64-ARCH", "modules.builtin", False), + ("7.2.6-1-aarch64-ARCH", "modules.builtin", True), + ]) + self.assertIn("PROMPT: Linux kernel has been updated.", output) + + def test_other_reboot_reasons_survive_unknown_kernel_layout(self): + output = self.run_restart(reboot_required=True, deleted_hyprland=True) + self.assertIn("PROMPT: Updates require reboot.", output) + self.assertIn("PROMPT: Hyprland has been updated.", output) + self.assertNotIn("PROMPT: Linux kernel", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/guest/tests/verify.py b/guest/tests/verify.py index ff3d7ec..d8616cf 100755 --- a/guest/tests/verify.py +++ b/guest/tests/verify.py @@ -241,6 +241,7 @@ def main() -> None: "notification-hover-close", "notification-screen-privacy", "update-free-space-message", + "update-restart-arm-kernel", "pkg-add-aarch64-unavailable", "pkg-aur-add-aarch64-unavailable", "dropbox-aarch64-unavailable", From 3276e5a9ec54f1c61df1d2bf0d643b9da3a88055 Mon Sep 17 00:00:00 2001 From: Fail-Safe <10307870+Fail-Safe@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:17:04 -0400 Subject: [PATCH 2/2] Distinguish unknown kernel reboot status --- guest/README.md | 3 ++- .../omarchy/update-restart-arm-kernel.patch | 21 ++++++++++++++++--- guest/spec.json | 6 +++--- guest/tests/test_update_restart_arm_kernel.py | 5 +++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/guest/README.md b/guest/README.md index 29935f6..e47f492 100644 --- a/guest/README.md +++ b/guest/README.md @@ -66,7 +66,8 @@ The kernel reboot check recognizes package-owned `modules.builtin` metadata as well as `vmlinuz` under `/usr/lib/modules//`. Arch Linux ARM does not place `vmlinuz` there, so requiring that file alone produces a false kernel-update prompt after every no-op update. A matching release suppresses that prompt; -unrecognized layouts do not claim an update. Other reboot and service-restart +unrecognized layouts report that kernel reboot status could not be determined +instead of claiming either a match or an update. Other reboot and service-restart reasons still apply. This check ships as the `update-restart-arm-kernel` reviewed backport, with a fixture from the pinned upstream command for regression tests. diff --git a/guest/patches/omarchy/update-restart-arm-kernel.patch b/guest/patches/omarchy/update-restart-arm-kernel.patch index e0a2a3c..524fb97 100644 --- a/guest/patches/omarchy/update-restart-arm-kernel.patch +++ b/guest/patches/omarchy/update-restart-arm-kernel.patch @@ -1,12 +1,12 @@ diff --git a/bin/omarchy-update-restart b/bin/omarchy-update-restart --- a/bin/omarchy-update-restart +++ b/bin/omarchy-update-restart -@@ -9,10 +9,14 @@ confirm_reboot() { +@@ -9,20 +9,28 @@ confirm_reboot() { } running_kernel=$(uname -r) -kernel_updated=true -+kernel_updated=false ++kernel_status=unknown -for kernel in /usr/lib/modules/*/vmlinuz; do +# Arch Linux ARM installs its image outside the modules directory. Its @@ -14,7 +14,22 @@ diff --git a/bin/omarchy-update-restart b/bin/omarchy-update-restart +# No recognized package-owned marker means unknown, not an updated kernel. +for kernel in /usr/lib/modules/*/vmlinuz /usr/lib/modules/*/modules.builtin; do if [[ -f $kernel ]] && pacman -Qo "$kernel" &>/dev/null; then -+ kernel_updated=true ++ kernel_status=changed installed_kernel=$(basename "$(dirname "$kernel")") if [[ $installed_kernel == $running_kernel ]]; then +- kernel_updated=false ++ kernel_status=matched + break + fi + fi + done + +-if [[ $kernel_updated == "true" ]]; then ++if [[ $kernel_status == unknown ]]; then ++ echo "Unable to determine kernel reboot status: no recognized package-owned kernel files found." >&2 ++fi ++ ++if [[ $kernel_status == changed ]]; then + confirm_reboot "Linux kernel has been updated. Reboot?" + elif [[ -f $HOME/.local/state/omarchy/reboot-required ]]; then diff --git a/guest/spec.json b/guest/spec.json index aef58a7..45ca5fd 100644 --- a/guest/spec.json +++ b/guest/spec.json @@ -382,15 +382,15 @@ }, { "id": "update-restart-arm-kernel", - "description": "Recognize package-owned ARM kernel module metadata when checking for a required reboot, without treating unknown layouts as updates.", + "description": "Recognize package-owned ARM kernel module metadata and explicitly distinguish matching, changed, and unknown kernel reboot states.", "reference": "https://github.com/basecamp/omarchy/blob/346e69e1cec6c4e8924531874af6ba010a1bc99e/bin/omarchy-update-restart", "patch": "patches/omarchy/update-restart-arm-kernel.patch", - "patchSha256": "632a32ae673824a1de094156e0fd81d1c82a72f6aef25dc45ac53617002ab1a4", + "patchSha256": "e6cf6d70a8fdfb9e027f28a2c115918e4b8db4d827b249a2941ceb64975c7a47", "targets": [ { "path": "bin/omarchy-update-restart", "beforeSha256": "7bc95f8a0a3ae35cfd81453e08194f5660c27cf59528c7d95b4345f362030b60", - "afterSha256": "d509ec49d59f7b74fd02bfabdda281cd8d94f952861b1de18700ba08098f1725" + "afterSha256": "c791af27312db4cf4f11ec4d112a3e4f50654d82143368166bf385c25f92f5d9" } ] }, diff --git a/guest/tests/test_update_restart_arm_kernel.py b/guest/tests/test_update_restart_arm_kernel.py index 7d82f24..0a80803 100644 --- a/guest/tests/test_update_restart_arm_kernel.py +++ b/guest/tests/test_update_restart_arm_kernel.py @@ -65,6 +65,11 @@ def run_restart(self, markers=(), *, reboot_required=False, deleted_hyprland=Fal self.assertIn("SERVICE_RESTARTED", completed.stdout) self.assertIn("SHELL_RESTARTED", completed.stdout) self.assertNotIn("UNEXPECTED_REBOOT", completed.stdout) + expected_unknown = not any(owned for _, _, owned in markers) + self.assertEqual( + "Unable to determine kernel reboot status" in completed.stderr, + expected_unknown, + ) return completed.stdout def test_matching_arm_modules_without_vmlinuz_do_not_prompt(self):