Skip to content
Draft
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
32 changes: 19 additions & 13 deletions bin/omarchy-hw-platform
Original file line number Diff line number Diff line change
@@ -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,<model>" from Asahi's m1n1 on every
# Apple Silicon Mac, "qcom,<soc>" on Snapdragon laptops. Each compatible string
# is a NUL-separated list, and only a token's vendor prefix counts.
# Apple Silicon Mac, "qcom,<soc>" on Snapdragon laptops, "raspberrypi,<model>"
# 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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions bin/omarchy-lifecycle-dispatch
Original file line number Diff line number Diff line change
Expand Up @@ -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
;;
Expand Down
4 changes: 2 additions & 2 deletions bin/omarchy-pkg-defaults
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
96 changes: 87 additions & 9 deletions bin/omarchy-provision-owner
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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=$?
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1201,6 +1275,7 @@ run_setup() {
local status

platform_ready || return 1
load_answers

while true; do
keyboard_form || return 1
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bin/omarchy-update-keyring
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/file-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,15 +371,15 @@ 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=<omarchy-hw-platform value>`, 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.

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-<platform>.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-<platform>.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`)

Expand Down
8 changes: 6 additions & 2 deletions docs/lifecycle-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`, 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: <operation> on <platform> needs <package>, which provides <path>; 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: <operation> on <platform> needs <path>, which <package> <version> does not provide; update <package>` | 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: <operation> 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: <operation> runs as the user, never as root` | same error |
| The entrypoint fails the trust rules | exit 1: `Error: refusing <path>: ...` (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 |
Expand Down Expand Up @@ -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 |
| `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 `<implementation directory>/<operation>`. 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.
Expand Down
4 changes: 2 additions & 2 deletions install/helpers/image-target.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading