From 282ea23eefad82def82293f823c8b806fb8074d6 Mon Sep 17 00:00:00 2001 From: Lawrence Lin <8616+linyiru@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:14:08 -0500 Subject: [PATCH 1/5] Detect Raspberry Pi as its own platform A Pi has no Limine or UKI to boot, so it can't share generic-aarch64's path. omarchy-hw-platform now prints raspberrypi for a device tree whose root compatible carries the raspberrypi, vendor prefix, and every place that lists platforms accepts it, treating it as generic-aarch64 until it has packages of its own. --- bin/omarchy-hw-platform | 32 ++++++++++++-------- bin/omarchy-lifecycle-dispatch | 4 +-- bin/omarchy-pkg-defaults | 4 +-- bin/omarchy-update-keyring | 2 +- docs/file-layout.md | 2 +- docs/lifecycle-dispatch.md | 4 +-- install/helpers/image-target.sh | 4 +-- install/helpers/pacman.sh | 4 +-- install/post-install/pacman.sh | 4 +-- test/shell.d/base-test.sh | 3 +- test/shell.d/channel-sudo-boundary-test.sh | 4 +-- test/shell.d/hw-platform-test.sh | 30 +++++++++++++----- test/shell.d/image-deferred-hardware-test.sh | 2 +- test/shell.d/image-target-platform-test.sh | 11 +++++++ test/shell.d/lifecycle-dispatch-test.sh | 8 ++--- test/shell.d/mkinitcpio-hooks-test.sh | 4 +-- test/shell.d/pacman-templates-test.sh | 9 +++--- test/shell.d/platform-packages-test.sh | 4 +-- test/shell.d/update-boot-verify-test.sh | 4 +-- test/shell.d/update-keyring-test.sh | 2 +- 20 files changed, 87 insertions(+), 54 deletions(-) diff --git a/bin/omarchy-hw-platform b/bin/omarchy-hw-platform index d3885912bb8..98baa54fc9f 100755 --- a/bin/omarchy-hw-platform +++ b/bin/omarchy-hw-platform @@ -1,11 +1,13 @@ #!/bin/bash -p -# omarchy:summary=Print the hardware platform: apple-silicon, qualcomm, generic-aarch64 or generic. +# omarchy:summary=Print the hardware platform: apple-silicon, qualcomm, raspberrypi, generic-aarch64 or generic. # ARM laptops have no DMI, so the board identity is the root node of the device # tree the firmware hands the kernel: "apple," from Asahi's m1n1 on every -# Apple Silicon Mac, "qcom," on Snapdragon laptops. Each compatible string -# is a NUL-separated list, and only a token's vendor prefix counts. +# Apple Silicon Mac, "qcom," on Snapdragon laptops, "raspberrypi," +# on every Raspberry Pi the firmware boots (Pi 4, 400, 5, 500 and the compute +# modules alike). Each compatible string is a NUL-separated list, and only a +# token's vendor prefix counts. # # An image is built away from the machine it will run on, so while this root is # being built rather than booted, the image-target manifest the builder wrote @@ -92,7 +94,7 @@ trusted() { [[ $owner =~ ^[0-9]+$ && $mode =~ ^[0-7]+$ ]] && (( owner == image_owner && (8#$mode & 8#022) == 0 )) } -# The manifest: format=1, platform= one of the four names, comments and unknown +# The manifest: format=1, platform= one of the five names, comments and unknown # keys ignored, and any other line refused. The detector reads it itself, so it # stands alone wherever a package ships a copy of it. image_platform() { @@ -121,7 +123,7 @@ image_platform() { echo "Error: the image manifest $manifest is not format=1" >&2 return 1 fi - if [[ ! $platform =~ ^(apple-silicon|qualcomm|generic-aarch64|generic)$ ]]; then + if [[ ! $platform =~ ^(apple-silicon|qualcomm|raspberrypi|generic-aarch64|generic)$ ]]; then echo "Error: the image manifest $manifest names no known platform: ${platform:-none}" >&2 return 1 fi @@ -147,18 +149,22 @@ for source in "${sources[@]}"; do apple=0 qcom=0 + rpi=0 mapfile -d '' -t tokens <"$source" for token in "${tokens[@]}"; do [[ $token == "apple,"* ]] && apple=1 [[ $token == "qcom,"* ]] && qcom=1 + [[ $token == "raspberrypi,"* ]] && rpi=1 done - if (( apple && qcom )); then - contradiction "$source names both Apple and Qualcomm hardware" + if (( apple + qcom + rpi > 1 )); then + contradiction "$source names more than one of Apple, Qualcomm and Raspberry Pi hardware" elif (( apple )); then found=apple elif (( qcom )); then found=qcom + elif (( rpi )); then + found=raspberrypi else found=none fi @@ -171,13 +177,13 @@ for source in "${sources[@]}"; do done case $vendor in - apple | qcom) + apple | qcom | raspberrypi) [[ $machine == "aarch64" ]] || contradiction "$vendor_source names $vendor hardware but the CPU is $machine" - if [[ $vendor == "apple" ]]; then - echo apple-silicon - else - echo qualcomm - fi + case $vendor in + apple) echo apple-silicon ;; + qcom) echo qualcomm ;; + raspberrypi) echo raspberrypi ;; + esac ;; *) if [[ $machine == "aarch64" ]]; then diff --git a/bin/omarchy-lifecycle-dispatch b/bin/omarchy-lifecycle-dispatch index deabe118eea..3db401a3fec 100755 --- a/bin/omarchy-lifecycle-dispatch +++ b/bin/omarchy-lifecycle-dispatch @@ -86,8 +86,8 @@ if ! platform=$("${detect[@]}"); then exit 1 fi -# Generic, generic-aarch64 and Qualcomm machines have no registered package: -# every operation is a no-op there and callers keep their generic path. +# Generic, generic-aarch64, Raspberry Pi and Qualcomm machines have no registered +# package: every operation is a no-op there and callers keep their generic path. # Apple Silicon's boot operations belong to omarchy-mac-boot, and its system and # user setup and app-install hooks to omarchy-mac (both omacom/omarchy-mac-pkgs). # Provisioning and factory reset have no other path on a Mac, only update-verify diff --git a/bin/omarchy-pkg-defaults b/bin/omarchy-pkg-defaults index 9d28a4a2897..f1cda482e3f 100755 --- a/bin/omarchy-pkg-defaults +++ b/bin/omarchy-pkg-defaults @@ -1,7 +1,7 @@ #!/bin/bash # omarchy:summary=Print the default package set for this computer's platform, or for the named one: the base list plus its architecture's and platform's additions. -# omarchy:args=[apple-silicon|qualcomm|generic-aarch64|generic] +# omarchy:args=[apple-silicon|qualcomm|raspberrypi|generic-aarch64|generic] # omarchy:hidden=true set -eo pipefail @@ -12,7 +12,7 @@ if [[ -z $platform ]]; then fi case $platform in - apple-silicon | qualcomm | generic-aarch64) lists=(base aarch64 "$platform") ;; + apple-silicon | qualcomm | raspberrypi | generic-aarch64) lists=(base aarch64 "$platform") ;; generic) lists=(base) ;; *) echo "Error: unknown platform '$platform'" >&2 diff --git a/bin/omarchy-update-keyring b/bin/omarchy-update-keyring index c7ccd0fd0af..5fbcd8ee072 100755 --- a/bin/omarchy-update-keyring +++ b/bin/omarchy-update-keyring @@ -26,7 +26,7 @@ fi # their own, so its keyring is kept current the same way. keyrings=(archlinux-keyring) case $(omarchy-hw-platform 2>/dev/null) in - apple-silicon | qualcomm | generic-aarch64) + apple-silicon | qualcomm | raspberrypi | generic-aarch64) if omarchy-pkg-present archlinuxarm-keyring; then keyrings+=(archlinuxarm-keyring) fi diff --git a/docs/file-layout.md b/docs/file-layout.md index 0e11a0a4af3..080c36fb6db 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -371,7 +371,7 @@ finalization. It sources: Logging goes to `/var/log/omarchy-install.log` via `install/helpers/logging.sh`. -Platform-specific setup asks `omarchy-hw-platform`, which prints `apple-silicon`, `qualcomm`, `generic-aarch64` or `generic`. It reads the vendor prefix of each token in the device tree's root `compatible` (`apple,` or `qcom,`, from `/proc/device-tree` or `/sys/firmware/devicetree/base`) and the CPU architecture, and fails when they contradict each other. +Platform-specific setup asks `omarchy-hw-platform`, which prints `apple-silicon`, `qualcomm`, `raspberrypi`, `generic-aarch64` or `generic`. It reads the vendor prefix of each token in the device tree's root `compatible` (`apple,`, `qcom,` or `raspberrypi,`, from `/proc/device-tree` or `/sys/firmware/devicetree/base`) and the CPU architecture, and fails when they contradict each other. An image built away from the machine it will run on names its target in a root-owned manifest, `/var/lib/omarchy/image/target` (`format=1`, `platform=`, unknown keys ignored). While the root is being built rather than booted, the detector answers from the manifest and never reads the build host's device tree. The root counts as built when it shows it: no `/run/systemd/system`, PID 1's root is another one (a chroot), or PID 1 is not systemd (a PID namespace). A booted system always answers from its hardware, even with a manifest left behind, so no unit that asks the detector may use `PrivatePIDs=`. As root the detector restarts in an empty environment and ignores the fixture variables its tests use. diff --git a/docs/lifecycle-dispatch.md b/docs/lifecycle-dispatch.md index 5592e11428c..ddec9773f99 100644 --- a/docs/lifecycle-dispatch.md +++ b/docs/lifecycle-dispatch.md @@ -13,7 +13,7 @@ The first form runs the operation. `--resolve` prints the entrypoint the operati | Situation | Run | `--resolve` | | --- | --- | --- | -| The platform registers no package (`generic`, `generic-aarch64`, and `qualcomm` today) | no-op, exit 0 | prints nothing, exit 0 | +| The platform registers no package (`generic`, `generic-aarch64`, `raspberrypi` and `qualcomm` today) | no-op, exit 0 | prints nothing, exit 0 | | The entrypoint exists and passes the trust rules | execs it; its exit status is the result | prints its path | | A required operation has no entrypoint, and the package is not installed | exit 3 (an entrypoint's own status could also be 3; with `--resolve` it is only this): `Error: on needs , which provides ; it is not installed` | same error | | A required operation has no entrypoint, but the package is installed (its pacman record says so) | exit 1: `Error: on needs , which does not provide; update ` | same error | @@ -55,7 +55,7 @@ Registration is code in `bin/omarchy-lifecycle-dispatch`, not configuration. No | --- | --- | --- | --- | | `apple-silicon` | `/usr/lib/omarchy/mac-boot` | `omarchy-mac-boot` | all its operations | | `apple-silicon`: `setup-system`, `setup-user`, `post-install`, `pre-remove` | `/usr/lib/omarchy/mac` | `omarchy-mac` | none | -| `generic`, `generic-aarch64`, `qualcomm` | none | none | none: every operation is a no-op, and callers keep their generic path | +| `generic`, `generic-aarch64`, `raspberrypi`, `qualcomm` | none | none | none: every operation is a no-op, and callers keep their generic path | The entrypoint for an operation is `/`. A registered platform's required operations must be shipped. Its optional operations may be left out, and then they are no-ops. diff --git a/install/helpers/image-target.sh b/install/helpers/image-target.sh index b3c68176383..2bac9949f38 100644 --- a/install/helpers/image-target.sh +++ b/install/helpers/image-target.sh @@ -8,7 +8,7 @@ # # /var/lib/omarchy/image/target # format=1 -# platform=apple-silicon|qualcomm|generic-aarch64|generic +# platform=apple-silicon|qualcomm|raspberrypi|generic-aarch64|generic # # While it exists, omarchy-apply-hardware queues each hardware leaf in # /var/lib/omarchy/image/deferred-steps instead of running it, and arms @@ -95,7 +95,7 @@ omarchy_image_read_manifest() { return 1 fi case $platform in - apple-silicon | qualcomm | generic-aarch64 | generic) ;; + apple-silicon | qualcomm | raspberrypi | generic-aarch64 | generic) ;; *) echo "Error: $omarchy_image_manifest names no known platform: ${platform:-none}" >&2 return 1 diff --git a/install/helpers/pacman.sh b/install/helpers/pacman.sh index 5fb4d3fe784..d4309c4504b 100644 --- a/install/helpers/pacman.sh +++ b/install/helpers/pacman.sh @@ -8,7 +8,7 @@ omarchy_pacman_templates() { case ${1:-} in generic) echo "$OMARCHY_PATH/default/pacman" ;; - qualcomm | generic-aarch64) echo "$OMARCHY_PATH/default/pacman/aarch64" ;; + qualcomm | raspberrypi | generic-aarch64) echo "$OMARCHY_PATH/default/pacman/aarch64" ;; apple-silicon) echo "$OMARCHY_PATH/default/pacman/apple-silicon" ;; *) echo "Error: Unknown platform '${1:-}'." >&2 @@ -24,7 +24,7 @@ omarchy_pacman_templates() { omarchy_pacman_default_channel() { case ${1:-} in generic) echo stable ;; - qualcomm | generic-aarch64 | apple-silicon) echo edge ;; + qualcomm | raspberrypi | generic-aarch64 | apple-silicon) echo edge ;; *) echo "Error: Unknown platform '${1:-}'." >&2 return 1 diff --git a/install/post-install/pacman.sh b/install/post-install/pacman.sh index ee4ffa9c827..397d4cd235c 100644 --- a/install/post-install/pacman.sh +++ b/install/post-install/pacman.sh @@ -9,7 +9,7 @@ source "$OMARCHY_PATH/install/helpers/image-target.sh" platform=$(omarchy-hw-platform) templates=$(omarchy_pacman_templates "$platform") -if [[ $platform == "qualcomm" || $platform == "generic-aarch64" ]]; then +if [[ $platform == "qualcomm" || $platform == "raspberrypi" || $platform == "generic-aarch64" ]]; then omarchy-pkg-add archlinuxarm-keyring fi @@ -22,7 +22,7 @@ cp -f "$templates/mirrorlist-$channel" /etc/pacman.d/mirrorlist # An image build leaves the keyring to each machine's first boot, so no two # share a master key (install/helpers/image-target.sh). -if [[ $platform == "qualcomm" || $platform == "generic-aarch64" ]]; then +if [[ $platform == "qualcomm" || $platform == "raspberrypi" || $platform == "generic-aarch64" ]]; then if omarchy_image_init && omarchy_image_manifest_present; then install -m 0644 /dev/null "$omarchy_image_keyring_request" else diff --git a/test/shell.d/base-test.sh b/test/shell.d/base-test.sh index 7852269a6d5..f0fd2fb8b87 100644 --- a/test/shell.d/base-test.sh +++ b/test/shell.d/base-test.sh @@ -100,7 +100,8 @@ fake_platform() { case $platform in apple-silicon) compatible=(apple,j416c apple,t6021 apple,arm-platform) ;; qualcomm) compatible=(lenovo,yoga-slim7x qcom,x1e80100) ;; - generic-aarch64) compatible=(raspberrypi,5-model-b brcm,bcm2712) ;; + raspberrypi) compatible=(raspberrypi,5-model-b brcm,bcm2712) ;; + generic-aarch64) compatible=(linux,dummy-virt) ;; generic) machine=x86_64 ;; *) fail "fake_platform knows the platform $platform" ;; esac diff --git a/test/shell.d/channel-sudo-boundary-test.sh b/test/shell.d/channel-sudo-boundary-test.sh index c8acc3cddbf..701a49d3f6e 100755 --- a/test/shell.d/channel-sudo-boundary-test.sh +++ b/test/shell.d/channel-sudo-boundary-test.sh @@ -70,7 +70,7 @@ for channel in stable rc edge dev; do done # Every aarch64 platform switches to edge and dev, copying its own templates. -for platform in qualcomm generic-aarch64 apple-silicon; do +for platform in qualcomm raspberrypi generic-aarch64 apple-silicon; do case $platform in apple-silicon) templates=default/pacman/apple-silicon ;; *) templates=default/pacman/aarch64 ;; @@ -90,7 +90,7 @@ pass "aarch64 platforms switch to edge and dev through their own templates" # stable and rc would install the release line, which has no aarch64 support: # an aarch64 machine refuses them before anything changes. -for platform in qualcomm generic-aarch64 apple-silicon; do +for platform in qualcomm raspberrypi generic-aarch64 apple-silicon; do for channel in stable rc; do reset_boundary if SUDO_TEST_PLATFORM=$platform run_channel "$channel"; then fail "$platform accepted $channel"; fi diff --git a/test/shell.d/hw-platform-test.sh b/test/shell.d/hw-platform-test.sh index 1f05dc825ff..1a5639078d1 100644 --- a/test/shell.d/hw-platform-test.sh +++ b/test/shell.d/hw-platform-test.sh @@ -8,7 +8,7 @@ detector="$ROOT/bin/omarchy-hw-platform" test_tmp=$(mktemp -d) trap 'rm -rf "$test_tmp"' EXIT -for platform in apple-silicon qualcomm generic-aarch64 generic; do +for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do fake_platform "$test_tmp/$platform" "$platform" done @@ -21,8 +21,8 @@ if (( EUID != 0 )); then fi if (( EUID == 0 )) || unshare --user --map-root-user true 2>/dev/null; then live=$("${root_runner[@]}" "$detector") || fail "root detects the live platform" - [[ $live =~ ^(apple-silicon|qualcomm|generic-aarch64|generic)$ ]] || fail "root detects the live platform" "live: $live" - for platform in apple-silicon qualcomm generic-aarch64 generic; do + [[ $live =~ ^(apple-silicon|qualcomm|raspberrypi|generic-aarch64|generic)$ ]] || fail "root detects the live platform" "live: $live" + for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do fixture="$test_tmp/$platform" if [[ -f $fixture/proc/device-tree/compatible ]]; then mkdir -p "$fixture/sys/firmware/devicetree/base" @@ -51,8 +51,8 @@ pass "the detector and the Apple predicate refuse an ordinary Bash launch with a require_platform_fixtures "the platform fixtures" -# The four platforms every caller is written against. -for platform in apple-silicon qualcomm generic-aarch64 generic; do +# The five platforms every caller is written against. +for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do fixture="$test_tmp/$platform" actual=$(OMARCHY_PROC_ROOT="$fixture/proc" PATH="$fixture/bin:$ROOT/bin:$PATH" "$detector") || fail "the $platform fixture is detected" @@ -136,18 +136,29 @@ for board in yoga-slim7x xps13-9345 t14s; do done pass "real Apple and Snapdragon device trees are recognised" +# The Pi 4 Model B the Pi-hole runs on, and the Pi 5 and 400 as their +# firmware's device trees name them. +write_tree pi4-model-b proc raspberrypi,4-model-b brcm,bcm2711 +write_tree pi400 proc raspberrypi,400 brcm,bcm2711 +write_tree pi5-model-b proc raspberrypi,5-model-b brcm,bcm2712 +for board in pi4-model-b pi400 pi5-model-b; do + expect "$board" aarch64 raspberrypi "the $board device tree is a Raspberry Pi" +done +# The board vendor names a Pi, never the SoC token after it. +write_tree brcm-only proc vendor,board brcm,bcm2711 +expect brcm-only aarch64 generic-aarch64 "a Broadcom SoC without the Raspberry Pi vendor is generic aarch64" +pass "real Raspberry Pi device trees are recognised" + write_tree qemu-virt proc linux,dummy-virt -write_tree raspberry-pi proc raspberrypi,5-model-b brcm,bcm2712 mkdir -p "$test_tmp/cases/acpi/proc" "$test_tmp/cases/acpi/sys" expect qemu-virt aarch64 generic-aarch64 "a QEMU virt board is generic aarch64" -expect raspberry-pi aarch64 generic-aarch64 "a Raspberry Pi is generic aarch64" expect acpi aarch64 generic-aarch64 "an aarch64 machine without a device tree is generic aarch64" expect acpi x86_64 generic "an x86 machine without a device tree is generic" pass "unknown aarch64 and x86 machines are generic" # Only a token's vendor prefix identifies the board; the old detector matched # "apple," anywhere in the file. -write_tree substring proc pineapple,board acmeqcom,soc vendor,apple,x vendor,qcom,y +write_tree substring proc pineapple,board acmeqcom,soc myraspberrypi,x vendor,apple,x vendor,qcom,y vendor,raspberrypi,z expect substring aarch64 generic-aarch64 "vendor names inside other tokens do not identify the board" pass "only a token's vendor prefix identifies the board" @@ -163,6 +174,8 @@ pass "sysfs is the fallback for the device tree" write_tree both-vendors proc apple,j314s qcom,x1e80100 expect_contradiction both-vendors aarch64 "a device tree naming Apple and Qualcomm fails" +write_tree pi-and-qcom proc raspberrypi,5-model-b qcom,x1e80100 +expect_contradiction pi-and-qcom aarch64 "a device tree naming a Raspberry Pi and Qualcomm fails" write_tree sources-disagree proc apple,j314s apple,t6000 apple,arm-platform write_tree sources-disagree sys lenovo,yoga-slim7x qcom,x1e80100 expect_contradiction sources-disagree aarch64 "proc and sysfs naming different vendors fails" @@ -171,6 +184,7 @@ write_tree vendor-vs-none sys linux,dummy-virt expect_contradiction vendor-vs-none aarch64 "proc naming Apple while sysfs names nobody fails" expect_contradiction m1-pro x86_64 "an Apple device tree on an x86 CPU fails" expect_contradiction yoga-slim7x x86_64 "a Qualcomm device tree on an x86 CPU fails" +expect_contradiction pi4-model-b x86_64 "a Raspberry Pi device tree on an x86 CPU fails" if TEST_ARCH=x86_64 OMARCHY_PROC_ROOT="$test_tmp/cases/m1-pro/proc" PATH="$stub_bin:$ROOT/bin:$PATH" \ "$ROOT/bin/omarchy-hw-apple-silicon" 2>/dev/null; then fail "the Apple predicate fails closed on contradictory identity" diff --git a/test/shell.d/image-deferred-hardware-test.sh b/test/shell.d/image-deferred-hardware-test.sh index 964e0384db6..76a92d9a8aa 100644 --- a/test/shell.d/image-deferred-hardware-test.sh +++ b/test/shell.d/image-deferred-hardware-test.sh @@ -378,7 +378,7 @@ keyring_boot() { PATH="$keyring_bin:$base_path" "$ROOT/bin/omarchy-provision-hardware" } -for platform in qualcomm generic-aarch64; do +for platform in qualcomm raspberrypi generic-aarch64; do rm -f "$KEYRING" root=$(new_root "install-$platform") finalize "$root" "$platform" || fail "$platform: install finalization succeeds" diff --git a/test/shell.d/image-target-platform-test.sh b/test/shell.d/image-target-platform-test.sh index 9ac03f91d39..11145abe4c8 100644 --- a/test/shell.d/image-target-platform-test.sh +++ b/test/shell.d/image-target-platform-test.sh @@ -253,6 +253,17 @@ world on-a-mac aarch64 apple,j416c apple,t6021 apple,arm-platform pid1 on-a-mac chroot manifest on-a-mac $'format=1\nplatform=generic-aarch64\n' expect on-a-mac generic-aarch64 "a generic aarch64 image built on a Mac is generic aarch64" +world pi-on-x86 x86_64 +pid1 pi-on-x86 chroot +manifest pi-on-x86 $'format=1\nplatform=raspberrypi\n' +if detect pi-on-x86 >/dev/null 2>&1; then + fail "a Raspberry Pi manifest on an x86 CPU fails" +fi +pass "a Raspberry Pi manifest on an x86 CPU fails" +world pi-on-arm aarch64 linux,dummy-virt +pid1 pi-on-arm chroot +manifest pi-on-arm $'format=1\nplatform=raspberrypi\n' +expect pi-on-arm raspberrypi "a Raspberry Pi image built on a generic aarch64 host is a Raspberry Pi" world odd-host aarch64 apple,j416c qcom,x1e80100 pid1 odd-host chroot manifest odd-host $'format=1\nplatform=qualcomm\n' diff --git a/test/shell.d/lifecycle-dispatch-test.sh b/test/shell.d/lifecycle-dispatch-test.sh index 27b098b8e63..8d5e8d4fe8c 100755 --- a/test/shell.d/lifecycle-dispatch-test.sh +++ b/test/shell.d/lifecycle-dispatch-test.sh @@ -11,7 +11,7 @@ trap 'rm -rf "$tmp"' EXIT operations=(provision-prepare provision-commit provision-verify reset-prepare reset-verify reset-commit reset-rollback update-verify update-takeover luks-slots setup-boot) apple_optional=() -for platform in apple-silicon qualcomm generic-aarch64 generic; do +for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do fake_platform "$tmp/$platform" "$platform" done mkdir -p "$tmp/contradiction/proc/device-tree" @@ -64,7 +64,7 @@ mkdir -p "$empty" # x86, generic aarch64 and Qualcomm register no boot package: every operation # is a no-op there, even with Mac entrypoints on disk that would fail. echo 9 >"$tmp/fail-with" -for platform in generic generic-aarch64 qualcomm; do +for platform in generic raspberrypi generic-aarch64 qualcomm; do for operation in "${operations[@]}"; do rm -f "$tmp/ran" output=$(on "$platform" "$full" "$operation" --flag 2>&1) || fail "$platform: $operation is a no-op" "$output" @@ -257,7 +257,7 @@ rm -rf "$boot_only" install_setup "$boot_only" "$implementation" echo 9 >"$tmp/fail-with" -for platform in generic generic-aarch64 qualcomm; do +for platform in generic raspberrypi generic-aarch64 qualcomm; do for operation in "${setup_operations[@]}"; do rm -f "$tmp/ran" output=$(on "$platform" "$with_mac" "$operation" 2>&1) && [[ -z $output && ! -e $tmp/ran ]] || @@ -319,7 +319,7 @@ pass "apple: a setup entrypoint that fails the trust rules never runs" # installer run with sudo still finishes elsewhere. As root the dispatcher runs # the detector beside it, so each platform gets a copy beside a stub. if unshare --user --map-root-user true 2>/dev/null; then - for platform in apple-silicon generic qualcomm generic-aarch64; do + for platform in apple-silicon generic qualcomm raspberrypi generic-aarch64; do mkdir -p "$tmp/root-$platform" cp "$dispatch" "$tmp/root-$platform/" printf '#!/bin/bash\necho %s\n' "$platform" >"$tmp/root-$platform/omarchy-hw-platform" diff --git a/test/shell.d/mkinitcpio-hooks-test.sh b/test/shell.d/mkinitcpio-hooks-test.sh index 920ef016fed..01d1eb9c913 100755 --- a/test/shell.d/mkinitcpio-hooks-test.sh +++ b/test/shell.d/mkinitcpio-hooks-test.sh @@ -9,7 +9,7 @@ require_platform_fixtures "the composed mkinitcpio HOOKS" test_tmp=$(mktemp -d) trap 'rm -rf "$test_tmp"' EXIT -for platform in apple-silicon qualcomm generic-aarch64 generic; do +for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do fake_platform "$test_tmp/platforms/$platform" "$platform" done # omarchy-settings without the runtime package: no detector on PATH. @@ -245,7 +245,7 @@ assert_composed "a Surface is unchanged" generic \ # Snapdragon and other aarch64 machines build the same image as x86, whatever # systemd line their mkinitcpio.conf starts from. -for platform in qualcomm generic-aarch64; do +for platform in qualcomm raspberrypi generic-aarch64; do new_etc drop_in omarchy_resume.conf <<<"HOOKS+=(resume)" drop_in 99-omarchy-provisioning-key.conf <<<"FILES+=(/etc/omarchy/provisioning.key)" diff --git a/test/shell.d/pacman-templates-test.sh b/test/shell.d/pacman-templates-test.sh index 9b31a26ced2..4f9fadd5816 100755 --- a/test/shell.d/pacman-templates-test.sh +++ b/test/shell.d/pacman-templates-test.sh @@ -15,7 +15,7 @@ trap 'rm -rf "$work"' EXIT export OMARCHY_PATH="$ROOT" source "$ROOT/install/helpers/pacman.sh" -platforms="generic qualcomm generic-aarch64 apple-silicon" +platforms="generic qualcomm raspberrypi generic-aarch64 apple-silicon" # aarch64 has edge alone: stable and rc there would install the release line, # which has no aarch64 support. channels_for() { @@ -26,13 +26,14 @@ channels_for() { [[ $(omarchy_pacman_templates generic) == "$ROOT/default/pacman" ]] || fail "x86 keeps its templates where they were" [[ $(omarchy_pacman_templates qualcomm) == "$ROOT/default/pacman/aarch64" ]] || fail "Snapdragon uses the aarch64 templates" +[[ $(omarchy_pacman_templates raspberrypi) == "$ROOT/default/pacman/aarch64" ]] || fail "a Raspberry Pi uses the aarch64 templates" [[ $(omarchy_pacman_templates generic-aarch64) == "$ROOT/default/pacman/aarch64" ]] || fail "generic aarch64 uses the aarch64 templates" [[ $(omarchy_pacman_templates apple-silicon) == "$ROOT/default/pacman/apple-silicon" ]] || fail "Apple Silicon uses its own templates" ! omarchy_pacman_templates riscv 2>/dev/null || fail "an unknown platform has no templates" pass "each platform's templates sit in a directory of their own, x86_64's where they always were" [[ $(omarchy_pacman_default_channel generic) == stable ]] || fail "x86 defaults to stable" -for platform in qualcomm generic-aarch64 apple-silicon; do +for platform in qualcomm raspberrypi generic-aarch64 apple-silicon; do [[ $(omarchy_pacman_default_channel "$platform") == edge ]] || fail "$platform defaults to edge" done ! omarchy_pacman_default_channel riscv 2>/dev/null || fail "an unknown platform has no default channel" @@ -58,7 +59,7 @@ for platform in $platforms; do list=$(repos "$templates" "$channel") || fail "$platform $channel: pacman reads the template" case $platform in generic) expected="core extra multilib omarchy " ;; - qualcomm | generic-aarch64) expected="core extra alarm aur omarchy " ;; + qualcomm | raspberrypi | generic-aarch64) expected="core extra alarm aur omarchy " ;; apple-silicon) expected="omarchy asahi-alarm core extra alarm aur " ;; esac [[ $list == "$expected" ]] || fail "$platform $channel: repositories in order" "$list" @@ -123,7 +124,7 @@ events() { for platform in $platforms; do case $platform in generic) templates=$SUDO_TEST_ROOT/default/pacman ;; - qualcomm | generic-aarch64) templates=$SUDO_TEST_ROOT/default/pacman/aarch64 ;; + qualcomm | raspberrypi | generic-aarch64) templates=$SUDO_TEST_ROOT/default/pacman/aarch64 ;; apple-silicon) templates=$SUDO_TEST_ROOT/default/pacman/apple-silicon ;; esac # No channel named refreshes to the platform's default one. diff --git a/test/shell.d/platform-packages-test.sh b/test/shell.d/platform-packages-test.sh index 21c2c2cfd79..cd068a68dd1 100644 --- a/test/shell.d/platform-packages-test.sh +++ b/test/shell.d/platform-packages-test.sh @@ -18,7 +18,7 @@ names() { base=$(names "$ROOT/install/omarchy-base.packages") aarch64=$(names "$ROOT/install/omarchy-aarch64.packages") -for platform in apple-silicon qualcomm generic-aarch64 generic; do +for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do fake_platform "$work/$platform" "$platform" defaults=$(OMARCHY_PROC_ROOT="$work/$platform/proc" PATH="$work/$platform/bin:$ROOT/bin:$PATH" omarchy-pkg-defaults) [[ $defaults == "$(omarchy-pkg-defaults "$platform")" ]] || @@ -65,7 +65,7 @@ tree="$work/tree" mkdir -p "$tree/install" cp "$ROOT"/install/omarchy-*.packages "$tree/install/" printf '# test addition\nexample-board-support\nzram-generator\n' >"$tree/install/omarchy-generic-aarch64.packages" -for platform in apple-silicon qualcomm generic-aarch64 generic; do +for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do defaults=$(OMARCHY_PATH="$tree" omarchy-pkg-defaults "$platform") if [[ $platform == "generic-aarch64" ]]; then [[ $(tail -n 1 <<<"$defaults") == "example-board-support" ]] || fail "a platform list is added after the others" "$defaults" diff --git a/test/shell.d/update-boot-verify-test.sh b/test/shell.d/update-boot-verify-test.sh index b2a0195c3ff..a5400ee453d 100644 --- a/test/shell.d/update-boot-verify-test.sh +++ b/test/shell.d/update-boot-verify-test.sh @@ -21,7 +21,7 @@ export OMARCHY_UPDATE_LOGGED=1 tmp=$boundary_tmp/boot mkdir -p "$tmp" -for platform in apple-silicon qualcomm generic-aarch64 generic; do +for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do fake_platform "$tmp/$platform" "$platform" done @@ -64,7 +64,7 @@ reboot_offered() { # x86, generic aarch64 and Qualcomm: the boot check is a no-op, even with a # failing boot-package entrypoint on disk, and nothing asks for root. -for platform in generic generic-aarch64 qualcomm; do +for platform in generic raspberrypi generic-aarch64 qualcomm; do run_update "$platform" "$tmp/failing" (( status == 0 )) || fail "$platform: an update reports success" "status $status: $(cat "$tmp/err")" [[ ! -e $tmp/boot-ran ]] || fail "$platform: no boot-package entrypoint runs" "$(cat "$tmp/boot-ran")" diff --git a/test/shell.d/update-keyring-test.sh b/test/shell.d/update-keyring-test.sh index c7c50bac5b0..cf31edbce13 100644 --- a/test/shell.d/update-keyring-test.sh +++ b/test/shell.d/update-keyring-test.sh @@ -111,7 +111,7 @@ pass "update-keyring still reinstalls archlinux-keyring" # An aarch64 machine keeps Arch Linux ARM's keyring current alongside Arch's. : >"$log_file" rm -f "$test_tmp/list-calls" -for platform in apple-silicon qualcomm generic-aarch64; do +for platform in apple-silicon qualcomm raspberrypi generic-aarch64; do : >"$log_file" rm -f "$test_tmp/list-calls" KEYRING_TEST_ARM=1 KEYRING_TEST_PLATFORM=$platform run_keyring >"$test_tmp/arm.out" From ef0fe4304be727374217923a3c9c3fdf6bcfab0b Mon Sep 17 00:00:00 2001 From: Lawrence Lin <8616+linyiru@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:14:40 -0500 Subject: [PATCH 2/5] Add the Raspberry Pi's default package additions --- docs/file-layout.md | 2 +- install/omarchy-raspberrypi.packages | 7 +++++++ test/shell.d/platform-packages-test.sh | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 install/omarchy-raspberrypi.packages diff --git a/docs/file-layout.md b/docs/file-layout.md index 080c36fb6db..665122a0846 100644 --- a/docs/file-layout.md +++ b/docs/file-layout.md @@ -379,7 +379,7 @@ The package lists the ISO pacstraps live at `install/omarchy-base.packages` and `install/omarchy-other.packages`; the ISO builder also reads them when constructing its offline mirror. -A platform's default package set is the base list, then its architecture's additions, then its own: `install/omarchy-aarch64.packages` on every aarch64 platform, then `install/omarchy-.packages` when that platform has one (`install/omarchy-apple-silicon.packages` on Apple Silicon, `install/omarchy-qualcomm.packages` on Qualcomm). x86_64 installs the base list alone. `omarchy-pkg-defaults [platform]` prints the composed set, for the running machine by default (via `omarchy-hw-platform`, so an image build gets its target's set), and `omarchy-reinstall-pkgs` installs it. +A platform's default package set is the base list, then its architecture's additions, then its own: `install/omarchy-aarch64.packages` on every aarch64 platform, then `install/omarchy-.packages` when that platform has one (`install/omarchy-apple-silicon.packages` on Apple Silicon, `install/omarchy-qualcomm.packages` on Qualcomm, `install/omarchy-raspberrypi.packages` on a Raspberry Pi). x86_64 installs the base list alone. `omarchy-pkg-defaults [platform]` prints the composed set, for the running machine by default (via `omarchy-hw-platform`, so an image build gets its target's set), and `omarchy-reinstall-pkgs` installs it. ## Platform display hints (`/usr/share/omarchy-platform/displays.conf`) diff --git a/install/omarchy-raspberrypi.packages b/install/omarchy-raspberrypi.packages new file mode 100644 index 00000000000..2affcbd8595 --- /dev/null +++ b/install/omarchy-raspberrypi.packages @@ -0,0 +1,7 @@ +# Raspberry Pi additions, composed after omarchy-base.packages and +# omarchy-aarch64.packages (omarchy-pkg-defaults raspberrypi). The kernel, +# firmware blobs and boot partition come with the Pi's image, as the kernel +# and bootloader do from the ISO on x86_64. + +# Wi-Fi and Bluetooth firmware for the Pi's onboard radio (Arch Linux ARM). +firmware-raspberrypi diff --git a/test/shell.d/platform-packages-test.sh b/test/shell.d/platform-packages-test.sh index cd068a68dd1..5dd67a27118 100644 --- a/test/shell.d/platform-packages-test.sh +++ b/test/shell.d/platform-packages-test.sh @@ -47,6 +47,11 @@ for platform in apple-silicon qualcomm raspberrypi generic-aarch64 generic; do else ! grep -Fxq linux-firmware-qcom "$work/$platform.packages" || fail "$platform: no Qualcomm firmware" fi + if [[ $platform == "raspberrypi" ]]; then + grep -Fxq firmware-raspberrypi "$work/$platform.packages" || fail "Raspberry Pi adds its radio firmware" + else + ! grep -Fxq firmware-raspberrypi "$work/$platform.packages" || fail "$platform: no Raspberry Pi firmware" + fi # The Mac's packages, and wf-recorder, which records its screen: nothing else # captures on Apple Silicon. for package in omarchy-mac omarchy-mac-boot wf-recorder; do From 35611deb185ebe4b8aed8d25a42996e93548401b Mon Sep 17 00:00:00 2001 From: Lawrence Lin <8616+linyiru@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:16:41 -0500 Subject: [PATCH 3/5] Register the Raspberry Pi's lifecycle operations A Pi's firmware boots config.txt, the kernel and the initramfs from the FAT boot partition, so the generic Limine path has nothing to drive there. Its boot operations resolve in omarchy-rpi-boot and its setup hooks in omarchy-rpi; until omarchy-rpi-boot is published, the required operations stop on a Pi naming the package. --- bin/omarchy-lifecycle-dispatch | 26 ++++++++++++- docs/lifecycle-dispatch.md | 10 +++-- test/shell.d/lifecycle-dispatch-test.sh | 49 ++++++++++++++++++++++--- test/shell.d/update-boot-verify-test.sh | 2 +- 4 files changed, 76 insertions(+), 11 deletions(-) diff --git a/bin/omarchy-lifecycle-dispatch b/bin/omarchy-lifecycle-dispatch index 3db401a3fec..0d083cfd758 100755 --- a/bin/omarchy-lifecycle-dispatch +++ b/bin/omarchy-lifecycle-dispatch @@ -86,8 +86,8 @@ if ! platform=$("${detect[@]}"); then exit 1 fi -# Generic, generic-aarch64, Raspberry Pi and Qualcomm machines have no registered -# package: every operation is a no-op there and callers keep their generic path. +# Generic, generic-aarch64 and Qualcomm machines have no registered package: +# every operation is a no-op there and callers keep their generic path. # Apple Silicon's boot operations belong to omarchy-mac-boot, and its system and # user setup and app-install hooks to omarchy-mac (both omacom/omarchy-mac-pkgs). # Provisioning and factory reset have no other path on a Mac, only update-verify @@ -117,6 +117,28 @@ case $platform:$operation in setup-boot ) ;; + # A Raspberry Pi boots its firmware's config.txt, kernel and initramfs from + # the FAT boot partition, never Limine or a UKI, so the generic boot path has + # nothing to drive there. Its boot operations belong to omarchy-rpi-boot and + # its setup and app-install hooks to omarchy-rpi. Provisioning, factory + # reset, the update's boot check and takeover, and boot setup are required, + # as on a Mac. luks-slots is optional until the Pi's boot checks prove + # encrypted roots, and migrate is optional. + raspberrypi:setup-system | raspberrypi:setup-user | raspberrypi:post-install | raspberrypi:pre-remove) + implementation=/usr/lib/omarchy/rpi + package=omarchy-rpi + required=() + ;; + raspberrypi:*) + implementation=/usr/lib/omarchy/rpi-boot + package=omarchy-rpi-boot + required=( + provision-prepare provision-commit provision-verify + reset-prepare reset-verify reset-commit reset-rollback + update-verify update-takeover + setup-boot + ) + ;; *) exit 0 ;; diff --git a/docs/lifecycle-dispatch.md b/docs/lifecycle-dispatch.md index ddec9773f99..07186146b29 100644 --- a/docs/lifecycle-dispatch.md +++ b/docs/lifecycle-dispatch.md @@ -13,12 +13,12 @@ The first form runs the operation. `--resolve` prints the entrypoint the operati | Situation | Run | `--resolve` | | --- | --- | --- | -| The platform registers no package (`generic`, `generic-aarch64`, `raspberrypi` and `qualcomm` today) | no-op, exit 0 | prints nothing, exit 0 | +| The platform registers no package (`generic`, `generic-aarch64` and `qualcomm` today) | no-op, exit 0 | prints nothing, exit 0 | | The entrypoint exists and passes the trust rules | execs it; its exit status is the result | prints its path | | A required operation has no entrypoint, and the package is not installed | exit 3 (an entrypoint's own status could also be 3; with `--resolve` it is only this): `Error: on needs , which provides ; it is not installed` | same error | | A required operation has no entrypoint, but the package is installed (its pacman record says so) | exit 1: `Error: on needs , which does not provide; update ` | same error | | An optional operation has no entrypoint (every setup and app-install operation is optional, so none of them ever fails for a missing package) | no-op, exit 0 | prints nothing, exit 0 | -| A user operation (`setup-user`, `post-install`, `pre-remove`) is run as root on a platform that registers it (Apple Silicon), its entrypoint installed or not | exit 1: `Error: runs as the user, never as root` | same error | +| A user operation (`setup-user`, `post-install`, `pre-remove`) is run as root on a platform that registers it (Apple Silicon, Raspberry Pi), its entrypoint installed or not | exit 1: `Error: runs as the user, never as root` | same error | | The entrypoint fails the trust rules | exit 1: `Error: refusing : ...` (optional operations too) | same error | | `omarchy-hw-platform` can't settle the platform | exit 1 | exit 1 | | No operation, or one outside the fixed set | exit 2 with usage | exit 2 | @@ -55,12 +55,16 @@ Registration is code in `bin/omarchy-lifecycle-dispatch`, not configuration. No | --- | --- | --- | --- | | `apple-silicon` | `/usr/lib/omarchy/mac-boot` | `omarchy-mac-boot` | all its operations | | `apple-silicon`: `setup-system`, `setup-user`, `post-install`, `pre-remove` | `/usr/lib/omarchy/mac` | `omarchy-mac` | none | -| `generic`, `generic-aarch64`, `raspberrypi`, `qualcomm` | none | none | none: every operation is a no-op, and callers keep their generic path | +| `raspberrypi` | `/usr/lib/omarchy/rpi-boot` | `omarchy-rpi-boot` | all its operations except `luks-slots` and `migrate` | +| `raspberrypi`: `setup-system`, `setup-user`, `post-install`, `pre-remove` | `/usr/lib/omarchy/rpi` | `omarchy-rpi` | none | +| `generic`, `generic-aarch64`, `qualcomm` | none | none | none: every operation is a no-op, and callers keep their generic path | The entrypoint for an operation is `/`. A registered platform's required operations must be shipped. Its optional operations may be left out, and then they are no-ops. On a Mac, owner provisioning and factory reset have no other path, so their operations are required: a Mac without `omarchy-mac-boot`'s entrypoints stops before the owner form or before the reset is confirmed, with the dispatcher's error naming the package, where the generic Limine path would leave the boot-partition key behind or rebuild a UKI the Mac does not boot. `luks-slots` is required because the Mac's boot checks prove the disk's key slots. `update-verify` is required because nothing else checks what an update left in a Mac's boot chain. `update-takeover` is required because a Mac's boot chain and image write files no package owns, so a Mac whose boot package can't vouch for a takeover keeps its files and the update stops at the conflict. `setup-boot` is required because the Mac's boot setup has no other path in hardware setup: a Mac whose `omarchy-mac-boot` lacks it stops the hardware leaf, asking for the update, where an optional operation would skip the Limine activation. System and user setup and the app-install hooks belong to the Mac's runtime package, `omarchy-mac`, not its boot package, and are optional: a Mac without `omarchy-mac` gets Omarchy's generic setup and installs. +A Raspberry Pi's firmware boots `config.txt`, the kernel and the initramfs from the FAT boot partition, with the kernel command line in `cmdline.txt`; there is no Limine or UKI for the generic path to rebuild. So its provisioning, factory reset, `update-verify`, `update-takeover` and `setup-boot` operations are required, as on a Mac, and belong to `omarchy-rpi-boot`. `luks-slots` stays optional until the Pi's boot checks prove an encrypted root's slots, and `migrate` is optional. Its setup and app-install hooks belong to `omarchy-rpi` and are optional. + ## Trust rules - The dispatcher runs as `bash -p`, so a root caller's `BASH_ENV` and exported functions run nothing in it, and it refuses an ordinary Bash launch. As root it uses a fixed `PATH`, runs the detector installed beside it with nothing in its environment but that `PATH` (the detector reads only the live device tree as root), and resolves only the fixed implementation directory. diff --git a/test/shell.d/lifecycle-dispatch-test.sh b/test/shell.d/lifecycle-dispatch-test.sh index 8d5e8d4fe8c..dd4ffa17ed4 100755 --- a/test/shell.d/lifecycle-dispatch-test.sh +++ b/test/shell.d/lifecycle-dispatch-test.sh @@ -64,7 +64,7 @@ mkdir -p "$empty" # x86, generic aarch64 and Qualcomm register no boot package: every operation # is a no-op there, even with Mac entrypoints on disk that would fail. echo 9 >"$tmp/fail-with" -for platform in generic raspberrypi generic-aarch64 qualcomm; do +for platform in generic generic-aarch64 qualcomm; do for operation in "${operations[@]}"; do rm -f "$tmp/ran" output=$(on "$platform" "$full" "$operation" --flag 2>&1) || fail "$platform: $operation is a no-op" "$output" @@ -129,6 +129,33 @@ for operation in "${operations[@]}"; do done pass "apple: an installed omarchy-mac-boot that lacks a required operation fails asking for its update" +# A Raspberry Pi has no generic boot path either: its operations run from +# omarchy-rpi-boot's directory, never the Mac's, and without that package its +# required ones fail naming it. luks-slots and migrate are optional there. +rpi_implementation=usr/lib/omarchy/rpi-boot +rpi_optional=(luks-slots migrate) +rpi_full=$tmp/rpi-full +implementation=$rpi_implementation install_implementation "$rpi_full" +for operation in "${operations[@]}"; do + rm -f "$tmp/ran" + on raspberrypi "$rpi_full" "$operation" first || fail "raspberrypi: $operation runs its entrypoint" + [[ $(cat "$tmp/ran") == "$operation first" ]] || fail "raspberrypi: $operation passes its arguments" "$(cat "$tmp/ran")" + resolved=$(on raspberrypi "$rpi_full" --resolve "$operation") && [[ $resolved == "$rpi_full/$rpi_implementation/$operation" ]] || + fail "raspberrypi: $operation resolves to omarchy-rpi-boot's entrypoint" "$resolved" + + rm -f "$tmp/ran" + status=0 + output=$(on raspberrypi "$full" "$operation" 2>&1) || status=$? + [[ ! -e $tmp/ran ]] || fail "raspberrypi: $operation never runs a Mac entrypoint" + if [[ " ${rpi_optional[*]} " == *" $operation "* ]]; then + (( status == 0 )) && [[ -z $output ]] || fail "raspberrypi: optional $operation is a no-op without the boot package" "$output" + else + (( status == 3 )) && [[ $output == "Error: $operation on raspberrypi needs omarchy-rpi-boot, which provides /usr/lib/omarchy/rpi-boot/$operation; it is not installed" ]] || + fail "raspberrypi: required $operation names the missing package" "status $status: $output" + fi +done +pass "raspberrypi: operations run omarchy-rpi-boot's entrypoints, and without it required ones fail naming it" + # An entrypoint anyone but root could have changed never runs, optional or not. untrusted() { local description=$1 operation=$2 @@ -257,7 +284,7 @@ rm -rf "$boot_only" install_setup "$boot_only" "$implementation" echo 9 >"$tmp/fail-with" -for platform in generic raspberrypi generic-aarch64 qualcomm; do +for platform in generic generic-aarch64 qualcomm; do for operation in "${setup_operations[@]}"; do rm -f "$tmp/ran" output=$(on "$platform" "$with_mac" "$operation" 2>&1) && [[ -z $output && ! -e $tmp/ran ]] || @@ -280,6 +307,18 @@ for operation in "${setup_operations[@]}"; do done pass "apple: setup and the app-install hooks resolve in omarchy-mac's directory, and are no-ops without omarchy-mac" +with_rpi=$tmp/with-rpi +rm -rf "$with_rpi" +install_setup "$with_rpi" usr/lib/omarchy/rpi +for operation in "${setup_operations[@]}"; do + resolved=$(on raspberrypi "$with_rpi" --resolve "$operation") && [[ $resolved == "$with_rpi/usr/lib/omarchy/rpi/$operation" ]] || + fail "raspberrypi: $operation resolves to omarchy-rpi's entrypoint" "$resolved" + rm -f "$tmp/ran" + output=$(on raspberrypi "$with_mac" "$operation" 2>&1) && [[ -z $output && ! -e $tmp/ran ]] || + fail "raspberrypi: $operation is a no-op without omarchy-rpi, even with omarchy-mac's on disk" "$output" +done +pass "raspberrypi: setup and the app-install hooks resolve in omarchy-rpi's directory, and are no-ops without omarchy-rpi" + session=(CALLER_SECRET=leak HOME=/home/owner USER=owner XDG_RUNTIME_DIR=/run/user/1000 XDG_CONFIG_HOME=/home/owner/.cfg XDG_STATE_HOME=/home/owner/.st XDG_DATA_HOME=/home/owner/.data DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus OMARCHY_PATH=/usr/share/omarchy) rm -f "$tmp/ran" @@ -331,16 +370,16 @@ if unshare --user --map-root-user true 2>/dev/null; then output=$(OMARCHY_LIFECYCLE_ROOT="$with_mac" unshare --user --map-root-user \ "$tmp/root-$platform/omarchy-lifecycle-dispatch" $arguments 2>&1) || status=$? [[ ! -e $tmp/ran ]] || fail "$platform: root never runs '$arguments'" - if [[ $platform == "apple-silicon" ]]; then + if [[ $platform == "apple-silicon" || $platform == "raspberrypi" ]]; then (( status == 1 )) && [[ $output == "Error: $operation runs as the user, never as root" ]] || - fail "apple: root is refused '$arguments'" "status $status: $output" + fail "$platform: root is refused '$arguments'" "status $status: $output" else (( status == 0 )) && [[ -z $output ]] || fail "$platform: '$arguments' is a no-op for root" "status $status: $output" fi done done done - pass "setup-user, post-install and pre-remove refuse root on Apple Silicon, and are no-ops for root elsewhere" + pass "setup-user, post-install and pre-remove refuse root on Apple Silicon and Raspberry Pi, and are no-ops for root elsewhere" else skip "no unprivileged user namespace; skipping the root refusal of the user operations" fi diff --git a/test/shell.d/update-boot-verify-test.sh b/test/shell.d/update-boot-verify-test.sh index a5400ee453d..7eeec937307 100644 --- a/test/shell.d/update-boot-verify-test.sh +++ b/test/shell.d/update-boot-verify-test.sh @@ -64,7 +64,7 @@ reboot_offered() { # x86, generic aarch64 and Qualcomm: the boot check is a no-op, even with a # failing boot-package entrypoint on disk, and nothing asks for root. -for platform in generic raspberrypi generic-aarch64 qualcomm; do +for platform in generic generic-aarch64 qualcomm; do run_update "$platform" "$tmp/failing" (( status == 0 )) || fail "$platform: an update reports success" "status $status: $(cat "$tmp/err")" [[ ! -e $tmp/boot-ran ]] || fail "$platform: no boot-package entrypoint runs" "$(cat "$tmp/boot-ran")" From 272ae8233e7d6f925fd6dcafef2e6f28a7dd8ccc Mon Sep 17 00:00:00 2001 From: Lawrence Lin <8616+linyiru@users.noreply.github.com> Date: Fri, 2 Oct 2026 16:02:21 -0500 Subject: [PATCH 4/5] Unpack the bundled Node tarball built for the machine's architecture --- install/user/mise-work.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/install/user/mise-work.sh b/install/user/mise-work.sh index 75ca0916c43..ec73b6bdaa8 100644 --- a/install/user/mise-work.sh +++ b/install/user/mise-work.sh @@ -11,8 +11,14 @@ case ${OMARCHY_SETUP_CONTEXT:-runtime} in *) NODE_PACKAGE_DIR="" ;; esac +# Node names its x86_64 build x64 and its 64-bit Arm build arm64. +case $(uname -m) in + aarch64) NODE_ARCH=arm64 ;; + *) NODE_ARCH=x64 ;; +esac + if [[ -n $NODE_PACKAGE_DIR ]]; then - NODE_TARBALL=$(find "$NODE_PACKAGE_DIR" -name "node-v*-linux-x64.tar.gz" -type f 2>/dev/null | head -n1) + NODE_TARBALL=$(find "$NODE_PACKAGE_DIR" -name "node-v*-linux-$NODE_ARCH.tar.gz" -type f 2>/dev/null | head -n1) if [[ -z $NODE_TARBALL ]]; then if [[ ${OMARCHY_SETUP_CONTEXT:-} == "provision-owner" ]]; then # A factory snapshot predating the bundled tarball may not have it staged. @@ -24,7 +30,7 @@ if [[ -n $NODE_PACKAGE_DIR ]]; then exit 1 fi else - NODE_VERSION=$(basename "$NODE_TARBALL" | sed 's/node-v\(.*\)-linux-x64.tar.gz/\1/') + NODE_VERSION=$(basename "$NODE_TARBALL" | sed "s/node-v\(.*\)-linux-$NODE_ARCH.tar.gz/\1/") NODE_INSTALL_DIR="$HOME/.local/share/mise/installs/node/$NODE_VERSION" mkdir -p "$NODE_INSTALL_DIR" From e5e835ee8b3098c0736d17a1b71d46608f3e23e9 Mon Sep 17 00:00:00 2001 From: Lawrence Lin <8616+linyiru@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:52:55 -0500 Subject: [PATCH 5/5] Skip the first-boot setup fields the platform staged answers for --- bin/omarchy-provision-owner | 96 +++++++++++++++-- test/shell.d/luks-rekey-journal-test.sh | 2 +- test/shell.d/provision-owner-answers-test.sh | 102 ++++++++++++++++++ test/shell.d/provision-owner-keyboard-test.sh | 1 + 4 files changed, 191 insertions(+), 10 deletions(-) create mode 100755 test/shell.d/provision-owner-answers-test.sh diff --git a/bin/omarchy-provision-owner b/bin/omarchy-provision-owner index 2c4ff0b4788..80051574950 100755 --- a/bin/omarchy-provision-owner +++ b/bin/omarchy-provision-owner @@ -38,6 +38,34 @@ export PATH="$OMARCHY_PATH/bin:$PATH" REKEY_STATE=$PROVISIONING_DIR/luks-rekey.state source "$OMARCHY_PATH/install/provisioning/luks-rekey.sh" +# Answers the owner gave before this boot, staged by the platform as key=value +# lines: a Raspberry Pi's comes from the settings Raspberry Pi Imager writes to +# the card. The form skips what they cover and shows them on its last screen, +# where "No, change it" asks for everything again. A password arrives only as a +# crypt(3) hash, so an encrypted install, whose disk takes the password itself, +# asks for it anyway. +ANSWERS_FILE=$PROVISIONING_DIR/answers +declare -A answers=() + +load_answers() { + local key value + answers=() + [[ -f $ANSWERS_FILE ]] || return 0 + while IFS='=' read -r key value; do + [[ -n $value ]] || continue + case $key in + keymap | username | hostname | timezone) answers[$key]=$value ;; + # Only a hash chpasswd -e can store: a bare string would lock the account. + password_hash) + if [[ $value =~ ^\$[a-z0-9]+\$[^:[:space:]]+$ ]]; then + answers[$key]=$value + fi + ;; + esac + done <"$ANSWERS_FILE" + return 0 +} + # The shared setup form — the same questions, prompts, and validation the ISO # configurator's user step asks. The ISO vendors this very file out of the # runtime package it bundles, so an install and the first boot that finishes it @@ -592,6 +620,17 @@ greeter_screen() { keyboard_form() { local status failures=0 + if [[ -n ${answers[keymap]:-} ]]; then + if apply_keyboard "${answers[keymap]}"; then + keyboard=${answers[keymap]} + keyboard_label=$(printf '%s\n' "$OMARCHY_KEYBOARD_LAYOUTS" | awk -F'|' -v k="$keyboard" '$2==k{print $1; exit}') + keyboard_label=${keyboard_label:-$keyboard} + return 0 + fi + log_step "the staged keymap ${answers[keymap]} did not take; asking for a layout" + unset 'answers[keymap]' + fi + while true; do step "Let's setup your keyboard..." omarchy_prompt_keyboard && status=0 || status=$? @@ -706,29 +745,59 @@ user_form() { username=$(<"$PROVISIONING_DIR/setup-user") say "Continuing setup for user: $username" echo + elif staged_username_valid; then + username=${answers[username]} else omarchy_prompt_username || return $? fi - omarchy_prompt_password || return $? + if [[ -n ${answers[password_hash]:-} ]] && ! encrypted_install; then + password="" password_confirmation="" + password_hash=${answers[password_hash]} + else + password_hash="" + omarchy_prompt_password || return $? + fi omarchy_prompt_identity || return $? # Hostname and timezone are deferred to first boot with the rest of the user # step: the deferred install seeds neutral placeholders (omarchy/UTC) and the # owner overwrites them here. - omarchy_prompt_hostname || return $? + if [[ ${answers[hostname]:-} =~ $OMARCHY_HOSTNAME_PATTERN ]]; then + hostname=${answers[hostname]} + else + omarchy_prompt_hostname || return $? + fi - step "Let's set your timezone..." - omarchy_prompt_timezone || return $? + if [[ -n ${answers[timezone]:-} && -f /usr/share/zoneinfo/${answers[timezone]} ]]; then + timezone=${answers[timezone]} + else + step "Let's set your timezone..." + omarchy_prompt_timezone || return $? + fi +} + +# The checks omarchy_prompt_username makes of a typed name. +staged_username_valid() { + local name=${answers[username]:-} + [[ -n $name && $name =~ $OMARCHY_USERNAME_PATTERN && ! $name =~ $OMARCHY_RESERVED_USERNAMES ]] && + ! omarchy_username_taken "$name" } confirm_form() { + local shown_password + if [[ -n ${password_hash:-} ]]; then + shown_password="[Set before first boot]" + else + shown_password=$(printf "%${#password}s" | tr ' ' '*') + fi + clear_logo echo echo -e "Field,Value Keyboard,${keyboard_label:-English (US)} Username,$username -Password,$(printf "%${#password}s" | tr ' ' '*') +Password,$shown_password Full name,${full_name:-[Skipped]} Email address,${email_address:-[Skipped]} Hostname,${hostname:-omarchy} @@ -806,8 +875,13 @@ create_user() { ${full_name:+-c "$full_name"} "$username" fi - printf '%s:%s\n' "$username" "$password" | chpasswd - printf '%s:%s\n' root "$password" | chpasswd + if [[ -n ${password_hash:-} ]]; then + printf '%s:%s\n' "$username" "$password_hash" | chpasswd -e + printf '%s:%s\n' root "$password_hash" | chpasswd -e + else + printf '%s:%s\n' "$username" "$password" | chpasswd + printf '%s:%s\n' root "$password" | chpasswd + fi # deferred-provisioning installs skip archinstall's create_users, which is what normally # uncomments %wheel in /etc/sudoers. Always write the drop-in: detecting an @@ -1124,7 +1198,7 @@ reset_limine_config() { cleanup_oem_state() { # Keep groups + packages: omarchy-system-factory-reset stages the bundled Node # tarball from these live copies when the factory snapshot predates it. - rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user" "$REKEY_STATE" + rm -f "$PROVISIONING_DIR/pending" "$PROVISIONING_DIR/authorized_keys" "$PROVISIONING_DIR/setup-user" "$ANSWERS_FILE" "$REKEY_STATE" rm -f /etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service systemctl daemon-reload 2>/dev/null || true @@ -1201,6 +1275,7 @@ run_setup() { local status platform_ready || return 1 + load_answers while true; do keyboard_form || return 1 @@ -1211,7 +1286,10 @@ run_setup() { ((status == OMARCHY_FORM_SIGNAL)) && confirm_reboot && exec systemctl reboot continue fi - confirm_form || continue + if ! confirm_form; then + answers=() + continue + fi if ! rekey_accepts_password; then notice "Setup already set the disk password. Enter that same password." 4 continue diff --git a/test/shell.d/luks-rekey-journal-test.sh b/test/shell.d/luks-rekey-journal-test.sh index d7db293a9d3..db642eab5ca 100755 --- a/test/shell.d/luks-rekey-journal-test.sh +++ b/test/shell.d/luks-rekey-journal-test.sh @@ -89,7 +89,7 @@ sed -n '/^PROVISIONING_UNLOCK_FILES=(/,/^)/p; /^UNLOCK_OWNER=/p; /^limine_auto_u grep -q '^luks_auto_unlock_drop() {' "$tmp/unlock.sh" && grep -q '^limine_auto_unlock_drop() {' "$tmp/unlock.sh" && grep -q '^luks_record_slots() {' "$tmp/unlock.sh" || fail "omarchy-provision-owner defines the dispatched and Limine auto-unlock callbacks and the slot record" -sed -n '/^rekey_luks() {/,/^}/p; /^run_provisioning() {/,/^}/p; /^cleanup_oem_state() {/,/^}/p +sed -n '/^ANSWERS_FILE=/p; /^declare -A answers=/p; /^load_answers() {/,/^}/p; /^rekey_luks() {/,/^}/p; /^run_provisioning() {/,/^}/p; /^cleanup_oem_state() {/,/^}/p /^platform_ready() {/,/^}/p; /^run_setup() {/,/^}/p /^rekey_accepts_password() {/,/^}/p' \ "$ROOT/bin/omarchy-provision-owner" | sed "s|/etc/|$tmp/etc/|g" >"$tmp/provision.sh" diff --git a/test/shell.d/provision-owner-answers-test.sh b/test/shell.d/provision-owner-answers-test.sh new file mode 100755 index 00000000000..5abeb0202a0 --- /dev/null +++ b/test/shell.d/provision-owner-answers-test.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +set -euo pipefail + +source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" + +# First-boot setup with answers the platform staged before the boot, as a +# Raspberry Pi stages the settings Raspberry Pi Imager wrote: the form skips the +# fields they cover, checks each the way its prompt would, and asks for the +# rest. load_answers, keyboard_form, user_form and staged_username_valid run as +# omarchy-provision-owner defines them, with the prompts faked. + +tmp=$(cd -- "$(mktemp -d)" && pwd -P) +trap 'rm -rf "$tmp"' EXIT + +sed -n '/^load_answers() {/,/^}/p; /^keyboard_form() {/,/^}/p; /^user_form() {/,/^}/p; /^staged_username_valid() {/,/^}/p' \ + "$ROOT/bin/omarchy-provision-owner" >"$tmp/form-steps.sh" +for fn in load_answers keyboard_form user_form staged_username_valid; do + grep -q "^$fn() {" "$tmp/form-steps.sh" || fail "omarchy-provision-owner defines $fn" +done + +cat >"$tmp/form.sh" <>"\$LOG_FILE"; } +confirm_reboot() { exit 5; } +encrypted_install() { [[ -e $tmp/encrypted ]]; } +omarchy_username_taken() { [[ \$1 == "taken" ]]; } +apply_keyboard() { [[ \$1 != "broken" ]]; } +asked() { echo "\$1" >>"$tmp/asked"; } +omarchy_prompt_keyboard() { asked keyboard; keyboard=us keyboard_label="English (US)"; } +omarchy_prompt_username() { asked username; username=typed; } +omarchy_prompt_password() { asked password; password=typed-password; } +omarchy_prompt_identity() { asked identity; full_name="" email_address=""; } +omarchy_prompt_hostname() { asked hostname; hostname=omarchy; } +omarchy_prompt_timezone() { asked timezone; timezone=UTC; } +load_answers +keyboard_form +user_form +declare -p keyboard keyboard_label username password password_hash hostname timezone >"$tmp/result" +SH + +run_form() { + rm -rf "$tmp/provisioning" "$tmp/asked" "$tmp/result" "$tmp/log" "$tmp/encrypted" + mkdir -p "$tmp/provisioning" + : >"$tmp/asked" + if (($#)); then + printf '%s\n' "$@" >"$tmp/provisioning/answers" + fi + bash "$tmp/form.sh" || fail "the form finishes" "$(cat "$tmp/log" 2>/dev/null)" +} + +asked_list() { + paste -sd' ' "$tmp/asked" +} + +value_of() { + (source "$tmp/result" && printf '%s' "${!1}") +} + +hash='$y$j9T$abcdefghijklmnop$0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef' + +run_form +[[ $(asked_list) == "keyboard username password identity hostname timezone" ]] || + fail "with no answers every field is asked" "$(asked_list)" +pass "with no staged answers the form asks every field" + +run_form "keymap=de" "username=alice" "password_hash=$hash" "hostname=pi-desk" "timezone=Europe/Berlin" +[[ $(asked_list) == "identity" ]] || fail "only what Imager has no field for is asked" "$(asked_list)" +[[ $(value_of keyboard) == "de" && $(value_of keyboard_label) == "German" ]] || + fail "the staged keymap is applied with the form's label" "$(cat "$tmp/result")" +[[ $(value_of username) == "alice" && $(value_of hostname) == "pi-desk" && $(value_of timezone) == "Europe/Berlin" ]] || + fail "the staged account, hostname and timezone are kept" "$(cat "$tmp/result")" +[[ $(value_of password_hash) == "$hash" && -z $(value_of password) ]] || + fail "the staged password stays a hash" "$(cat "$tmp/result")" +pass "a complete set of answers leaves only the name and email to ask" + +run_form "keymap=broken" "username=taken" "password_hash=plaintext" "hostname=-bad-" "timezone=Not/AZone" "shell=/bin/zsh" +[[ $(asked_list) == "keyboard username password identity hostname timezone" ]] || + fail "answers the prompts would reject are asked again" "$(asked_list)" +grep -q "staged keymap broken did not take" "$tmp/log" || fail "a keymap that did not take is logged" "$(cat "$tmp/log")" +[[ -z $(value_of password_hash) && $(value_of password) == "typed-password" ]] || + fail "a password that is not a crypt hash is never stored" "$(cat "$tmp/result")" +pass "an answer the form would reject is asked for instead" + +rm -f "$tmp/encrypted" +mkdir -p "$tmp/provisioning" +printf '%s\n' "username=alice" "password_hash=$hash" >"$tmp/provisioning/answers" +touch "$tmp/encrypted" +: >"$tmp/asked" +bash "$tmp/form.sh" || fail "the encrypted form finishes" +grep -qx password "$tmp/asked" || fail "an encrypted install asks for the password the disk takes" "$(asked_list)" +[[ -z $(value_of password_hash) ]] || fail "an encrypted install ignores the staged hash" "$(cat "$tmp/result")" +pass "an encrypted install asks for its password despite a staged hash" diff --git a/test/shell.d/provision-owner-keyboard-test.sh b/test/shell.d/provision-owner-keyboard-test.sh index 04f1397f42e..c122c6a48fb 100644 --- a/test/shell.d/provision-owner-keyboard-test.sh +++ b/test/shell.d/provision-owner-keyboard-test.sh @@ -81,6 +81,7 @@ cat >"$tmp/form.sh" <>"$tmp/screen"; } notice() { echo "notice: \$1" >>"$tmp/screen"; }