#!/bin/bash -p # omarchy:summary=Run a fixed lifecycle operation through the platform's own packages # omarchy:args=[--resolve] [arguments...] # omarchy:requires-sudo=true # omarchy:hidden=true # Provisioning, factory reset, update, boot, system and user setup and app # installs call a fixed set of operations here. A platform that needs its own # handling registers directories of root-owned entrypoints below; on every other # platform each operation is a no-op and the caller keeps its generic path. The # contract is docs/lifecycle-dispatch.md. # # --resolve prints the entrypoint an operation would run, or nothing when it is # a no-op here, so a caller can choose between a platform implementation and # its own generic path. # # Root runs this from provisioning, factory reset and update, and it decides # what runs as root. -p keeps a root caller's environment from running code in # this shell before the first line (bash then ignores BASH_ENV and exported # functions), so a launch without it is refused. if [[ $- != *p* ]]; then echo "Refusing an unsafe Bash startup." >&2 exit 126 fi set -euo pipefail operations=( provision-prepare provision-commit provision-verify reset-prepare reset-verify reset-commit reset-rollback update-verify update-takeover luks-slots setup-boot setup-system setup-user post-install pre-remove ) # Run as the user, never as root, with that user's session. user_operations=(setup-user post-install pre-remove) usage() { echo "Usage: omarchy-lifecycle-dispatch [--resolve] [arguments...]" >&2 echo "Operations: ${operations[*]}" >&2 exit 2 } listed() { local item=$1 candidate shift for candidate in "$@"; do [[ $candidate == "$item" ]] && return 0 done return 1 } resolve=0 if [[ ${1:-} == "--resolve" ]]; then resolve=1 shift fi operation=${1:-} [[ -n $operation ]] || usage shift listed "$operation" "${operations[@]}" || usage # A privileged caller must not let its environment choose what runs as root: # root resolves only the fixed paths below, and the detector beside this file # ignores its fixture roots. Fixture roots exist for unprivileged tests. fixed_path=/usr/local/sbin:/usr/local/bin:/usr/bin detect=("$(PATH=$fixed_path dirname -- "${BASH_SOURCE[0]}")/omarchy-hw-platform") if (( EUID == 0 )); then export PATH=$fixed_path detect=(/usr/bin/env -i PATH="$fixed_path" "${detect[@]}") root="" else root=${OMARCHY_LIFECYCLE_ROOT:-} root=${root%/} if [[ -n $root && $root != /* ]]; then echo "Error: OMARCHY_LIFECYCLE_ROOT must be an absolute path" >&2 exit 1 fi fi if ! platform=$("${detect[@]}"); then echo "Error: cannot determine the hardware platform for $operation" >&2 exit 1 fi # x86 and plain aarch64 machines (Snapdragon laptops among them) 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 # checks what an update left in its boot chain, its boot checks prove the kept # LUKS slots that luks-slots records, and hardware setup has no other way to run # the Mac's boot setup (setup-boot), so those operations are required. So is # update-takeover: the Mac boot chain and its image own files no package does, # and a Mac whose boot package can't vouch for a takeover keeps them. Setup and # the app-install hooks are optional: a Mac without omarchy-mac keeps Omarchy's # generic setup and installs. Moving an operation to required also changes # apple_optional in test/shell.d/lifecycle-dispatch-test.sh and the Apple column # and registration table in docs/lifecycle-dispatch.md. case $platform:$operation in aarch64-apple:setup-system | aarch64-apple:setup-user | aarch64-apple:post-install | aarch64-apple:pre-remove) implementation=/usr/lib/omarchy/mac package=omarchy-mac required=() ;; aarch64-apple:*) implementation=/usr/lib/omarchy/mac-boot package=omarchy-mac-boot required=( provision-prepare provision-commit provision-verify reset-prepare reset-verify reset-commit reset-rollback update-verify update-takeover luks-slots setup-boot ) ;; *) exit 0 ;; esac # Checked once the platform registers the operation, so an installer run with # sudo still works where the hook is a no-op. if listed "$operation" "${user_operations[@]}" && (( EUID == 0 )); then echo "Error: $operation runs as the user, never as root" >&2 exit 1 fi # Every path from the entrypoint up to / (or the fixture root) must be a real # file or directory owned by root, or by the unprivileged caller inside a # fixture root, and writable by nobody else. trusted() { local path=$1 top=${root:-/} owner mode [[ -f $path && -x $path ]] || return 1 while true; do [[ ! -L $path ]] || return 1 read -r owner mode < <(stat -c '%u %a' -- "$path") || return 1 if (( owner != 0 )); then [[ -n $root ]] && (( EUID != 0 && owner == EUID )) || return 1 fi (( (8#$mode & 8#022) == 0 )) || return 1 [[ $path != "$top" ]] || return 0 [[ $path != "/" ]] || return 1 path=$(dirname -- "$path") done } entrypoint=$root$implementation/$operation # The package's own pacman record tells a machine without it from one whose # copy predates the operation. installed_version() { local record for record in "$root/var/lib/pacman/local/$package"-[0-9]*/; do [[ -d $record ]] || continue record=${record%/} printf '%s\n' "${record##*/"$package"-}" return 0 done return 1 } # 3 is a machine without the platform's boot package at all, which a caller may # tell apart from a failure; 1 is everything else. if [[ ! -e $entrypoint && ! -L $entrypoint ]]; then if listed "$operation" "${required[@]}"; then if version=$(installed_version); then echo "Error: $operation on $platform needs $implementation/$operation, which $package $version does not provide; update $package" >&2 exit 1 fi echo "Error: $operation on $platform needs $package, which provides $implementation/$operation; it is not installed" >&2 exit 3 fi exit 0 fi if ! trusted "$entrypoint"; then echo "Error: refusing $implementation/$operation: it must be a root-owned executable file in root-owned directories, writable by no one else" >&2 exit 1 fi if (( resolve )); then printf '%s\n' "$entrypoint" exit 0 fi # A user operation also gets the user's home and session, when there is one, # and nothing else. environment=(PATH="$fixed_path") if listed "$operation" "${user_operations[@]}"; then for name in HOME USER XDG_CONFIG_HOME XDG_STATE_HOME XDG_RUNTIME_DIR DBUS_SESSION_BUS_ADDRESS WAYLAND_DISPLAY OMARCHY_PATH; do [[ -z ${!name:-} ]] || environment+=("$name=${!name}") done fi exec /usr/bin/env -i "${environment[@]}" "$entrypoint" "$@"