Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions guest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ 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/<release>/`. 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 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.

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
Expand Down
35 changes: 35 additions & 0 deletions guest/patches/omarchy/update-restart-arm-kernel.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
diff --git a/bin/omarchy-update-restart b/bin/omarchy-update-restart
--- a/bin/omarchy-update-restart
+++ b/bin/omarchy-update-restart
@@ -9,20 +9,28 @@ confirm_reboot() {
}

running_kernel=$(uname -r)
-kernel_updated=true
+kernel_status=unknown

-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_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
14 changes: 14 additions & 0 deletions guest/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,20 @@
}
]
},
{
"id": "update-restart-arm-kernel",
"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": "e6cf6d70a8fdfb9e027f28a2c115918e4b8db4d827b249a2941ceb64975c7a47",
"targets": [
{
"path": "bin/omarchy-update-restart",
"beforeSha256": "7bc95f8a0a3ae35cfd81453e08194f5660c27cf59528c7d95b4345f362030b60",
"afterSha256": "c791af27312db4cf4f11ec4d112a3e4f50654d82143368166bf385c25f92f5d9"
}
]
},
{
"id": "pkg-add-aarch64-unavailable",
"description": "Refuse known aarch64-unavailable packages from omarchy-pkg-add with the shared message.",
Expand Down
51 changes: 51 additions & 0 deletions guest/tests/fixtures/omarchy-update-restart
Original file line number Diff line number Diff line change
@@ -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
116 changes: 116 additions & 0 deletions guest/tests/test_update_restart_arm_kernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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)
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):
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()
1 change: 1 addition & 0 deletions guest/tests/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading