Converge omarchy-mac and omarchy-mx-mac into upstream Omarchy (#14431)

* Add omarchy-hw-platform to tell Apple Silicon and Qualcomm machines apart

aarch64 covers Apple Silicon Macs, Snapdragon laptops, Raspberry Pis and
VMs, so the CPU architecture can't pick platform setup. omarchy-hw-platform
prints apple-silicon, qualcomm, generic-aarch64 or generic from the vendor
prefixes in the device tree's root compatible (/proc/device-tree, then
/sys/firmware/devicetree/base) and the CPU, and fails when they contradict
each other. Its qualcomm answer uses the same evidence as the dragon
branch's omarchy-hw-qualcomm-soc, and the fixtures check the two agree.
omarchy-hw-apple-silicon is the Apple predicate on top of it.

An image built away from its hardware names its target in a root-owned
manifest, /var/lib/omarchy/image/target (format=1, platform=). While the
root is being built rather than booted, the detector answers from it and
never reads the build host's device tree; a booted system always answers
from its hardware. As root it restarts in an empty environment, so no
variable picks the platform for privileged setup.

Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>

* Reject platform gates keyed only on the CPU architecture

An architecture check is right for an ABI, a binary or a repository's
$arch, not for picking a platform. The lint lists each shipped check after
review and fails on a new one, so platform setup goes through
omarchy-hw-platform.

* Compose default packages from base, architecture and platform lists

omarchy-reinstall-pkgs installed the base list on every machine. A
platform's default set is now the base list, then install/omarchy-aarch64.packages
on aarch64, then the platform's own list when it has one
(install/omarchy-qualcomm.packages adds linux-firmware-qcom).
omarchy-pkg-defaults prints the set for the detected platform, or for the
one named, and reinstall installs it. x86_64 still gets exactly the base
list, and an image build gets its target's set.

* Early-load thunderbolt only where the kernel builds the module

linux-aarch64 has no thunderbolt module, and mkinitcpio fails the whole
image over a MODULES entry it cannot find. The optional form, thunderbolt?,
is not enough: a systemd initramfs copies MODULES as written into
modules-load.d, where "thunderbolt?" names no module, and a kernel without
the module would still list it there. thunderbolt_module.conf now adds plain
thunderbolt only when modinfo finds it as a module for the kernel being
built.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Compose mkinitcpio HOOKS from an early baseline drop-in

omarchy_hooks.conf assigned HOOKS outright, and mkinitcpio sources drop-ins in
name order, so it ran after any numbered drop-in (90-*.conf) and threw its
hooks away. HOOKS now comes from 00-omarchy-hooks.conf, which sorts first, and
later drop-ins add to it. The NVIDIA kms filter and the vconsole handling stay
in omarchy_hooks.conf. With the drop-ins Omarchy writes, machines keep the
same HOOKS, MODULES and FILES; a user drop-in that sets HOOKS and sorts before
omarchy_hooks.conf now takes effect instead of being overwritten.

Migration 1786605598 now sources the baseline first, since without it the
migration would read empty HOOKS and also rebuild hybrid systems. It skips a
machine that lacks the baseline.

* Resolve the LUKS parent from raw lsblk output

lsblk -s prints the ancestor tree with box-drawing prefixes ("└─/dev/nvme0n1p6"), so the crypto_LUKS parent came back as a path that does not exist and first-boot setup stopped with "Could not locate the LUKS device to re-key". -r prints raw names. Factory reset uses the same lookup.

* Check the factory-reset passphrase against keyslots only

cryptsetup open tries enrolled tokens before the key file, so a TPM2 or
FIDO2 token could authorize the re-key with a wrong passphrase.

* Test LUKS parent lookup during owner setup and factory reset

Exercise raw ancestry output without invoking provisioning or reset. Cover btrfs subvolume roots, absent encryption, failed probes, and explicit cryptdevice specifications.

* Cover LUKS parent lookup through an LVM volume group

* Journal the first-boot LUKS re-key so an interrupted attempt resumes

A first boot killed after the staged install key's slot was retired left the
key file behind, and every retry then stopped at "the staged LUKS key no
longer unlocks". A lost key file with the boot-time auto-unlock still
configured let setup finish with the disk unlocking unattended.

The re-key now runs from install/provisioning/luks-rekey.sh and records its
phase and slot numbers (never key material) in
/var/lib/omarchy/provisioning/luks-rekey.state. It reuses the owner's slot
instead of adding a duplicate, retires LUKS1 slots too, and destroys the
staged key only after the volume proves it opens nothing. Setup refuses to
finish while any part of the provisioning unlock remains. The journal ends at
phase=done and leaves together with pending, so a retry after the staged key
is retired must use the password the disk already holds.

Key checks exclude LUKS2 tokens: cryptsetup open tries an enrolled token
(say a TPM2 slot a previous owner left behind) before the key and reports its
slot whatever key it was given. Slot listings read only the keyslot section,
since a luks2-keyring token looks like a keyslot in luksDump.

* Change the root volume's LUKS key before the login and root passwords

Installation gives the login user, root and the disk holding / one password,
but omarchy-drive-password changed only the LUKS key, so the three drifted
apart. For the disk holding / it now checks the current password opens the
disk, changes the LUKS key, confirms the new key opens the disk and the old
one no longer does, and only then sets the login and root passwords. Any other
encrypted drive changes its LUKS key alone. Every drive now asks for the
current password with gum instead of cryptsetup's prompt.

A journal in ~/.local/state/omarchy/drive-password.state (disk UUID, key slots
and phase, never a password, written with the re-key journal helpers) lets the
next run finish an interrupted change. The rerun accepts only the old key
(removing the slot the interrupted change added) or the new one (retiring the
old slot), so another key such as a recovery passphrase never becomes the login
password. luksChangeKey is judged by whether the new key opens the disk, not
by its exit status, and key checks exclude LUKS2 tokens, which cryptsetup
open would otherwise accept in place of any password.

Runs are serialised, cryptsetup calls hold a disk lock, a / whose LUKS
partition cannot be resolved fails closed, and passwords are never traced. A
new password equal to the current one is refused, and --pbkdf argon2id is
passed only to LUKS2, so LUKS1 drives can change their password again.

* Add omarchy-lifecycle-dispatch for platform boot operations

Provisioning, factory reset and update call a fixed set of operations
through it. A platform whose boot chain the generic Limine path can't drive
registers a directory of root-owned entrypoints in the dispatcher; every
other platform gets a no-op and keeps its generic path. Apple Silicon is
registered to omarchy-mac-boot's /usr/lib/omarchy/mac-boot, with its
required operations failing clearly when the package or an entrypoint is
missing. docs/lifecycle-dispatch.md is the contract, including how a
Qualcomm boot package would plug in.

* Let the platform's boot package own first-boot setup's boot chain and key slots

Owner provisioning asks provision-prepare before the owner form, hands the
staged unlock to provision-commit and provision-verify where the platform
implements both, and records the kept LUKS slot through luks-slots before
the staged key is destroyed; the disk password change records the owner's
new slot the same way. On x86, Snapdragon and generic aarch64 every call is
a no-op and first boot is unchanged.

* Reset through the platform's boot package where it owns the boot chain

Where a platform implements the reset operations, reset-prepare and
reset-verify stage and prove the factory root's boot files, the throwaway
slot is added only after that, and reset-commit gets the key on standard
input once the factory root is active. A failure before the switch rolls the
boot state back and revokes the slot. Elsewhere the Limine UKI path is
unchanged.

* Check the platform's boot chain after an update

omarchy-update-boot runs update-verify after AUR packages, the last
sudo-capable step, so it also covers the initramfs rebuilds they trigger.
It follows third-party build code, so like AUR it authenticates through the
no-update wrapper. A failed verification finishes the update's remaining
steps, then exits non-zero without offering the reboot. Where the platform
does not implement it nothing runs and nothing asks for root.

* Hand a machine to its platform's migration through lifecycle dispatch

migrations/1790347292.sh resolves the new optional migrate operation and,
where the platform's boot package implements it, runs it as root. The
entrypoint moves the machine onto the platform's official package set,
idempotently and resumably; its failure leaves the migration pending. Once
it succeeds, a root-owned marker spares other accounts the migration and its
root prompt. Everywhere else, Macs without the boot package included, the
migration is a no-op, and an undetermined platform keeps it pending.

* Refuse packages tagged for another platform in a pacman hook

A platform package names its platform with groups=('omarchy-platform-<name>'),
<name> being what omarchy-hw-platform prints. omarchy-settings ships a
pre-transaction hook, the guard it runs and a copy of the detector, so the
guard is resident before the runtime and any platform package. The guard
reads tags from the synced databases and refuses a transaction that installs
a package tagged for another platform; a transaction without a tagged
package passes without asking which machine this is. Image builds answer
from the image's root-owned target manifest, as the detector does.

The guard ships on aarch64 only: no platform package is built for x86_64,
so x86 transactions run no hook. Hardware setup starts by proving the guard
is resident and checking the packages an installer placed without it. On
generic aarch64, where no platform packages exist, a missing guard is
logged, not fatal, and on x86 it is expected.
docs/platform-guard.md is the contract.

Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>

* Enable Voxtype's Vulkan backend only when it is installed

omarchy-hw-vulkan reports a Vulkan runtime, not whether this voxtype build
has a Vulkan backend. The aarch64 voxtype-bin package ships only the CPU and
ONNX binaries, so on any aarch64 machine with a Vulkan driver setup ran
`voxtype setup gpu --enable` and printed "Vulkan backend not installed" in
the middle of an otherwise working install. Require the backend binary too.

x86_64 voxtype-bin always ships /usr/lib/voxtype/voxtype-vulkan, so x86
installs still switch to the GPU backend as before.

Ported from omacom/omarchy-mac#483.

Co-authored-by: Oliver Lukschander <33756270+oliverlukschander@users.noreply.github.com>

* Link the system Widevine CDM into Brave

The widevine package registers its CDM for Chromium and Firefox only.
Brave looks for the CDM in its own install directory, finds nothing, and
DRM sites (Netflix, Apple TV+, Spotify) fail with a blank player and no
useful error. This hits machines where Widevine comes from that package,
such as aarch64 ones, since no Widevine component is published for them.

Link /opt/WidevineCdm/chromium into /opt/brave-bin and /opt/brave-origin-bin
through as_root when Brave or Brave Origin is installed, and add a migration
that repairs existing installs. Both do nothing when the widevine package is
absent or the browser already has a CDM. A standard x86 install has no
widevine package, so nothing changes there.

Ported from omacom/omarchy-mac#474, extended to Brave Origin.

Co-authored-by: Caleb Marshall <68087341+cajomar@users.noreply.github.com>

* Report failed setup steps on the console

With file logging, run_logged sends a failing leaf's output and its
"Failed:" marker to the install log only, so the caller sees setup stop
with an exit status and nothing else. A missing command (exit 127) was the
case that made this hard to diagnose.

Print the failed script, its exit status and the log path on stderr. Stdout
logging mode already shows the failure marker and is unchanged, and
successful steps stay quiet.

Ported from omacom/omarchy-mac#389 (the logging part).

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Let a migration defer itself without stopping the update

A migration that exits 75 changed nothing and cannot finish yet: omarchy-migrate
leaves it pending with its login notification, runs the migrations after it and
exits 0, so omarchy update carries on. Any other failure still stops the queue.
The platform migration passes a deferred refusal of its entrypoint through, and
defers itself where the dispatcher can't resolve its entrypoint (the platform
can't be told, or the entrypoint isn't trusted): nothing has run then, so the
update goes on to its boot verification, which fails there, as documented.

* Keep Apple Silicon's systemd initramfs on the HOOKS baseline

00-omarchy-hooks.conf set the busybox line on every machine. On a Mac,
omarchy-mac-boot's drop-ins then add the Asahi firmware hooks, but its
encryption drop-in reads busybox encrypt as a Mac unlocked through
cryptdevice= and leaves the line alone. The image came out as
"... block encrypt asahi omarchy-vendorfw filesystems ...", with no
systemd, sd-encrypt or omarchy-mac-encrypt, so an encrypted Mac could not
unlock its disk. Reproduced against an M1 Pro's installed drop-ins.

The baseline now asks omarchy-hw-platform. Apple Silicon starts from the
systemd line its boot package builds on. A Mac whose own mkinitcpio.conf
carries the asahi hook, or busybox encrypt, keeps that line, as a legacy
install needs. x86, Snapdragon and generic aarch64 keep today's busybox
line, as does a machine with no detector. The detector is the runtime's,
or else the copy omarchy-settings ships for its platform guard, so a
settings package newer than the runtime still places a Mac. A detector
that cannot place the machine stops the build.

The HOOKS test now composes the baseline on each platform and on legacy
Mac lines. A new test composes it with omarchy-mac-boot 20260926-1.45's
drop-ins, kept as fixtures, and checks the exact HOOKS the M1 Pro builds.
The NVIDIA test pins its platform.

* Hide optional installs that have no package on this machine

Not every optional install exists on every architecture: some vendors ship no
aarch64 build, and some repositories don't carry a package at all. Each
Install row now carries a `when:` guard naming every package its install
command adds, checked with omarchy-pkg-available, so a row with nothing to
install disappears instead of failing halfway through a pacman transaction.
The rows that build from the AUR (Chrome, Edge, Brave, Brave Origin, Zen,
NordVPN) are guarded on the CPU architectures their vendors publish for.

omarchy-pkg-available asks the sync databases: one `pacman -Slq` per batch,
and `pacman -Sp` for a name that list doesn't hold, so a provided name
(libappindicator-gtk3) and a version constraint resolve the way `pacman -S`
will. The menu's guard prelude sources the same function, so the whole batch
still costs one fork. omarchy-reinstall-pkgs reports and skips a default
that no configured repository offers instead of failing the transaction.

On x86_64 every guarded row resolves against the stable and edge
repositories, so the menu is unchanged there.

Tests derive each row's packages from its install command on x86_64 and
aarch64 and fail when the guard names different ones.

Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>

* Run 1Password with software GL where there is no render GPU

Electron draws a blank window when the machine has no DRM render node: a VM,
a headless or passthrough GPU, or a GPU whose driver isn't up yet (Apple M3
and later on Linux today). omarchy-hw-render-gpu tells whether a renderD node
exists, and omarchy-cmd-electron-gl-args prints --ozone-platform=wayland
--disable-gpu only when it doesn't.

omarchy-cmd-electron-gl-wrap installs a marked PATH wrapper in /usr/local/bin
that execs the real binary with those flags, so it follows the machine rather
than the moment it was installed. It refuses to replace a launcher it didn't
write, or to wrap a binary over itself, and stages the write so a failure
leaves the old launcher in place. omarchy-cmd-desktop-exec-repair points the
user's desktop entry at the wrapper by replacing only the executable token,
keeping arguments, other actions and customizations, with a backup.

The 1Password installer uses them when the machine has no render node. With
one, which is every x86 machine with a working GPU, nothing changes.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Record with wf-recorder when gpu-screen-recorder can't start

gpu-screen-recorder knows NVIDIA, AMD, Intel and Broadcom GPUs and aborts
during GL init on anything else, such as Apple Silicon's, before a recording
starts. Screen recording now falls back to wf-recorder, when it is installed,
if gpu-screen-recorder is missing or exits before the file appears. The
picker, file naming, notifications and post-processing are shared. wf-recorder
records one audio device, so desktop plus microphone go through a temporary
null sink that stop unloads, and its track is pinned to 48 kHz. Portal
capture and --resolution stay gpu-screen-recorder only.

The finalize pass keeps 48 kHz too: loudnorm declares 192 kHz output, so
without -ar the AAC encoder wrote 96 kHz from a 48 kHz recording, which many
editors decode as silence, and which on Apple Silicon makes the speaker
safety daemon lock the amps. #11092 makes the same one-line change; whichever
lands second drops it.

Stop and status matched `pgrep -f ^gpu-screen-recorder`, which misses
wf-recorder and matches any command line that starts with that path. The
script now records the recorder's pid, and the new
omarchy-capture-screenrecording-process checks or signals it by its
executable, through a pidfd opened before the check so a reused pid can't be
signalled. When that pid is no longer a recorder (a recording started before
this change, or one that died), every recorder the user runs counts, as it
does for the bar indicator and the Stop row, which use the same helper.

Where gpu-screen-recorder works, which is every x86 GPU it supports today, it
records as before.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Issac Trotts <15530+ijt@users.noreply.github.com>

* Hold logind's lid switch while a display it doesn't count is in use

systemd-logind treats a closed lid as docked only when an external display
is on a connector type it lists (HDMI, DisplayPort, DVI, VGA...). Displays on
"USB" connectors, which Apple Silicon uses for USB-C displays, aren't on that
list, and a missing comma fuses DVI-A and Composite into one entry. With such
a display in use, closing the lid suspended a laptop that was still being
used on it.

omarchy-system-lid-inhibit holds a handle-lid-switch inhibitor while one of
those connectors is connected and enabled in Hyprland, on a laptop where
HandleLidSwitchDocked is ignore, which is exactly the case logind would have
ignored had it counted the display. It releases the inhibitor when the display
goes. A compositor or logind that doesn't answer changes nothing, since
releasing then would suspend a closed lid. The inhibitor runs as a transient
user unit bound to the monitor watcher's unit, so a watcher that dies can't
leave the lid inhibited until logout.

The monitor watcher runs it alongside the clamshell sync. On a machine whose
external displays are all on connectors logind counts, it does nothing.

* Keep the focused workspace in front when a monitor goes away

When a monitor is unplugged, or the lid turns off the laptop panel, Hyprland
first warps focus to whichever monitor it lists first and only then moves the
removed monitor's workspaces over. The workspace you were on lands hidden
behind that monitor's own (Hyprland 0.56.2, CMonitor::onDisconnect).

monitor.focused fires before Hyprland records the new focus, so the monitor
still reported as active is the one being left: its workspace is remembered
until the event loop next turns, and a monitor.removed in that window focuses
it again once the removal has finished. An open special workspace already
comes along, and any focus or workspace change made before the restore runs
wins.

* Drive the keyboard backlight from the ambient light sensor

Laptops with an ambient light sensor and a keyboard backlight LED now light
the keys in the dark and dim them as the room brightens, inverse to display
auto-brightness. omarchy-brightness-keyboard-auto polls the sensor every five
seconds (preferring an IIO device that names itself ALS, and converting a raw
channel with its offset and scale) and maps lux onto the LED, with a deadband
so it doesn't chase noise. It pauses while the screen is locked or the lid is
closed.

A level set with the keyboard-brightness keys wins until the light changes
enough that it no longer fits. omarchy-brightness-keyboard records each level
it sets in the session's runtime directory (a private state directory without
one), so the loop can tell keys turned off on purpose from keys left off by
the lock screen or a restored zero, which it lights again.

The user unit runs it in the graphical session. Its ExecCondition asks
--available, so it is a no-op on machines without both a sensor and a
keyboard LED. First run enables it, and a migration enables it for existing
installs, writing the enable symlink itself when there is no live user
manager.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Gabe Ohlson <3355909+6abe@users.noreply.github.com>

* Defer an image build's hardware setup to the machine's first boot

An image is built away from the machine it will run on, so its hardware
leaves would probe the builder. While the image manifest the detector
already reads (/var/lib/omarchy/image/target) exists, omarchy-apply-hardware
queues every leaf of install/hardware/all.sh instead of running it and arms
omarchy-provision-hardware.service. On the first boot, before owner setup
and the login screen, omarchy-provision-hardware retires the manifest and
runs the queue on the real hardware: a failed step stays queued for the
next boot (exit 75), and the initramfs is rebuilt once after the last step
when a step changed its inputs or asked for it. Bluetooth starts its
service on that boot, whose target is already up. Root ignores any
environment override of these paths. Without a manifest, as on every
ISO install, hardware setup runs exactly as before. Nothing here fetches
package databases: an image carries its sync databases and every package
its steps install.

* Give an image-built root a UTF-8 locale when LANG is still C

A root built from a distribution tarball instead of the ISO (Arch Linux
ARM's ships LANG=C) never went through the ISO's locale step, so it ran
non-UTF-8. The config phase now selects en_US.UTF-8, generating it if
needed, but only when LANG is unset, C or POSIX: any named locale is
somebody's choice and stays. The ISO writes the locale before this runs,
so an ISO install is unchanged.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Let a platform's packages run their own boot, system and user setup

Three dispatch operations give a platform one place for the setup it
needs beyond Omarchy's, instead of inline platform branches in the setup
leaves. The last hardware leaf runs setup-boot, then setup-system, as
root, so an image build queues them with the others and its first boot
runs them, with image-first-boot (offline-safe, asking for the one boot
rebuild instead of building). setup-boot belongs to the boot package and
is required on Apple Silicon, where hardware setup has no other way to
run the Mac's boot setup; a failed setup-boot stops the leaf before
setup-system. setup-user runs as the user being set up, refuses root,
and gets only the user's home and session variables: from the last user
leaf at finalization (skipped inside an image build, where the detector
would read the build host) and again from first run once the session is
up. On Apple Silicon setup-boot resolves in omarchy-mac-boot's directory
and the other two, both optional, in omarchy-mac's, /usr/lib/omarchy/mac;
x86, generic aarch64 and Qualcomm register none, so all three are no-ops
there.

A failed finalization now keeps first run pending, like any failed
first-run step, instead of letting the other steps mark it complete.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Find batteries, lids and panels that aren't named like a PC's

Laptop detection assumed PC names: a battery called BAT*, a lid in /proc/acpi, a
touchpad or trackpad in the device name, a GPU backlight. Apple Silicon has
none of them (macsmc-battery, the SMC lid switch, apple-mtp-multi-touch,
apple-panel-bl), and some Framework and Chromebook firmware calls its battery
CMB0, so those machines lost the battery panel, laptop-only services, lid
handling, the touchpad toggle and display brightness.

- Battery presence goes by type instead of name, and skips a battery scoped
  Device (a wireless mouse or keyboard), so a desktop with one isn't a laptop.
- Battery status takes the first UPower battery that powers the machine, reads
  thresholds and cycles from it (BAT* stays the fallback), shows the magnitude
  of a signed power_now, and rounds the percentage half-up for display like the
  bar, while the charge-hold check keeps the unrounded value.
- A laptop is also one with any input device that reports a lid switch
  (SW_LID), whatever its name: Apple's SMC input device has the same name on a
  Mac mini, where it reports none. Whether the lid is closed comes from ACPI
  where there is an ACPI lid, as before; otherwise logind is asked, with a
  short timeout, only when a device reports a lid switch.
- A device named touchpad or trackpad still wins; only when there is none does
  the touchpad match take a mouse udev marks as a touchpad (Apple's
  multi-touch trackpad), never a touchscreen.
- The platform package can describe its displays in
  /usr/share/omarchy-platform/displays.conf: backlights that never drive the
  panel (a Touch Bar's), the panel's own where there is no GPU backlight, and
  a display driver without DDC channels, where a probe walks unrelated I2C
  buses for about 3 s: there only a connector with a ddc node is probed, and
  a monitor without one reports no brightness instead of dimming the built-in
  panel. Omarchy ships no such file and names no device in it; without it the
  backlight choice and DDC probing are unchanged.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>
Co-authored-by: Sparsh Kumar <22640187+skkumarsparsh@users.noreply.github.com>
Co-authored-by: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com>

* Keep the user's XDG config and state directories in platform user setup

setup-user runs with an allowlisted environment, which dropped
XDG_CONFIG_HOME and XDG_STATE_HOME. A platform's user setup reads the user's
configuration and keeps its setup markers there, so for a user who sets
either it looked in, and wrote to, the default directories instead. Both now
pass through with the rest of the session; XDG_DATA_HOME and everything else
still don't.

* Let a platform package add Hyprland defaults of its own

A platform's runtime package may need Hyprland defaults of its own (a Mac's
capture keys and three-finger workspace swipe, say), and Omarchy's config
should carry no branch for them. Three directories under the platform root,
/usr/share/omarchy-platform, now take them. The one installed platform package
owns that root, Omarchy reads it by literal path and ships nothing there, so
without a platform package nothing changes. Each file is loaded by its path,
so it can't shadow a module, and one that fails is logged and shown as a
notification while the rest of the config loads.

- hypr/defaults/*.lua loads before Omarchy's defaults. A
  chord bound there replaces Omarchy's default for it, and a decorator it adds
  runs before every later bind, so it can bind what has to run first. It sees
  the command a bind runs, including the command a shell shortcut such as
  { menu = "root" } stands for when it binds the shell's global shortcut.
- hypr/settings/*.lua loads after Omarchy's defaults and
  before the theme, for settings that replace Omarchy's.
- hypr/gestures/*.lua loads after the user's files, only for what must
  see them: the gesture registry, which now records on every platform, lets a
  default gesture step aside for the user's own.

The user's files load after the first two and can unbind or rebind any of it.

* Let a platform package name keys in the keybindings menu

The keybindings menu shows a key by its keysym, which on some keyboards is not
what the key says: on a MacBook, XF86MonBrightnessUp and Down are the F2 and
F1 keys, so the menu listed rows like "SHIFT + XF86MonBrightnessUp". A
platform package can now list names for such keys in /usr/share/omarchy-
platform/key-names, one "<keysym> <name>" per line, and the menu shows those
instead. The file is part of the menu cache's key, so installing or changing
it refreshes the menu. Without it the menu is as before.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: kx0101 <90087463+kx0101@users.noreply.github.com>

* Keep the top bar out of a panel's camera cutout

A laptop panel with a camera notch at its top (Apple Silicon MacBooks, for
one) covered the middle of the top bar, and a bar shorter than the notch left
a sliver of every window beside the camera. Omarchy knows no panel's cutout
itself: the platform's own package describes them in
/usr/share/omarchy-platform/display-cutouts.json, by
connector, physical mode and the rows the cutout covers. On a described panel
a top bar is never shorter than the cutout, its center section sits beside
the right one, and hiding it parks all of it; [bar] notch-height sets that
floor by hand, on that panel only. Without a description, which is every
machine without such a package, the bar is as before.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Keep PC and Intel Mac quirks off Apple Silicon

A few hardware steps and commands written for PCs and Intel Macs do the wrong
thing on Apple Silicon:

- The Intel Mac Wi-Fi quirk turns off the Broadcom firmware supplicant for
  every Mac whose DMI vendor is Apple and whose chip is on its list. Apple
  Silicon reports Apple Inc. through U-Boot's DMI tables, and the M1 Pro and
  Max's BCM4387 is on the list, but there the firmware supplicant is the one
  that works: turning it off makes firmware commands time out and breaks
  scanning. The install step and its migration now skip Apple Silicon.
- fnmode=2 for Apple-like keyboards is left to the Mac's own platform package
  on Apple Silicon, where the built-in keyboard's top row depends on it.
- The SPI keyboard check reads the DMI product name without failing the
  hardware pass where there is none.
- The Windows VM, an x86_64 guest under KVM, runs on x86_64 only, and direct
  boot (U-Boot finds the bootloader and makes its boot entries itself)
  refuses Apple Silicon; the menu hides both there. Hibernation setup skips
  it; its kernel has no hibernation today, and resume there is not wired.

Everywhere else each step runs as before.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Let a platform's runtime package adjust the apps it needs to

Some apps need a platform's own touch once they are installed. On Apple
Silicon, Chromium-family browsers must decode video in software (the hardware
decoder renders green frames in them), and Steam only runs through a
FEX launcher, which then depends on Steam and must go before Steam can be
removed. Until now the Mac runtime did this with inline branches in the
installers.

Two optional dispatch operations take their place. post-install <app> runs
as the user once an installer has installed the app and finished its own
setup of it: every browser install (after its flags file, before it says the
browser is installed) and Steam's (after the package, before the 32-bit
drivers and the first launch). pre-remove <app> runs before Steam's removal
drops its packages. Where a platform registers them they refuse root (a
platform with nothing registered is a no-op, so sudo installs still work
there) and get the same home and session variables as setup-user, and a
failure fails the install or stops the removal. On Apple Silicon they
resolve in omarchy-mac's directory; every other platform registers none, so
installs and removal run as before there.

Steam's 32-bit driver step also stops failing the install on a machine with no
Intel, AMD or NVIDIA GPU (Apple Silicon, but also Snapdragon or a VM): it has
nothing to add there and now says so.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Ask the platform's boot package before an update takes over its files

When an Omarchy package's upgrade stops only on files no package owns, the
update moves them aside and retries. On a Mac some of those files belong to its
boot chain or were written by its image (initramfs and pacman configuration,
say), and moving them aside can leave it unable to boot or update. The Mac
runtime refused those paths with an inline list.

update-takeover <path>... asks the platform's boot package instead, before
anything moves: read-only, exit 0 lets the takeover go ahead, anything else
keeps every file and the update fails as it did before the retry existed. The
update resolves it as the user and runs it with sudo only where it resolves,
inside the authorization the move needs anyway. On Apple Silicon it is
required: a Mac without its boot package, or with one too old to answer, keeps
its files. Every other platform registers none, so the takeover goes ahead as
before, asking no one.

* Install the Mac's package set on Apple Silicon

omarchy-pkg-defaults already composes install/omarchy-<platform>.packages
after the base and aarch64 lists, but Apple Silicon had no list, so a Mac got
the generic aarch64 set: no omarchy-mac or omarchy-mac-boot, whose entrypoints
the dispatcher requires there.

install/omarchy-apple-silicon.packages names the Mac's runtime and boot
packages, plus the defaults an owner may remove: the startup-volume tool,
hardware video decode, Widevine, and wf-recorder, since gpu-screen-recorder
can't capture on Apple Silicon and screen recording falls back to it. What a
Mac can't work without (its speaker stack, PipeWire's PulseAudio server and
realtime scheduling, the GPU's Vulkan driver) comes as omarchy-mac's
dependencies, so a new Mac driver needs no Omarchy change. Other platforms
compose exactly what they did.

Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>

* Give existing aarch64 machines the zram swap Omarchy configures

omarchy-settings configures swap on zram and tunes memory reclaim for it.
x86_64 gets zram-generator from its installer and new aarch64 installs from
their default packages, but an aarch64 machine installed before
omarchy-aarch64.packages named it has no generator, so the configuration asks
for a device nothing creates.

Migration 1790328426 installs zram-generator where the platform's default
packages name it and it is missing, and starts the zram swap wherever it is
down, unless no device is configured (then the generator makes no swap unit)
or the administrator masked it. A swap that won't start, or whose unit can't
be read, only warns: the generator's unit starts with every boot, so a reboot
retries it, and failing the migration would stop every update. A failed
install waits (75) for the next migrate, as it does where the platform can't
be told, changing nothing. It keeps the name it has in omarchy-mac, so Macs
that already ran it skip it. It also retires an /etc copy of the drop-in
identical to the packaged one, which earlier Mac setups made and which would
hide the packaged file's updates; an edited copy stays. On x86_64 the default
packages don't name the generator and there is no copy, so it does nothing.

* Keep an ARM machine's repositories when it changes channel

omarchy-refresh-pacman and install finalization copied x86_64 templates
everywhere, so on an aarch64 machine a channel change or a finished install
wrote Arch Linux's x86 [core]/[extra]/[multilib] and dropped Arch Linux ARM
and the platform's own repositories.

Channel changes now tell the platform apart. x86_64 is unchanged. On aarch64
the machine's configuration is kept, with its repositories, mirrors and
signature policies, and only the [omarchy] server moves to the new channel;
a configuration whose Omarchy server can't be identified is refused
unchanged, and one with no Omarchy repository gets it at the end (not on
Apple Silicon, whose own repository must not come first). Only pacman.conf is
backed up and rewritten, straight from this process through sudo, never
through a file another process could change first.

Before that, a channel must be qualified for the platform: a signed upgrade
and reboot tested there, not only a published repository. x86_64 keeps
stable, rc and edge; Snapdragon and other aarch64 machines take edge, as
dragon does; Apple Silicon takes none until the Mac packages qualify on edge.
The refresh then checks, as the user, that the channel's omarchy.db
publishes the runtime pair, and on Apple Silicon omarchy-mac and
omarchy-mac-boot. omarchy-channel-set refuses an unqualified channel before
anything else, the dev confirmation included.

Install finalization writes a whole template, as before on x86_64. Other
aarch64 platforms get Arch Linux ARM (dragon's pacman-aarch64.conf and
mirrorlist-aarch64, byte for byte) with its keyring, plus Omarchy on a
qualified channel. Apple Silicon gets omarchy-mac's template on a qualified
channel and keeps its image's configuration otherwise. In an image build,
finalization leaves the keyring to the machine's first boot, so machines
flashed from one image don't share a pacman master key. On an unqualified
channel, a machine that gets no Omarchy repository is told why.

Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jim Martin <1334144+jdvmi00@users.noreply.github.com>

* Keep Arch Linux ARM's signing keys current on ARM machines

omarchy-update-keyring reinstalls archlinux-keyring before every system
upgrade, since keys change without a version bump. An aarch64 machine's Arch
Linux ARM repositories are signed with keys from archlinuxarm-keyring, which
nothing refreshed, so a rotated ALARM key failed the upgrade that followed.

On an aarch64 platform where archlinuxarm-keyring is installed, it is
reinstalled in the same transaction. On x86_64 the keyring step runs exactly
as before, even where that package happens to be installed.

* Mark the runtime's platform files as deciding at runtime

The omarchy-settings recipe leaves the mkinitcpio, Limine, memory and USB
drop-ins out of its aarch64 package, because they used to assume x86_64. That
leaves an aarch64 install without the HOOKS baseline its initramfs needs, and
without the zram and reclaim settings every other Omarchy machine gets.

Each of those files now decides at runtime whether it applies: the HOOKS
baseline asks the platform, thunderbolt is added only where the kernel has the
module, the Limine files are read only where Limine is installed, the oomd
drop-ins are inert without systemd-oomd, aarch64 installs the zram generator
the zram drop-in needs, and the USB autosuspend option is ignored by a kernel
that builds usbcore in. default/settings-runtime-profile says so, file by
file; a recipe that finds it ships the same files on every architecture
(omacom/omarchy-pkgs#638), and older sources keep today's aarch64 package. A
test checks that every file it names exists and that each runtime decision is
in place.

* Set the wireless regulatory domain from the owner's timezone at first-boot setup

A deferred install (an Apple Silicon image, an OEM ISO install) runs its
hardware steps before the owner picks a timezone, on the UTC placeholder,
which names no country, so install/hardware/set-wireless-regdom.sh leaves the
regulatory domain unset and the radios stay on the world domain. Owner setup
now runs that step again once the owner's timezone is set, and applies it to
the radios through wireless-regdb's helper, since no reboot follows setup. An
up-front install, which picks the timezone before its hardware steps, is
unchanged.

* Ask again for a keyboard layout first-boot setup could not apply

First-boot setup kept going when the chosen layout did not take: loadkeys
errors were ignored, a keymap localectl did not list kept the default, and a
failed or partial write of /etc/vconsole.conf was only logged. The owner then
typed the disk password in one layout while the boot files asked for it in
another, and could only unlock the disk by typing US-style.

The keyboard step now fails closed. On a console loadkeys must succeed, the
keymap must be in localectl's list exactly (a list localectl cannot produce
counts as a failure too, logged apart), and after systemd-firstboot or the
localectl fallback, /etc/vconsole.conf is read back in a clean shell and must
name the chosen keymap with an XKB layout. Any failure is logged, the owner is
told the layout could not be set, and the layout is asked for again, so the
password form is never reached with a layout that did not take.

* Refresh a platform's own signing keyrings before an update

omarchy-update-keyring reinstalls archlinux-keyring (and Arch Linux ARM's on
aarch64) before the system upgrade, so a rotated key never fails that
upgrade's signature checks. A platform's own repository has a keyring of its
own that nothing refreshed: on Apple Silicon, asahi-alarm-keyring for
[asahi-alarm].

A platform package now lists those keyrings in
/usr/share/omarchy-platform/keyrings, one package name per line, and
omarchy-update-keyring reinstalls each one that is installed in the same
transaction. It adds no repository the machine didn't already trust. A line
that isn't one keyring package name is skipped with a warning, so a mistake in
the list can't stop every update. Without the file, as on x86 today, the step
runs as before.

* Drive an untyped default input from the audio panel

Quickshell types only exact media classes, so a virtual source (an
Audio/Source/Virtual: EasyEffects' source, or the microphone mapping a
platform package adds) has no PwNode.audio. With one as the default input the
audio panel showed the input at 0% and its slider changed nothing, the peak
meter stayed flat, the microphone widget read it as muted, and the panel could
not list it unless it was already the default.

Such an input now goes through wpctl for its volume and mute (UntypedInput in
shell/Commons, one reader and one pactl subscribe for the whole shell), the
panel meters it with the new omarchy-audio-source-level, and the input list
takes an untyped node PulseAudio lists as a source.

A platform whose audio runs through its own processing can also name nodes
that are neither devices nor apps in /usr/share/omarchy-platform/audio.json,
which Omarchy ships none of: the panel and the microphone widget leave them
out, and a "replaced" node only while its replacement exists. Without the
file nothing is hidden.

On every machine: the input list leaves out a source whose ports are all
unavailable (an empty headset jack) unless it is the default, as the output
list does for sinks; choosing an input no longer moves a filter's own capture
(a source output with node.link-group), which would feed the filter from the
new input or from itself; the shell's own level meters no longer light the
microphone widget; and omarchy-audio-sink-availability reads pactl in the C
locale for sinks too, since "not available" is translated.

* Give first-boot setup an XKB layout for keymaps systemd cannot map

systemd-firstboot writes an XKB layout only for keymaps listed in
kbd-model-map. Nine the setup form offers are not (Colemak, Azerbaijani,
Bulgarian, Czech, Swiss German, Kyrgyz, Norwegian, Polish, Ukrainian), so it
wrote KEYMAP= alone, the readback refused it, and owner setup could never get
past the keyboard step for them.

Setup now adds the XKB settings itself when firstboot left none, matching the
console map so the shared password types the same on the desktop: kbd's cz is
QWERTY, bg-cp1251 phonetic and azerty French AZERTY, and non-Latin layouts keep
a US layout to switch to, as kbd-model-map's own rows do. A test checks every
layout the form offers ends with an XKB layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Leave keys a firmware key turned off dark

The ambient keyboard backlight treated any 0 the keyboard-brightness keys had
not recorded as an accident and relit it within five seconds. Firmware Fn keys
(Fn+Space on ThinkPads and Framework laptops, Fn+F10 on Dells) write the LED
directly, so on those laptops turning the keys off never stuck.

The lock screen's off now records a blank, and only that blank or a 1%
leftover is taken back; any other off is a choice that pauses auto like the
keys do. A wake clears the blank only once the restore succeeded, the command
runs one at a time so a blank racing a wake can't leave the LED and record
disagreeing, and the loop drops a blank once it has taken the keys back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Drop the optional-install availability guards

Each menu open checked every Install row against the sync databases, which
costs most of a second of pacman work on every machine to hide rows only an
ARM machine is missing. The fix for those is publishing the ARM packages, not
hiding the rows, so this removes omarchy-pkg-available, the Install rows'
availability and architecture guards, and reinstall's skipping of defaults no
repository offers.

The Windows VM row keeps its x86_64 guard, since no package fixes a guest that
needs an x86_64 host, and reinstall keeps its aarch64 channel handling.

This reverts commit aea001cbde4d0852b311ef42b5e8c380c719f833.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Put the current root back when the factory root cannot take its place

Factory reset moved the running root aside, marked the reset committed, then
renamed the staged factory root to @. If that second rename failed, cleanup
saw a committed reset: it skipped the boot package's rollback and kept the
throwaway LUKS slot, and since the swap had not happened it deleted the staged
root as well, leaving nothing at @.

The reset now commits only once the factory root is at @. A failed activation
renames the old root back first, then cleanup rolls the platform's boot state
back and revokes the throwaway slot. If that rename fails too, both roots are
kept, cleanup still rolls back and revokes, and the error names the rename
that recovers the machine. The generic Limine path has already rebuilt its
boot files from the factory root by then, as it had before this change.

* Leave other recorders alone when the saved recorder pid is stale

A saved pid that no longer named a recorder made status and stop select every
gpu-screen-recorder and wf-recorder the user ran, so stopping could interrupt,
and after five seconds kill, a recording started somewhere else.

A stale pid now means no recording of ours is running: the toggle starts a new
one, and --stop-recording finds nothing to stop. Stop acts on the recorder the
status check selected rather than selecting again. Without a saved pid, as for
a recording started before the pid was saved, every recorder the user runs
still counts.

* Name the audio a wf-recorder recording could not capture

With desktop and microphone audio both requested, wf-recorder records a null
sink fed by two loopbacks, and a loopback that failed to load was ignored: the
recording went ahead without that source, or silent when both failed. A
missing null sink or default device dropped audio just as quietly.

A source that cannot be captured is now left out with a notification naming
it, the loopbacks that did load are unloaded, and the recording carries the
source that remains, or no audio track at all.

* Show the Edge, Spotify and Dropbox installers on x86_64 only

These vendors currently publish Linux builds for x86_64 alone (the AUR's
microsoft-edge-stable-bin and spotify are arch=('x86_64'), and Dropbox lists
no Linux ARM client), so no native aarch64 build is available and choosing
one on an ARM machine only fails. Like the Windows VM row they keep a uname
check, which the menu answers once per open without asking pacman. The other
Install rows are unchanged.

* Revoke the generic reset's throwaway slot when the reset fails

The generic Limine path added its throwaway LUKS slot without recording it,
so a reset that failed before activation never revoked it. Once the factory
root's UKI was on the ESP, a failed activation that put the old root back left
a machine that booted the factory kernel and unlocked unattended with a key
readable on the ESP, while the screen said the current root was back.

The generic slot is now recorded like the platform's and revoked by cleanup
whenever the reset fails before the factory root is active. A failed
activation on that path says the boot files were already rebuilt from the
factory system and to run limine-update before rebooting.

* Clear a stale recorder pid when a recording starts

The recorder's pid is saved only once its file exists. Until then a pid left
by a recording that ended without a stop still counted as stale, so pressing
the toggle again while the new recorder started found nothing of ours and
could start a second recording. Starting a recording now clears that pid,
and during startup the toggle falls back to the user's recorders as before.

* Word the dropped-audio notice for every reason a source is left out

The notice said the dropped source "could not be captured", which was wrong
when the null sink itself failed to load: the microphone was never tried, and
desktop audio recorded alone as it always has. It now says the source could
not be added to the recording, true for a missing device, a failed loopback
and a failed null sink alike.

* Show the Heroic and Minecraft installers on x86_64 only

Heroic publishes Linux builds for x86_64 alone (its arm64 builds are for
macOS and Windows), and Mojang's Linux launcher is a single x86_64 tarball.
They join Edge, Spotify and Dropbox under the same uname check, so every row
whose vendor ships no native Linux aarch64 build is treated alike.

* Ask the recorder script whether its stop has a recording to end

The bar indicator and the menu's Stop Screenrecording row asked the process
helper whether any recorder the user runs was alive, while the toggle's stop
acts only on the recording it saved. With a stale saved pid alongside an
unrelated recorder, both offered a stop that did nothing.

omarchy-capture-screenrecording --status answers with the selection the stop
uses, and both now ask it. Status needs no recordings directory and never
notifies, so the recordings-directory check moved below argument parsing and
skips it.

* Copy every platform's pacman templates into place, as x86_64 does

On aarch64 a channel change edited the machine's pacman.conf in place: an awk
pass found the [omarchy] server and rewrote its channel, appended a section
where there was none, checked the channel's omarchy.db first, and gated it all
on a per-platform table of qualified channels. Install finalization appended
Omarchy to a template, or on a Mac copied one bundled inside omarchy-mac. What
a machine ended up with was never in the repository.

Each platform now has a pacman.conf and mirrorlist per channel, copied whole:
x86_64's stay in default/pacman, Snapdragon and other aarch64 machines use
default/pacman/aarch64, and Apple Silicon default/pacman/apple-silicon, with
Omarchy and Asahi ALARM ahead of Arch Linux ARM. Omarchy publishes aarch64
packages on edge alone so far, so every ARM channel points at edge for now and
channel changes work there; moving a channel is a template edit. A channel
without both files is refused before anything changes, including a dev
checkout's own templates. Reinstall goes back to resetting to stable, and the
update confirmation loses the missing-repository notice it no longer needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Revert "Copy every platform's pacman templates into place, as x86_64 does"

This reverts commit 9a07e138cc6c3a19b58d2f3d7eb536dd3042ccc5.

* Copy every platform's pacman templates into place, as x86_64 does

On aarch64 a channel change edited the machine's pacman.conf in place: an awk
pass found the [omarchy] server and rewrote its channel, appended a section
where there was none, checked the channel's omarchy.db first, and gated it all
on a per-platform table of qualified channels. Install finalization appended
Omarchy to a template, or on a Mac copied one bundled inside omarchy-mac. What
a machine ended up with was never in the repository.

Each platform now has a pacman.conf and mirrorlist per channel, copied whole:
x86_64's stay in default/pacman, Snapdragon and other aarch64 machines use
default/pacman/aarch64, and Apple Silicon default/pacman/apple-silicon, with
Omarchy and Asahi ALARM ahead of Arch Linux ARM. Omarchy publishes aarch64
packages on edge alone so far, so every ARM channel points at edge for now and
channel changes work there; moving a channel is a template edit. A channel
without both files is refused before anything changes, including a dev
checkout's own templates. Reinstall goes back to resetting to stable, and the
update confirmation loses the missing-repository notice it no longer needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Drop the migrations no installed machine needs

Fresh installs mark every migration done, and every Apple Silicon machine is a
fresh install, so the Mac handoff to omarchy-mac-boot (and the migrate
operation it called), zram for aarch64 installs that predate the package list,
the Brave Widevine link and the Broadcom migration's Apple Silicon guard never
run anywhere. The exit-75 deferral in omarchy-migrate existed only for two of
them. Keyboard backlight auto stays on new installs through first run rather
than switching on for existing ones, and migration 1786605598 goes back to
quattro: the mkinitcpio drop-ins arrive by package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the generic changes Mac support doesn't need

The pacman platform guard (and the detector copy omarchy-settings shipped for
it, which the HOOKS baseline fell back to), the lint against uname gates, first
run staying pending after a failed finalization, run_logged echoing failures to
stderr, half-up battery rounding, hiding empty-jack inputs and leaving a
filter's capture alone when choosing an input all rode along with the Mac work
without serving it. Each goes back to quattro's behaviour and can come back on
its own merits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the platform Hyprland slots

A platform package could load its own Hyprland binds, settings and gestures:
binds that replaced Omarchy's chords and decorated every later bind, settings
ahead of the theme, and gestures after the user's files, which needed every
hl.gesture call wrapped so a default could step aside. On a Mac those carried
macOS habits (a three-finger workspace swipe, tap-to-click off, menus opening
on the built-in screen, other screenshot and keyboard-backlight chords), not
anything the hardware needs, and Omarchy should behave the same on every
machine. The one bind a Mac does need, its lid switch under the SMC's name,
joins Omarchy's own lid binds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep aarch64 machines off the release channels until a release supports them

Every aarch64 channel's template pointed at the edge repository, but a switch
to stable or rc installs the release-named omarchy and omarchy-settings. On
edge/aarch64 those are the 4.0.4 release line, which has no omarchy-hw-platform,
lifecycle dispatch or omarchy-pkg-defaults, so `omarchy-channel-set stable` on
a Mac replaced the quattro runtime it runs with one that cannot run it, and
succeeded.

aarch64 platforms now have edge templates alone, so a switch to stable or rc is
refused before anything changes; adding those templates once a release
supports aarch64 opens them. Refresh with no channel named, as reinstall runs
it, and install finalization for a channel the platform has no templates for
take the platform's default channel: stable on x86_64, edge on aarch64.

* Leave a filter's own capture alone when choosing an input again

d0b94d98c returned omarchy-audio-input-set-default to moving every source
output, as not serving Mac support. It does: asahi-audio's microphone DSP on
Apple Silicon records the raw array through a filter-chain capture stream
(audio_effect.jNNN-mic, node.link-group "filter-chain-..."). On an M1 Pro,
choosing the Mac's microphone in the audio panel moved that stream onto the
DSP's own output, and the microphone recorded digital silence (-91 dB, from
-55 dB of room noise) until PipeWire restarted. With the skip it keeps
recording (-54 dB).

This restores the skip of source outputs carrying node.link-group, and its
test, as they were before that commit.

* Say aarch64 offers edge alone where the templates are described

The aarch64 edge templates, the template helper and the update-process docs
still said every platform has a template per channel and every ARM channel
installs from edge, which the previous commit made untrue.

* Point the Mac packages at omacom/omarchy-mac-pkgs

omarchy-mac and omarchy-mac-boot now live in omacom/omarchy-mac-pkgs, with
each package at the top level. The dispatcher, its documentation and the
Apple Silicon package list name that repository, and the drop-in test's
fixtures, byte for byte the same as omarchy-mac-boot/files/etc/mkinitcpio.conf.d
at 4399105, cite it there.

* Stop naming a platform migration among the dispatcher's callers

The migrate operation went with the Mac migrations, and no caller hands a
machine to a platform migration any more, so the header lists only the
flows that still dispatch. The optional-operations comment is rewrapped.

* Keep the Intel Mac Broadcom migration off Apple Silicon again

A fresh install marks every migration done only for the user it creates, so
an account added later runs them all on its first update. On an Apple
Silicon Mac this one matches: an M1 Pro reports sys_vendor "Apple Inc." and
a BCM4387 (14e4:4433), so it would append feature_disable=0x82000, which
stops that chip scanning once brcmfmac reloads. Restore the guard and its
test case.

* Say when Steam's post-install hook runs now that the drivers go first

Steam's installer now adds the 32-bit drivers before Steam itself, so the hook runs after both and before the first launch.

* Release fallback audio after the recorder exits

When wf-recorder ends outside the normal Stop path, status rejects the saved PID before unloading the combined-audio loopbacks. Release those owned modules when the saved recorder is gone, so the next status or stop call ends microphone capture without signaling another recorder. Leave a mix without a saved PID alone: its recorder can still be starting.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Codex Medium <noreply@openai.com>

* Serialize recording state changes and stale audio cleanup

Status can finish checking an exited recorder after another command has replaced its audio modules, and stop finalization can outlive the recorder while a new recording starts. Share a lock across start, stop and cleanup so each command finishes updating its resources before another changes them. Status uses a read-only check when the lock is busy, keeping the bar responsive during the picker. Close the lock descriptor in persistent children so recording and preview cleanup cannot retain it.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>

* Keep automatic and manual keyboard brightness updates together

A brightness key can update the LED and its chosen-level record after the automatic loop checks for a manual change but before it applies a new target. The automatic write then overwrites the user's choice and deletes the record. Take the manual command's lock for the whole tick, skipping a busy tick and releasing the descriptor on every return. Create the private fallback directory before opening the lock outside a session runtime directory.

The normal lock screen waits five seconds before blanking, so an ordinary tick should already see the session locked. The regression exercises a brightness key arriving within a tick instead of relying on an implausibly delayed lock-state check.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Read the split hook baseline in the NVIDIA migration

The NVIDIA firmware migration decides whether to rebuild by sourcing the hardware and filter drop-ins. With HOOKS now defined in the earlier baseline, that partial read treats every hybrid machine as if kms had been removed. Source the new baseline first when it exists, keeping the legacy two-file layout working and skipping rebuilds when the effective hooks still include kms.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Use a hardware helper for x86-only menu entries

Keep the x86_64 availability checks for Windows and vendor-specific installers in a hardware predicate, so menu conditions use the same command style as other hardware checks.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>

* Keep restored keyboard levels from pausing automatic brightness

Idle wake, quick unlock and hibernate can restore a saved level older than the ambient loop current level without the lock blanking first. Treat that restoration as a one-tick event, preserving existing manual pauses and the normal blank-and-restore path. Consume unmatched and unchanged restore records too, so a later firmware key cannot look like the earlier restore.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>

Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Preserve restored keyboard levels across a later lock blank

The ambient loop skips a locked session, so a restored level may still be unconsumed when the next blank saves it. Carry that origin through blanking and restore failures so fingerprint unlock cannot make the old saved level look like a manual choice. A brightness key replaces the marker with its chosen level and retains the ordinary blank behavior.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>

Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Preserve firmware brightness choices before a lock blank

Firmware keys change the LED without updating the record. Carry a pending restoration through the next blank only if the observed LED still matches that restoration, or is still zero from its previous blank. A firmware change otherwise follows the ordinary blank-and-restore path so automatic lighting pauses on the chosen level.

Co-Authored-By: GPT-6 in Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 High via Pi <noreply@anthropic.com>

* Change only the disk password in omarchy-drive-password

Changing the system disk's password also set the login and root passwords
to match. That changes account-password policy on every platform, x86
included, and overwrites passwords someone chose to keep different, so it
moves to its own change with an explicit confirm.

The disk change itself stays as it was: the new key must open the disk and
the old one must not before the change counts, the journal finishes an
interrupted change, and where the platform's boot checks prove the key
slots, the owner's new slot is recorded before the run finishes. The
journal's confirmed phase is now "record" and no longer needs a login user.

* Cancel a pending recording start instead of queueing behind it

Starting a recording holds the recorder lock through the picker and until the
recorder creates its file. A second toggle or a stop then waited on that lock,
with no end while a portal pick or a stuck recorder never finished, and once
the start gave up the queued toggle began another recording.

While a start is pending, a toggle or stop that cannot take the lock within a
second now leaves a cancel marker and returns. The start checks it after the
picker, after the webcam overlay and while waiting on its recorder, then stops
that recorder and releases its audio. A recorder other than the portal's that
has not created its file after 15 seconds is stopped and the next backend
tried.

* Move automatic keyboard lighting to its own change

Ambient-light keyboard backlight is a new feature on every laptop with the
sensor, not something Apple hardware needs to run Omarchy, so it leaves this
change for a separate review. The keyboard-brightness command returns to
upstream's version, and first run no longer enables the user service.

* Move workspace refocus on monitor removal to its own change

Keeping the viewed workspace in front after a monitor is unplugged changes
desktop behaviour on every machine, not just Apple hardware, so it leaves
this change for a separate review.

* Move prebuilt-image hardware deferral to its own change

Building an image away from its machine needs a manifest the detector reads,
a queue of hardware steps for the first boot, a first-boot service and a
per-machine pacman keyring. An install on the machine itself needs none of
it, so it leaves this change for a separate review with an installer and
package boundary of its own. Hardware and user setup now run where they are
installed, and the platform setup operations take no image argument.

* Move the Qualcomm platform to its own change

Snapdragon support is separable from Apple compatibility, so the detector no
longer names a qualcomm platform: a "qcom," device tree is generic aarch64,
which Snapdragon already behaves as everywhere else here, and a tree naming
both Apple and Qualcomm is still refused. linux-firmware-qcom's platform list
leaves with it.

* Keep a cancel that arrives while a recording start finishes

A stop could land after the start's last cancel check, while it saved its
recording as started, and be dropped with the marker. The start now removes
its starting marker before a final check, a request reads that marker only
after leaving its own, and one that finds the start gone waits for the lock
and acts unless the start already used its request. A recorder ended by a
cancel or a readiness timeout gets two seconds after TERM before KILL, so a
recorder ignoring TERM cannot hold the start and its lock.

* Defer an image build's hardware setup to the machine's first boot

Brings back prebuilt-image support split out of #14431 so it gets its own review. An image built away from its machine names its target in a root-owned manifest the detector reads; hardware setup is queued for the machine's first boot, run there by omarchy-provision-hardware, which also makes the machine's own pacman keyring; and the platform setup operations learn they run on an image's first boot. Apple Silicon images built by the omarchy-mac installer need this; an install on the machine itself does not.

* Rebuild the initramfs when deferred setup changes module options

mkinitcpio's modconf hook copies /etc/modprobe.d into the initramfs, but the
deferred first-boot setup only compared mkinitcpio's configuration and the
kernel command line when deciding whether to rebuild. A step that only wrote
module options, as nvidia.sh does, left the old ones in the boot image.

* Put the Omarchy repository first on aarch64, as on x86_64

quattro now searches [omarchy] before the Arch repositories (34016cfb2), and
its migration 1791403252 moves the section first in /etc/pacman.conf on every
machine. The aarch64 edge template still listed it last, so refreshing pacman
on a generic aarch64 machine would undo that migration. Apple Silicon's
template already lists it first.

* Refuse disk password changes that recovery could not settle

An interrupted change to the system disk is finished from the one key slot
the password opened. If the old password also sat in another slot, a LUKS2
change made in place and stopped before the old key was retired let the rerun
accept the new password as the old one and finish while the other copy still
opened the disk. A system disk whose current password opens more than one slot
is now refused before anything changes, naming the extra slot to remove and,
where the platform records key slots, how to record the ones kept.

The journal now notes that check (checked=1). A change journaled without it,
by an earlier version, on a disk that has other key slots may still hold a
copy of the old password, and no password entered now can show which slot is
whose, so it is left for the owner to settle by hand, in every phase, with the
commands to do it. On a disk with one slot it settles as before.

A new password that already opens a key slot is refused on every drive too:
the new key's slot is how the change is followed, and an existing copy made
the run take the wrong slot as the owner's. A settled interrupted change now
says the disk opens with the password just entered, which is true whether it
finished the change or rolled it back.

* Ignore the start marker of a recording start that was killed

A start killed before it removed its marker left it behind, and a later toggle that had to wait for the lock took that marker for a start still in progress and exited without doing anything. The marker now holds the starting process's pid and counts only while that process runs.

* Say why generic aarch64 searches Omarchy's repository first

Putting [omarchy] ahead of Arch Linux ARM follows quattro's x86_64 order and its migration 1791403252, which already reorders existing aarch64 machines. It means a package Omarchy also publishes now comes from Omarchy, and a refresh can move it there, downgrading it if Omarchy's build is older. The update-process notes and the template now say so.

* Keep a working key in the manual settlement steps

When an earlier version's interrupted change has to be settled by hand, the steps now start by finding and keeping the slot of the password to keep, test the other slots with passphrases only, never remove the kept slot, and record a Mac's slots before the journal goes. Before, removing every slot the old password opened could remove the only key when the change had not landed.

* Keep Superwhisper's x86_64-only default off aarch64

quattro now ships Superwhisper as the default dictation backend and puts
superwhisper-bin in the base list, but it has no aarch64 build. On an aarch64
machine omarchy-reinstall-pkgs and omarchy-upgrade-to-quattro asked for a
package no repository carries, Setup > Defaults offered an installer that
cannot work, new users were preset to that backend, and the dictation keys, F9
and Right Alt (AltGr on many layouts) included, were bound to it.

install/omarchy-x86_64-only.packages names the base packages only x86_64
builds. omarchy-pkg-defaults leaves them out of every aarch64 platform's set,
and omarchy-upgrade-to-quattro out of the base list it installs there. The
Superwhisper default row shows on x86_64 only, like the other x86_64-only
rows. User setup on aarch64 removes the shipped Superwhisper preset, so a new
user starts with no backend and picks one; any other selection stays.
The dictation keys are bound only once the selected backend's own program is
installed, which changes nothing where it is.

* Install the stub dictation backend where binding tests expect its keys

The dictation keys are now bound only once the selected backend's program is installed. The binding-conflict check selected Voxtype without providing it, so it no longer saw the dictation keys and failed its F9 check; the default-config check now also covers a selected backend that is not installed.

* Make the stale start marker test's dead pid reliably dead

The test killed a backgrounded sleep to get a pid that no longer runs. When the signal landed before sleep started, wait never returned and the suite timed out, about one run in three. A process that exits on its own gives the same dead pid without the race.

* Drop the virtual input level meter the panel ran through a helper

Volume and mute for an untyped input still go through wpctl; typed inputs
keep Quickshell's peak monitor. Quickshell's monitor takes typed nodes only,
so the panel hides the level bar for a virtual input rather than showing an
empty bar that reads as a silent mic.

* Find recording processes with pgrep instead of a Python /proc scan

pgrep matches the kernel's 15-character process name, so gpu-screen-recorder
is matched as gpu-screen-reco. That prefix also lets any gpu-screen-reco*
executable, or a process that renames itself, pass for a recorder, so each
match is confirmed against its /proc exe link, ignoring the " (deleted)"
suffix an upgrade leaves.

--signal re-checks that link just before signalling each recorder, which
narrows the pid-reuse window the old pidfd-based scan closed, and exits 1 when
no signal was delivered. --pid rejects values above the Linux pid limit
instead of letting bash arithmetic wrap them.

* Leave 1Password's software GL wrapper to its own change

This reverts f200105c2; the render-GPU check, Electron GL wrapper and desktop entry repair return in a separate branch.

* Keep Gliff's x86_64-only package off aarch64

quattro added gliff to the base list and a migration that installs it, but its recipe builds for x86_64 alone. It joins Superwhisper in install/omarchy-x86_64-only.packages, so aarch64 package sets leave it out, and the migration installs it on x86_64 only.

* Stub accessory enrollment in the first-boot re-key test

Owner setup now enrolls the owner's USB devices and Thunderbolt accessories after the staged LUKS key is gone, and the re-key test's provisioning run reached that step with nothing to answer it. It now stubs both and checks that enrollment runs once, after the staged unlock is retired.

* Keep USB authorization from boot off Apple Silicon

Blocking USB from the start of kernel boot writes Limine's entry-tool drop-in
and checks the Limine menu and UKI directly, but a Mac's boot chain belongs to
its boot package. omarchy-setup-security-usb-authorization --boot is refused
there before anything changes, and Setup and Remove > Security hide USB at
Boot. The boot helper itself refuses enable on Apple Silicon and treats
disable as done when nothing is configured, so turning USB device
authorization off still works on a Mac; a boot-time setting configured some
other way is left with steps to remove it by hand rather than reported
removed. USB device authorization after login is unchanged.

* Skip fixture-only checks when the shell suite runs as root

Root's locale step always writes the real /etc/locale.conf and locale.gen, and root's platform check always reads this machine, so their fixtures do not apply there. The locale test now stops after its static checks as root instead of changing the runner's locale, and the Apple Silicon refusal in the USB authorization test runs unprivileged only.

* Fetch aarch64 packages from Omarchy's HTTPS Arch Linux ARM mirror

* Name platforms x86, aarch64 and aarch64-apple

omarchy-hw-platform now prints the CPU architecture, then the hardware
family when it needs handling of its own: x86, aarch64 (every ARM machine
without a family) and aarch64-apple (Apple Silicon). A later family such
as aarch64-qualcomm or aarch64-n1x follows the same form, so aarch64*
always means ARM.

The Apple predicate is now omarchy-hw-aarch64-apple; omarchy-hw-apple-silicon
stays as a hidden alias while omarchy-mac's packages and units call it.
The Apple package list and pacman templates move to
install/omarchy-aarch64-apple.packages and default/pacman/aarch64-apple/.

Image manifests and omarchy-pkg-defaults still accept apple-silicon,
generic-aarch64 and generic from builders written before the names
settled, and the mkinitcpio baseline accepts a detector that still says
apple-silicon while omarchy-settings and omarchy update apart.

* Add omarchy-hw-aarch64 and gate architecture checks on the helpers

omarchy-hw-aarch64 is the counterpart of omarchy-hw-x86: true on every ARM
machine, Apple Silicon included. The Windows VM asks omarchy-hw-x86 instead
of reading uname itself, and the keyring refresh asks omarchy-hw-aarch64, so
an x86 update no longer runs the platform detector for it.

* Document platform names, helpers and the consistency rule for agents

AGENTS.md gains a Platforms section: the x86 / aarch64 / aarch64-<family>
naming standard, which helper answers architecture and which answers
hardware family, where platform-specific packages, templates, setup and
boot code live, and that every platform should get the same experience,
with missing packages fixed upstream before any workaround and alternative
packages kept as a last resort.

---------

Co-authored-by: Marcelo Alcantara <maralc@gmail.com>
Co-authored-by: Birk Skyum <74932975+birkskyum@users.noreply.github.com>
Co-authored-by: Jimmy Van Veen <8206925+JimmayVV@users.noreply.github.com>
Co-authored-by: Scott Jones <444693+scottjones@users.noreply.github.com>
Co-authored-by: Scott Jones <scottajones@gmail.com>
Co-authored-by: Marcelo Alcantara <marcelo.alcantara@bp.com>
Co-authored-by: Oliver Lukschander <33756270+oliverlukschander@users.noreply.github.com>
Co-authored-by: Caleb Marshall <68087341+cajomar@users.noreply.github.com>
Co-authored-by: Ryan Hughes <1630358+ryanrhughes@users.noreply.github.com>
Co-authored-by: Issac Trotts <15530+ijt@users.noreply.github.com>
Co-authored-by: Gabe Ohlson <3355909+6abe@users.noreply.github.com>
Co-authored-by: Sparsh Kumar <22640187+skkumarsparsh@users.noreply.github.com>
Co-authored-by: dl-alexandre <166029845+dl-alexandre@users.noreply.github.com>
Co-authored-by: kx0101 <90087463+kx0101@users.noreply.github.com>
Co-authored-by: Jim Martin <1334144+jdvmi00@users.noreply.github.com>
Co-authored-by: Ryan Hughes <ryan@heyoodle.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
170 files changed
+13030 -366

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# PC and Intel Mac quirks that must leave Apple Silicon alone, run on both sides
# of the detector.
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls.log"
conf="$test_tmp/modprobe.d/hid_apple.conf"
mkdir -p "$stub_bin"
cat >"$stub_bin/omarchy-hw-aarch64-apple" <<'SH'
#!/bin/bash
[[ ${APPLE_SILICON:-0} == "1" ]]
SH
cat >"$stub_bin/uname" <<'SH'
#!/bin/bash
if [[ $1 == "-m" ]]; then
echo "${UNAME_M:-x86_64}"
else
exec /usr/bin/uname "$@"
fi
SH
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$TEST_LOG"
"$@"
SH
chmod +x "$stub_bin"/*
run_fkeys() {
APPLE_SILICON="$1" OMARCHY_HID_APPLE_CONF="$conf" PATH="$stub_bin:$PATH" TEST_LOG="$calls" \
bash -eE -o pipefail -c 'source "$1"' _ "$ROOT/install/hardware/fix-fkeys.sh"
}
run_fkeys 0
[[ $(<"$conf") == "options hid_apple fnmode=2" ]] || fail "a PC keeps F-keys first on Apple-like keyboards" "$(cat "$conf")"
pass "a PC keeps F-keys first on Apple-like keyboards"
printf 'options hid_apple fnmode=0\n' >"$conf"
run_fkeys 0
[[ $(<"$conf") == "options hid_apple fnmode=0" ]] || fail "an existing hid_apple.conf is left alone"
pass "an existing hid_apple.conf is left alone"
rm -rf "$test_tmp/modprobe.d"
: >"$calls"
run_fkeys 1
[[ ! -e $conf && ! -s $calls ]] || fail "Apple Silicon keeps its own keyboard mode" "$(cat "$calls")"
pass "Apple Silicon keeps its own keyboard mode"
# The Windows guest is x86_64, so every other CPU refuses it before it touches
# anything, Apple Silicon or not.
for machine in aarch64 armv7l; do
output=$(UNAME_M=$machine APPLE_SILICON=0 PATH="$stub_bin:$ROOT/bin:$PATH" bash "$ROOT/bin/omarchy-windows-vm" install 2>&1) &&
fail "Windows VM install fails on $machine"
[[ $output == *"needs an x86_64 machine"* ]] || fail "Windows VM says why it refuses on $machine" "$output"
done
pass "Windows VM refuses every CPU but x86_64"
# The firmware boot entry and hibernation are refused or skipped on Apple
# Silicon before they touch anything.
output=$(APPLE_SILICON=1 PATH="$stub_bin:$PATH" bash "$ROOT/bin/omarchy-setup-direct-boot" 2>&1) &&
fail "direct boot fails on Apple Silicon"
[[ $output == *"not supported on Apple Silicon"* ]] || fail "direct boot says why it refuses" "$output"
output=$(APPLE_SILICON=1 PATH="$stub_bin:$PATH" bash "$ROOT/bin/omarchy-hibernation-setup" --force 2>&1) ||
fail "hibernation setup skips Apple Silicon without failing" "$output"
[[ $output == "Skipping hibernation setup (not supported on Apple Silicon)" ]] ||
fail "hibernation setup stops before its own checks on Apple Silicon" "$output"
pass "direct boot and hibernation step aside on Apple Silicon"
# On x86_64 the Windows VM goes on to its own commands, as before.
output=$(UNAME_M=x86_64 PATH="$stub_bin:$ROOT/bin:$PATH" bash "$ROOT/bin/omarchy-windows-vm" help 2>&1) || true
[[ $output != *"x86_64 machine"* && $output == *"install"* ]] || fail "Windows VM runs as before on x86_64" "$output"
pass "Windows VM runs as before on x86_64"
run_node_test <<'JS'
const fs = require('fs')
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
const items = menu.parseMenuJsonc(fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.jsonc'), 'utf8'))
const when = id => (items.find(entry => entry.id === id) || {}).when
assertEqual(when('install.windows'), 'omarchy-hw-x86', 'install.windows is offered on x86_64 only')
assertEqual(when('setup.direct-boot'), '! omarchy-hw-aarch64-apple', 'setup.direct-boot is hidden on Apple Silicon only')
JS
pass "the menu offers Windows on x86_64 only and hides Direct Boot on Apple Silicon only"
@@ -0,0 +1,76 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/bin"
cat >"$work/bin/wpctl" <<'SH'
#!/bin/bash
printf 'wpctl %s\n' "$*" >>"$CALLS"
SH
cat >"$work/bin/pactl" <<'SH'
#!/bin/bash
printf 'pactl %s\n' "$*" >>"$CALLS"
if [[ $* == "list source-outputs" ]]; then
[[ ${LC_ALL:-} == C ]] || echo "pactl read outside the C locale" >>"$CALLS"
cat "$OUTPUTS"
fi
exit 0
SH
chmod +x "$work/bin/wpctl" "$work/bin/pactl"
moved() {
grep '^pactl move-source-output ' "$CALLS" | awk '{ print $3 }' | tr '\n' ' '
}
export CALLS="$work/calls" OUTPUTS="$work/outputs"
# A filter chain's capture half (joined to its source by node.link-group)
# records its device all the time. Choosing an input moves apps, never it.
cat >"$OUTPUTS" <<'OUT'
Source Output #67
Driver: PipeWire
Properties:
node.name = "filter_capture.mic"
node.group = "filter-chain-100-18"
node.link-group = "filter-chain-100-18"
media.name = "Microphone filter"
Source Output #90
Driver: PipeWire
Properties:
application.name = "Firefox"
node.name = "Firefox"
Source Output #91
Properties:
node.name = "native-recorder"
media.name = "node.link-group = in a title"
OUT
: >"$CALLS"
PATH="$work/bin:$PATH" "$ROOT/bin/omarchy-audio-input-set-default" 113 virtual_mic
grep -qx 'wpctl set-default 113' "$CALLS" || fail 'input set-default selects the node'
grep -qx 'pactl set-default-source virtual_mic' "$CALLS" || fail 'input set-default names the default source'
! grep -q 'outside the C locale' "$CALLS" || fail 'input set-default reads the recordings in the C locale'
[[ $(moved) == "90 91 " ]] || fail 'input set-default moves apps but not a filter'"'"'s own capture' "$(moved)"
pass 'input set-default moves apps but not a filter'"'"'s own capture'
# Without filters every recording moves, as before, whatever its properties.
cat >"$OUTPUTS" <<'OUT'
Source Output #12
Properties:
application.name = "OBS"
Source Output #13
Properties:
node.name = "native-recorder"
Source Output #14
Properties:
node.name = "filter_capture.mic-copy"
node.group = "pipewire.dummy"
OUT
: >"$CALLS"
PATH="$work/bin:$PATH" "$ROOT/bin/omarchy-audio-input-set-default" 43 alsa_input.pci-0000_00_1f.3.analog-stereo
[[ $(moved) == "12 13 14 " ]] || fail 'input set-default still moves every other recording' "$(moved)"
pass 'input set-default still moves every other recording'
@@ -0,0 +1,65 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/bin"
cat >"$work/bin/pactl" <<'SH'
#!/bin/bash
printf '%s %s\n' "${LC_ALL:-unset}" "$*" >>"$CALLS"
case $* in
"list sources") cat "$SOURCES" ;;
"list sinks") cat "$SINKS" ;;
esac
SH
cat >"$work/bin/omarchy-audio-tuning" <<'SH'
#!/bin/bash
exit 1
SH
chmod +x "$work/bin/pactl" "$work/bin/omarchy-audio-tuning"
export CALLS="$work/calls" SOURCES="$work/sources" SINKS="$work/sinks"
# A headset jack input with nothing plugged in, a virtual source (no ports)
# and a filter's source (one port of unknown availability).
cat >"$SOURCES" <<'OUT'
Source #1074
Name: alsa_input.pci-0000_00_1f.3.analog-stereo
Ports:
[In] analog-input-headset-mic: Headset Microphone (type: Headset, priority: 300, availability group: Headset Mic, not available)
Active Port: [In] analog-input-headset-mic
Formats:
pcm
Source #126
Name: easyeffects_source
Formats:
pcm
Source #1102
Name: effect_output.mic-filter
Ports:
[In] Mic: Microphone (type: Mic, priority: 100, availability unknown)
Active Port: [In] Mic
OUT
cat >"$SINKS" <<'OUT'
Sink #57
Name: alsa_output.pci-0000_00_1f.3.analog-stereo
Ports:
[Out] analog-output-headphones: Headphones (type: Headphones, priority: 300, availability group: Headphone, not available)
Active Port: [Out] analog-output-headphones
OUT
: >"$CALLS"
expected=$'alsa_input.pci-0000_00_1f.3.analog-stereo\t0\neasyeffects_source\t1\neffect_output.mic-filter\t1'
actual=$(PATH="$work/bin:$PATH" "$ROOT/bin/omarchy-audio-sink-availability" sources)
[[ $actual == "$expected" ]] || fail 'source availability reports an empty jack input unavailable' "$actual"
grep -qx 'C list sources' "$CALLS" || fail 'source availability reads pactl in the C locale' "$(cat "$CALLS")"
pass 'source availability reports an empty jack input unavailable'
: >"$CALLS"
actual=$(LC_ALL=de_DE.UTF-8 PATH="$work/bin:$PATH" "$ROOT/bin/omarchy-audio-sink-availability")
[[ $actual == $'alsa_output.pci-0000_00_1f.3.analog-stereo\t0' ]] || fail 'sink availability reports an empty headphone jack unavailable' "$actual"
grep -qx 'C list sinks' "$CALLS" || fail 'sink availability reads pactl in the C locale whatever the caller'"'"'s' "$(cat "$CALLS")"
pass 'sink availability reports an empty headphone jack unavailable, in the C locale'
+68
View File
@@ -57,4 +57,72 @@ assertEqual(audio.matchingMprisStreamLabel('Chromium', players), 'Chromium', 'au
assertEqual(audio.unmatchedMprisStreamLabel('audio-src', players, streams), 'Spotify', 'audio uses unmatched MPRIS player for generic streams')
assertEqual(audio.streamLabel(streams[1], players, streams), 'Spotify', 'audio labels generic streams from MPRIS')
assert(audio.streamRepresentsPlayer(streams[1], players[0], players, streams), 'audio links generic streams to active player')
// A virtual source is untyped in Quickshell (no audio); PulseAudio's source
// listing is what names it a source.
const availability = { virtual_mic: true, 'alsa_input.usb': false }
assert(audio.isUntypedSource({ name: 'virtual_mic', isSink: false, isStream: false }, availability), 'audio finds an untyped source PulseAudio lists')
assert(audio.isUntypedSource({ name: 'unplugged_virtual', isSink: false, isStream: false }, { unplugged_virtual: false }), 'an untyped source PulseAudio lists as unavailable is still a source')
assert(!audio.isUntypedSource({ name: 'v4l2_input.camera', isSink: false, isStream: false }, availability), 'audio skips an untyped node PulseAudio does not list')
assert(!audio.isUntypedSource({ name: 'virtual_mic', audio: {}, isSink: false, isStream: false }, availability), 'a typed source is not untyped')
assert(!audio.isUntypedSource({ name: 'virtual_mic', isSink: false, isStream: true }, availability), 'a stream is never an untyped source')
assert(!audio.isUntypedSource({ name: '', isSink: false, isStream: false }, { '': true }), 'a nameless node is never an untyped source')
assert(!audio.isUntypedSource({ name: 'virtual_mic', isSink: false, isStream: false }, {}), 'nothing is untyped before the listing')
const nodes = requireFromRoot('shell/Commons/AudioNodesModel.js')
for (const name of ['quickshell', 'quickshell-peak-monitor'])
assert(nodes.isShellLevelMeter(name), 'audio knows the shell meter ' + name)
for (const name of ['Firefox', 'quickshell-other', '', undefined])
assert(!nodes.isShellLevelMeter(name), 'audio counts ' + name + ' as a recording')
// Platform hints: none by default, whole-name patterns, and "replaced" only
// while its replacement exists.
const none = nodes.parsePlatformAudio('')
assertDeepEqual([none.hidden.length, none.replaced.length], [0, 0], 'audio has no platform hints without the file')
for (const text of ['not json', '[]', 'null', '{"hidden": "x"}', '{"replaced": [1, {"node": "a"}, {"node": "(", "by": "b"}]}']) {
const parsed = nodes.parsePlatformAudio(text)
assertEqual(parsed.hidden.length + parsed.replaced.length, 0, 'audio ignores platform hints ' + text)
}
const hints = nodes.parsePlatformAudio(JSON.stringify({
hidden: ['dsp_capture\\.[a-z]+', 'raw_speakers', '(', 7],
replaced: [{ node: 'dsp_mic\\.[0-9]+', by: 'stereo_mic' }]
}))
assertEqual(hints.hidden.length, 2, 'audio skips invalid hidden patterns')
const names = ['dsp_capture.mic', 'dsp_mic.1', 'stereo_mic', 'raw_speakers']
assert(nodes.platformHidesNode('dsp_capture.mic', hints, names), 'platform hints hide a matching node')
assert(nodes.platformHidesNode('raw_speakers', hints, []), 'platform hints hide a literal name')
assert(!nodes.platformHidesNode('raw_speakers.monitor', hints, []), 'platform patterns match whole names only')
assert(!nodes.platformHidesNode('xdsp_capture.mic', hints, []), 'platform patterns are anchored at the start')
assert(nodes.platformHidesNode('dsp_mic.1', hints, names), 'platform hints hide a replaced node while its replacement exists')
assert(!nodes.platformHidesNode('dsp_mic.1', hints, ['dsp_mic.1']), 'platform hints keep a replaced node without its replacement')
assert(!nodes.platformHidesNode('stereo_mic', hints, names), 'platform hints keep the replacement')
for (const name of ['Firefox', 'alsa_input.pci-0000_00_1f.3.analog-stereo', '', undefined])
assert(!nodes.platformHidesNode(name, hints, names), 'platform hints keep ' + name)
assert(!nodes.platformHidesNode('dsp_capture.mic', none, names), 'no platform hints hide nothing')
// An untyped source's level is read from wpctl.
assertDeepEqual(nodes.parseWpctlVolume('Volume: 0.50\n'), { volume: 0.5, muted: false }, 'audio reads a wpctl volume')
assertDeepEqual(nodes.parseWpctlVolume('Volume: 1.00 [MUTED]'), { volume: 1, muted: true }, 'audio reads a muted wpctl volume')
for (const text of ['', 'Translate ID error: 404', 'Volume: loud', 'Volume: 0.50 [muted]'])
assertEqual(nodes.parseWpctlVolume(text), null, 'audio rejects wpctl output ' + JSON.stringify(text))
// Source wiring (no Quickshell here): the hints come from the fixed platform
// root, and the audio code names no platform or hardware of its own.
const fs = require('fs')
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
const audioNodes = read('shell/Commons/AudioNodes.qml')
assert(/path: "\/usr\/share\/omarchy-platform\/audio\.json"/.test(audioNodes) && !/Quickshell\.env|OMARCHY_/.test(audioNodes),
'audio reads the platform hints from the fixed platform root')
assert(/onLoadFailed: missing = true/.test(audioNodes), 'a removed hints file hides nothing')
const sources = ['shell/plugins/panels/audio/Panel.qml', 'shell/plugins/panels/audio/Model.js', 'shell/Commons/AudioNodes.qml',
'shell/Commons/AudioNodesModel.js', 'shell/Commons/UntypedInput.qml', 'shell/plugins/bar/widgets/Microphone.qml',
'bin/omarchy-audio-input-set-default', 'bin/omarchy-audio-sink-availability']
for (const file of sources)
assert(!/apple|asahi|macbook|j[0-9]{3}|omarchy-hw-|platform-sound/i.test(read(file)), file + ' names no platform')
const panel = read('shell/plugins/panels/audio/Panel.qml')
assert(/inputPeakNode: inputViaWpctl \? null : source/.test(panel) && /inputLevelShown: !!inputPeakNode/.test(panel) &&
/visible: root\.inputLevelShown[^}]*inputPeakMonitor\.peak/.test(panel),
'the input level bar is hidden when the input is driven through wpctl')
JS
+235
View File
@@ -0,0 +1,235 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const bar = requireFromRoot('shell/plugins/bar/BarModel.js')
const barSource = fs.readFileSync(root + '/shell/plugins/bar/Bar.qml', 'utf8')
const styleSource = fs.readFileSync(root + '/shell/Commons/Style.qml', 'utf8')
const shellJson = fs.readFileSync(root + '/config/omarchy/shell.json', 'utf8')
// A platform package's cutout description, as it would ship it.
const cutouts = bar.parseCutouts(JSON.stringify({ panels: [
{ connector: 'eDP', width: 3024, height: 1964, top: 64 },
{ connector: 'eDP', width: 2560, height: 1664, top: 56 }
] }))
assertEqual(cutouts.length, 2, 'a well-formed description yields its panels')
// Malformed descriptions and entries are dropped, never guessed at.
for (const text of ['', 'not json', '[]', '{}', '{"panels": {}}'])
assertDeepEqual(bar.parseCutouts(text), [], `an unusable description (${JSON.stringify(text)}) yields no cutouts`)
assertDeepEqual(bar.parseCutouts(JSON.stringify({ panels: [
{ width: 3024, height: 1964, top: 64 },
{ connector: '', width: 3024, height: 1964, top: 64 },
{ connector: 'eDP', width: 0, height: 1964, top: 64 },
{ connector: 'eDP', width: 3024, height: 1964, top: 0 },
{ connector: 'eDP', width: 3024, height: 1964, top: 1964 },
null
] })), [], 'entries without a connector, a size or a sensible cutout are dropped')
// A cutout is described in physical rows; the bar works in logical pixels at
// any scale. Hyprland reports each output's physical mode, as hyprctl monitors
// does; Qt's devicePixelRatio is a whole number even at a fractional scale.
const mode = (width, height, transform = 0) => ({ width, height, transform })
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1512, 982, 2, mode(3024, 1964)), 32, 'a 64-row cutout is 32 px at scale 2')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 3024, 1964, 1, mode(3024, 1964)), 64, 'scale 1 uses the rows as they are')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1280, 832, 2, mode(2560, 1664)), 28, 'each described panel gets its own cutout')
// A 16" MacBook Pro panel at scale 1.33: Qt says 2, Hyprland says 3456x2234.
const pro16 = [{ connector: 'eDP', width: 3456, height: 2234, top: 64 }]
assertEqual(bar.cutoutTop(pro16, 'eDP-1', 2592, 1676, 2, mode(3456, 2234)), 48, 'a fractional scale matches its panel by the mode Hyprland reports')
assertEqual(bar.notchFloor(pro16, 'top', 'eDP-1', 2592, 1676, 2, mode(3456, 2234), 0), 48, 'a top bar at a fractional scale floors at the cutout')
assertEqual(bar.centerBesideRight(pro16, 'top', 'eDP-1', 2592, 1676, 2, mode(3456, 2234)), true, 'a top bar at a fractional scale moves the center section')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1890, 1228, 2, mode(3024, 1964)), 40, 'scale 1.6 matches too')
// Until Hyprland answers, the logical size times Qt's ratio stands in for the
// mode, which holds at whole scales only.
for (const pending of [null, undefined, {}, mode(0, 0)])
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1512, 982, 2, pending), 32, `a whole scale matches before Hyprland reports the mode (${JSON.stringify(pending)})`)
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 3024, 1964, 0, null), 64, 'a missing scale reads as 1')
assertEqual(bar.cutoutTop(pro16, 'eDP-1', 2592, 1676, 2, null), 0, 'a fractional scale finds nothing on Qt\'s ratio alone')
// A mode Hyprland reports is the answer: one no panel describes has no cutout,
// whatever the logical size times the ratio would say.
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1512, 982, 2, mode(3024, 1890)), 0, 'the same panel with its cutout rows hidden has none')
assertEqual(bar.cutoutTop(cutouts, 'DP-1', 1512, 982, 2, mode(3024, 1964)), 0, 'another connector with the same mode has none')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1728, 1117, 2, mode(3456, 2234)), 0, 'a panel nobody described has none')
assertEqual(bar.cutoutTop([], 'eDP-1', 1512, 982, 2, mode(3024, 1964)), 0, 'with no description no panel has a cutout')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 0, 0, 2, mode(3024, 1964)), 0, 'degenerate screen sizes have none')
// Turned a quarter or upside down, the cutout is on another edge; mirrored,
// it is still at the top.
for (const transform of [1, 2, 3, 5, 6, 7])
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', transform % 2 ? 982 : 1512, transform % 2 ? 1512 : 982, 2, mode(3024, 1964, transform)), 0, `a panel with transform ${transform} has no cutout at its top`)
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1512, 982, 2, mode(3024, 1964, 4)), 32, 'a mirrored panel keeps its cutout at the top')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 982, 1512, 2, mode(3024, 1964)), 0, 'a portrait screen on a landscape mode is turned, whatever its transform says')
// Only a top bar on a described panel has a floor; a calibrated [bar]
// notch-height replaces the described value there and nowhere else.
assertEqual(bar.notchFloor(cutouts, 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964), 0), 32, 'a top bar floors at the cutout')
for (const position of ['bottom', 'left', 'right'])
assertEqual(bar.notchFloor(cutouts, position, 'eDP-1', 1512, 982, 2, mode(3024, 1964), 0), 0, `a ${position} bar has no floor`)
assertEqual(bar.notchFloor(cutouts, 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964), 36), 36, 'a calibrated notch-height overrides the described floor')
assertEqual(bar.notchFloor(cutouts, 'top', 'DP-1', 2560, 1440, 1, mode(2560, 1440), 36), 0, 'a calibrated notch-height never floors an external monitor')
assertEqual(bar.notchFloor([], 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964), 36), 0, 'a calibrated notch-height never floors a panel without a cutout')
assertEqual(bar.notchFloor(cutouts, 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964), 'x'), 32, 'an unreadable calibration falls back to the described floor')
// Source wiring only (no Quickshell here): the bar reads the description from
// the fixed platform root, which no environment variable moves, not from a
// detector.
assert(
/path: "\/usr\/share\/omarchy-platform\/display-cutouts\.json"/.test(barSource) &&
/readonly property var displayCutouts: displayCutoutsFile\.missing \? \[\] : BarModel\.parseCutouts\(displayCutoutsFile\.text\(\)\)/.test(barSource),
'bar reads the platform package\'s cutout description from the platform root'
)
assert(
/id: displayCutoutsFile[\s\S]{0,400}blockLoading: true[\s\S]{0,200}onLoadFailed: missing = true/.test(barSource),
'bar reads the description as the shell starts, before its surfaces map, and a removed file describes nothing'
)
assert(!/_PACKAGED_PATH|packagedPath/.test(barSource), 'no environment variable moves the cutout description')
assert(!/omarchy-hw-/.test(barSource), 'bar asks no hardware detector')
assert(
/BarModel\.notchFloor\(root\.displayCutouts, root\.position, screen\.name, screen\.width, screen\.height, screen\.devicePixelRatio, root\.panelModeFor\(screen\), Style\.bar\.notchHeight\)/.test(barSource),
'bar derives the floor from its own screen geometry, its mode and the calibration'
)
assert(
/function panelModeFor\(screen\) \{\s*var monitor = screen \? Hyprland\.monitorFor\(screen\) : null\s*return monitor \? \(\{\s*width: monitor\.width,\s*height: monitor\.height,\s*transform: monitor\.lastIpcObject \? monitor\.lastIpcObject\.transform : 0\s*\}\) : null/.test(barSource) &&
/readonly property var panelMode: root\.panelModeFor\(screen\)/.test(barSource),
'bar takes its screen\'s physical mode from Hyprland'
)
assert(
/function thicknessFor\(screen\) \{\s*if \(root\.vertical \|\| !screen\) return root\.barSize\s*return Math\.max\(root\.barSize, BarModel\.notchFloor\(/.test(barSource) &&
/readonly property int thickness: root\.thicknessFor\(screen\)/.test(barSource) &&
/implicitHeight: root\.vertical \? 0 : thickness/.test(barSource),
'bar height is floored at the cutout, never shrunk to it'
)
// A hidden bar parks by that same thickness (bar-test.sh), so a floored top
// bar leaves no strip of itself on screen.
// The calibration must not scale with the font: it describes physical pixels
// beside the camera.
assert(
/notchHeight:[\s\S]{0,240}barOverrides\["notch-height"\]/.test(styleSource) &&
!/barToken\("notch-height"/.test(styleSource),
'notch-height is read raw, not through the font-scaled bar tokens'
)
// A top bar on a described panel draws its center section beside the right
// one; everything else keeps the configured layout.
for (const scale of [1, 2])
assertEqual(bar.centerBesideRight(cutouts, 'top', 'eDP-1', 3024 / scale, 1964 / scale, scale, mode(3024, 1964)), true, `a described panel at scale ${scale} moves the center section`)
assertEqual(bar.centerBesideRight(cutouts, 'top', 'USB-2', 3440, 1440, 1, mode(3440, 1440)), false, 'an external monitor keeps the center section')
assertEqual(bar.centerBesideRight([], 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964)), false, 'a machine without a description keeps the center section')
for (const position of ['bottom', 'left', 'right'])
assertEqual(bar.centerBesideRight(cutouts, position, 'eDP-1', 1512, 982, 2, mode(3024, 1964)), false, `a ${position} bar keeps the center section`)
// The move is per screen and draw-time only: the user's layout is read as
// configured, and the center entries keep their order and their own region.
assert(
/centerBesideRight: BarModel\.centerBesideRight\(root\.displayCutouts, root\.position, screen\.name, screen\.width, screen\.height, screen\.devicePixelRatio, panelMode\)/.test(barSource),
'each bar surface decides the move from its own screen'
)
assert(
/CenterModules \{\s*anchors\.fill: parent\s*entries: barWindow\.centerBesideRight \? \[\] : root\.layoutEntries\("center"\)\s*\}/.test(barSource),
'a moved center section draws nothing in the middle of the bar'
)
assert(
/ModuleList \{\s*entries: barWindow\.centerBesideRight \? root\.layoutEntries\("center"\) : \[\]\s*region: "center"\s*anchors\.right: rightModules\.left\s*anchors\.rightMargin: Style\.space\(\d+\)/.test(barSource),
'the center entries sit just left of the right section in their own region'
)
// With no gap the last center slot and the first right slot would share an
// edge, and a drop there would always land in whichever registered first.
const seam = [
{ slot: 'right-first', x: 104, width: 20 },
{ slot: 'center-last', x: 80, width: 20 }
]
assertDeepEqual(bar.nearestDropTarget(seam, { x: 101 }, false), { slot: 'center-last', after: true }, 'a drop at the seam nearer the center section lands after its last entry')
assertDeepEqual(bar.nearestDropTarget(seam, { x: 103 }, false), { slot: 'right-first', after: false }, 'a drop at the seam nearer the right section lands before its first entry')
assert(
/anchorEntry: root\.findCenterAnchorEntry\(entries\)/.test(barSource),
'an emptied center section does not keep a hidden copy of its anchor'
)
// Until Hyprland reports the mode, a top bar waits to map where the mode may
// still bring a cutout: a described connector whose whole-scale rebuild
// matched nothing, as at a fractional scale. It never guesses a floor there.
assertEqual(bar.cutoutPending(pro16, 'top', 'eDP-1', 2592, 1676, 2, null), true, 'a fractional scale on a described connector waits for the mode')
for (const pending of [undefined, {}, mode(0, 0)])
assertEqual(bar.cutoutPending(pro16, 'top', 'eDP-1', 2592, 1676, 2, pending), true, `no mode yet (${JSON.stringify(pending)}) still waits`)
assertEqual(bar.notchFloor(pro16, 'top', 'eDP-1', 2592, 1676, 2, null, 0), 0, 'a waiting bar has no guessed floor')
assertEqual(bar.cutoutPending(pro16, 'top', 'eDP-1', 2592, 1676, 2, mode(3456, 2234)), false, 'the mode ends the wait')
assertEqual(bar.cutoutPending(pro16, 'top', 'eDP-1', 1728, 1117, 2, mode(3456, 2160)), false, 'a mode no panel describes ends the wait too')
assertEqual(bar.cutoutPending(cutouts, 'top', 'eDP-1', 1512, 982, 2, null), false, 'a whole scale that matches needs no wait')
assertEqual(bar.cutoutPending(cutouts, 'top', 'DP-1', 2560, 1440, 1, null), false, 'a connector no entry names never waits')
assertEqual(bar.cutoutPending([], 'top', 'eDP-1', 2592, 1676, 2, null), false, 'without a description nothing waits')
for (const position of ['bottom', 'left', 'right'])
assertEqual(bar.cutoutPending(pro16, position, 'eDP-1', 2592, 1676, 2, null), false, `a ${position} bar never waits`)
assert(
/readonly property bool cutoutPending: BarModel\.cutoutPending\(root\.displayCutouts, root\.position, screen\.name, screen\.width, screen\.height, screen\.devicePixelRatio, panelMode\)/.test(barSource) &&
/visible: !remapGuard\.remapping && \(cutoutWaitOver \|\| !cutoutPending\)/.test(barSource),
'a bar surface maps only once its floor is known, and still remaps on a screen move'
)
assert(
/property bool cutoutWaitOver: false\s*onCutoutPendingChanged: if \(!cutoutPending\) cutoutWaitOver = true\s*Component\.onCompleted: if \(!cutoutPending\) cutoutWaitOver = true/.test(barSource) &&
/Timer \{\s*interval: 2000\s*running: barWindow\.cutoutPending && !barWindow\.cutoutWaitOver\s*onTriggered: barWindow\.cutoutWaitOver = true\s*\}/.test(barSource),
'the wait ends once, for good: when the mode settles it or after two seconds'
)
// Toasts and plugins clear the bar as thick as it is on their screen. The bar
// publishes each screen's thickness by name from the same calculation its
// surfaces use; barSize stays the configured size the widgets are drawn at.
const sizes = { 'eDP-1': 40, 'DP-1': 26 }
assertEqual(bar.barSizeFor(sizes, 'eDP-1', 26), 40, 'a notch screen reports its floored thickness')
assertEqual(bar.barSizeFor(sizes, 'DP-1', 26), 26, 'another screen reports its own')
assertEqual(bar.barSizeFor(sizes, 'HDMI-A-1', 26), 26, 'a screen without a surface falls back to the configured size')
assertEqual(bar.barSizeFor({}, 'eDP-1', 26), 26, 'no published sizes fall back to the configured size')
assertEqual(bar.barSizeFor(sizes, 'constructor', 26), 26, 'inherited object keys are no screen')
assert(
/readonly property var screenBarSizes: \{\s*var sizes = \(\{\}\)\s*var screens = Quickshell\.screens\s*for \(var i = 0; i < screens\.length; i\+\+\) sizes\[screens\[i\]\.name\] = root\.thicknessFor\(screens\[i\]\)\s*return sizes\s*\}/.test(barSource) &&
/function barSizeFor\(screenName\) \{\s*return BarModel\.barSizeFor\(root\.screenBarSizes, screenName, root\.barSize\)\s*\}/.test(barSource),
'bar publishes every screen\'s thickness from the same calculation its surfaces use'
)
assert(
/readonly property int barSize: vertical \? Style\.bar\.sizeVertical : Style\.bar\.sizeHorizontal/.test(barSource),
'barSize stays the configured size'
)
// The plugin facade copies the sizes, holding no reference into the bar, and
// keeps barSize the configured size.
const shellSource = fs.readFileSync(root + '/shell/shell.qml', 'utf8')
const stateApiSource = fs.readFileSync(root + '/shell/services/PluginBarStateApi.qml', 'utf8')
const extract = (source, name) => {
const match = source.match(new RegExp(`function ${name}\\((\\w+)\\) \\{([\\s\\S]*?)\\n \\}\\n`))
assert(match, `${name} is defined`)
return match
}
const detached = extract(shellSource, 'detachedBarSizes')
const Util = { isPlainObject: value => value !== null && typeof value === 'object' && !Array.isArray(value) }
const detachedBarSizes = new Function('Util', detached[1], detached[2])
const live = { 'eDP-1': 40, 'DP-1': 26, broken: 'x', none: 0 }
const copy = detachedBarSizes.call(null, Util, live)
assertDeepEqual(copy, { 'eDP-1': 40, 'DP-1': 26 }, 'the plugin facade copies each screen\'s thickness')
assert(copy !== live, 'the copy is detached from the bar\'s map')
assertDeepEqual(detachedBarSizes.call(null, Util, undefined), {}, 'a bar without per-screen sizes gives an empty map')
assert(
/api\.barSize = Qt\.binding\(function\(\) \{ return shell\.bar \? Math\.max\(0, shell\.bar\.barSize \|\| 0\) : 0 \}\)/.test(shellSource) &&
/api\.barSizes = Qt\.binding\(function\(\) \{ return shell\.bar \? shell\.detachedBarSizes\(shell\.bar\.screenBarSizes\) : \(\{\}\) \}\)/.test(shellSource),
'plugins get the configured barSize and every screen\'s thickness'
)
const apiFor = extract(stateApiSource, 'barSizeFor')
const stateBarSizeFor = new Function('barSizes', 'barSize', apiFor[1], apiFor[2])
assertEqual(stateBarSizeFor(copy, 26, 'eDP-1'), 40, 'a plugin reads a notch screen\'s thickness')
assertEqual(stateBarSizeFor(copy, 26, 'HDMI-A-1'), 26, 'a plugin falls back to barSize for a screen with none')
assertEqual(stateBarSizeFor({}, 26, 'eDP-1'), 26, 'a plugin under a bar without per-screen sizes gets barSize')
const parsed = JSON.parse(shellJson)
assertEqual(parsed.bar.centerAnchor, 'omarchy.clock', 'shipped bar layout still anchors on the clock')
assert(
JSON.stringify(parsed.bar.layout.center).indexOf('omarchy.indicators') >= 0,
'shipped bar layout still keeps the default center widgets'
)
JS
+2 -2
View File
@@ -81,8 +81,8 @@ assert(
)
for (const edge of ['top', 'bottom', 'left', 'right']) {
assert(
new RegExp(`${edge}: root\\.barHidden && root\\.position === "${edge}" \\? -root\\.barSize : 0`).test(barSource),
`a hidden bar parks past the ${edge} edge`
new RegExp(`${edge}: root\\.barHidden && root\\.position === "${edge}" \\? -barWindow\\.thickness : 0`).test(barSource),
`a hidden bar parks past the ${edge} edge by its whole thickness`
)
}
+52
View File
@@ -89,6 +89,58 @@ require_compositor() {
exit 0
}
# Fake the hardware identity omarchy-hw-platform reads: a device tree under
# $1/proc and a uname in $1/bin reporting the platform's CPU. Run the code under
# test with OMARCHY_PROC_ROOT="$1/proc" and PATH="$1/bin:$ROOT/bin:...", so the
# real detector and its wrappers answer from the fixture.
fake_platform() {
local dir="$1" platform="$2" machine=aarch64
local -a compatible=()
case $platform in
aarch64-apple) compatible=(apple,j416c apple,t6021 apple,arm-platform) ;;
aarch64) compatible=(raspberrypi,5-model-b brcm,bcm2712) ;;
x86) machine=x86_64 ;;
*) fail "fake_platform knows the platform $platform" ;;
esac
rm -rf "$dir/proc"
mkdir -p "$dir/proc" "$dir/bin"
if (( ${#compatible[@]} > 0 )); then
mkdir -p "$dir/proc/device-tree"
printf '%s\0' "${compatible[@]}" >"$dir/proc/device-tree/compatible"
fi
cat >"$dir/bin/uname" <<SH
#!/bin/bash
[[ \${1:-} == -m ]] && { printf '%s\\n' $machine; exit 0; }
exec /usr/bin/uname "\$@"
SH
chmod +x "$dir/bin/uname"
}
# omarchy-hw-platform ignores fixture roots when it runs as root, so a root run
# cannot fake a platform. Skip the rest of the file there.
require_platform_fixtures() {
(( EUID != 0 )) && return 0
skip "running as root, where omarchy-hw-platform ignores fixtures; skipping $1"
exit 0
}
# A script reads the installed platform package's files from the fixed
# /usr/share/omarchy-platform, which nothing in its environment moves. Copies
# script $1 to $2 reading fixture root $3 instead (a directory that doesn't
# exist stands for no platform package), the way the security tests rewrite
# other fixed paths.
platform_root_copy() {
local script="$1" copy="$2" fixture="$3"
sed -e "s|/usr/share/omarchy-platform|$fixture|g" "$script" >"$copy" &&
chmod +x "$copy" || fail "copy $script for a fixture platform root"
grep -qF -- "$fixture" "$copy" && ! grep -qF /usr/share/omarchy-platform "$copy" ||
fail "$(basename -- "$script") reads the platform root from the fixture"
}
run_node_test() {
require_command node
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/macsmc-ac" "$test_tmp/CMB0"
printf 'Mains\n' >"$test_tmp/macsmc-ac/type"
printf 'Battery\n' >"$test_tmp/CMB0/type"
printf '1\n' >"$test_tmp/CMB0/present"
OMARCHY_POWER_SUPPLY_PATH="$test_tmp" "$ROOT/bin/omarchy-battery-present" || fail "battery presence accepts non-BAT native paths"
pass "battery presence accepts non-BAT native paths"
printf '0\n' >"$test_tmp/CMB0/present"
if OMARCHY_POWER_SUPPLY_PATH="$test_tmp" "$ROOT/bin/omarchy-battery-present"; then
fail "battery presence rejects an absent battery"
fi
pass "battery presence rejects an absent battery"
# A wireless mouse registers a Battery of its own, scoped Device; it must not
# make a desktop look like a laptop.
printf '1\n' >"$test_tmp/CMB0/present"
printf 'System\n' >"$test_tmp/CMB0/scope"
mkdir -p "$test_tmp/hidpp_battery_0"
printf 'Battery\n' >"$test_tmp/hidpp_battery_0/type"
printf '1\n' >"$test_tmp/hidpp_battery_0/present"
printf 'Device\n' >"$test_tmp/hidpp_battery_0/scope"
OMARCHY_POWER_SUPPLY_PATH="$test_tmp" "$ROOT/bin/omarchy-battery-present" || fail "battery presence accepts a System-scoped battery beside a peripheral"
pass "battery presence accepts a System-scoped battery beside a peripheral"
rm -r "$test_tmp/CMB0"
if OMARCHY_POWER_SUPPLY_PATH="$test_tmp" "$ROOT/bin/omarchy-battery-present"; then
fail "battery presence ignores a peripheral's Device-scoped battery"
fi
pass "battery presence ignores a peripheral's Device-scoped battery"
+83 -7
View File
@@ -8,20 +8,40 @@ tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir/bin"
mkdir -p "$tmp_dir/power/BAT0"
printf '900000\n' >"$tmp_dir/power/BAT0/current_now"
printf '12000000\n' >"$tmp_dir/power/BAT0/voltage_now"
# Apple Silicon: a battery not called BAT*, power_now signed by direction, and
# the thresholds and cycle count only under the native path.
mkdir -p "$tmp_dir/power/macsmc-battery"
printf '%s\n' '-10800000' >"$tmp_dir/power/macsmc-battery/power_now"
printf '75\n' >"$tmp_dir/power/macsmc-battery/charge_control_start_threshold"
printf '80\n' >"$tmp_dir/power/macsmc-battery/charge_control_end_threshold"
printf '212\n' >"$tmp_dir/power/macsmc-battery/cycle_count"
cat >"$tmp_dir/bin/upower" <<'STUB'
#!/bin/bash
# A wireless mouse enumerates first, as a battery_ device that is not a power
# supply; the machine's own battery follows it.
if [[ $1 == "-e" ]]; then
echo "/org/freedesktop/UPower/devices/battery_BAT0"
echo "/org/freedesktop/UPower/devices/line_power_macsmc_ac"
echo "/org/freedesktop/UPower/devices/battery_hidpp_battery_0"
echo "/org/freedesktop/UPower/devices/battery_${OMARCHY_TEST_NATIVE_PATH//-/_}"
exit 0
fi
if [[ $1 == "-i" && $2 == */battery_hidpp_battery_0 ]]; then
cat <<'INFO'
native-path: hidpp_battery_0
model: Wireless Mouse
power supply: no
state: discharging
percentage: 5%
INFO
exit 0
fi
if [[ $1 == "-i" ]]; then
cat <<'INFO'
native-path: BAT0
native-path: macsmc-battery
power supply: yes
state: discharging
energy: 28.3 Wh
energy-full: 56.7 Wh
@@ -36,13 +56,69 @@ exit 1
STUB
chmod +x "$tmp_dir/bin/upower"
shell_output=$(OMARCHY_POWER_SUPPLY_PATH="$tmp_dir/power" PATH="$tmp_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
shell_output=$(OMARCHY_TEST_NATIVE_PATH=macsmc-battery OMARCHY_POWER_SUPPLY_PATH="$tmp_dir/power" PATH="$tmp_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
grep -Fx $'percentage\t51%' <<<"$shell_output" >/dev/null || fail "battery status reports percentage"
grep -Fx $'percentage\t51%' <<<"$shell_output" >/dev/null || fail "battery status reports the machine's battery, not the mouse's"
grep -Fx $'state\tdischarging' <<<"$shell_output" >/dev/null || fail "battery status reports state"
grep -Fx $'rate\t10.8W' <<<"$shell_output" >/dev/null || fail "battery status reports live sysfs power rate"
grep -Fx $'size\t56Wh' <<<"$shell_output" >/dev/null || fail "battery status reports full capacity"
grep -Fx $'time\t2h 30m' <<<"$shell_output" >/dev/null || fail "battery status reports remaining time"
grep -Fx $'cycles\t212' <<<"$shell_output" >/dev/null || fail "battery status reports native-path cycle count"
grep -Fx $'threshold\t75-80%' <<<"$shell_output" >/dev/null || fail "battery status reports native-path charge thresholds"
# The BAT* name the script used to key on is one of many; CMB0 is another.
mkdir -p "$tmp_dir/power/CMB0"
printf '7300000\n' >"$tmp_dir/power/CMB0/power_now"
sed -i 's/native-path: macsmc-battery/native-path: CMB0/' "$tmp_dir/bin/upower"
generic_output=$(OMARCHY_TEST_NATIVE_PATH=CMB0 OMARCHY_POWER_SUPPLY_PATH="$tmp_dir/power" PATH="$tmp_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
grep -Fx $'rate\t7.3W' <<<"$generic_output" >/dev/null || fail "battery status accepts arbitrary UPower battery paths"
pass "battery status supports Apple Silicon and arbitrary native battery paths"
# An 80% hold threshold doesn't trip while the battery is still below it.
hold_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir" "$hold_dir"' EXIT
mkdir -p "$hold_dir/bin" "$hold_dir/power/BAT0" "$hold_dir/power/ac"
printf 'Mains\n' >"$hold_dir/power/ac/type"
printf '1\n' >"$hold_dir/power/ac/online"
printf '80\n' >"$hold_dir/power/BAT0/charge_control_end_threshold"
cat >"$hold_dir/bin/upower" <<'STUB'
#!/bin/bash
if [[ $1 == "-e" ]]; then
echo "/org/freedesktop/UPower/devices/battery_BAT0"
exit 0
fi
if [[ $1 == "-i" ]]; then
cat <<'INFO'
native-path: BAT0
power supply: yes
state: charging
energy-full: 69.6 Wh
energy-rate: 0.1 W
time to full: 0.2 hours
percentage: 79.5%
charge-end-threshold: 80%
INFO
exit 0
fi
exit 1
STUB
chmod +x "$hold_dir/bin/upower"
hold_output=$(OMARCHY_POWER_SUPPLY_PATH="$hold_dir/power" PATH="$hold_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
grep -Fx $'state\tcharging' <<<"$hold_output" >/dev/null || fail "a hold below its threshold is still charging"
pass "battery status doesn't trip a hold early"
# Once the battery reaches the threshold, idle charging is holding.
sed -i 's/percentage: 79.5%/percentage: 80.0%/' "$hold_dir/bin/upower"
held_output=$(OMARCHY_POWER_SUPPLY_PATH="$hold_dir/power" PATH="$hold_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
grep -Fx $'percentage\t80%' <<<"$held_output" >/dev/null || fail "threshold percentage still displays as 80%"
grep -Fx $'state\tholding' <<<"$held_output" >/dev/null || fail "idle charging at the threshold is holding"
pass "battery status reports holding once the raw percentage reaches the threshold"
if matches=$(rg -n 'omarchy-battery-(capacity|remaining|remaining-time)' "$ROOT/bin" "$ROOT/test" "$ROOT/shell" "$ROOT/docs"); then
fail "battery status owns capacity and remaining calculations" "$matches"
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
omarchy_path="$test_tmp/omarchy"
opt_path="$test_tmp/opt"
cdm="$opt_path/WidevineCdm/chromium"
calls="$test_tmp/calls.log"
mkdir -p "$stub_bin" "$test_tmp/home" "$omarchy_path/install/helpers" "$omarchy_path/config"
# The real policy helper writes under /etc without sudo when run as root, so
# stand it in; this test covers only the CDM link. Its as_root goes through the
# sudo stub, which logs every link, whatever the test's EUID.
printf 'browser_policy_setup_dir() { :; }\nas_root() { sudo "$@"; }\n' >"$omarchy_path/install/helpers/browser-policy.sh"
cp "$ROOT/config/chromium-flags.conf" "$omarchy_path/config/"
for command in omarchy-pkg-add omarchy-pkg-aur-add omarchy-install-chromium-copy-url omarchy-install-chromium-ytdlp omarchy-theme-set-browser omarchy-lifecycle-dispatch; do
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/$command"
done
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo:%s\n' "$*" >>"$TEST_LOG"
"$@"
SH
chmod +x "$stub_bin"/*
reset_opt() {
rm -rf "$opt_path"
mkdir -p "$opt_path"
local dir
for dir in "$@"; do
mkdir -p "$opt_path/$dir"
done
}
run() {
: >"$calls"
HOME="$test_tmp/home" OMARCHY_PATH="$omarchy_path" OMARCHY_OPT_PATH="$opt_path" \
TEST_LOG="$calls" PATH="$stub_bin:$PATH" "$@" >/dev/null
}
install_browser() {
run bash "$ROOT/bin/omarchy-install-browser" "$1"
}
links_made() {
grep -c '^sudo:ln ' "$calls" || true
}
assert_cdm_linked() {
[[ -L $opt_path/$1/WidevineCdm && $(readlink "$opt_path/$1/WidevineCdm") == "$cdm" ]] ||
fail "$2" "$(ls -la "$opt_path/$1" 2>&1)"
}
for browser in brave brave-origin; do
install_dir=$browser-bin
reset_opt WidevineCdm/chromium "$install_dir"
install_browser "$browser"
assert_cdm_linked "$install_dir" "$browser install links the system Widevine CDM into $install_dir"
install_browser "$browser"
(( $(links_made) == 0 )) || fail "$browser reinstall keeps the existing CDM link" "$(<"$calls")"
assert_cdm_linked "$install_dir" "$browser reinstall keeps the existing CDM link"
pass "$browser install links the system Widevine CDM into $install_dir once"
reset_opt "$install_dir"
install_browser "$browser"
[[ ! -e $opt_path/$install_dir/WidevineCdm && ! -L $opt_path/$install_dir/WidevineCdm ]] ||
fail "$browser install links nothing without the widevine package"
pass "$browser install links nothing without the widevine package"
reset_opt WidevineCdm/chromium "$install_dir" own-cdm
ln -s "$opt_path/own-cdm" "$opt_path/$install_dir/WidevineCdm"
install_browser "$browser"
[[ $(readlink "$opt_path/$install_dir/WidevineCdm") == "$opt_path/own-cdm" ]] ||
fail "$browser install keeps a CDM the browser already has"
pass "$browser install keeps a CDM the browser already has"
done
reset_opt WidevineCdm/chromium
install_browser chromium
(( $(links_made) == 0 )) || fail "Chromium install leaves Widevine to the widevine package" "$(<"$calls")"
pass "Chromium install leaves Widevine to the widevine package"
+25 -4
View File
@@ -9,6 +9,11 @@ fix_t2="$ROOT/install/hardware/apple/fix-t2.sh"
all="$ROOT/install/hardware/all.sh"
migration="$ROOT/migrations/1786391100.sh"
# Apple Silicon's BCM4387 is in the ID list, but there the firmware supplicant
# is the one that works, so the leaf and the migration step aside: a fresh
# install marks the migration done only for its first user.
grep -Fq 'omarchy-hw-aarch64-apple && exit 0' "$migration" || fail "Broadcom migration excludes Apple Silicon"
grep -q 'apple/fix-brcmfmac-supplicant.sh' "$all" ||
fail "the brcmfmac quirk runs during hardware setup"
@@ -61,13 +66,19 @@ printf '\t%s' "$@" >>"$TEST_LOG"
printf '\n' >>"$TEST_LOG"
SH
cat >"$stub_bin/omarchy-hw-aarch64-apple" <<'SH'
#!/bin/bash
(( ${APPLE_SILICON:-0} == 1 ))
SH
chmod +x "$stub_bin"/*
# The leaf reads the vendor from an absolute path, so point it at a fixture by
# running with a fake root on PATH-independent state. pipefail is on, so a
# grep -q gate would go silent here the way #6608 did.
run_leaf() {
local vendor="$1" wifi_id="${2:-}" t2="${3:-0}"
local vendor="$1" wifi_id="${2:-}" t2="${3:-0}" apple_silicon="${4:-0}"
rm -rf "$test_tmp/etc"
mkdir -p "$test_tmp/etc"
printf '%s' "$vendor" >"$test_tmp/dmi/sys_vendor"
@@ -78,7 +89,7 @@ run_leaf() {
-e "s|/etc/modprobe.d|$test_tmp/etc/modprobe.d|g" \
"$leaf" >"$script"
WIFI_ID="$wifi_id" T2_HARDWARE="$t2" PATH="$stub_bin:$PATH" \
WIFI_ID="$wifi_id" T2_HARDWARE="$t2" APPLE_SILICON="$apple_silicon" PATH="$stub_bin:$PATH" \
bash -eE -o pipefail -c 'source "$1"' bash "$script" </dev/null
}
@@ -97,6 +108,10 @@ for wifi_id in 43ba 43bb 43bc 43a3 43dc 4464 4425 4433; do
done
pass "every brcmfmac part on a Mac without a T2 gets the quirk"
run_leaf "Apple Inc." 4433 0 1 >/dev/null
[[ ! -f $conf ]] || fail "Apple Silicon is excluded from the Intel Mac quirk"
pass "Apple Silicon is excluded from the Intel Mac quirk"
# Older Macs report the vendor differently.
run_leaf "Apple Computer, Inc." 43ba 0 >/dev/null
[[ -f $conf ]] || fail "the older Apple vendor string is recognized"
@@ -120,11 +135,11 @@ pass "a Mac with no wireless device is left alone"
# Installs that predate the quirk never ran the leaf, so the migration has to
# reach them. It runs as the user under pipefail, the context #6608 was about.
run_migration() {
local vendor="$1" wifi_id="${2:-}" t2="${3:-0}"
local vendor="$1" wifi_id="${2:-}" t2="${3:-0}" apple_silicon="${4:-0}"
printf '%s' "$vendor" >"$test_tmp/dmi/sys_vendor"
: >"$calls"
WIFI_ID="$wifi_id" T2_HARDWARE="$t2" PATH="$stub_bin:$PATH" TEST_LOG="$calls" \
WIFI_ID="$wifi_id" T2_HARDWARE="$t2" APPLE_SILICON="$apple_silicon" PATH="$stub_bin:$PATH" TEST_LOG="$calls" \
OMARCHY_BRCMFMAC_DMI_VENDOR="$test_tmp/dmi/sys_vendor" \
OMARCHY_BRCMFMAC_CONF="$conf" \
bash -euo pipefail "$migration" >/dev/null
@@ -148,6 +163,12 @@ run_migration "Apple Inc." 4488 1
[[ ! -s $calls ]] || fail "a repaired install is left untouched" "$(cat "$calls")"
pass "the migration is idempotent"
rm -rf "$test_tmp/etc"
run_migration "Apple Inc." 4433 0 1
[[ ! -e $conf ]] || fail "the migration leaves Apple Silicon Wi-Fi alone"
[[ ! -s $calls ]] || fail "the migration escalates nothing on Apple Silicon" "$(cat "$calls")"
pass "the migration leaves Apple Silicon Wi-Fi alone"
# The machine this was written for, with no T2 to fall back on.
rm -rf "$test_tmp/etc"
run_migration "Apple Inc." 43ba 0
+52 -2
View File
@@ -53,9 +53,19 @@ SH
chmod +x "$mock_bin"/*
# A copy of the command reads its platform file from a fixture root (none
# unless a case writes one, whatever the machine running the suite has
# installed) and its DRM connectors from a fixture directory.
platform_root="$test_tmp/platform"
mkdir -p "$platform_root" "$test_tmp/copy"
brightness_display="$test_tmp/copy/omarchy-brightness-display"
platform_root_copy "$ROOT/bin/omarchy-brightness-display" "$brightness_display" "$platform_root"
sed -i "s|/sys/class/drm|$test_tmp/drm|g" "$brightness_display"
grep -qF "$test_tmp/drm/card" "$brightness_display" || fail "omarchy-brightness-display reads DRM connectors from the fixture"
run_brightness() {
CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" PATH="$mock_bin:$ROOT/bin:$PATH" \
"$ROOT/bin/omarchy-brightness-display" "$@"
CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" \
PATH="$mock_bin:$ROOT/bin:$PATH" "$brightness_display" "$@"
}
brightness=$(run_brightness --monitor DP-1)
@@ -144,3 +154,43 @@ if PATH="$mock_bin:$PATH" "$ROOT/bin/omarchy-hyprland-monitor-focused-apple"; th
fail "focused non-Apple display is not detected as Apple"
fi
pass "named Apple display is detected independently of focus"
# A platform whose display driver has no DDC channel says so: an external
# monitor whose connector has no ddc node is not probed and never dims the
# built-in panel in its place.
mkdir -p "$test_tmp/drm/card2-USB-1" "$test_tmp/drm/card2-eDP-1"
# The last line needs no newline.
printf '%s\n%s' '# no DDC channel on the display driver' 'ddc-require-connector-ddc' >"$platform_root/displays.conf"
: >"$call_log"
if run_brightness --monitor USB-1 >/dev/null 2>&1; then
fail "an external monitor without a ddc node has no brightness backend"
fi
if run_brightness --no-osd --monitor USB-1 +5% >/dev/null 2>&1; then
fail "setting an external monitor without a ddc node fails"
fi
printf 'ddc-require-connector-ddc\r\n' >"$platform_root/displays.conf"
if run_brightness --monitor USB-1 >/dev/null 2>&1; then
fail "a displays.conf saved with CRLF line ends reads the same"
fi
printf '%s\n%s' '# no DDC channel on the display driver' 'ddc-require-connector-ddc' >"$platform_root/displays.conf"
[[ ! -s $call_log ]] || fail "an external monitor without a ddc node probes nothing" "$(<"$call_log")"
brightness=$(run_brightness --monitor eDP-1)
(( brightness == 40 )) || fail "the built-in panel still uses the kernel backlight" "actual: $brightness"
pass "a platform without DDC channels skips DDC for a connector without one"
rm -f "$runtime_dir/omarchy-brightness-display-ddc/"*
: >"$test_tmp/drm/card2-USB-1/ddc"
brightness=$(DDC_CONNECTOR=USB-1 run_brightness --monitor USB-1)
(( brightness == 50 )) || fail "an external monitor with a ddc node uses DDC" "actual: $brightness"
pass "a platform without DDC channels still uses DDC where a connector has one"
# Without the directive, every external monitor is probed, ddc node or not.
rm -f "$runtime_dir/omarchy-brightness-display-ddc/"* "$test_tmp/drm/card2-USB-1/ddc"
for conf in "" "ddc-require-connector-ddc extra" "ddc-future"; do
printf '%s\n' "$conf" >"$platform_root/displays.conf"
rm -f "$runtime_dir/omarchy-brightness-display-ddc/"*
brightness=$(DDC_CONNECTOR=USB-1 run_brightness --monitor USB-1)
(( brightness == 50 )) || fail "an external monitor is probed without the directive" "conf: $conf, actual: $brightness"
done
rm -f "$platform_root/displays.conf"
pass "every external monitor is probed without the directive"
@@ -69,6 +69,51 @@ for channel in stable rc edge dev; do
pass "$channel starts cold, authorizes the switch per command, runs the refresh hook cold, hands off to one update authorization and exits cold"
done
# Every aarch64 platform switches to edge and dev, copying its own templates.
for platform in aarch64 aarch64-apple; do
case $platform in
aarch64-apple) templates=default/pacman/aarch64-apple ;;
*) templates=default/pacman/aarch64 ;;
esac
for channel in edge dev; do
# dev refreshes from the checkout it links, on edge.
pacman_channel=$channel root=$SUDO_TEST_ROOT
[[ $channel != "dev" ]] || pacman_channel=edge root=$SUDO_TEST_HOME/omarchy
reset_boundary
SUDO_TEST_PLATFORM=$platform run_channel "$channel" || fail "$platform $channel failed" "$(<"$boundary_tmp/output")"
assert_scoped_channel "$platform $channel"
grep -Fqx "step:cp -f $root/$templates/pacman-$pacman_channel.conf /etc/pacman.conf" "$SUDO_TEST_LOG" ||
fail "$platform $channel copies its own template" "$(<"$SUDO_TEST_LOG")"
done
done
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 aarch64 aarch64-apple; do
for channel in stable rc; do
reset_boundary
if SUDO_TEST_PLATFORM=$platform run_channel "$channel"; then fail "$platform accepted $channel"; fi
if grep -Eq '^step:|^sudo -N ' "$SUDO_TEST_LOG"; then fail "$platform $channel was refused before any change" "$(<"$SUDO_TEST_LOG")"; fi
grep -q "Omarchy has no $channel channel for $platform" "$boundary_tmp/output" || fail "$platform $channel: the refusal says why" "$(<"$boundary_tmp/output")"
assert_boundary_cold "$platform $channel"
done
done
pass "aarch64 platforms refuse stable and rc before any change"
# A channel the platform has no template for stops before anything, the dev
# confirmation included.
mv "$SUDO_TEST_ROOT/default/pacman/aarch64-apple/pacman-edge.conf" "$boundary_tmp/saved-template"
for channel in edge dev; do
reset_boundary
if SUDO_TEST_PLATFORM=aarch64-apple run_channel "$channel"; then fail "a channel without a template was accepted ($channel)"; fi
if grep -Eq '^step:|^sudo -N ' "$SUDO_TEST_LOG"; then fail "$channel was refused before any change" "$(<"$SUDO_TEST_LOG")"; fi
grep -q "Omarchy has no edge channel for aarch64-apple" "$boundary_tmp/output" || fail "the refusal says why" "$(<"$boundary_tmp/output")"
assert_boundary_cold "missing template $channel"
done
mv "$boundary_tmp/saved-template" "$SUDO_TEST_ROOT/default/pacman/aarch64-apple/pacman-edge.conf"
pass "a channel without a template for the platform is refused before any change"
reset_boundary
wrapper="$SUDO_TEST_HOME/omarchy/default/omarchy/sudo-no-update/sudo"
mv "$wrapper" "$boundary_tmp/saved-wrapper"
@@ -81,6 +126,40 @@ assert_boundary_cold "stale checkout"
mv "$boundary_tmp/saved-wrapper" "$wrapper"
pass "a stale dev checkout is rejected before linking or privileged work"
# On aarch64 the checkout's own refresh keeps the machine's repositories only
# once it tells platforms apart: a checkout without that is refused the same way.
checkout="$SUDO_TEST_HOME/omarchy"
for required in bin/omarchy-hw-platform install/helpers/pacman.sh; do
for platform in aarch64; do
reset_boundary
mv "$checkout/$required" "$boundary_tmp/saved-required"
if SUDO_TEST_PLATFORM=$platform run_channel dev; then fail "$platform: a dev checkout without $required was accepted"; fi
mv "$boundary_tmp/saved-required" "$checkout/$required"
if grep -Eq '^step:omarchy-(dev-link|state)|^step:pacman|^sudo -N ' "$SUDO_TEST_LOG"; then
fail "$platform: a dev checkout without $required changed the system before rejection" "$(<"$SUDO_TEST_LOG")"
fi
grep -q "Update the checkout before switching to dev; on $platform it needs $required" "$boundary_tmp/output" ||
fail "$platform: the rejection names $required" "$(<"$boundary_tmp/output")"
assert_boundary_cold "$platform checkout without $required"
done
done
pass "on aarch64 a dev checkout that can't keep the machine's repositories is rejected before linking"
# The checkout's own refresh copies its own templates, so one without them for
# this platform is refused before linking.
template=$checkout/default/pacman/aarch64/mirrorlist-edge
mv "$template" "$boundary_tmp/saved-template"
reset_boundary
if SUDO_TEST_PLATFORM=aarch64 run_channel dev; then fail "a dev checkout without its templates was accepted"; fi
mv "$boundary_tmp/saved-template" "$template"
if grep -Eq '^step:omarchy-(dev-link|state)|^step:pacman|^sudo -N ' "$SUDO_TEST_LOG"; then
fail "a dev checkout without its templates changed the system before rejection" "$(<"$SUDO_TEST_LOG")"
fi
grep -q "Update the checkout before switching to dev; it has no edge templates for aarch64" "$boundary_tmp/output" ||
fail "the rejection names the missing templates" "$(<"$boundary_tmp/output")"
assert_boundary_cold "checkout without templates"
pass "a dev checkout without its templates for the platform is rejected before linking"
reset_boundary
OMARCHY_PATH="$SUDO_TEST_HOME/omarchy" run_channel stable || fail "leaving dev failed" "$(<"$boundary_tmp/output")"
assert_scoped_channel "dev to stable"
+3
View File
@@ -125,6 +125,9 @@ for setup_command in \
ln -s omarchy-test-setup-call "$mock_bin/$setup_command"
done
# The real browser installer below calls the platform's post-install hook; on a
# Mac with the platform package installed that would run, so answer it here.
printf '#!/bin/bash\nexit 0\n' >"$mock_bin/omarchy-lifecycle-dispatch"
chmod +x "$mock_bin"/*
export HOME="$test_home"
+1 -1
View File
@@ -22,7 +22,7 @@ cat >"$mock_bin/omarchy-font-set" <<'SH'
printf 'font:%s\n' "$*" >>"$OMARCHY_TEST_LOG"
SH
for command in omarchy-pkg-aur-add omarchy-install-emacs omazed omarchy-theme-set-vscode omarchy-install-gaming-gpu-lib32; do
for command in omarchy-pkg-aur-add omarchy-install-emacs omazed omarchy-theme-set-vscode omarchy-install-gaming-gpu-lib32 omarchy-lifecycle-dispatch; do
cat >"$mock_bin/$command" <<'SH'
#!/bin/bash
exit 0
+34 -7
View File
@@ -212,11 +212,15 @@ pass "selection updates are atomic for concurrent readers"
lua <<'LUA'
local root = os.getenv("ROOT")
local configured, bindings = false, {}
local selected, installed, bindings = nil, true, {}
io.popen = function(command)
assert(command == "omarchy-default-dictation 2>/dev/null")
return { read = function() return selected end, close = function() end }
end
o = {
shell_succeeds = function(command)
assert(command == "omarchy-default-dictation")
return configured
cmd_missing = function(command)
assert(command == selected)
return not installed
end,
bind = function(...) table.insert(bindings, {...}) end,
bind_hold = function(keys, _, press, _, release, options)
@@ -226,9 +230,9 @@ o = {
}
dofile(root .. "/default/hypr/bindings/dictation.lua")
assert(#bindings == 0, "unconfigured dictation leaves application keys available")
configured = true
selected = "some-backend"
dofile(root .. "/default/hypr/bindings/dictation.lua")
assert(#bindings == 5, "any configured backend receives the same bindings")
assert(#bindings == 5, "any installed backend receives the same bindings")
local function binds(keys, command)
for _, binding in ipairs(bindings) do
if binding[1] == keys and binding[2] == command then return true end
@@ -238,8 +242,11 @@ for _, keys in ipairs({ "F9", "ALT + Alt_R" }) do
assert(binds(keys, "omarchy-dictation start") and binds(keys, "omarchy-dictation stop"),
keys .. " starts dictation on press and stops it on release")
end
bindings, installed = {}, false
dofile(root .. "/default/hypr/bindings/dictation.lua")
assert(#bindings == 0, "a selected backend that is not installed leaves application keys available")
LUA
pass "dictation shortcuts require a selection without limiting backend names"
pass "dictation shortcuts require an installed selection without limiting backend names"
# Fresh users receive the backend choice through the settings package's skel.
cp "$ROOT/config/omarchy/defaults/dictation" "$config"
@@ -247,6 +254,26 @@ cp "$ROOT/config/omarchy/defaults/dictation" "$config"
printf '%s\n' voxtype > "$config"
pass "fresh users get Superwhisper as their default backend"
# On aarch64, where Superwhisper has no build, user setup drops that preset; an
# owner's own selection, and every x86_64 preset, stay.
leaf_bin="$test_tmp/leaf-bin"
mkdir -p "$leaf_bin"
printf '#!/bin/bash\n[[ ${TEST_ARCH:-aarch64} == x86_64 ]]\n' >"$leaf_bin/omarchy-hw-x86"
printf '#!/bin/bash\n[[ -z ${TEST_HAS_SUPERWHISPER:-} ]]\n' >"$leaf_bin/omarchy-cmd-missing"
chmod +x "$leaf_bin"/*
run_leaf() { env "$@" PATH="$leaf_bin:$PATH" bash -c 'source "$1"' bash "$ROOT/install/user/dictation-default.sh"; }
cp "$ROOT/config/omarchy/defaults/dictation" "$config"
run_leaf
[[ ! -e $config ]] || fail "aarch64 user setup drops the Superwhisper preset"
printf '%s\n' voxtype >"$config"
run_leaf
[[ $(<"$config") == "voxtype" ]] || fail "aarch64 user setup keeps a selection someone made"
cp "$ROOT/config/omarchy/defaults/dictation" "$config"
run_leaf TEST_ARCH=x86_64
[[ $(<"$config") == "superwhisper" ]] || fail "x86_64 keeps the Superwhisper preset"
printf '%s\n' voxtype >"$config"
pass "aarch64 new users start with no dictation backend instead of an unbuildable one"
lua <<'LUA'
local root = os.getenv("ROOT")
package.path = root .. "/?.lua;" .. package.path
+819 -37
View File
@@ -4,46 +4,828 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir=$(mktemp -d)
trap 'rm -r "$tmp_dir"' EXIT
# omarchy-drive-password against fake drives. findmnt and lsblk describe a
# system drive under / and a data drive, and blkid has the empty cache a user
# sees until root runs blkid in that boot; cryptsetup is either a slot-table fake
# or the real binary on file-backed volumes; a chpasswd fake shows the login and
# root passwords never change. Each cryptsetup call is a crash point, so a run
# can be killed after every step and rerun, like a power loss. The runs repeat on an Apple fixture,
# where a fake boot package records the owner's slot through the real
# omarchy-lifecycle-dispatch; on the x86 fixture that is a no-op.
cat >"$tmp_dir/blkid" <<'EOF'
#!/bin/bash
echo /dev/test-luks
EOF
cat >"$tmp_dir/gum" <<'EOF'
#!/bin/bash
head -n 1 "$TEST_INPUTS"
sed -i '1d' "$TEST_INPUTS"
EOF
cat >"$tmp_dir/sudo" <<'EOF'
#!/bin/bash
printf '%s\n' "$@" >"$TEST_ARGS"
cat >"$TEST_STDIN"
EOF
chmod +x "$tmp_dir/blkid" "$tmp_dir/gum" "$tmp_dir/sudo"
export PATH="$tmp_dir:$ROOT/bin:$PATH"
export TEST_ARGS="$tmp_dir/args" TEST_INPUTS="$tmp_dir/inputs" TEST_STDIN="$tmp_dir/stdin"
printf '\n' >"$TEST_INPUTS"
if "$ROOT/bin/omarchy-drive-password" >/dev/null; then
fail "drive password rejects an empty passphrase"
# Root's dispatcher ignores the fixtures and sees this machine.
if (( EUID == 0 )) && [[ $("$ROOT/bin/omarchy-hw-platform") == "aarch64-apple" ]]; then
skip "running as root on Apple Silicon, where dispatch ignores fixtures; skipping"
exit 0
fi
[[ ! -e $TEST_ARGS ]] || fail "drive password does not run cryptsetup for an empty passphrase"
printf 'secret123\n*\n' >"$TEST_INPUTS"
if "$ROOT/bin/omarchy-drive-password" >/dev/null; then
fail "drive password rejects a mismatched confirmation"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
old_password=old-password
new_password='new pass:word'
recovery_key=recovery-passphrase
data_password=data-password
journal=$tmp/state/omarchy/drive-password.state
mkdir -p "$tmp/bin" "$tmp/real" "$tmp/dev"
cat >"$tmp/crash.sh" <<'SH'
crash_point() {
local count
count=$(( $(cat "$TEST_TMP/steps") + 1 ))
echo "$count" >"$TEST_TMP/steps"
printf '%s %s\n' "$count" "$1" >>"$TEST_TMP/trace"
if (( count == ${CRASH_AT:-0} )); then
kill -9 "$TEST_PID"
# An orphan outlives the script for a moment, like a cryptsetup child.
if [[ -n ${CRASH_ORPHAN:-} ]]; then sleep 1; else kill -9 $$; fi
fi
}
SH
cat >"$tmp/bin/blkid" <<'SH'
#!/bin/bash
exit 2
SH
cat >"$tmp/bin/findmnt" <<'SH'
#!/bin/bash
[[ $* == "-no SOURCE /" && -z ${TEST_FINDMNT_FAIL:-} ]] || exit 1
echo '/dev/mapper/root[/@]'
SH
cat >"$tmp/bin/lsblk" <<'SH'
#!/bin/bash
if [[ $1 == "-dno" && $2 == "UUID" ]]; then
cat "$3.uuid"
exit
fi
[[ ! -e $TEST_ARGS ]] || fail "drive password does not run cryptsetup for a mismatched confirmation"
if [[ $* == "-nrpo NAME,FSTYPE" ]]; then
echo "/dev/fake-disk "
while IFS= read -r drive; do
printf '%s crypto_LUKS\n/dev/mapper/%s btrfs\n' "$drive" "${drive##*/}"
done <"$TEST_TMP/drives"
exit
fi
[[ $* == "-nsrpo NAME,TYPE,FSTYPE /dev/mapper/root" ]] || exit 98
cat "$TEST_TMP/root-ancestry"
SH
printf 'new password\nnew password\n' >"$TEST_INPUTS"
"$ROOT/bin/omarchy-drive-password" >/dev/null
cat >"$tmp/bin/omarchy-drive-select" <<'SH'
#!/bin/bash
printf '%s\n' "$@" >"$TEST_TMP/offered"
cat "$TEST_TMP/select"
SH
[[ $(<"$TEST_STDIN") == "new password" ]] || fail "drive password passes the validated passphrase without a newline"
grep -F 'cryptsetup luksChangeKey' "$TEST_ARGS" >/dev/null || fail "drive password changes the LUKS key"
grep -Fx /dev/test-luks "$TEST_ARGS" >/dev/null || fail "drive password targets the selected drive"
pass "drive password rejects empty and mismatched passphrases and passes validated input to cryptsetup"
cat >"$tmp/bin/gum" <<'SH'
#!/bin/bash
[[ $1 == "input" ]] || exit 97
printf '%s\n' "$*" >>"$TEST_TMP/prompts"
[[ -s $TEST_TMP/inputs ]] || exit 130
# Another key enrolled while the owner types the new password.
if [[ -n ${TEST_ENROLL_AT_PROMPT:-} && $* == *"$TEST_ENROLL_AT_PROMPT"* ]]; then
printf '3\tlater-key\n' >>"$TEST_ENROLL_DEVICE.slots"
fi
head -n 1 "$TEST_TMP/inputs"
sed -i '1d' "$TEST_TMP/inputs"
SH
# Runs as the caller, closing every descriptor above stderr as sudo does.
cat >"$tmp/bin/sudo" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_TMP/sudo-calls"
for fd in /proc/$$/fd/*; do
fd=${fd##*/}
(( fd > 2 && fd != 255 )) && eval "exec $fd>&-" 2>/dev/null
done
exec "$@"
SH
cat >"$tmp/bin/chpasswd" <<'SH'
#!/bin/bash
source "$TEST_TMP/crash.sh"
IFS= read -r line
name=${line%%:*}
{
grep -v "^$name " "$TEST_TMP/accounts" || true
printf '%s\t%s\n' "$name" "${line#*:}"
} >"$TEST_TMP/accounts.next"
mv -f "$TEST_TMP/accounts.next" "$TEST_TMP/accounts"
SH
# A LUKS1-style volume as a table of "slot<TAB>passphrase" lines next to the
# device: luksChangeKey adds the new key to a free slot, then removes the old,
# and luksKillSlot needs a key that opens another slot, as cryptsetup does.
cat >"$tmp/bin/cryptsetup" <<'SH'
#!/bin/bash
source "$TEST_TMP/crash.sh"
slot_of() {
local slot key
while IFS=$'\t' read -r slot key; do
[[ $key == "$1" && $slot != "${2:-}" ]] && { echo "$slot"; return; }
done <"$slots"
}
op=$1
shift
key_file="" token_type="" key_slot="" positional=()
while (( $# )); do
case $1 in
--key-file) key_file=$2; shift 2 ;;
--token-type) token_type=$2; shift 2 ;;
--key-slot) key_slot=$2; shift 2 ;;
--pbkdf | --iter-time) shift 2 ;;
-*) shift ;;
*) positional+=("$1"); shift ;;
esac
done
slots=${positional[0]}.slots
[[ -f $slots ]] || { echo "Device ${positional[0]} does not exist or access denied."; exit 4; }
if [[ $key_file == "-" ]]; then key=$(cat); elif [[ -n $key_file ]]; then key=$(cat "$key_file"); fi
[[ $op != "isLuks" ]] || exit 1
if [[ $op == "luksDump" ]]; then
echo "Keyslots:"
cut -f1 "$slots" | sed 's/^/ /; s/$/: luks2/'
[[ -z ${TEST_TOKEN_SLOT:-} ]] || printf 'Tokens:\n 0: luks2-keyring\n\tKeyslot: %s\n' "$TEST_TOKEN_SLOT"
printf 'Digests:\n 0: pbkdf2\n'
exit 0
fi
crash_point "cryptsetup $op"
[[ $op != "open" || -z ${TEST_OPEN_FAIL:-} ]] || exit 1
# An enrolled token unlocks its slot whatever key is given, unless the allowed
# token types exclude it, as with cryptsetup.
if [[ $op == "open" && -z $token_type && -n ${TEST_TOKEN_SLOT:-} ]]; then
echo "Key slot $TEST_TOKEN_SLOT unlocked."
exit 0
fi
target=""
[[ $op == "luksKillSlot" ]] && target=${positional[1]}
if [[ $op == "open" && -n $key_slot ]]; then
grep -qxF "$key_slot"$'\t'"$key" "$slots" || { echo "No key available with this passphrase."; exit 2; }
echo "Key slot $key_slot unlocked."
exit 0
fi
slot=$(slot_of "$key" "$target")
[[ -n $slot ]] || { echo "No key available with this passphrase."; exit 2; }
case $op in
open) echo "Key slot $slot unlocked." ;;
luksChangeKey)
[[ -z ${TEST_CHANGE_FAIL:-} ]] || exit 1
for (( next = 0; next < 32; next++ )); do
cut -f1 "$slots" | grep -qx "$next" || break
done
printf '%s\t%s\n' "$next" "$(cat "${positional[1]}")" >>"$slots"
crash_point "new key added"
[[ -z ${TEST_CHANGE_PARTIAL:-} ]] || exit 137
awk -F'\t' -v s="$slot" '$1 != s' "$slots" >"$slots.next"
mv -f "$slots.next" "$slots"
crash_point "old key removed"
;;
luksKillSlot)
[[ -z ${TEST_KILL_FAIL:-} ]] || exit 1
awk -F'\t' -v s="$target" '$1 != s' "$slots" >"$slots.next"
mv -f "$slots.next" "$slots"
crash_point "slot $target killed"
;;
*) exit 1 ;;
esac
SH
# The real cryptsetup, keeping the command's KDF but at a fast cost, counting
# the same crash points.
cat >"$tmp/real/cryptsetup" <<'SH'
#!/bin/bash
source "$TEST_TMP/crash.sh"
args=()
while (( $# )); do
case $1 in
--iter-time) shift 2 ;;
*) args+=("$1"); shift ;;
esac
done
if [[ ${args[0]} == "luksChangeKey" ]]; then
if [[ " ${args[*]} " == *" argon2id "* ]]; then
args+=(--pbkdf-force-iterations 4 --pbkdf-memory 32768)
else
args+=(--pbkdf-force-iterations 1000)
fi
fi
[[ ${args[0]} == "luksDump" || ${args[0]} == "isLuks" ]] || crash_point "cryptsetup ${args[0]}"
[[ ${args[0]} != "open" || -z ${TEST_OPEN_FAIL:-} ]] || exit 1
status=0
"$REAL_CRYPTSETUP" "${args[@]}" || status=$?
if (( status == 0 )) && [[ ${args[0]} == "luksChangeKey" || ${args[0]} == "luksKillSlot" ]]; then
crash_point "${args[0]} done"
fi
exit "$status"
SH
chmod +x "$tmp"/bin/* "$tmp/real/cryptsetup"
# The boot package's luks-slots, run by dispatch with an empty environment: it
# records its arguments and is a crash point like the rest.
fake_platform "$tmp/x86" x86
fake_platform "$tmp/apple" aarch64-apple
mkdir -p "$tmp/lifecycle/usr/lib/omarchy/mac-boot"
cat >"$tmp/lifecycle/usr/lib/omarchy/mac-boot/luks-slots" <<SH
#!/bin/bash
# --owner names the recorded owner slot, read-only; setup recorded slot 0. A Mac
# set up before slots were recorded has none (4); a record the header doesn't
# hold is a failure (1).
if [[ \$1 == --owner ]]; then
[[ ! -e $tmp/owner-query-fail ]] || { echo "The LUKS header has no key in the recorded owner slot 0." >&2; exit 1; }
[[ ! -e $tmp/owner-query-garbage ]] || { echo "slot zero"; exit 0; }
[[ ! -e $tmp/owner-slot ]] || { cat "$tmp/owner-slot"; exit 0; }
[[ ! -e $tmp/owner-unrecorded ]] || { echo "No owner key slot is recorded." >&2; exit 4; }
echo 0
exit 0
fi
[[ ! -e $tmp/record-fail ]] || exit 1
printf '%s\n' "\$*" >>"$tmp/slot-record"
[[ \$1 != owner=* ]] || echo "\${1#owner=}" >"$tmp/owner-slot"
count=\$(( \$(cat "$tmp/steps") + 1 ))
echo "\$count" >"$tmp/steps"
printf '%s slots recorded\n' "\$count" >>"$tmp/trace"
if (( count == \$(cat "$tmp/crash-at") )); then
kill -9 "\$PPID"
kill -9 \$\$
fi
SH
chmod 755 "$tmp/lifecycle/usr/lib/omarchy/mac-boot/luks-slots"
chmod -R go-w "$tmp/lifecycle"
export TEST_TMP=$tmp OMARCHY_PATH=$ROOT XDG_STATE_HOME=$tmp/state SUDO_USER=owner OMARCHY_LIFECYCLE_ROOT=$tmp/lifecycle
base_path=$PATH
platform=x86
# The command's PATH for this backend on this platform fixture.
use() {
backend=$1 platform=$2
export OMARCHY_PROC_ROOT=$tmp/$platform/proc
if [[ $backend == "fake" ]]; then
export PATH="$tmp/$platform/bin:$tmp/bin:$ROOT/bin:$base_path"
else
export PATH="$tmp/$platform/bin:$tmp/real:$tmp/bin:$ROOT/bin:$base_path"
fi
}
data=$tmp/dev/data
# The slot the key opens, or nothing.
opens() {
local device=$1 key=$2 slot k
if [[ $backend == "fake" ]]; then
while IFS=$'\t' read -r slot k; do
[[ $k == "$key" ]] && { echo "$slot"; return; }
done <"$device.slots"
else
LC_ALL=C "$REAL_CRYPTSETUP" open --test-passphrase --verbose --key-file <(printf '%s' "$key") "$device" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' || true
fi
}
account() {
awk -F'\t' -v n="$1" '$1 == n { print $2 }' "$tmp/accounts"
}
volume() {
local device=$1 key=$2 extra=${3:-}
echo "uuid-$(basename "$device")" >"$device.uuid"
if [[ $backend == "fake" ]]; then
: >"$device"
printf '0\t%s\n' "$key" >"$device.slots"
[[ -z $extra ]] || printf '1\t%s\n' "$extra" >>"$device.slots"
else
rm -f "$device"
truncate -s 32M "$device"
"$REAL_CRYPTSETUP" luksFormat -q --type "$backend" --pbkdf pbkdf2 --pbkdf-force-iterations 1000 "$device" <(printf '%s' "$key")
if [[ -n $extra ]]; then
"$REAL_CRYPTSETUP" luksAddKey --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --key-file <(printf '%s' "$key") "$device" <(printf '%s' "$extra")
fi
fi
}
# / on the system drive, which also holds a recovery key; a data drive beside it.
fixture() {
rm -rf "$tmp/state" "$tmp/output" "$tmp/trace" "$tmp/offered" "$tmp"/dev/* "$tmp/slot-record" "$tmp/record-fail" "$tmp/owner-slot" "$tmp/owner-query-fail" "$tmp/owner-query-garbage" "$tmp/owner-unrecorded"
unset TEST_ENROLL_AT_PROMPT TEST_ENROLL_DEVICE TEST_OPEN_FAIL TEST_CHANGE_FAIL TEST_CHANGE_PARTIAL TEST_KILL_FAIL TEST_FINDMNT_FAIL TEST_TOKEN_SLOT CRASH_ORPHAN
system=$tmp/dev/system
recovery=${1-$recovery_key}
volume "$system" "$old_password" "$recovery"
volume "$data" "$data_password"
printf 'owner\t%s\nroot\t%s\n' "$old_password" "$old_password" >"$tmp/accounts"
printf '%s\n%s\n' "$system" "$data" >"$tmp/drives"
printf '/dev/mapper/root crypt btrfs\n%s part crypto_LUKS\n/dev/fake-disk disk \n' "$system" >"$tmp/root-ancestry"
echo "$system" >"$tmp/select"
: >"$tmp/prompts"
: >"$tmp/sudo-calls"
}
# One run of the command: an optional crash step, then answers for its prompts.
attempt() {
local crash_at=$1
shift
if (( $# )); then printf '%s\n' "$@" >"$tmp/inputs"; else : >"$tmp/inputs"; fi
echo 0 >"$tmp/steps"
echo "$crash_at" >"$tmp/crash-at"
: >"$tmp/trace"
{
CRASH_AT=$crash_at bash -c 'TEST_PID=$$ exec bash ${TEST_TRACE:+-x} "$0"' "$ROOT/bin/omarchy-drive-password"
} >>"$tmp/output" 2>&1
}
consistent() {
local context=$1 password=$2 other=$old_password
[[ $password == "$old_password" ]] && other=$new_password
untouched_accounts "$context"
[[ -n $(opens "$system" "$password") ]] || fail "$backend: $context: the disk opens with that password" "$(cat "$tmp/output")"
[[ -z $(opens "$system" "$other") ]] || fail "$backend: $context: the other password no longer opens the disk" "$(cat "$tmp/trace" "$tmp/output")"
[[ -z $recovery || -n $(opens "$system" "$recovery") ]] || fail "$backend: $context: the recovery key still opens the disk"
[[ ! -e $journal ]] || fail "$backend: $context: the journal is gone" "$(cat "$journal")"
recorded "$context" "$password"
}
# On Apple the boot package holds the slot the owner's password opens: recorded
# whenever the key moved. x86 records nothing and runs no extra sudo.
recorded() {
local context=$1 password=$2 slot
if [[ $platform == "apple" ]]; then
slot=$(opens "$system" "$password")
if [[ -s $tmp/slot-record ]]; then
[[ $(tail -n 1 "$tmp/slot-record") == "owner=$slot" ]] ||
fail "$backend: apple: $context: the owner's slot is recorded" "$(cat "$tmp/slot-record")"
else
[[ $slot == "0" ]] || fail "$backend: apple: $context: a key that moved is recorded" "$(cat "$tmp/trace")"
fi
else
[[ ! -e $tmp/slot-record ]] && ! grep -q 'lifecycle-dispatch' "$tmp/sudo-calls" ||
fail "$backend: x86: $context: no slot is recorded and no dispatch runs under sudo"
fi
}
# The disk password never changes the login or root password.
untouched_accounts() {
[[ $(account owner) == "$old_password" && $(account root) == "$old_password" ]] && ! grep -q 'chpasswd' "$tmp/sudo-calls" ||
fail "$backend: $1: the login and root passwords stay as they were" "$(cat "$tmp/accounts" "$tmp/sudo-calls")"
}
said() {
grep -Fq "$1" "$tmp/output" || fail "$backend: the command says: $1" "$(cat "$tmp/output")"
}
no_secrets() {
local file
for file in "$journal" "$tmp/sudo-calls" "$tmp/prompts"; do
[[ -e $file ]] || continue
! grep -Fq -e "$old_password" -e "$new_password" -e "$recovery_key" -e "$data_password" "$file" ||
fail "$backend: $1: $(basename "$file") holds no password" "$(cat "$file")"
done
}
# Between runs the disk opens with the old or the new password, and the
# accounts never change.
recoverable() {
local context=$1
[[ -n $(opens "$system" "$old_password") || -n $(opens "$system" "$new_password") ]] ||
fail "$backend: $context: the disk opens with the old or the new password" "$(cat "$tmp/trace")"
untouched_accounts "$context"
[[ ! -e $journal || $(stat -c %a "$journal") == "600" ]] || fail "$backend: $context: the journal is private"
no_secrets "$context"
}
backends=(fake)
if REAL_CRYPTSETUP=$(command -v cryptsetup); then
export REAL_CRYPTSETUP
backends+=(luks2 luks1)
else
skip "cryptsetup is not installed; skipping the file-backed volume runs"
fi
matrix=()
for backend in "${backends[@]}"; do
matrix+=("$backend x86" "$backend apple")
done
for run_spec in "${matrix[@]}"; do
use $run_spec
fixture
attempt 0 "$old_password" "$new_password" "$new_password" || fail "$backend: changing the system disk password succeeds" "$(cat "$tmp/output")"
consistent "uninterrupted" "$new_password"
[[ $(opens "$data" "$data_password") == "0" ]] || fail "$backend: the data drive is untouched"
! grep -Fq 'login password' "$tmp/output" || fail "$backend: the command does not mention the login password" "$(cat "$tmp/output")"
no_secrets "uninterrupted"
total_steps=$(cat "$tmp/steps")
(( total_steps >= 4 )) || fail "$backend: every change is a crash point" "$(cat "$tmp/trace")"
pass "$backend: the system disk key changes and verifies, and the login and root passwords stay as they were"
fixture
if attempt 0 "not-the-password" "$new_password" "$new_password"; then fail "$backend: a wrong current password fails the command"; fi
said "That password does not open $system."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "$backend: a wrong current password changes nothing"
[[ -n $(opens "$system" "$old_password") && $(account owner) == "$old_password" ]] || fail "$backend: a wrong current password leaves the disk and accounts"
pass "$backend: a wrong current password is refused before anything changes"
fixture ""
attempt 0 "$old_password" "$new_password" "$new_password" || fail "$backend: a single-slot system disk changes" "$(cat "$tmp/output")"
consistent "single slot" "$new_password"
pass "$backend: a disk with one key slot keeps it through the change"
fixture
echo "$data" >"$tmp/select"
attempt 0 "$data_password" "$new_password" "$new_password" || fail "$backend: changing a data drive password succeeds" "$(cat "$tmp/output")"
[[ -n $(opens "$data" "$new_password") && -z $(opens "$data" "$data_password") ]] || fail "$backend: the data drive takes the new password"
untouched_accounts "data drive"
[[ -n $(opens "$system" "$old_password") ]] || fail "$backend: a data drive change leaves the system disk alone"
[[ ! -e $journal ]] || fail "$backend: a data drive change keeps no journal"
pass "$backend: changing a non-root encrypted drive's password leaves the system disk alone"
# Killed after each step, the owner runs the command again and answers with a
# password that opens the disk now: the new one, or the old one while it still
# works. Each rerun is itself killed after each of its steps until one finishes.
for (( step = 1; step <= total_steps; step++ )); do
for answer in "$new_password" "$old_password"; do
fixture
if attempt "$step" "$old_password" "$new_password" "$new_password"; then fail "$backend: the run is killed at step $step"; fi
point=$(sed -n "${step}p" "$tmp/trace")
recoverable "killed after '$point'"
if [[ ! -e $journal ]]; then
if [[ -n $(opens "$system" "$new_password") ]]; then
consistent "killed after '$point'" "$new_password"
else
consistent "killed after '$point'" "$old_password"
fi
continue
fi
[[ -n $(opens "$system" "$answer") ]] || continue
if grep -qx 'phase=record' "$journal" && [[ $answer == "$old_password" ]]; then
if attempt 0 "$answer"; then fail "$backend: after '$point' a confirmed change refuses the old password"; fi
[[ -e $journal ]] || fail "$backend: after '$point' a refused rerun keeps the journal"
answer=$new_password
fi
for (( again = 1; ; again++ )); do
status=0
attempt "$again" "$answer" || status=$?
(( status == 0 )) && break
(( status == 137 )) || fail "$backend: the rerun after '$point' finishes" "$(cat "$tmp/output")"
recoverable "rerun after '$point' killed at step $again"
done
consistent "rerun with $answer after '$point'" "$answer"
no_secrets "rerun after '$point'"
done
done
pass "$backend $platform: killed after each of $total_steps steps, and each rerun after each of its own, the disk ends on one password and the accounts never change"
done
use fake x86
fixture
printf '%s\n%s\n%s\n' "$system" "$data" "$data" >"$tmp/drives"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a fresh boot finds the system disk without a blkid cache" "$(cat "$tmp/output")"
[[ $(cat "$tmp/offered") == "$system"$'\n'"$data" ]] || fail "each LUKS drive lsblk names is offered once" "$(cat "$tmp/offered")"
consistent "fresh boot" "$new_password"
pass "before root has run blkid, every LUKS drive is offered once and the system disk changes"
fixture
printf '5\t%s\n' tpm-sealed-key >>"$system.slots"
export TEST_TOKEN_SLOT=5
if attempt 0 "not-the-password" "$new_password" "$new_password"; then fail "an enrolled token does not stand in for the current password"; fi
said "That password does not open $system."
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a disk with an enrolled token changes its password" "$(cat "$tmp/output")"
consistent "beside an enrolled token" "$new_password"
[[ $(opens "$system" tpm-sealed-key) == "5" ]] || fail "the token's slot is left alone"
pass "an enrolled token never answers for a password, and its slot is left alone"
fixture
export TEST_CHANGE_FAIL=1
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a failed luksChangeKey fails the command"; fi
[[ -n $(opens "$system" "$old_password") && ! -e $journal ]] || fail "a failed luksChangeKey leaves the disk and no journal"
said "The disk password did not change."
pass "a failed LUKS change leaves the disk on its old password"
fixture
export TEST_CHANGE_PARTIAL=1
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a luksChangeKey that dies after adding the new key is finished" "$(cat "$tmp/output")"
consistent "luksChangeKey died after adding the new key" "$new_password"
pass "a luksChangeKey that dies between keys has its old key retired"
fixture
export TEST_CHANGE_PARTIAL=1 TEST_KILL_FAIL=1
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a failed retirement fails the command"; fi
[[ -e $journal ]] || fail "a failed retirement keeps the journal"
unset TEST_CHANGE_PARTIAL TEST_KILL_FAIL
if attempt 0 "$recovery_key"; then fail "an unconfirmed change refuses the recovery key"; fi
said "That password does not finish the change"
[[ -e $journal && -n $(opens "$system" "$recovery_key") ]] || fail "refusing the recovery key changes nothing"
attempt 0 "$new_password" || fail "the rerun retires the old key and finishes" "$(cat "$tmp/output")"
consistent "rerun after a failed retirement" "$new_password"
pass "before the change is confirmed a rerun refuses the recovery key and retires the old key"
fixture
export TEST_CHANGE_PARTIAL=1 TEST_KILL_FAIL=1
attempt 0 "$old_password" "$new_password" "$new_password" || true
unset TEST_CHANGE_PARTIAL TEST_KILL_FAIL
attempt 0 "$old_password" || fail "the rerun with the old password rolls back" "$(cat "$tmp/output")"
consistent "rollback with the old password" "$old_password"
pass "a rerun with the old password removes the half-added new key"
# On Apple the slot record follows the confirmed change: a crash there leaves
# the confirmed phase.
use fake apple
fixture
attempt 0 "$old_password" "$new_password" "$new_password"
record_step=$(grep -n 'slots recorded' "$tmp/trace" | cut -d: -f1)
fixture
attempt "$record_step" "$old_password" "$new_password" "$new_password" || true
grep -qx 'phase=record' "$journal" || fail "a crash at the slot record leaves the confirmed phase" "$(cat "$journal" "$tmp/trace")"
if attempt 0 "$recovery_key"; then fail "a confirmed change refuses the recovery key"; fi
said "That password does not finish the change"
[[ -e $journal ]] || fail "refusing the recovery key keeps the journal"
if attempt 0 "not-the-password"; then fail "a rerun refuses a password that opens nothing"; fi
for suffix in "" .slots .uuid; do
mv "$system$suffix" "$tmp/dev/renumbered$suffix"
done
system=$tmp/dev/renumbered
printf '/dev/mapper/root crypt btrfs\n%s part crypto_LUKS\n/dev/fake-disk disk \n' "$system" >"$tmp/root-ancestry"
attempt 0 "$new_password" || fail "the confirmed change finishes on a renumbered disk" "$(cat "$tmp/output")"
consistent "rerun after a confirmed change" "$new_password"
pass "apple: once the new key is confirmed only its slot finishes the change, on the disk found by UUID"
fixture
export TEST_OPEN_FAIL=1
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a disk that cannot be checked fails the command"; fi
said "Could not check the password against $system."
! grep -q 'luksChangeKey' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "a disk that cannot be checked is not changed"
pass "the current password is checked before anything changes"
fixture
attempt "$record_step" "$old_password" "$new_password" "$new_password" || true
export TEST_FINDMNT_FAIL=1
if attempt 0 "$new_password"; then fail "a run that cannot find / fails"; fi
said "Could not tell which drive holds the system."
[[ -e $journal ]] || fail "a run that cannot find / keeps the journal"
unset TEST_FINDMNT_FAIL
attempt 0 "$new_password" || fail "the rerun finishes once / is found" "$(cat "$tmp/output")"
consistent "rerun after a failed root lookup" "$new_password"
pass "apple: a failed root lookup changes nothing and keeps the journal"
fixture
attempt "$record_step" "$old_password" "$new_password" "$new_password" || true
sed -i 's/^phase=record$/phase=accounts/' "$journal"
grep -qx 'phase=accounts' "$journal" || fail "the journal is rewritten as an earlier version wrote it" "$(cat "$journal")"
if attempt 0 "$old_password"; then fail "an earlier version's confirmed change refuses the old password"; fi
attempt 0 "$new_password" || fail "an earlier version's confirmed change finishes" "$(cat "$tmp/output")"
consistent "journal from an earlier version" "$new_password"
pass "apple: a confirmed change journaled by an earlier version records its slot and leaves the accounts alone"
use fake x86
fixture
attempt 0 "$old_password" "$new_password" "$new_password"
change_step=$(grep -n 'cryptsetup luksChangeKey' "$tmp/trace" | cut -d: -f1)
fixture
export CRASH_ORPHAN=1
attempt "$change_step" "$old_password" "$new_password" "$new_password" || true
unset CRASH_ORPHAN
if attempt 0 "$old_password"; then fail "a rerun waits for a luksChangeKey the killed run left behind"; fi
attempt 0 "$new_password" || fail "the rerun finishes after the orphaned change" "$(cat "$tmp/output")"
consistent "orphaned luksChangeKey" "$new_password"
pass "a luksChangeKey that outlives its run finishes before the rerun reads the disk"
fixture
printf '/dev/mapper/root crypt btrfs\n%s part \n/dev/fake-disk disk \n' "$system" >"$tmp/root-ancestry"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a crypt layer without a named LUKS partition fails"; fi
said "Could not tell which drive holds the system."
! grep -q 'luksChangeKey' "$tmp/sudo-calls" || fail "an unresolved crypt layer changes nothing"
pass "a crypt layer whose LUKS partition cannot be named fails closed"
fixture
mkdir -p "${journal%/*}"
printf 'uuid=uuid-system\nold_slot=0\nslots=\nphase=luks\n' >"$journal"
if attempt 0 "$recovery_key"; then fail "a journal without slots refuses to finish"; fi
said "is unreadable"
[[ -e $journal && -n $(opens "$system" "$old_password") ]] || fail "an unreadable journal changes nothing"
pass "a journal without its slot list is refused rather than trusted"
fixture
export TEST_CHANGE_PARTIAL=1 TEST_KILL_FAIL=1
attempt 0 "$old_password" "$new_password" "$new_password" || true
unset TEST_CHANGE_PARTIAL TEST_KILL_FAIL
printf '3\tlater-key\n' >>"$system.slots"
if attempt 0 "$new_password"; then fail "a disk that gained two slots refuses to finish"; fi
said "changed since the interrupted change"
[[ -e $journal && -n $(opens "$system" "later-key") && -n $(opens "$system" "$old_password") ]] || fail "a disk that gained two slots is left alone"
pass "a rerun never removes a slot enrolled after the interrupted change"
fixture
mkdir -p "${journal%/*}"
exec {held}>"${journal%/*}/drive-password.lock"
flock -n "$held"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a second run waits for the first"; fi
exec {held}>&-
grep -Fq 'Another drive password change is running.' "$tmp/output" && ! grep -q 'luksChangeKey' "$tmp/sudo-calls" ||
fail "a second run changes nothing while another holds the lock"
pass "two runs never interleave"
fixture
export TEST_TRACE=1
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a traced run succeeds" "$(cat "$tmp/output")"
unset TEST_TRACE
! grep -Fq -e "$old_password" -e "$new_password" "$tmp/output" || fail "tracing never shows a password" "$(cat "$tmp/output")"
pass "bash -x does not trace passwords"
fixture
printf '/dev/mapper/root lvm btrfs\n/dev/mapper/cryptlvm crypt LVM2_member\n%s part crypto_LUKS\n/dev/fake-disk disk \n' "$system" >"$tmp/root-ancestry"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "an LVM-on-LUKS system disk changes" "$(cat "$tmp/output")"
consistent "LVM on LUKS" "$new_password"
pass "the system disk is found beneath an LVM volume group"
fixture
printf '/dev/mapper/root part ext4\n/dev/fake-disk disk \n' >"$tmp/root-ancestry"
echo "$data" >"$tmp/drives"
attempt 0 "$data_password" "$new_password" "$new_password" || fail "an unencrypted root still changes a data drive" "$(cat "$tmp/output")"
[[ -n $(opens "$data" "$new_password") ]] || fail "an unencrypted root still changes the data drive"
untouched_accounts "unencrypted root"
pass "with / unencrypted, every encrypted drive changes alone"
fixture
mkdir -p "${journal%/*}"
printf 'uuid=uuid-elsewhere\nphase=record\nslot=0\n' >"$journal"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a journal for another disk does not block a change" "$(cat "$tmp/output")"
consistent "stale journal" "$new_password"
pass "a journal for a disk that is not / is dropped"
fixture
if attempt 0 "$old_password" ""; then fail "an empty password is refused"; fi
said "Password cannot be empty."
if attempt 0 "$old_password" "secret123" "*"; then fail "a mismatched confirmation is refused"; fi
said "Passwords do not match."
if attempt 0 "$old_password" "$old_password" "$old_password"; then fail "the current password is refused as the new one"; fi
said "The new password is the current one."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] ||
fail "refused passwords change nothing"
pass "drive password rejects empty, mismatched and unchanged passphrases before changing anything"
# A change interrupted part way is finished from the one slot the password
# opened, so a system disk whose password is in two slots is refused before
# anything changes: the other copy would outlive the change.
use fake x86
fixture "$old_password"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a password in two slots is refused"; fi
said "That password also opens key slot 1 on $system"
said "The system disk password did not change."
! grep -q 'luksChangeKey' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "a password in two slots changes nothing"
[[ $(opens "$system" "$old_password") == "0" ]] && grep -qxF $'1\t'"$old_password" "$system.slots" || fail "both copies are left as they were"
pass "a system disk whose password opens two key slots is refused before anything changes"
# A change journaled by an earlier version did not check the old password for
# other slots. With other slots on the disk a copy of it may have outlived the
# change, and no password entered now can show whose slot is whose, so it is
# left for the owner to settle by hand, in every phase. With one slot it settles.
for legacy_phase in luks record accounts; do
fixture ""
printf '0\t%s\n2\t%s\n' "$new_password" "$old_password" >"$system.slots"
mkdir -p "${journal%/*}"
printf 'uuid=uuid-system\nold_slot=0\nslots=0,2\nslot=0\nphase=%s\n' "$legacy_phase" >"$journal"
if attempt 0 "$new_password" "$old_password"; then fail "an earlier version's $legacy_phase change on a disk with other slots is not settled"; fi
said "may still hold a copy of the old password"
said "Settle it by hand"
! grep -q 'luksKillSlot\|luksChangeKey' "$tmp/sudo-calls" && [[ -e $journal && -n $(opens "$system" "$old_password") ]] ||
fail "an earlier version's $legacy_phase change is left as it was" "$(cat "$tmp/sudo-calls")"
done
fixture ""
printf '0\t%s\n' "$new_password" >"$system.slots"
mkdir -p "${journal%/*}"
printf 'uuid=uuid-system\nold_slot=0\nslots=0\nphase=luks\n' >"$journal"
attempt 0 "$new_password" || fail "an earlier version's change on a one-slot disk settles" "$(cat "$tmp/output")"
[[ ! -e $journal && $(opens "$system" "$new_password") == "0" ]] || fail "the one-slot change is settled"
pass "an earlier version's interrupted change is left to the owner when other slots remain, and settles on a one-slot disk"
# The new key's slot is how the change is followed, so a new password that
# already opens a slot (another key on the disk) is refused.
fixture
if attempt 0 "$old_password" "$recovery_key" "$recovery_key"; then fail "a new password that opens a slot already is refused"; fi
said "That password already opens a key slot on $system. Choose another."
! grep -q 'luksChangeKey' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "an enrolled new password changes nothing"
echo "$data" >"$tmp/select"
volume "$data" "$data_password" "$new_password"
if attempt 0 "$data_password" "$new_password" "$new_password"; then fail "a data drive refuses a new password it already has"; fi
said "That password already opens a key slot on $data. Choose another."
pass "a new password that already opens a key slot is refused, on the system disk and data drives"
# Only the owner's slot changes the system disk. A second key an earlier setup
# added (a recovery key) is refused before anything changes, and so is a change
# the boot package cannot vouch for.
use fake apple
fixture
if attempt 0 "$recovery_key" "$new_password" "$new_password"; then fail "apple: a key in another slot is refused"; fi
said "That password opens another key slot on the system disk"
said "The system disk password did not change."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal && ! -e $tmp/slot-record ]] || fail "apple: refusing another slot's key changes nothing"
[[ -n $(opens "$system" "$recovery_key") ]] || fail "apple: the other key still opens the disk"
consistent "another slot's key refused" "$old_password"
touch "$tmp/owner-query-fail"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "apple: a boot package that cannot name the owner's slot stops the change"; fi
said "Could not read which key slot holds the system disk password: The LUKS header has no key in the recorded owner slot 0."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "apple: an unanswered owner query changes nothing"
consistent "owner query failed" "$old_password"
rm "$tmp/owner-query-fail"
touch "$tmp/owner-query-garbage"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "apple: an owner slot that is not a number stops the change"; fi
said "named 'slot zero', which is not a key slot"
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "apple: a garbled owner slot changes nothing"
rm "$tmp/owner-query-garbage"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "apple: the owner's password changes the disk" "$(cat "$tmp/output")"
consistent "owner's password after refusals" "$new_password"
pass "apple: only the owner's slot changes the system disk; another slot's key and an unanswered query change nothing"
# A Mac set up before its boot package recorded slots has no owner slot (4).
# Its disk's only key is the owner's: the change goes through and records the
# new slot. With another key beside it the owner's can't be told, so the change
# is refused, saying how to settle it: record both, or remove the extra key.
use fake apple
fixture ""
touch "$tmp/owner-unrecorded"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "apple: an unrecorded single-key disk changes its password" "$(cat "$tmp/output")"
[[ $(cat "$tmp/slot-record") == "owner=$(opens "$system" "$new_password")" ]] || fail "apple: the owner's new slot is recorded" "$(cat "$tmp/slot-record")"
consistent "no owner slot recorded, one key" "$new_password"
step=$(grep -n 'cryptsetup' "$tmp/trace" | tail -1 | cut -d: -f1)
fixture ""
touch "$tmp/owner-unrecorded"
if attempt "$step" "$old_password" "$new_password" "$new_password"; then fail "apple: the unrecorded change is killed before the slot record"; fi
[[ -e $journal && ! -e $tmp/slot-record ]] || fail "apple: a change killed before the slot record keeps its journal" "$(cat "$tmp/trace")"
attempt 0 "$new_password" || fail "apple: the rerun finishes an unrecorded change" "$(cat "$tmp/output")"
[[ $(cat "$tmp/slot-record") == "owner=$(opens "$system" "$new_password")" ]] || fail "apple: the rerun records the owner's new slot" "$(cat "$tmp/slot-record")"
consistent "no owner slot recorded, interrupted" "$new_password"
pass "apple: with no owner slot recorded, the disk's only key changes and its new slot is recorded, after an interruption too"
fixture
touch "$tmp/owner-unrecorded"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "apple: an unrecorded disk with two keys refuses the change"; fi
said "has keys in slots 0,1, so which one is yours can't be told"
said "record both: sudo omarchy-lifecycle-dispatch luks-slots owner=0 recovery=1"
said "sudo cryptsetup luksKillSlot $system <slot>"
said "The system disk password did not change."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal && ! -e $tmp/slot-record ]] || fail "apple: an unrecorded two-key disk changes nothing"
"$ROOT/bin/omarchy-lifecycle-dispatch" luks-slots owner=0 recovery=1
: >"$tmp/slot-record"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "apple: once both slots are recorded the change goes through" "$(cat "$tmp/output")"
consistent "slots recorded as the refusal said" "$new_password"
pass "apple: with no owner slot recorded, a disk with another key refuses the change and says how to record or remove it"
fixture ""
touch "$tmp/owner-unrecorded"
export TEST_ENROLL_AT_PROMPT="Confirm new encryption password" TEST_ENROLL_DEVICE=$system
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "apple: a key enrolled during the prompts stops an unrecorded change"; fi
said "changed while you entered the new password"
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal && ! -e $tmp/slot-record ]] || fail "apple: a key enrolled during the prompts changes nothing"
pass "apple: an unrecorded disk that gains a key while the owner types is left alone"
# A slot the boot package could not record keeps the journal: the rerun records it.
use fake apple
fixture
touch "$tmp/record-fail"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a failed record fails the command"; fi
said "Could not record the new key slot for the boot checks."
[[ -e $journal && ! -e $tmp/slot-record ]] || fail "a failed record keeps the journal"
rm "$tmp/record-fail"
attempt 0 "$new_password" || fail "the rerun records the slot" "$(cat "$tmp/output")"
[[ -s $tmp/slot-record ]] || fail "the rerun records the moved key's slot"
consistent "rerun after a failed record" "$new_password"
pass "apple: the change finishes only once the boot package recorded the owner's new slot"
# An installed boot package older than luks-slots could not record the slot, so
# the system disk does not change; a data drive still does.
fixture
mv "$tmp/lifecycle/usr/lib/omarchy/mac-boot/luks-slots" "$tmp/luks-slots.off"
mkdir -p "$tmp/lifecycle/var/lib/pacman/local/omarchy-mac-boot-20260925-2"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "an Apple boot package without luks-slots fails the command"; fi
said "which omarchy-mac-boot 20260925-2 does not provide; update omarchy-mac-boot"
said "The system disk password did not change."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "a boot package without luks-slots changes nothing"
consistent "boot package without luks-slots" "$old_password"
echo "$data" >"$tmp/select"
attempt 0 "$data_password" "$new_password" "$new_password" || fail "a data drive changes without luks-slots" "$(cat "$tmp/output")"
pass "apple: a boot package without luks-slots stops a system disk change before it starts, naming the package"
# A Mac without its boot package at all predates it: nothing checks its slots,
# so the change goes through and records nothing.
fixture
rm -r "$tmp/lifecycle/var/lib/pacman"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a Mac without the boot package changes its disk password" "$(cat "$tmp/output")"
# Checked as on x86: no slot recorded and no dispatch under sudo.
platform=x86
consistent "no boot package" "$new_password"
platform=apple
! grep -q 'Error:' "$tmp/output" || fail "without the boot package nothing is reported" "$(cat "$tmp/output")"
mv "$tmp/luks-slots.off" "$tmp/lifecycle/usr/lib/omarchy/mac-boot/luks-slots"
pass "apple: a Mac without its boot package changes its disk password and records no slot"
fixture
echo "$data" >"$tmp/select"
attempt 0 "$data_password" "$new_password" "$new_password" || fail "apple: a data drive changes" "$(cat "$tmp/output")"
[[ ! -e $tmp/slot-record && -n $(opens "$data" "$new_password") ]] || fail "apple: a data drive records no slot"
pass "apple: only the system disk's slot is recorded"
+339
View File
@@ -0,0 +1,339 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Factory reset's boot step through the real omarchy-lifecycle-dispatch. On an
# x86 fixture the generic Limine UKI path runs and the boot-package entrypoints
# on disk never do. On an Apple fixture a fake boot package owns the factory
# root's boot chain: it prepares and verifies it before the throwaway slot is
# added, gets the key on standard input after the switch, and rolls back when
# the reset fails before the switch. Subvolumes are directories, and
# cryptsetup is a slot-table fake.
require_platform_fixtures "factory reset through lifecycle dispatch"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
fake_platform "$tmp/x86" x86
fake_platform "$tmp/apple" aarch64-apple
# Load the production functions without self-elevation or the reset entrypoint.
awk '
/^[a-z_]+\(\) \{/ { copying = 1 }
copying { print }
/^}/ { copying = 0 }
' "$ROOT/bin/omarchy-system-factory-reset" >"$tmp/functions"
# The fake boot package: each entrypoint records its call, reset-commit its
# standard input, and reset-verify fails on request.
lifecycle=$tmp/lifecycle
mac_boot=$lifecycle/usr/lib/omarchy/mac-boot
mkdir -p "$mac_boot"
for operation in reset-prepare reset-verify reset-commit reset-rollback; do
cat >"$mac_boot/$operation" <<SH
#!/bin/bash
echo "$operation\${*:+ \$*}" >>"$tmp/calls"
[[ $operation != reset-commit ]] || cat >"$tmp/committed-key"
[[ ! -e $tmp/$operation-fail ]] || { echo "$operation: the fixture refuses" >&2; exit 1; }
SH
done
chmod 755 "$mac_boot"/*
chmod -R go-w "$lifecycle"
cat >"$tmp/reset" <<'SH'
#!/bin/bash
set -euo pipefail
source "$TMP/functions"
TOP_MNT=$TMP/top
NEXT_NAME=@omarchy-reset-next
PROVISIONING_DIR=/var/lib/omarchy/provisioning
LOG_FILE=$TMP/reset.log
DISPATCH=${TEST_DISPATCH:-$ROOT/bin/omarchy-lifecycle-dispatch}
RESET_BOOT=""
RESET_BOOT_ERROR=""
swap_done=0
gum() {
if [[ $1 == "input" ]]; then
printf '%s\n' "$CURRENT"
elif [[ $1 == "style" ]]; then
printf '%s\n' "${!#}" >>"$TMP/screen"
fi
}
encrypted_install() { [[ -n $DEVICE ]]; }
luks_device() { echo "$DEVICE"; }
scrub_factory_accounts() { :; }
sanitize_factory_baseline() { :; }
install_provisioning_units() { :; }
systemd-id128() { printf '%032d\n' 7; }
sync() { :; }
mountpoint() { return 0; }
mount() { echo "mount $*" >>"$TMP/calls"; }
umount() { :; }
chroot() { echo "chroot ${*:2}" >>"$TMP/calls"; }
reset_limine_config() { echo "limine menu reset" >>"$TMP/calls"; }
verify_limine_hashes() { :; }
btrfs() {
case "$1 $2" in
"subvolume snapshot") cp -a "$3" "$4" ;;
"subvolume delete") rm -rf "${!#}" ;;
"subvolume show") return 1 ;;
esac
}
mv() {
if [[ -e $TMP/switch-fail && $1 == "$TOP_MNT/@" ]]; then
echo "mv: the fixture refuses the switch" >&2
return 1
fi
if [[ -e $TMP/activate-fail && $1 == "$TOP_MNT/$NEXT_NAME" ]]; then
echo "mv: the fixture refuses the activation" >&2
return 1
fi
if [[ -e $TMP/restore-fail && $1 == "$TOP_MNT"/@omarchy-old-* ]]; then
echo "mv: the fixture refuses the restore" >&2
return 1
fi
command mv "$@"
}
# "<slot> <key>" per line.
cryptsetup() {
local op=$1 key_file="" slot material next
local -a args=()
shift
while (( $# )); do
case $1 in
--key-file) key_file=$2; shift 2 ;;
--token-type) shift 2 ;;
-*) shift ;;
*) args+=("$1"); shift ;;
esac
done
echo "cryptsetup $op" >>"$TMP/calls"
case $op in
open)
material=$(cat "$key_file")
if [[ -e $TMP/lookup-fail-once && $material != "$CURRENT" ]]; then
rm "$TMP/lookup-fail-once"
return 2
fi
slot=$(awk -v m="$material" '$2 == m { print $1; exit }' "$TMP/slots")
[[ -n $slot ]] || return 2
echo "Key slot $slot unlocked."
;;
luksAddKey)
material=$(cat "$key_file")
awk -v m="$material" '$2 == m { found = 1 } END { exit !found }' "$TMP/slots" || return 2
for (( next = 0; next < 32; next++ )); do
awk '{ print $1 }' "$TMP/slots" | grep -qx "$next" || break
done
printf '%s %s\n' "$next" "$(cat "${args[1]}")" >>"$TMP/slots"
;;
luksDump)
echo "Keyslots:"
awk '{ printf " %s: luks2\n", $1 }' "$TMP/slots"
;;
luksKillSlot)
slot=${args[1]}
awk -v s="$slot" '$1 != s' "$TMP/slots" >"$TMP/slots.next"
command mv -f "$TMP/slots.next" "$TMP/slots"
;;
*) return 1 ;;
esac
}
case $MODE in
owner) reset_boot_owner && echo "$RESET_BOOT" ;;
reset)
trap cleanup EXIT
stage_full_reset
;;
esac
SH
current_password=owner-password
device=$tmp/luks-device
# A top-level Btrfs volume: the running root and a factory baseline whose ESP
# the generic path mounts from its fstab.
fixture() {
rm -rf "$tmp/top" "$tmp/calls" "$tmp/screen" "$tmp/reset.log" "$tmp/committed-key" "$tmp"/*-fail
mkdir -p "$tmp/top/@" "$tmp/top/@factory/etc" "$tmp/top/@factory/usr/bin" \
"$tmp/top/@factory/usr/share/omarchy/install/provisioning" "$tmp/top/@factory/boot"
touch "$tmp/top/@/old-system" "$tmp/top/@factory/factory-system" "$tmp/esp" \
"$tmp/top/@factory/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service"
printf '#!/bin/bash\n' >"$tmp/top/@factory/usr/bin/omarchy-provision-owner"
chmod +x "$tmp/top/@factory/usr/bin/omarchy-provision-owner"
printf '%s /boot vfat defaults 0 2\n' "$tmp/esp" >"$tmp/top/@factory/etc/fstab"
: >"$device"
printf '0 %s\n' "$current_password" >"$tmp/slots"
: >"$tmp/reset.log"
}
run() {
local mode=$1 platform=$2
TMP=$tmp ROOT=$ROOT MODE=$mode CURRENT=$current_password DEVICE=${DEVICE-$device} TEST_DISPATCH=${TEST_DISPATCH:-} \
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT=$lifecycle PATH="$tmp/$platform/bin:$PATH" \
bash "$tmp/reset" >"$tmp/out" 2>&1
}
activated() {
[[ -e $tmp/top/@/factory-system && -f $tmp/top/@/var/lib/omarchy/provisioning/pending ]] &&
compgen -G "$tmp/top/@omarchy-old-*/old-system" >/dev/null
}
untouched() {
[[ -e $tmp/top/@/old-system && ! -e $tmp/top/@omarchy-reset-next ]] && [[ $(cat "$tmp/slots") == "0 $current_password" ]]
}
# x86: the generic Limine UKI path, with boot-package entrypoints on disk that
# would record themselves if they ran.
fixture
run owner x86 && [[ $(cat "$tmp/out") == "generic" ]] || fail "x86: the reset boot step is generic" "$(cat "$tmp/out")"
run reset x86 || fail "x86: an encrypted reset stages" "$(cat "$tmp/out" "$tmp/reset.log")"
activated || fail "x86: the factory root is activated"
grep -qx 'chroot /usr/bin/limine-update' "$tmp/calls" || fail "x86: limine-update rebuilds the factory root's UKI" "$(cat "$tmp/calls")"
[[ -f $tmp/top/@/etc/omarchy/provisioning.key && -f $tmp/top/@/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf &&
-f $tmp/top/@/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf ]] || fail "x86: the UKI carries the reset's auto-unlock"
[[ $(awk 'NR == 2 { print $2 }' "$tmp/slots") == "$(cat "$tmp/top/@/var/lib/omarchy/provisioning/luks-key")" ]] ||
fail "x86: the throwaway slot opens with the staged key" "$(cat "$tmp/slots")"
! grep -q '^reset-' "$tmp/calls" || fail "x86: no boot-package entrypoint runs" "$(cat "$tmp/calls")"
pass "x86: dispatch is a no-op and the generic Limine UKI path resets the machine"
# A failed activation on the generic path puts the previous root back and
# revokes the throwaway slot. The ESP already holds the factory root's UKI, so
# the screen says to rebuild the boot files rather than claiming nothing changed.
fixture
touch "$tmp/activate-fail"
if run reset x86; then fail "x86: a failed activation fails the reset"; fi
untouched || fail "x86: a failed activation puts the previous root back and revokes the throwaway slot" "$(ls "$tmp/top"; cat "$tmp/slots")"
! grep -q '^reset-' "$tmp/calls" || fail "x86: a failed activation runs no boot-package entrypoint" "$(cat "$tmp/calls")"
grep -q "run 'sudo limine-update' before rebooting" "$tmp/screen" ||
fail "x86: a failed activation says the boot files need rebuilding" "$(cat "$tmp/screen")"
pass "x86: a failed activation puts the previous root back, revokes the throwaway slot and says to rebuild the boot files"
# Apple: the boot package prepares and verifies the factory root before the
# throwaway slot exists, and gets the key only once the factory root is active.
fixture
run owner apple && [[ $(cat "$tmp/out") == "platform" ]] || fail "apple: the boot package owns the reset boot step" "$(cat "$tmp/out")"
run reset apple || fail "apple: an encrypted reset stages" "$(cat "$tmp/out" "$tmp/reset.log")"
activated || fail "apple: the factory root is activated"
expected="reset-prepare $tmp/top/@omarchy-reset-next $device
reset-verify $tmp/top/@omarchy-reset-next
cryptsetup luksAddKey
reset-commit"
[[ $(grep -v '^cryptsetup \(open\|luksDump\)' "$tmp/calls") == "$expected" ]] ||
fail "apple: prepare, verify, the throwaway slot, then commit" "$(cat "$tmp/calls")"
staged=$(cat "$tmp/top/@/var/lib/omarchy/provisioning/luks-key")
[[ -n $staged && $(cat "$tmp/committed-key") == "$staged" ]] || fail "apple: reset-commit gets the staged key on standard input"
[[ $(awk 'NR == 2 { print $2 }' "$tmp/slots") == "$staged" ]] || fail "apple: the throwaway slot opens with the staged key" "$(cat "$tmp/slots")"
[[ ! -e $tmp/top/@/etc/omarchy/provisioning.key && ! -e $tmp/top/@/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf ]] ||
fail "apple: no Limine UKI unlock is written"
! grep -q 'limine' "$tmp/calls" || fail "apple: the Limine UKI path never runs" "$(cat "$tmp/calls")"
! grep -Fq "$staged" "$tmp/reset.log" "$tmp/calls" "$tmp/out" || fail "apple: the throwaway key is never logged"
pass "apple: the boot package prepares and verifies the factory root before the throwaway slot, and gets the key after the switch"
# A failed verification adds no credential and rolls the boot state back; the
# previous root keeps booting.
fixture
touch "$tmp/reset-verify-fail"
if run reset apple; then fail "apple: a failed verification fails the reset"; fi
[[ $(grep '^reset-' "$tmp/calls" | cut -d' ' -f1) == $'reset-prepare\nreset-verify\nreset-rollback' ]] ||
fail "apple: a failed verification rolls back" "$(cat "$tmp/calls")"
! grep -q 'luksAddKey' "$tmp/calls" || fail "apple: a failed verification adds no slot"
untouched || fail "apple: a failed verification leaves the previous root and its slots" "$(cat "$tmp/slots")"
grep -q 'reset-verify: the fixture refuses' "$tmp/screen" || fail "apple: the verification's reason is shown" "$(cat "$tmp/screen")"
pass "apple: a failed verification rolls back, adds no slot and keeps the previous root"
# A failure after the slot was added and before the switch revokes it too.
fixture
touch "$tmp/switch-fail"
if run reset apple; then fail "apple: a failed switch fails the reset"; fi
grep -q 'luksAddKey' "$tmp/calls" && grep -qx 'reset-rollback' "$tmp/calls" && ! grep -q '^reset-commit' "$tmp/calls" ||
fail "apple: a failed switch rolls back without committing" "$(cat "$tmp/calls")"
untouched || fail "apple: a failed switch revokes the throwaway slot and keeps the previous root" "$(cat "$tmp/slots")"
pass "apple: a failure before the switch revokes the slot it added and rolls the boot state back"
# The old root already moved aside when the factory root cannot take @: the old
# root goes back, and the reset is undone as if it failed before the switch.
fixture
touch "$tmp/activate-fail"
if run reset apple; then fail "apple: a failed activation fails the reset"; fi
grep -qx 'reset-rollback' "$tmp/calls" && ! grep -q '^reset-commit' "$tmp/calls" ||
fail "apple: a failed activation rolls back without committing" "$(cat "$tmp/calls")"
untouched || fail "apple: a failed activation puts the previous root back at @ and revokes the throwaway slot" "$(ls "$tmp/top"; cat "$tmp/slots")"
! compgen -G "$tmp/top/@omarchy-old-*" >/dev/null || fail "apple: a failed activation leaves no renamed root behind" "$(ls "$tmp/top")"
grep -q 'the current root is back at @' "$tmp/screen" || fail "apple: a failed activation says the current root is back" "$(cat "$tmp/screen")"
pass "apple: a failed activation puts the previous root back, rolls back and revokes the slot"
# When the old root cannot go back either, neither root is at @: both survive
# for recovery, and the screen names the rename that restores the machine.
fixture
touch "$tmp/activate-fail" "$tmp/restore-fail"
if run reset apple; then fail "apple: a failed activation and restore fails the reset"; fi
old=$(compgen -G "$tmp/top/@omarchy-old-*") || fail "apple: the previous root survives under its moved-aside name" "$(ls "$tmp/top")"
[[ -e $old/old-system && -e $tmp/top/@omarchy-reset-next/factory-system && ! -e $tmp/top/@ ]] ||
fail "apple: a failed restore keeps both roots" "$(ls "$tmp/top")"
grep -qx 'reset-rollback' "$tmp/calls" && [[ $(cat "$tmp/slots") == "0 $current_password" ]] ||
fail "apple: a failed restore still rolls the boot state back and revokes the slot" "$(cat "$tmp/calls" "$tmp/slots")"
grep -q "rename ${old##*/} to @" "$tmp/screen" || fail "apple: a failed restore names the recovery rename" "$(cat "$tmp/screen")"
pass "apple: a failed activation and restore keeps both roots and names the recovery rename"
# A slot added but not confirmed is found by its key and revoked all the same.
fixture
touch "$tmp/lookup-fail-once"
if run reset apple; then fail "apple: an unconfirmed throwaway slot fails the reset"; fi
grep -qx 'reset-rollback' "$tmp/calls" && grep -q 'luksKillSlot' "$tmp/calls" || fail "apple: an unconfirmed slot is rolled back and killed" "$(cat "$tmp/calls")"
untouched || fail "apple: an unconfirmed throwaway slot is revoked" "$(cat "$tmp/slots")"
pass "apple: a throwaway slot the reset could not confirm is found by its key and revoked"
# Once the factory root is active nothing rolls back: a failed commit is logged
# and costs the first boot one password prompt.
fixture
touch "$tmp/reset-commit-fail"
run reset apple || fail "apple: a failed commit still completes the reset" "$(cat "$tmp/out")"
activated && ! grep -qx 'reset-rollback' "$tmp/calls" || fail "apple: a failed commit keeps the factory root active" "$(cat "$tmp/calls")"
grep -q 'reset-commit: the fixture refuses' "$tmp/reset.log" || fail "apple: a failed commit is logged" "$(cat "$tmp/reset.log")"
[[ $(wc -l <"$tmp/slots") == 2 ]] || fail "apple: a failed commit keeps the throwaway slot"
pass "apple: a failed commit after the switch is logged and the reset stands"
# An unencrypted Mac still has its boot files rebuilt and verified.
fixture
DEVICE="" run reset apple || fail "apple: an unencrypted reset stages" "$(cat "$tmp/out")"
[[ $(grep '^reset-' "$tmp/calls") == "reset-prepare $tmp/top/@omarchy-reset-next"$'\n'"reset-verify $tmp/top/@omarchy-reset-next"$'\n'"reset-commit" ]] ||
fail "apple: an unencrypted reset names no LUKS device" "$(cat "$tmp/calls")"
! grep -q '^cryptsetup' "$tmp/calls" || fail "apple: an unencrypted reset touches no key slot"
pass "apple: an unencrypted reset rebuilds and verifies the boot files without a key"
# Without the boot package, or with one that lacks a reset operation, the
# reset stops before anything is asked or changed, naming the package.
fixture
mv "$mac_boot" "$tmp/mac-boot.off"
if run owner apple; then fail "apple without the boot package: the reset refuses"; fi
grep -q 'reset-prepare on aarch64-apple needs omarchy-mac-boot' "$tmp/out" || fail "apple without the boot package: the package is named" "$(cat "$tmp/out")"
mkdir -p "$mac_boot"
cp -p "$tmp/mac-boot.off/reset-prepare" "$tmp/mac-boot.off/reset-verify" "$mac_boot/"
chmod -R go-w "$lifecycle"
run owner x86 && [[ $(cat "$tmp/out") == "generic" ]] || fail "x86: a partial boot package on disk is ignored" "$(cat "$tmp/out")"
if run owner apple; then fail "apple with part of the boot package: the reset refuses"; fi
grep -q 'reset-commit on aarch64-apple needs omarchy-mac-boot' "$tmp/out" || fail "apple with part of the boot package: the missing operation is named" "$(cat "$tmp/out")"
rm -rf "$mac_boot"
mv "$tmp/mac-boot.off" "$mac_boot"
pass "apple: without the boot package's reset operations, or with only some of them, the reset refuses naming the package"
# A registration that left the reset operations optional and got only some of
# them would mix two boot paths: the reset refuses that too.
mkdir -p "$tmp/half"
cat >"$tmp/half/omarchy-lifecycle-dispatch" <<SH
#!/bin/bash
[[ \$* != "--resolve reset-prepare" ]] || echo /usr/lib/omarchy/mac-boot/reset-prepare
SH
chmod +x "$tmp/half/omarchy-lifecycle-dispatch"
fixture
if TEST_DISPATCH=$tmp/half/omarchy-lifecycle-dispatch run owner apple; then fail "a partial set of reset operations is refused"; fi
grep -q 'implements only some of the factory reset operations' "$tmp/out" || fail "a partial set of reset operations is explained" "$(cat "$tmp/out")"
pass "a boot package implementing only some of the reset operations is refused"
@@ -0,0 +1,18 @@
# Generated by the Omarchy Apple Silicon image builder.
_omarchy_asahi_hooks=()
_omarchy_asahi_added=false
for _omarchy_asahi_hook in "${HOOKS[@]}"; do
if [[ $_omarchy_asahi_hook == asahi ]]; then
_omarchy_asahi_added=true
fi
if [[ $_omarchy_asahi_hook == filesystems && $_omarchy_asahi_added == false ]]; then
_omarchy_asahi_hooks+=(asahi omarchy-vendorfw)
_omarchy_asahi_added=true
fi
_omarchy_asahi_hooks+=("$_omarchy_asahi_hook")
done
if [[ $_omarchy_asahi_added == false ]]; then
_omarchy_asahi_hooks+=(asahi omarchy-vendorfw)
fi
HOOKS=("${_omarchy_asahi_hooks[@]}")
unset _omarchy_asahi_hooks _omarchy_asahi_hook _omarchy_asahi_added
@@ -0,0 +1,90 @@
# Insert omarchy-mac-encrypt after vendorfw/block and sd-encrypt immediately
# before filesystems, each only if that hook is absent. 90-omarchy-mac.conf
# already put asahi and omarchy-vendorfw before filesystems; this drop-in is
# sourced after it.
#
# Both are systemd initrd units: the systemd hook replaces udev (mkinitcpio's
# stock HOOKS line) and keymap/consolefont become sd-vconsole. A HOOKS line
# carrying the busybox encrypt hook belongs to a Mac unlocked by cryptdevice=,
# which sd-encrypt cannot parse: that line is left exactly as it is.
_omarchy_mac_encrypt_hooks=()
_omarchy_mac_encrypt_have_systemd=false
_omarchy_mac_encrypt_have_vconsole=false
_omarchy_mac_encrypt_have_encrypt=false
for _omarchy_mac_encrypt_hook in "${HOOKS[@]}"; do
case $_omarchy_mac_encrypt_hook in
systemd) _omarchy_mac_encrypt_have_systemd=true ;;
sd-vconsole) _omarchy_mac_encrypt_have_vconsole=true ;;
encrypt) _omarchy_mac_encrypt_have_encrypt=true ;;
esac
done
if [[ $_omarchy_mac_encrypt_have_encrypt == false ]]; then
for _omarchy_mac_encrypt_hook in "${HOOKS[@]}"; do
case $_omarchy_mac_encrypt_hook in
udev)
if [[ $_omarchy_mac_encrypt_have_systemd == false ]]; then
_omarchy_mac_encrypt_hooks+=(systemd)
_omarchy_mac_encrypt_have_systemd=true
fi
;;
keymap|consolefont)
if [[ $_omarchy_mac_encrypt_have_vconsole == false ]]; then
_omarchy_mac_encrypt_hooks+=(sd-vconsole)
_omarchy_mac_encrypt_have_vconsole=true
fi
;;
*) _omarchy_mac_encrypt_hooks+=("$_omarchy_mac_encrypt_hook") ;;
esac
done
if [[ $_omarchy_mac_encrypt_have_systemd == false ]]; then
if [[ ${_omarchy_mac_encrypt_hooks[0]:-} == base ]]; then
_omarchy_mac_encrypt_hooks=(base systemd "${_omarchy_mac_encrypt_hooks[@]:1}")
else
_omarchy_mac_encrypt_hooks=(systemd "${_omarchy_mac_encrypt_hooks[@]}")
fi
fi
HOOKS=("${_omarchy_mac_encrypt_hooks[@]}")
_omarchy_mac_encrypt_hooks=()
_omarchy_mac_encrypt_have_ours=false
_omarchy_mac_encrypt_have_sd=false
_omarchy_mac_encrypt_added_ours=false
_omarchy_mac_encrypt_added_sd=false
for _omarchy_mac_encrypt_hook in "${HOOKS[@]}"; do
if [[ $_omarchy_mac_encrypt_hook == omarchy-mac-encrypt ]]; then
_omarchy_mac_encrypt_have_ours=true
fi
if [[ $_omarchy_mac_encrypt_hook == sd-encrypt ]]; then
_omarchy_mac_encrypt_have_sd=true
fi
done
for _omarchy_mac_encrypt_hook in "${HOOKS[@]}"; do
if [[ $_omarchy_mac_encrypt_hook == sd-encrypt && $_omarchy_mac_encrypt_have_ours == false &&
$_omarchy_mac_encrypt_added_ours == false ]]; then
_omarchy_mac_encrypt_hooks+=(omarchy-mac-encrypt)
_omarchy_mac_encrypt_added_ours=true
fi
if [[ $_omarchy_mac_encrypt_hook == filesystems ]]; then
if [[ $_omarchy_mac_encrypt_have_ours == false && $_omarchy_mac_encrypt_added_ours == false ]]; then
_omarchy_mac_encrypt_hooks+=(omarchy-mac-encrypt)
_omarchy_mac_encrypt_added_ours=true
fi
if [[ $_omarchy_mac_encrypt_have_sd == false && $_omarchy_mac_encrypt_added_sd == false ]]; then
_omarchy_mac_encrypt_hooks+=(sd-encrypt)
_omarchy_mac_encrypt_added_sd=true
fi
fi
_omarchy_mac_encrypt_hooks+=("$_omarchy_mac_encrypt_hook")
done
if [[ $_omarchy_mac_encrypt_have_ours == false && $_omarchy_mac_encrypt_added_ours == false ]]; then
_omarchy_mac_encrypt_hooks+=(omarchy-mac-encrypt)
fi
if [[ $_omarchy_mac_encrypt_have_sd == false && $_omarchy_mac_encrypt_added_sd == false ]]; then
_omarchy_mac_encrypt_hooks+=(sd-encrypt)
fi
HOOKS=("${_omarchy_mac_encrypt_hooks[@]}")
fi
unset _omarchy_mac_encrypt_hooks _omarchy_mac_encrypt_hook \
_omarchy_mac_encrypt_have_ours _omarchy_mac_encrypt_have_sd \
_omarchy_mac_encrypt_added_ours _omarchy_mac_encrypt_added_sd \
_omarchy_mac_encrypt_have_systemd _omarchy_mac_encrypt_have_vconsole \
_omarchy_mac_encrypt_have_encrypt
@@ -0,0 +1,31 @@
# Origin: omarchy-mx-mac install/hardware/apple/fix-asahi-hid-race.sh
# Apple Silicon internal keyboard and trackpad at the sd-encrypt passphrase
# prompt. dockchannel-hid creates the HID devices; hid_apple binds the
# keyboard and hid_magicmouse the SPI trackpad. MTP machines (M2 Air, M2 Max
# j416c) use dockchannel-hid — linux-aurora has no apple-mtp module. SPI HID
# (M1 Air, M1 Pro j314s) is spi-hid-apple-of, built-in on aurora. usbhid is
# an external USB keyboard at the prompt. thunderbolt (CONFIG_USB4=m) and
# thunderbolt_apple, the Apple Silicon USB4 host router (CONFIG_USB4_APPLE_SOC=m
# on linux-aurora), bring up the USB4/Thunderbolt tunnels, so a keyboard behind
# a USB-C or Thunderbolt dock types at the prompt too (omarchy-mx-mac#86).
#
# mkinitcpio fails the whole image over a MODULES entry it cannot find, or a
# built-in it reports as "(builtin)". Each name is added only when modinfo
# returns a real path. Ending on unset keeps the exit status zero.
for _omarchy_mac_hid_module in \
hid_apple hid_magicmouse dockchannel-hid usbhid \
spi-apple spi-hid-apple spi-hid-apple-of \
apple-dockchannel apple-rtkit-helper thunderbolt thunderbolt_apple; do
_omarchy_mac_hid_path=$(modinfo -k "${KERNELVERSION:-$(uname -r)}" -F filename \
"$_omarchy_mac_hid_module" 2>/dev/null) || continue
[[ $_omarchy_mac_hid_path == /* ]] || continue
MODULES+=("$_omarchy_mac_hid_module")
done
unset _omarchy_mac_hid_module _omarchy_mac_hid_path
# MTP trackpad firmware (apple/tpmtfw-<board>.bin) is not a MODULES entry.
# omarchy-vendorfw-initrd.service unpacks ESP vendorfw/firmware.cpio onto
# /lib/firmware/vendor (symlink to /vendorfw) before cryptsetup-pre.target.
# Do not FILES+= under /lib/firmware/vendor, and do not pre-create
# /vendorfw/apple in the image: that skipped ESP extraction.
@@ -0,0 +1,18 @@
# Plymouth draws the disk password prompt and the boot splash, as on x86
# Omarchy. It goes right after systemd so its initrd units order correctly,
# only when the hook is installed, and never twice.
if [[ -f /usr/lib/initcpio/install/plymouth && " ${HOOKS[*]} " != *" plymouth "* ]]; then
_omarchy_mac_plymouth_hooks=()
_omarchy_mac_plymouth_added=false
for _omarchy_mac_plymouth_hook in "${HOOKS[@]}"; do
_omarchy_mac_plymouth_hooks+=("$_omarchy_mac_plymouth_hook")
if [[ $_omarchy_mac_plymouth_hook == systemd && $_omarchy_mac_plymouth_added == false ]]; then
_omarchy_mac_plymouth_hooks+=(plymouth)
_omarchy_mac_plymouth_added=true
fi
done
if [[ $_omarchy_mac_plymouth_added == true ]]; then
HOOKS=("${_omarchy_mac_plymouth_hooks[@]}")
fi
unset _omarchy_mac_plymouth_hooks _omarchy_mac_plymouth_added _omarchy_mac_plymouth_hook
fi
@@ -0,0 +1,46 @@
# The disk passphrase prompt types with the owner's keyboard layout, as on
# x86 Omarchy: sd-vconsole loads KEYMAP on the console (systemd-ask-password)
# and /etc/vconsole.conf gives Plymouth its XKBLAYOUT. The aarch64
# omarchy-settings drops upstream's omarchy_hooks.conf, so this drop-in
# carries its guard: a layout that does not type Latin letters stays out of
# the initramfs, because a Latin passphrase would be untypeable in it
# (upstream #6229). The prompt then uses the kernel's US map, which is what
# such a passphrase was typed with.
#
# A systemd HOOKS line gets sd-vconsole exactly once, after keyboard (or
# after systemd without one); keymap and consolefont are its busybox
# counterparts and never belong on such a line. A busybox line (a Mac
# unlocked by cryptdevice=, which 91 leaves alone) keeps its hooks; it only
# gets the file for Plymouth, as upstream does.
# No vconsole.conf is the kernel's US map: sd-vconsole stays, nothing to bundle.
_omarchy_mac_vconsole_latin=true
if [[ -f /etc/vconsole.conf ]]; then
_omarchy_mac_vconsole_layout=$(unset XKBLAYOUT; . /etc/vconsole.conf 2>/dev/null; printf '%s' "${XKBLAYOUT:-}")
case ${_omarchy_mac_vconsole_layout%%,*} in
af | am | ara | bd | bg | by | et | ge | gr | il | in | iq | ir | kg | kh | kz | la | lk | mk | mm | mn | mv | np | rs | ru | sy | th | tj | ua)
_omarchy_mac_vconsole_latin=false ;;
esac
fi
if [[ " ${HOOKS[*]} " == *" systemd "* ]]; then
_omarchy_mac_vconsole_hooks=()
_omarchy_mac_vconsole_anchor=systemd
[[ " ${HOOKS[*]} " != *" keyboard "* ]] || _omarchy_mac_vconsole_anchor=keyboard
for _omarchy_mac_vconsole_hook in "${HOOKS[@]}"; do
case $_omarchy_mac_vconsole_hook in
sd-vconsole | keymap | consolefont) continue ;;
esac
_omarchy_mac_vconsole_hooks+=("$_omarchy_mac_vconsole_hook")
if [[ $_omarchy_mac_vconsole_hook == "$_omarchy_mac_vconsole_anchor" && $_omarchy_mac_vconsole_latin == true ]]; then
_omarchy_mac_vconsole_hooks+=(sd-vconsole)
_omarchy_mac_vconsole_anchor=
fi
done
HOOKS=("${_omarchy_mac_vconsole_hooks[@]}")
fi
if [[ $_omarchy_mac_vconsole_latin == true && -f /etc/vconsole.conf ]]; then
FILES+=(/etc/vconsole.conf)
fi
unset _omarchy_mac_vconsole_latin _omarchy_mac_vconsole_layout _omarchy_mac_vconsole_hooks \
_omarchy_mac_vconsole_anchor _omarchy_mac_vconsole_hook
+13 -2
View File
@@ -32,6 +32,17 @@ PY
copy_boundary_file bin/omarchy-security-functions
copy_boundary_file bin/omarchy-update-pacman
copy_boundary_file default/omarchy/sudo-no-update/sudo
copy_boundary_file install/helpers/pacman.sh
# Channel changes copy the platform's templates; the copy itself is a stand-in.
cp -r "$ROOT/default/pacman" "$SUDO_TEST_ROOT/default/"
# Channel changes ask for the platform. x86 unless a test says otherwise, so no
# test reads the host's hardware.
cat >"$SUDO_TEST_ROOT/bin/omarchy-hw-platform" <<'STUB'
#!/bin/bash
echo "${SUDO_TEST_PLATFORM:-x86}"
STUB
chmod +x "$SUDO_TEST_ROOT/bin/omarchy-hw-platform"
cat >"$SUDO_TEST_ROOT/mock/sudo" <<'STUB'
#!/bin/bash
@@ -132,7 +143,7 @@ case "$step" in
esac
STUB
chmod +x "$SUDO_TEST_ROOT/bin/test-step"
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman systemd-run cp yay; do
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-boot omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman systemd-run cp yay; do
ln -s test-step "$SUDO_TEST_ROOT/bin/$step"
done
ln -s ../bin/test-step "$SUDO_TEST_ROOT/mock/pacman"
@@ -140,7 +151,7 @@ ln -s ../bin/test-step "$SUDO_TEST_ROOT/mock/pacman"
reset_boundary() {
: >"$SUDO_TEST_LOG"
/usr/bin/rm -f "$SUDO_TEST_CACHE"
unset SUDO_TEST_FAIL_STEP SUDO_TEST_SIGNAL_STEP SUDO_TEST_SUDO_FAIL SUDO_TEST_REVOKE_FAIL SUDO_TEST_UNSUPPORTED SUDO_TEST_REMOVE_WRAPPER_STEP
unset SUDO_TEST_FAIL_STEP SUDO_TEST_SIGNAL_STEP SUDO_TEST_SUDO_FAIL SUDO_TEST_REVOKE_FAIL SUDO_TEST_UNSUPPORTED SUDO_TEST_REMOVE_WRAPPER_STEP SUDO_TEST_PLATFORM
}
assert_boundary_cold() {
[[ ! -e $SUDO_TEST_CACHE ]] || fail "$1 left cached authorization"
+81 -3
View File
@@ -17,8 +17,18 @@ write_backlights() {
done
}
# A copy of the command reads its platform file from a fixture root: none
# unless a case writes one, whatever the machine running the suite has installed.
platform_root="$tmp_dir/platform"
mkdir -p "$platform_root" "$tmp_dir/bin"
platform_root_copy "$ROOT/bin/omarchy-hw-display" "$tmp_dir/bin/omarchy-hw-display" "$platform_root"
write_displays_conf() {
printf '%s\n' "$@" >"$platform_root/displays.conf"
}
hw_display() {
OMARCHY_BACKLIGHT_PATH="$tmp_dir/backlight" "$ROOT/bin/omarchy-hw-display"
OMARCHY_BACKLIGHT_PATH="$tmp_dir/backlight" "$tmp_dir/bin/omarchy-hw-display"
}
write_backlights intel_backlight
@@ -48,9 +58,9 @@ pass "a T2 Mac uses gmux instead of the Touch Bar"
write_backlights appletb_backlight
if hw_display >/dev/null 2>&1; then
fail "the Touch Bar is never used as the display backlight"
fail "the T2 Touch Bar is never used as the display backlight"
fi
pass "the Touch Bar is never used as the display backlight"
pass "the T2 Touch Bar is never used as the display backlight"
write_backlights acpi_video0 amdgpu_bl0 appletb_backlight gmux_backlight intel_backlight
device=$(hw_display)
@@ -62,3 +72,71 @@ if hw_display >/dev/null 2>&1; then
fail "no backlight device reports failure"
fi
pass "no backlight device reports failure"
# An empty platform file changes nothing.
: >"$platform_root/displays.conf"
write_backlights acpi_video0 amdgpu_bl1
device=$(hw_display)
[[ $device == "amdgpu_bl1" ]] || fail "an empty platform file keeps the candidate order" "actual: $device"
write_backlights nvidia_wmi_ec_backlight
device=$(hw_display)
[[ $device == "nvidia_wmi_ec_backlight" ]] || fail "an empty platform file keeps the fallback" "actual: $device"
pass "an empty platform file changes nothing"
write_displays_conf '# accessory panels' 'backlight-skip accessory-pipe' 'backlight-skip *.dsi.0' 'backlight-prefer panel-bl'
write_backlights 1000.dsi.0 accessory-pipe
if hw_display >/dev/null 2>&1; then
fail "a skipped backlight is never the fallback"
fi
write_backlights 1000.dsi.0 accessory-pipe zz-other
device=$(hw_display)
[[ $device == "zz-other" ]] || fail "the fallback passes over skipped backlights" "actual: $device"
pass "a platform's skipped backlights are never the fallback"
write_backlights 1000.dsi.0 amdgpu_bl0 panel-bl
device=$(hw_display)
[[ $device == "panel-bl" ]] || fail "a preferred backlight beats the GPU backlights" "actual: $device"
printf 'backlight-prefer panel-bl\r\n' >"$platform_root/displays.conf"
device=$(hw_display)
[[ $device == "panel-bl" ]] || fail "a file saved with CRLF line ends reads the same" "actual: $device"
write_displays_conf '# accessory panels' 'backlight-skip accessory-pipe' 'backlight-skip *.dsi.0' 'backlight-prefer panel-bl'
write_backlights gmux_backlight panel-bl
device=$(hw_display)
[[ $device == "gmux_backlight" ]] || fail "gmux still beats a preferred backlight" "actual: $device"
pass "a platform's preferred backlight follows gmux and beats the GPU backlights"
write_displays_conf 'backlight-prefer second-bl' 'backlight-prefer first-bl'
write_backlights first-bl second-bl
device=$(hw_display)
[[ $device == "second-bl" ]] || fail "preferences are tried in file order" "actual: $device"
pass "preferences are tried in file order"
write_displays_conf 'backlight-skip intel_backlight' 'backlight-prefer panel-bl' 'backlight-skip panel-bl'
write_backlights acpi_video0 intel_backlight panel-bl
device=$(hw_display)
[[ $device == "acpi_video0" ]] || fail "a skip beats preferred and built-in candidates" "actual: $device"
write_displays_conf 'backlight-skip gmux_backlight'
write_backlights gmux_backlight intel_backlight
device=$(hw_display)
[[ $device == "intel_backlight" ]] || fail "a skip beats gmux" "actual: $device"
pass "a skip beats preferred and built-in candidates"
write_displays_conf 'backlight-prefer panel-*' 'backlight-prefer .' 'backlight-prefer ..'
write_backlights amdgpu_bl0 panel-bl
device=$(hw_display)
[[ $device == "amdgpu_bl0" ]] || fail "a preference is a literal name" "actual: $device"
pass "a preference is a literal name"
# Only whole directives count: an unknown one, a path, an extra word or a
# trailing comment leaves the line out; the last line needs no newline.
printf '%s\n' 'backlight-future panel-bl' 'backlight-prefer ../panel-bl' 'backlight-prefer panel-bl extra' \
'backlight-skip acpi_video0 # comment' >"$platform_root/displays.conf"
printf 'backlight-prefer last-bl' >>"$platform_root/displays.conf"
write_backlights acpi_video0 last-bl panel-bl
device=$(hw_display)
[[ $device == "last-bl" ]] || fail "malformed directives are ignored and the last line is read" "actual: $device"
write_displays_conf 'backlight-skip acpi_video0 # comment'
write_backlights acpi_video0
device=$(hw_display)
[[ $device == "acpi_video0" ]] || fail "a directive with a trailing comment is ignored" "actual: $device"
pass "malformed directives are ignored and the last line is read"
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
acpi_lid="$test_tmp/acpi/button/lid"
input_class="$test_tmp/input"
dmi_chassis="$test_tmp/chassis_type"
mkdir -p "$stub_bin" "$acpi_lid/LID0" "$input_class"
busctl_log="$test_tmp/busctl.log"
cat >"$stub_bin/busctl" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$BUSCTL_LOG"
[[ ${OMARCHY_TEST_LID_STATE:-} != "fail" ]] || exit 1
printf '%s\n' "${OMARCHY_TEST_LID_STATE:-}"
SH
chmod +x "$stub_bin/busctl"
run_laptop() {
OMARCHY_DMI_CHASSIS_TYPE_PATH="$dmi_chassis" \
OMARCHY_ACPI_LID_PATH="$acpi_lid" \
OMARCHY_INPUT_CLASS_PATH="$input_class" \
PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-hw-laptop"
}
run_lid_closed() {
OMARCHY_ACPI_LID_PATH="$acpi_lid" \
OMARCHY_INPUT_CLASS_PATH="$input_class" \
BUSCTL_LOG="$busctl_log" \
PATH="$stub_bin:$PATH" \
OMARCHY_TEST_LID_STATE="$1" \
"$ROOT/bin/omarchy-hw-laptop-closed"
}
# Input devices as sysfs shows them: a name and a switch capability bitmap
# whose last word holds SW_LID in bit 0.
input_device() {
mkdir -p "$input_class/$1/capabilities"
printf '%s\n' "$2" >"$input_class/$1/name"
printf '%s\n' "$3" >"$input_class/$1/capabilities/sw"
}
# No ACPI lid, no lid switch, desktop chassis: a Mac mini, a Studio or a tower.
: >"$acpi_lid/LID0/state"
rm -f "$acpi_lid/LID0/state"
printf '3\n' >"$dmi_chassis"
input_device input0 "Power Button" 0
input_device input1 "Apple SMC power/lid events" 0
input_device input2 "MacBook Pro J416 Headphone Jack" 14
if run_laptop; then
fail "a desktop without a lid is not classified as a laptop"
fi
pass "a desktop without a lid is not classified as a laptop, even with the SMC's power device"
# Apple Silicon MacBook: no ACPI lid, but the SMC reports a lid switch.
input_device input1 "Apple SMC power/lid events" 1
run_laptop || fail "an Apple SMC lid switch is classified as a laptop"
pass "an Apple SMC lid switch is classified as a laptop"
input_device input1 "Lid Switch" "0 1"
run_laptop || fail "a lid switch in the low word of a longer bitmap is classified as a laptop"
pass "a lid switch in the low word of a longer bitmap is classified as a laptop"
input_device input1 "Lid Switch" "1 0"
if run_laptop; then
fail "a switch bit in a higher word is not a lid"
fi
pass "a switch bit in a higher word is not a lid"
# x86 laptop via DMI when ACPI and evdev lids are absent.
input_device input1 "Power Button" 0
printf '9\n' >"$dmi_chassis"
run_laptop || fail "DMI laptop chassis is recognized when ACPI is absent"
pass "DMI laptop chassis is recognized when ACPI is absent"
printf '3\n' >"$dmi_chassis"
if run_laptop; then
fail "a desktop DMI chassis is not classified as a laptop"
fi
pass "a desktop DMI chassis is not classified as a laptop"
printf 'closed\n' >"$acpi_lid/LID0/state"
run_laptop || fail "an ACPI lid is classified as a laptop"
pass "an ACPI lid is classified as a laptop"
# An ACPI lid answers for itself, as it always has, without asking logind over
# D-Bus: this runs in sudo's PAM stack where fingerprint login is set up.
: >"$busctl_log"
run_lid_closed 'b false' ||
fail "an ACPI lid reports itself closed"
pass "an ACPI lid reports itself closed"
printf 'open\n' >"$acpi_lid/LID0/state"
if run_lid_closed 'b true'; then
fail "an ACPI lid reports itself open"
fi
pass "an ACPI lid reports itself open"
[[ ! -s $busctl_log ]] || fail "an ACPI lid leaves logind unasked" "$(cat "$busctl_log")"
pass "an ACPI lid leaves logind unasked"
# Without an ACPI lid, logind answers only for a lid switch it can track: it
# says "open" of a machine it tracks no lid on.
rm -f "$acpi_lid/LID0/state"
input_device input1 "Power Button" 0
if run_lid_closed 'b true'; then
fail "a machine without a lid is never closed"
fi
[[ ! -s $busctl_log ]] || fail "a machine without a lid leaves logind unasked" "$(cat "$busctl_log")"
pass "a machine without a lid is never closed and leaves logind unasked"
input_device input1 "Apple SMC power/lid events" 1
run_lid_closed 'b true' ||
fail "logind recognizes a closed lid switch"
pass "logind recognizes a closed lid switch"
[[ $(head -1 "$busctl_log") == "--timeout=2 "*" LidClosed" ]] ||
fail "logind gets a short timeout, not busctl's 25 s" "$(cat "$busctl_log")"
pass "logind gets a short timeout, not busctl's 25 s"
if run_lid_closed 'b false'; then
fail "logind recognizes an open lid switch"
fi
pass "logind recognizes an open lid switch"
input_device input1 "Lid Switch" "0 1"
run_lid_closed 'b true' ||
fail "a lid switch in the low word of a longer bitmap is asked about"
pass "a lid switch in the low word of a longer bitmap is asked about"
for answer in '' 'not-a-property' 'fail'; do
if run_lid_closed "$answer"; then
fail "a logind that does not answer leaves the lid open" "$answer"
fi
done
pass "a logind that does not answer leaves the lid open"
+203
View File
@@ -0,0 +1,203 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
detector="$ROOT/bin/omarchy-hw-platform"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
for platform in aarch64-apple aarch64 x86; do
fake_platform "$test_tmp/$platform" "$platform"
done
# A privileged caller reads the live device tree and the system uname, never
# the fixture roots or PATH its environment names. Run it as root when the suite
# is root and as namespaced root otherwise.
root_runner=()
if (( EUID != 0 )); then
root_runner=(unshare --user --map-root-user)
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 =~ ^(aarch64-apple|aarch64|x86)$ ]] || fail "root detects the live platform" "live: $live"
for platform in aarch64-apple aarch64 x86; do
fixture="$test_tmp/$platform"
if [[ -f $fixture/proc/device-tree/compatible ]]; then
mkdir -p "$fixture/sys/firmware/devicetree/base"
cp "$fixture/proc/device-tree/compatible" "$fixture/sys/firmware/devicetree/base/compatible"
fi
overridden=$(OMARCHY_PROC_ROOT="$fixture/proc" OMARCHY_SYS_ROOT="$fixture/sys" PATH="$fixture/bin:$ROOT/bin:$PATH" \
"${root_runner[@]}" "$detector") || fail "root detects the live platform with a $platform fixture in its environment"
[[ $overridden == "$live" ]] ||
fail "root ignores a $platform fixture in its environment" "live: $live
with fixture: $overridden"
done
pass "root ignores fixture roots and PATH when detecting the platform"
else
skip "no unprivileged user namespace; skipping the root override probe"
fi
# -p in the shebang is what keeps exported functions and BASH_ENV out, so an
# ordinary Bash launch with a decoy -p argument is refused before it reads anything.
for command in omarchy-hw-platform omarchy-hw-aarch64-apple omarchy-hw-apple-silicon; do
if /usr/bin/bash "$ROOT/bin/$command" -p >/dev/null 2>"$test_tmp/error"; then
fail "$command refuses an ordinary Bash launch"
fi
grep -Fq "Refusing an unsafe Bash startup" "$test_tmp/error" || fail "$command explains the refusal" "$(cat "$test_tmp/error")"
done
pass "the detector and the Apple predicate refuse an ordinary Bash launch with a decoy -p"
require_platform_fixtures "the platform fixtures"
# The three platforms every caller is written against.
for platform in aarch64-apple aarch64 x86; do
fixture="$test_tmp/$platform"
actual=$(OMARCHY_PROC_ROOT="$fixture/proc" PATH="$fixture/bin:$ROOT/bin:$PATH" "$detector") ||
fail "the $platform fixture is detected"
[[ $actual == "$platform" ]] || fail "the $platform fixture is detected" "actual: $actual"
apple_status=0
OMARCHY_PROC_ROOT="$fixture/proc" PATH="$fixture/bin:$ROOT/bin:$PATH" "$ROOT/bin/omarchy-hw-aarch64-apple" || apple_status=$?
if [[ $platform == "aarch64-apple" ]]; then
(( apple_status == 0 )) || fail "the Apple predicate accepts the Apple fixture"
else
(( apple_status != 0 )) || fail "the Apple predicate rejects the $platform fixture"
fi
pass "the $platform fixture is detected and the Apple predicate agrees"
done
# Systemd and the shell run the predicate by absolute path with whatever PATH
# they have; it must use the detector shipped beside it.
fixture="$test_tmp/aarch64-apple"
OMARCHY_PROC_ROOT="$fixture/proc" PATH="$fixture/bin:/usr/bin:/bin" "$ROOT/bin/omarchy-hw-aarch64-apple" ||
fail "the Apple predicate finds its detector without Omarchy on PATH"
pass "the Apple predicate finds its detector without Omarchy on PATH"
stub_bin="$test_tmp/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/uname" <<'SH'
#!/bin/bash
[[ ${1:-} == -m ]] && { printf '%s\n' "${TEST_ARCH:-aarch64}"; exit 0; }
exec /usr/bin/uname "$@"
SH
chmod +x "$stub_bin/uname"
# $1 names a case directory holding proc/ and sys/ roots; $2 is the CPU.
detect() {
local case_dir="$test_tmp/cases/$1"
TEST_ARCH="${2:-aarch64}" OMARCHY_PROC_ROOT="$case_dir/proc" OMARCHY_SYS_ROOT="$case_dir/sys" \
PATH="$stub_bin:$ROOT/bin:$PATH" "$detector" 2>"$test_tmp/error"
}
# $1 case, $2 proc|sys, then the compatible tokens.
write_tree() {
local case_dir="$test_tmp/cases/$1" tree="$2" file
shift 2
mkdir -p "$case_dir/proc" "$case_dir/sys"
if [[ $tree == "proc" ]]; then
file="$case_dir/proc/device-tree/compatible"
else
file="$case_dir/sys/firmware/devicetree/base/compatible"
fi
mkdir -p "$(dirname "$file")"
printf '%s\0' "$@" >"$file"
}
expect() {
local name="$1" machine="$2" expected="$3" description="$4" actual
actual=$(detect "$name" "$machine") || fail "$description" "$(cat "$test_tmp/error")"
[[ $actual == "$expected" ]] || fail "$description" "expected: $expected
actual: $actual"
}
expect_contradiction() {
local name="$1" machine="$2" description="$3"
if detect "$name" "$machine" >/dev/null; then
fail "$description"
fi
grep -Fq "contradictory platform identity" "$test_tmp/error" || fail "$description explains itself" "$(cat "$test_tmp/error")"
}
# Real boards: the M1 Pro and M2 Max test Macs, an M1 Mac mini, and the
# Snapdragon X laptops Dragon supports.
write_tree m1-pro proc apple,j314s apple,t6000 apple,arm-platform
write_tree m2-max proc apple,j416c apple,t6021 apple,arm-platform
write_tree m1-mini proc apple,j274 apple,t8103 apple,arm-platform
write_tree yoga-slim7x proc lenovo,yoga-slim7x qcom,x1e80100
write_tree xps13-9345 proc dell,xps13-9345 qcom,x1e80100
write_tree t14s proc lenovo,thinkpad-t14s qcom,x1e78100 qcom,x1e80100
for board in m1-pro m2-max m1-mini; do
expect "$board" aarch64 aarch64-apple "the $board device tree is Apple Silicon"
done
for board in yoga-slim7x xps13-9345 t14s; do
expect "$board" aarch64 aarch64 "the $board device tree is plain aarch64, never Apple Silicon"
done
pass "real Apple and Snapdragon 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 aarch64 "a QEMU virt board is plain aarch64"
expect raspberry-pi aarch64 aarch64 "a Raspberry Pi is plain aarch64"
expect acpi aarch64 aarch64 "an aarch64 machine without a device tree is plain aarch64"
expect acpi x86_64 x86 "an x86 machine without a device tree is x86"
pass "unknown aarch64 and x86 machines need no family"
# 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
expect substring aarch64 aarch64 "vendor names inside other tokens do not identify the board"
pass "only a token's vendor prefix identifies the board"
# /proc/device-tree is a link into sysfs; read sysfs when it is missing.
write_tree sysfs-qualcomm sys lenovo,yoga-slim7x qcom,x1e80100
write_tree sysfs-apple sys apple,j314s apple,t6000 apple,arm-platform
expect sysfs-qualcomm aarch64 aarch64 "sysfs reads a Snapdragon tree without /proc/device-tree"
expect sysfs-apple aarch64 aarch64-apple "sysfs identifies Apple Silicon without /proc/device-tree"
write_tree agree proc apple,j314s apple,t6000 apple,arm-platform
write_tree agree sys apple,j314s apple,t6000 apple,arm-platform
expect agree aarch64 aarch64-apple "matching proc and sysfs trees agree"
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 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"
write_tree vendor-vs-none proc apple,j314s apple,t6000 apple,arm-platform
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"
if TEST_ARCH=x86_64 OMARCHY_PROC_ROOT="$test_tmp/cases/m1-pro/proc" PATH="$stub_bin:$ROOT/bin:$PATH" \
"$ROOT/bin/omarchy-hw-aarch64-apple" 2>/dev/null; then
fail "the Apple predicate fails closed on contradictory identity"
fi
pass "contradictory identity fails with an explanation"
failing_uname="$test_tmp/failing-uname"
mkdir -p "$failing_uname"
printf '#!/bin/bash\nexit 1\n' >"$failing_uname/uname"
chmod +x "$failing_uname/uname"
if OMARCHY_PROC_ROOT="$test_tmp/cases/acpi/proc" PATH="$failing_uname:$PATH" "$detector" 2>"$test_tmp/error"; then
fail "an unreadable CPU architecture fails instead of guessing generic"
fi
grep -Fq "cannot read the CPU architecture" "$test_tmp/error" || fail "an unreadable CPU architecture explains itself" "$(cat "$test_tmp/error")"
pass "an unreadable CPU architecture fails instead of guessing generic"
# The architecture predicates answer from the CPU alone: aarch64 covers every
# ARM family, Apple Silicon included, and x86 none of them.
for platform in x86 aarch64 aarch64-apple; do
fake_platform "$test_tmp/arch-$platform" "$platform"
arch_path="$test_tmp/arch-$platform/bin:$ROOT/bin:$PATH"
if [[ $platform == "x86" ]]; then
PATH=$arch_path omarchy-hw-x86 || fail "x86: omarchy-hw-x86 accepts it"
! PATH=$arch_path omarchy-hw-aarch64 || fail "x86: omarchy-hw-aarch64 rejects it"
else
PATH=$arch_path omarchy-hw-aarch64 || fail "$platform: omarchy-hw-aarch64 accepts it"
! PATH=$arch_path omarchy-hw-x86 || fail "$platform: omarchy-hw-x86 rejects it"
fi
done
pass "omarchy-hw-x86 and omarchy-hw-aarch64 split every platform by its CPU"
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
touchpad="$ROOT/bin/omarchy-hw-touchpad"
require_command jq
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
input_class="$test_tmp/input"
mkdir -p "$stub_bin"
# Hyprland's mice, in its order, from a space-separated MICE.
cat >"$stub_bin/hyprctl" <<'SH'
#!/bin/bash
jq -n --arg names "${MICE:-}" \
'{mice: [$names | split(" ")[] | select(. != "") | {name: .}], keyboards: []}'
SH
# udev's properties for an event device, kept beside it in the fixture.
cat >"$stub_bin/udevadm" <<'SH'
#!/bin/bash
cat "${!#}/udev" 2>/dev/null
SH
chmod +x "$stub_bin"/*
# An event device as sysfs shows it: its input device's name, and what udev
# made of it.
event_device() {
mkdir -p "$input_class/$1/device"
printf '%s\n' "$2" >"$input_class/$1/device/name"
printf '%s\n' "$3" >"$input_class/$1/udev"
}
detected() {
MICE="$1" OMARCHY_INPUT_CLASS_PATH="$input_class" PATH="$stub_bin:$PATH" bash "$touchpad"
}
# Apple Silicon's trackpad is named after its MTP HID interface, so neither
# word is in its name; udev still calls it a touchpad.
event_device event1 "Apple MTP keyboard" "ID_INPUT_KEYBOARD=1"
event_device event2 "Apple MTP multi-touch" "ID_INPUT_TOUCHPAD=1"
[[ $(detected "apple-mtp-multi-touch") == "apple-mtp-multi-touch" ]] ||
fail "the Apple Silicon trackpad is detected" "$(detected "apple-mtp-multi-touch")"
pass "the Apple Silicon trackpad is detected"
# omarchy-toggle-input-device passes the name straight to hl.device, so a name
# that does not match leaves the menu entry hidden and the toggle dead.
for name in "elan-touchpad" "apple-magic-trackpad-2"; do
[[ $(detected "$name") == "$name" ]] || fail "the touchpads that already worked still do" "$(detected "$name")"
done
pass "the touchpads that already worked still do"
# Some touchscreens' mouse interfaces are called multi-touch as well. A named
# touchpad wins over one listed first, and without one it takes udev calling
# the device a touchpad, not its name.
rm -rf "$input_class"
event_device event5 "ELAN9008:00 04F3:2C82 Multi-Touch" "ID_INPUT_TOUCHSCREEN=1"
event_device event7 "SYNA2BA6:00 06CB:CEF5 Touchpad" "ID_INPUT_TOUCHPAD=1"
[[ $(detected "elan9008:00-04f3:2c82-multi-touch syna2ba6:00-06cb:cef5-touchpad") == "syna2ba6:00-06cb:cef5-touchpad" ]] ||
fail "a touchscreen listed first does not take the touchpad's place" "$(detected "elan9008:00-04f3:2c82-multi-touch syna2ba6:00-06cb:cef5-touchpad")"
[[ -z $(detected "elan9008:00-04f3:2c82-multi-touch") ]] ||
fail "a multi-touch touchscreen alone is no touchpad" "$(detected "elan9008:00-04f3:2c82-multi-touch")"
pass "a multi-touch touchscreen is never taken for the touchpad"
# Hyprland turns commas into dashes as well as spaces.
event_device event9 "Acme, Inc. Precision Pad" "ID_INPUT_TOUCHPAD=1"
[[ $(detected "acme--inc.-precision-pad") == "acme--inc.-precision-pad" ]] ||
fail "a touchpad named with a comma is found by Hyprland's name for it" "$(detected "acme--inc.-precision-pad")"
pass "a touchpad named with a comma is found by Hyprland's name for it"
# Hyprland adds -1, -2 and so on to a name another device already took, as when
# the MTP interface exposes the trackpad twice; that name is the one to toggle,
# and an unrelated name that merely starts the same is not.
event_device event11 "Apple MTP multi-touch" "ID_INPUT_TOUCHPAD=1"
[[ $(detected "apple-mtp-multi-touch-1") == "apple-mtp-multi-touch-1" ]] ||
fail "a touchpad under Hyprland's numbered name is found" "$(detected "apple-mtp-multi-touch-1")"
[[ -z $(detected "apple-mtp-multi-touch-extra") ]] ||
fail "a name that only starts with the touchpad's is not taken for it" "$(detected "apple-mtp-multi-touch-extra")"
pass "a touchpad under Hyprland's numbered name for a duplicate is found"
# udev's touchpad has to be one of Hyprland's mice for its name to toggle.
[[ -z $(detected "logitech-mx-master-3") ]] ||
fail "a udev touchpad Hyprland does not list is not reported" "$(detected "logitech-mx-master-3")"
pass "an ordinary mouse is not mistaken for a touchpad"
[[ -z $(detected "") ]] ||
fail "no touchpad answers with an empty detection" "$(detected "")"
pass "no touchpad answers with an empty detection"
@@ -151,7 +151,9 @@ cat > "$stub_bin/omarchy-default-dictation" <<'SH'
#!/bin/bash
echo voxtype
SH
chmod +x "$stub_bin/omarchy-default-dictation"
# The dictation keys are bound only once the selected backend is installed.
printf '#!/bin/bash\nexit 0\n' > "$stub_bin/voxtype"
chmod +x "$stub_bin/omarchy-default-dictation" "$stub_bin/voxtype"
bindings=$(PATH="$stub_bin:$PATH" list_bindings "$home")
[[ -n $bindings ]] || fail "default bindings load for the conflict check"
+5 -1
View File
@@ -158,6 +158,10 @@ echo future-backend
SH
chmod +x "$missing_bin/omarchy-default-dictation"
dictation_output=$(PATH="$missing_bin" run_omarchy_bindings "$dictation_home")
if grep -Fq 'dictation' <<<"$dictation_output"; then fail "a selected backend that is not installed leaves application shortcuts available"; fi
printf '#!/bin/bash\nexit 0\n' > "$missing_bin/future-backend"
chmod +x "$missing_bin/future-backend"
dictation_output=$(PATH="$missing_bin" run_omarchy_bindings "$dictation_home")
for binding in \
$'SUPER + CTRL + X\tToggle dictation' \
$'F9\tStart dictation (push-to-talk)' \
@@ -166,7 +170,7 @@ for binding in \
$'ALT + Alt_R\tStop dictation (push-to-talk)'; do
grep -Fq "$binding" <<<"$dictation_output" || fail "dictation bindings load for any selected backend" "$binding"
done
pass "dictation bindings require a selection without hardcoding backends"
pass "dictation bindings require an installed selection without hardcoding backends"
# Lazydocker is optional on new installs, while existing installs keep the hotkey.
lazydocker_bin="$tmpdir/lazydocker-bin"
@@ -0,0 +1,665 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Root never reads the fixture root, so as root the build and first boot below
# would act on this machine. A skip is a pass.
if (( EUID == 0 )); then
skip "running as root, where image setup ignores fixture roots; skipping"
exit 0
fi
umask 022
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
unit_name=omarchy-provision-hardware.service
unit_source="$ROOT/install/provisioning/$unit_name"
# omarchy-apply-hardware refuses anyone but root, and root never reads the
# fixture root. Run a copy whose only change is that check, so a normal user
# can build an image into a fixture root.
apply_hardware="$test_tmp/omarchy-apply-hardware"
sed 's/^if (( EUID != 0 )); then$/if false; then/' "$ROOT/bin/omarchy-apply-hardware" >"$apply_hardware"
chmod +x "$apply_hardware"
grep -q '^if false; then$' "$apply_hardware" || fail "the test copy of omarchy-apply-hardware drops only its root check"
# Commands a hardware leaf would reach for. Any call means a leaf ran.
stub_bin="$test_tmp/stub-bin"
mkdir -p "$stub_bin"
for command in sudo pacman pacman-key systemctl lspci omarchy-pkg-add modinfo; do
cat >"$stub_bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$LEAF_CALLS"
exit 1
SH
done
stub_rebuild() {
cat >"$stub_bin/$1" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$REBUILDS"
SH
chmod +x "$stub_bin/$1"
}
stub_rebuild mkinitcpio
# Only the stubs count as present, so a machine with Limine never runs its own
# limine-mkinitcpio here.
cat >"$stub_bin/omarchy-cmd-present" <<SH
#!/bin/bash
[[ -x $stub_bin/\$1 ]]
SH
chmod +x "$stub_bin"/*
export LEAF_CALLS="$test_tmp/leaf-calls" REBUILDS="$test_tmp/rebuilds"
fake_platform "$test_tmp/hw" aarch64-apple
base_path="$test_tmp/hw/bin:$stub_bin:$ROOT/bin:/usr/local/bin:/usr/bin:/bin"
# An Omarchy tree whose hardware setup is three leaves that log their runs.
# Leaves a and b write initramfs drop-ins; b then fails while $FAIL_B exists.
fixture="$test_tmp/omarchy"
mkdir -p "$fixture/install/helpers" "$fixture/install/provisioning" "$fixture/install/hardware/apple" "$fixture/bin"
cp "$ROOT/install/helpers/logging.sh" "$ROOT/install/helpers/image-target.sh" "$fixture/install/helpers/"
cp "$unit_source" "$fixture/install/provisioning/"
cat >"$fixture/install/hardware/all.sh" <<'SH'
run_logged "$OMARCHY_INSTALL/hardware/a.sh"
run_logged "$OMARCHY_INSTALL/hardware/apple/b.sh"
run_logged "$OMARCHY_INSTALL/hardware/c.sh"
SH
cat >"$fixture/install/hardware/a.sh" <<'SH'
printf 'a user=%s path=%s\n' "${OMARCHY_INSTALL_USER-unset}" "$OMARCHY_PATH" >>"$RUNS"
mkdir -p "$OMARCHY_IMAGE_ROOT/etc/mkinitcpio.conf.d"
echo 'MODULES+=(a)' >"$OMARCHY_IMAGE_ROOT/etc/mkinitcpio.conf.d/a.conf"
SH
cat >"$fixture/install/hardware/apple/b.sh" <<'SH'
mkdir -p "$OMARCHY_IMAGE_ROOT/etc/mkinitcpio.conf.d"
echo 'MODULES+=(b)' >"$OMARCHY_IMAGE_ROOT/etc/mkinitcpio.conf.d/b.conf"
[[ ! -e $FAIL_B ]] || return 1
echo b >>"$RUNS"
SH
cat >"$fixture/install/hardware/c.sh" <<'SH'
echo c >>"$RUNS"
[[ ! -e $REQUEST_REBUILD ]] || touch "$OMARCHY_IMAGE_BOOT_REBUILD"
SH
export RUNS="$test_tmp/runs" FAIL_B="$test_tmp/fail-b" REQUEST_REBUILD="$test_tmp/request-rebuild"
new_root() {
local root="$test_tmp/root-$1"
rm -rf "$root"
mkdir -p "$root/var/log"
printf '%s\n' "$root"
}
write_manifest() {
local root=$1 body=${2:-$'format=1\nplatform=aarch64-apple\n'}
mkdir -p "$root/var/lib/omarchy/image"
chmod 0755 "$root/var/lib/omarchy/image"
printf '%s' "$body" >"$root/var/lib/omarchy/image/target"
chmod 0644 "$root/var/lib/omarchy/image/target"
}
build() {
local root=$1 omarchy=${2:-$fixture}
OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$omarchy" OMARCHY_INSTALL_LOG_FILE="$root/var/log/omarchy-install.log" \
PATH="$base_path" "$apply_hardware" --defer-provisioning
}
first_boot() {
local root=$1
OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$fixture" OMARCHY_PROC_ROOT="$test_tmp/hw/proc" \
PATH="$base_path" "$ROOT/bin/omarchy-provision-hardware"
}
reset_logs() {
rm -f "$RUNS" "$LEAF_CALLS" "$REBUILDS" "$FAIL_B"
}
queue_of() {
cat "$1/var/lib/omarchy/image/deferred-steps"
}
# --- The build ---------------------------------------------------------------
# The real hardware setup: every leaf is queued in order and none of them runs.
reset_logs
root=$(new_root real)
write_manifest "$root"
output=$(build "$root" "$ROOT") || fail "an image build defers the real hardware setup" "$output"
expected=$(sed -n 's|^run_logged "\$OMARCHY_INSTALL/\(hardware/[^"]*\)"$|install/\1|p' "$ROOT/install/hardware/all.sh")
[[ -n $expected && $(queue_of "$root") == "$expected" ]] ||
fail "an image build queues every hardware leaf in order" "expected:
$expected
queued:
$(queue_of "$root")"
[[ ! -e $LEAF_CALLS ]] || fail "an image build runs no hardware leaf" "$(cat "$LEAF_CALLS")"
[[ $output == *"Image build for aarch64-apple: deferred $(wc -l <<<"$expected") hardware steps to first boot"* ]] ||
fail "an image build reports what it deferred" "$output"
pass "an image build queues every hardware leaf in order and runs none of them"
[[ $(<"$unit_source") == "$(<"$root/etc/systemd/system/$unit_name")" ]] ||
fail "an image build installs the first-boot hardware service"
[[ $(readlink "$root/etc/systemd/system/multi-user.target.wants/$unit_name") == "/etc/systemd/system/$unit_name" ]] ||
fail "an image build enables the first-boot hardware service"
[[ -f $root/var/lib/omarchy/image/target && ! -e $root/var/lib/omarchy/image/target.booted ]] ||
fail "an image build leaves its manifest for the first boot"
[[ $(stat -c '%a' "$root/var/lib/omarchy/image/deferred-steps") == "644" ]] ||
fail "the queue is readable and only its owner writes it"
pass "an image build arms the first-boot hardware service"
before=$(queue_of "$root")
build "$root" "$ROOT" >/dev/null || fail "an image build can run hardware setup again"
[[ $(queue_of "$root") == "$before" ]] || fail "running hardware setup again in the build queues the same steps"
pass "running hardware setup again in the build queues the same steps"
# Without a manifest nothing is deferred: the leaves run as they always did.
reset_logs
root=$(new_root live)
build "$root" >/dev/null || fail "hardware setup without a manifest runs"
[[ $(cat "$RUNS") == $'a user= path='"$fixture"$'\nb\nc' ]] || fail "hardware setup without a manifest runs every leaf" "$(cat "$RUNS")"
[[ ! -e $root/var/lib/omarchy/image && ! -e $root/etc/systemd/system/$unit_name ]] ||
fail "hardware setup without a manifest queues and arms nothing"
pass "hardware setup without a manifest runs every leaf and defers nothing"
# Hardware setup that has to wait for a first boot in progress decides only
# once it has the lock: by then the manifest is retired and the leaves run here.
reset_logs
root=$(new_root race)
write_manifest "$root"
(
exec {lock}>>"$root/var/lib/omarchy/image/lock"
flock "$lock"
touch "$test_tmp/locked"
until [[ -e $test_tmp/release || ! -d $test_tmp ]]; do sleep 0.1; done
mv "$root/var/lib/omarchy/image/target" "$root/var/lib/omarchy/image/target.booted"
) &
first_boot_pid=$!
until [[ -e $test_tmp/locked ]]; do sleep 0.1; done
build "$root" >/dev/null &
setup_pid=$!
sleep 1
kill -0 "$setup_pid" 2>/dev/null || fail "hardware setup waits while a first boot holds the lock"
touch "$test_tmp/release"
wait "$first_boot_pid"
wait "$setup_pid" || fail "hardware setup runs after waiting for a first boot"
[[ $(cat "$RUNS") == $'a user= path='"$fixture"$'\nb\nc' && ! -e $root/var/lib/omarchy/image/deferred-steps ]] ||
fail "hardware setup that waited for a first boot runs its leaves instead of queueing them"
pass "hardware setup waits for a first boot in progress and then runs on the machine"
# A manifest that is not unambiguously root's own stops hardware setup outright.
check_refused() {
local description=$1 root=$2
reset_logs
if build "$root" >/dev/null 2>&1; then
fail "hardware setup refuses $description"
fi
[[ ! -e $RUNS && ! -e $root/var/lib/omarchy/image/deferred-steps && ! -e $root/etc/systemd/system/$unit_name ]] ||
fail "hardware setup refuses $description without running, queueing or arming anything"
pass "hardware setup refuses $description"
}
root=$(new_root format)
write_manifest "$root" $'format=2\nplatform=aarch64-apple\n'
check_refused "a manifest of an unknown format" "$root"
root=$(new_root platform)
write_manifest "$root" $'format=1\nplatform=apple\n'
check_refused "a manifest naming an unknown platform" "$root"
root=$(new_root malformed)
write_manifest "$root" $'format=1\napple-silicon\n'
check_refused "a malformed manifest" "$root"
root=$(new_root writable)
write_manifest "$root"
chmod 0666 "$root/var/lib/omarchy/image/target"
check_refused "a manifest others can write" "$root"
root=$(new_root writable-dir)
write_manifest "$root"
chmod 0777 "$root/var/lib/omarchy/image"
check_refused "a manifest in a directory others can write" "$root"
root=$(new_root symlink)
write_manifest "$root"
mv "$root/var/lib/omarchy/image/target" "$root/var/lib/omarchy/image/real-target"
ln -s real-target "$root/var/lib/omarchy/image/target"
check_refused "a symlinked manifest" "$root"
# --- The first boot ----------------------------------------------------------
reset_logs
root=$(new_root fresh)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
[[ $(queue_of "$root") == $'install/hardware/a.sh\ninstall/hardware/apple/b.sh\ninstall/hardware/c.sh' ]] ||
fail "the fixture image queues its three leaves" "$(queue_of "$root")"
output=$(first_boot "$root") || fail "the first boot finishes the deferred hardware setup" "$output"
[[ $(cat "$RUNS") == $'a user= path='"$fixture"$'\nb\nc' ]] ||
fail "the first boot runs each deferred leaf once, in order, with no install user" "$(cat "$RUNS")"
[[ $output == *"First boot of an image built for aarch64-apple, on aarch64-apple hardware"* ]] ||
fail "the first boot reports the image target and the live platform" "$output"
[[ ! -e $root/var/lib/omarchy/image/target && -f $root/var/lib/omarchy/image/target.booted ]] ||
fail "the first boot retires the build manifest"
[[ ! -e $root/var/lib/omarchy/image/deferred-steps ]] || fail "the first boot empties the queue"
[[ ! -L $root/etc/systemd/system/multi-user.target.wants/$unit_name && -f $root/etc/systemd/system/$unit_name ]] ||
fail "the first boot disables its service and keeps the unit, so a start job already queued is skipped rather than failed"
[[ $(cat "$REBUILDS") == "mkinitcpio -P" ]] ||
fail "the first boot rebuilds the initramfs once after a leaf changed it" "$(cat "$REBUILDS" 2>/dev/null)"
[[ ! -e $root/var/lib/omarchy/image/initramfs-inputs ]] || fail "the first boot clears the rebuild it owed"
grep -q 'Completed: .*/install/hardware/c.sh' "$root/var/log/omarchy-install.log" ||
fail "the first boot logs each step to the install log"
pass "the first boot runs the deferred hardware setup with no command from the owner"
rm -f "$RUNS" "$REBUILDS"
output=$(first_boot "$root") || fail "running the first-boot hardware setup again succeeds"
[[ -z $output && ! -e $RUNS && ! -e $REBUILDS ]] ||
fail "running the first-boot hardware setup again does nothing" "$output"
pass "running the first-boot hardware setup again does nothing"
# The live platform is asked once the manifest is retired, so it is the
# hardware's even where the detector cannot tell a booted root (this fixture
# runs no systemd): an Apple Silicon image booted in a VM reports plain aarch64.
fake_platform "$test_tmp/vm" aarch64
reset_logs
root=$(new_root vm)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds for a VM boot"
output=$(OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$fixture" OMARCHY_PROC_ROOT="$test_tmp/vm/proc" \
PATH="$test_tmp/vm/bin:$stub_bin:$ROOT/bin:/usr/local/bin:/usr/bin:/bin" "$ROOT/bin/omarchy-provision-hardware") ||
fail "the first boot on other hardware finishes" "$output"
[[ $output == *"First boot of an image built for aarch64-apple, on aarch64 hardware"* ]] ||
fail "the first boot reports the hardware it runs on, not the image target" "$output"
pass "the first boot reports the hardware it runs on, not the image target"
# A failed step stays queued with everything after it; the next boot resumes there.
reset_logs
root=$(new_root resume)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
# A repository without a sync database, which the image never has: the failed
# step is not retried after a fetch.
mkdir -p "$root/etc"
printf '[options]\nArchitecture = auto\n\n[core]\nServer = https://mirror.invalid/$arch/$repo\n' >"$root/etc/pacman.conf"
touch "$FAIL_B"
status=0
output=$(first_boot "$root" 2>&1) || status=$?
(( status == 75 )) || fail "a failed step exits 75, so a platform first boot can tell it from a refusal" "status $status: $output"
[[ $output == *"Deferred hardware step failed: install/hardware/apple/b.sh"* ]] ||
fail "the first boot names the failed step" "$output"
[[ $(cat "$RUNS") == "a user= path=$fixture" ]] || fail "the first boot stops at the failed step" "$(cat "$RUNS")"
[[ $(queue_of "$root") == $'install/hardware/apple/b.sh\ninstall/hardware/c.sh' ]] ||
fail "the failed step and those after it stay queued" "$(queue_of "$root")"
[[ -f $root/var/lib/omarchy/image/target.booted && ! -e $root/var/lib/omarchy/image/target ]] ||
fail "the machine stops being an image build once its first boot starts"
[[ -L $root/etc/systemd/system/multi-user.target.wants/$unit_name && -e $root/var/lib/omarchy/image/initramfs-inputs && ! -e $REBUILDS ]] ||
fail "the service stays armed and the initramfs rebuild stays owed until the queue is empty"
[[ ! -e $LEAF_CALLS ]] || fail "a failed step fetches no package databases" "$(cat "$LEAF_CALLS")"
pass "a failed step stays queued with the steps after it"
rm -f "$FAIL_B" "$RUNS"
stub_rebuild limine-mkinitcpio
output=$(first_boot "$root") || fail "the next boot finishes the deferred hardware setup" "$output"
[[ $(cat "$RUNS") == $'b\nc' ]] || fail "the next boot resumes at the failed step" "$(cat "$RUNS")"
[[ $(cat "$REBUILDS") == "limine-mkinitcpio " ]] ||
fail "the next boot rebuilds the initramfs the earlier boot owed, through Limine when present" "$(cat "$REBUILDS" 2>/dev/null)"
[[ ! -e $root/var/lib/omarchy/image/deferred-steps && ! -L $root/etc/systemd/system/multi-user.target.wants/$unit_name ]] ||
fail "the next boot disarms the service"
pass "the next boot resumes at the failed step and finishes"
# The step that changed the initramfs inputs is the one that failed: its rerun
# changes nothing more, and the rebuild is still owed.
reset_logs
rm -f "$stub_bin/limine-mkinitcpio"
root=$(new_root rebuild)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/apple/b.sh install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
touch "$FAIL_B"
first_boot "$root" >/dev/null 2>&1 && fail "the first boot reports the failed step"
rm -f "$FAIL_B"
first_boot "$root" >/dev/null || fail "the next boot finishes the deferred hardware setup"
[[ $(cat "$RUNS") == $'b\nc' && $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" ]] ||
fail "a step that changed the initramfs and then failed still gets it rebuilt" "$(cat "$REBUILDS" 2>/dev/null)"
pass "a step that changed the initramfs and then failed still gets it rebuilt"
# A step that only adds to the kernel command line Limine's entry tool builds
# (as intel/fred.sh does) gets the rebuild too.
reset_logs
root=$(new_root cmdline)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
cat >"$fixture/install/hardware/cmdline.sh" <<'SH'
mkdir -p "$OMARCHY_IMAGE_ROOT/etc/limine-entry-tool.d"
echo 'KERNEL_CMDLINE[default]+=" example=on"' >"$OMARCHY_IMAGE_ROOT/etc/limine-entry-tool.d/example.conf"
SH
printf '%s\n' install/hardware/cmdline.sh >"$root/var/lib/omarchy/image/deferred-steps"
first_boot "$root" >/dev/null || fail "a step that changes the kernel command line finishes"
[[ $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" ]] ||
fail "a step that only changed Limine's kernel command line gets the rebuild" "$(cat "$REBUILDS" 2>/dev/null)"
rm -f "$fixture/install/hardware/cmdline.sh"
pass "a step that only changed Limine's kernel command line gets the rebuild"
# Module options reach the initramfs through mkinitcpio's modconf hook, so a
# step that only writes /etc/modprobe.d (as nvidia.sh does) gets the rebuild too.
reset_logs
root=$(new_root modprobe)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
cat >"$fixture/install/hardware/modprobe.sh" <<'SH'
mkdir -p "$OMARCHY_IMAGE_ROOT/etc/modprobe.d"
echo 'options example flag=1' >"$OMARCHY_IMAGE_ROOT/etc/modprobe.d/example.conf"
SH
printf '%s\n' install/hardware/modprobe.sh >"$root/var/lib/omarchy/image/deferred-steps"
first_boot "$root" >/dev/null || fail "a step that changes module options finishes"
[[ $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" ]] ||
fail "a step that only changed module options gets the rebuild" "$(cat "$REBUILDS" 2>/dev/null)"
rm -f "$fixture/install/hardware/modprobe.sh"
pass "a step that only changed module options gets the rebuild"
# --- The pacman keyring -------------------------------------------------------
# Install finalization makes the pacman keyring on plain aarch64.
# An image build asks for it instead, so every machine flashed from the image
# makes its own master key on its first boot, before any step installs a package.
keyring_bin="$test_tmp/keyring-bin"
mkdir -p "$keyring_bin" "$fixture/install/post-install"
# Finalization's repositories are pacman-templates-test.sh's; here they land in
# the root under test, never the host's /etc.
sed 's|/etc/pacman|$OMARCHY_IMAGE_ROOT/etc/pacman|g' "$ROOT/install/post-install/pacman.sh" >"$fixture/install/post-install/pacman.sh"
cp "$ROOT/install/helpers/pacman.sh" "$fixture/install/helpers/pacman.sh"
mkdir -p "$fixture/default"
cp -r "$ROOT/default/pacman" "$fixture/default/"
: >"$fixture/install/hardware/pacman.sh"
for command in pacman-key omarchy-pkg-add; do
cat >"$keyring_bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$KEYRING"
[[ ${0##*/} != "pacman-key" || ! -e $KEYRING_FAIL ]]
SH
done
printf '#!/bin/bash\necho "$PLATFORM"\n' >"$keyring_bin/omarchy-hw-platform"
chmod +x "$keyring_bin"/*
export KEYRING="$test_tmp/keyring" KEYRING_FAIL="$test_tmp/keyring-fail"
request=var/lib/omarchy/image/pacman-keyring
finalize() {
mkdir -p "$1/etc/pacman.d"
OMARCHY_IMAGE_ROOT="$1" OMARCHY_PATH="$fixture" OMARCHY_INSTALL="$fixture/install" PLATFORM=$2 \
PATH="$keyring_bin:$base_path" bash -e -c 'source "$1"' bash "$fixture/install/post-install/pacman.sh"
}
keyring_boot() {
OMARCHY_IMAGE_ROOT="$1" OMARCHY_PATH="$fixture" OMARCHY_PROC_ROOT="$test_tmp/hw/proc" \
PATH="$keyring_bin:$base_path" "$ROOT/bin/omarchy-provision-hardware"
}
for platform in aarch64; do
rm -f "$KEYRING"
root=$(new_root "install-$platform")
finalize "$root" "$platform" || fail "$platform: install finalization succeeds"
[[ $(cat "$KEYRING") == $'omarchy-pkg-add archlinuxarm-keyring\npacman-key --init\npacman-key --populate' ]] ||
fail "$platform: an install makes its pacman keyring in finalization, as before" "$(cat "$KEYRING")"
[[ ! -e $root/$request ]] || fail "$platform: an install requests no first-boot keyring"
rm -f "$KEYRING"
root=$(new_root "image-$platform")
write_manifest "$root" $'format=1\nplatform='"$platform"$'\n'
build "$root" >/dev/null || fail "$platform: the fixture image builds"
finalize "$root" "$platform" || fail "$platform: image finalization succeeds"
[[ $(cat "$KEYRING") == "omarchy-pkg-add archlinuxarm-keyring" ]] ||
fail "$platform: an image build makes no pacman keyring" "$(cat "$KEYRING")"
[[ -f $root/$request ]] || fail "$platform: an image build asks its first boot for the keyring"
done
for platform in aarch64-apple x86; do
rm -f "$KEYRING"
root=$(new_root "image-$platform")
write_manifest "$root" $'format=1\nplatform='"$platform"$'\n'
build "$root" >/dev/null || fail "$platform: the fixture image builds"
finalize "$root" "$platform" || fail "$platform: image finalization succeeds"
finalize "$(new_root "install-$platform")" "$platform" || fail "$platform: install finalization succeeds"
[[ ! -e $KEYRING && ! -e $root/$request ]] || fail "$platform: finalization leaves the keyring alone" "$(cat "$KEYRING" 2>/dev/null)"
done
pass "an image build on plain aarch64 leaves the pacman keyring to the first boot"
reset_logs
rm -f "$KEYRING"
root=$(new_root image-aarch64-apple)
write_manifest "$root"
build "$root" >/dev/null || fail "the Mac fixture image builds"
keyring_boot "$root" >/dev/null || fail "a Mac image's first boot finishes"
[[ ! -e $KEYRING ]] || fail "a first boot nobody asked for a keyring leaves the keyring alone" "$(cat "$KEYRING")"
pass "a first boot nobody asked for a keyring leaves the keyring alone"
reset_logs
root=$test_tmp/root-image-aarch64
mkdir -p "$root/etc/pacman.d/gnupg"
touch "$root/etc/pacman.d/gnupg/build-master-key" "$KEYRING_FAIL"
status=0
output=$(KEYRING=$RUNS keyring_boot "$root" 2>&1) || status=$?
(( status == 75 )) || fail "a keyring that can't be made exits 75" "status $status: $output"
[[ $output == *"could not create the pacman keyring"* ]] || fail "the first boot says the keyring failed" "$output"
[[ $(cat "$RUNS") == "pacman-key --gpgdir $root/etc/pacman.d/gnupg --init" ]] ||
fail "no step runs before the keyring exists" "$(cat "$RUNS")"
[[ -f $root/$request && $(queue_of "$root") == $'install/hardware/a.sh\ninstall/hardware/apple/b.sh\ninstall/hardware/c.sh' ]] ||
fail "the keyring request and the whole queue stay for the next boot"
pass "a keyring that can't be made stays requested and holds back every step"
rm -f "$KEYRING_FAIL" "$RUNS"
mkdir -p "$root/etc/pacman.d/gnupg"
touch "$root/etc/pacman.d/gnupg/build-master-key"
output=$(KEYRING=$RUNS keyring_boot "$root") || fail "the next boot makes the keyring and finishes" "$output"
[[ $(cat "$RUNS") == "pacman-key --gpgdir $root/etc/pacman.d/gnupg --init"$'\n'"pacman-key --gpgdir $root/etc/pacman.d/gnupg --populate"$'\na user= path='"$fixture"$'\nb\nc' ]] ||
fail "the first boot makes the keyring, then runs the steps" "$(cat "$RUNS")"
[[ ! -e $root/etc/pacman.d/gnupg/build-master-key ]] || fail "the first boot replaces a keyring the build left"
[[ ! -e $root/$request && ! -e $root/var/lib/omarchy/image/deferred-steps ]] || fail "the first boot clears the keyring request"
pass "the first boot replaces any build keyring with the machine's own before the steps run"
# A keyring request with nothing queued (a build whose hardware setup queued no
# step) still gets its keyring.
reset_logs
rm -f "$RUNS"
root=$test_tmp/root-keyring-only
mkdir -p "$root/var/lib/omarchy/image"
install -m 0644 /dev/null "$root/$request"
output=$(KEYRING=$RUNS keyring_boot "$root") || fail "a first boot with only a keyring request finishes" "$output"
[[ $(cat "$RUNS") == "pacman-key --gpgdir $root/etc/pacman.d/gnupg --init"$'\n'"pacman-key --gpgdir $root/etc/pacman.d/gnupg --populate" && ! -e $root/$request ]] ||
fail "a keyring request is honoured with no queued step" "$(cat "$RUNS" 2>/dev/null)"
grep -qx 'ConditionPathExists=|/var/lib/omarchy/image/pacman-keyring' "$ROOT/install/provisioning/omarchy-provision-hardware.service" ||
fail "the first-boot unit also starts for a keyring request alone"
pass "a keyring request with no queued step still gets the machine's own keyring"
# A leaf a later Omarchy no longer ships is dropped; a queue entry outside the
# hardware setup stops the run before anything runs.
reset_logs
root=$(new_root gone)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/removed.sh install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
output=$(first_boot "$root") || fail "a step no longer shipped does not block the rest" "$output"
[[ $output == *"Skipping deferred hardware step install/hardware/removed.sh"* && $(cat "$RUNS") == "c" ]] ||
fail "a step no longer shipped is skipped and the rest run" "$output"
pass "a deferred step Omarchy no longer ships is skipped"
# A step that would build the boot image itself asks for the rebuild after the
# last step instead: one rebuild, even when no
# initramfs input changed, and the request outlives a run that stopped early.
reset_logs
root=$(new_root request)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
first_boot "$root" >/dev/null || fail "a step that asks for nothing finishes"
[[ ! -e $REBUILDS ]] || fail "no rebuild without a changed input or a request" "$(cat "$REBUILDS")"
reset_logs
root=$(new_root request)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
touch "$REQUEST_REBUILD"
first_boot "$root" >/dev/null || fail "a step that asks for the rebuild finishes"
rm -f "$REQUEST_REBUILD"
[[ $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" && ! -e $root/var/lib/omarchy/image/boot-rebuild ]] ||
fail "the request alone, with no changed input, gets one rebuild and is then cleared" "$(cat "$REBUILDS" 2>/dev/null)"
reset_logs
root=$(new_root request)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/c.sh install/hardware/apple/b.sh >"$root/var/lib/omarchy/image/deferred-steps"
rm -f "$root/etc/mkinitcpio.conf.d/b.conf"
touch "$REQUEST_REBUILD" "$FAIL_B"
first_boot "$root" >/dev/null 2>&1 && fail "the first boot reports the failed step"
[[ -f $root/var/lib/omarchy/image/boot-rebuild && ! -e $REBUILDS ]] ||
fail "a requested rebuild waits for the queue to empty, and stays requested"
rm -f "$FAIL_B" "$REQUEST_REBUILD"
first_boot "$root" >/dev/null || fail "the next boot finishes the deferred hardware setup"
[[ $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" && ! -e $root/var/lib/omarchy/image/boot-rebuild ]] ||
fail "the requested rebuild runs once after the last step and is then cleared" "$(cat "$REBUILDS" 2>/dev/null)"
pass "a step can ask for the one rebuild after the last step"
for entry in /etc/passwd install/hardware/../../bin/x.sh install/login/sddm.sh; do
reset_logs
root=$(new_root unsafe)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' "$entry" install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
status=0
first_boot "$root" >/dev/null 2>&1 || status=$?
(( status == 1 )) || fail "the first boot refuses the queue entry $entry with status 1, not a step failure's 75" "status $status"
[[ ! -e $RUNS && $(head -n 1 "$root/var/lib/omarchy/image/deferred-steps") == "$entry" ]] ||
fail "the first boot refuses the queue entry $entry before running anything"
done
pass "the first boot runs only hardware setup leaves from its queue"
reset_logs
root=$(new_root nothing)
output=$(first_boot "$root") || fail "the first-boot hardware setup succeeds with nothing deferred"
[[ -z $output && ! -e $root/var/lib/omarchy ]] || fail "the first-boot hardware setup does nothing with nothing deferred" "$output"
pass "with nothing deferred the first-boot hardware setup does nothing"
if env -u OMARCHY_IMAGE_ROOT OMARCHY_PATH="$fixture" PATH="$base_path" "$ROOT/bin/omarchy-provision-hardware" >/dev/null 2>&1; then
fail "the first-boot hardware setup refuses a normal user"
fi
pass "the first-boot hardware setup refuses a normal user"
# The service fires on the queue this helper writes and hands the machine to
# owner setup and the login screen only after it.
grep -qx 'ConditionPathExists=|/var/lib/omarchy/image/deferred-steps' "$unit_source" &&
grep -qx 'ExecStart=/usr/bin/omarchy-provision-hardware' "$unit_source" &&
grep -qx 'Before=omarchy-provision-owner.service display-manager.service' "$unit_source" &&
grep -qx 'WantedBy=multi-user.target' "$unit_source" ||
fail "the first-boot hardware service runs on the queue before owner setup and the login screen"
pass "the first-boot hardware service runs on the queue before owner setup and the login screen"
# --- Bluetooth on the first boot ---------------------------------------------
# The adapter brings up bluetooth.target before the first boot runs the
# Bluetooth leaf, and a target never starts a unit enabled after its start job
# was made, so that boot has to start bluetooth.service itself. Nothing else
# does: an install or a rerun of hardware setup leaves the service as it finds it.
bt_fixture="$test_tmp/omarchy-bluetooth"
mkdir -p "$bt_fixture/install/helpers" "$bt_fixture/install/provisioning" "$bt_fixture/install/hardware"
cp "$ROOT/install/helpers/logging.sh" "$ROOT/install/helpers/image-target.sh" "$bt_fixture/install/helpers/"
cp "$unit_source" "$bt_fixture/install/provisioning/"
cp "$ROOT/install/hardware/bluetooth.sh" "$bt_fixture/install/hardware/"
cp "$fixture/install/hardware/c.sh" "$bt_fixture/install/hardware/"
printf 'run_logged "$OMARCHY_INSTALL/hardware/%s"\n' bluetooth.sh c.sh >"$bt_fixture/install/hardware/all.sh"
bt_bin="$test_tmp/bt-bin"
mkdir -p "$bt_bin"
cat >"$bt_bin/systemctl" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$SYSTEMCTL_CALLS"
case $* in
"start --no-block bluetooth.service") [[ ! -e $BT_START_REFUSED ]] ;;
*) exit 0 ;;
esac
SH
chmod +x "$bt_bin/systemctl"
export SYSTEMCTL_CALLS="$test_tmp/systemctl-calls" BT_START_REFUSED="$test_tmp/bt-start-refused"
bt_path="$bt_bin:$base_path"
bt_image() {
local root
root=$(new_root "bt-$1")
write_manifest "$root"
OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$bt_fixture" OMARCHY_INSTALL_LOG_FILE="$root/var/log/omarchy-install.log" \
PATH="$bt_path" "$apply_hardware" --defer-provisioning >/dev/null || fail "the Bluetooth fixture image builds"
printf '%s\n' "$root"
}
bt_first_boot() {
rm -f "$SYSTEMCTL_CALLS" "$RUNS"
OMARCHY_IMAGE_ROOT="$1" OMARCHY_PATH="$bt_fixture" OMARCHY_PROC_ROOT="$test_tmp/hw/proc" \
PATH="$bt_path" "$ROOT/bin/omarchy-provision-hardware" >/dev/null 2>&1
}
started=$'enable bluetooth.service\nstart --no-block bluetooth.service'
reset_logs
rm -f "$BT_START_REFUSED" "$SYSTEMCTL_CALLS"
root=$(bt_image started)
[[ ! -e $SYSTEMCTL_CALLS ]] || fail "an image build touches no Bluetooth unit" "$(cat "$SYSTEMCTL_CALLS")"
bt_first_boot "$root" || fail "the first boot finishes the Bluetooth leaf"
[[ $(<"$SYSTEMCTL_CALLS") == "$started" ]] ||
fail "the first boot enables Bluetooth and starts it without waiting for the job" "$(<"$SYSTEMCTL_CALLS")"
[[ $(cat "$RUNS") == c && ! -e $root/var/lib/omarchy/image/deferred-steps ]] ||
fail "the first boot goes on past the Bluetooth leaf"
pass "the first boot enables Bluetooth and starts it in the same session"
touch "$BT_START_REFUSED"
root=$(bt_image refused)
bt_first_boot "$root" || fail "a refused Bluetooth start does not fail the first boot"
[[ $(<"$SYSTEMCTL_CALLS") == "$started" && $(cat "$RUNS") == c && ! -e $root/var/lib/omarchy/image/deferred-steps ]] ||
fail "a refused Bluetooth start leaves the step done and the queue going" "$(<"$SYSTEMCTL_CALLS")"
pass "a refused Bluetooth start does not fail the first boot"
rm -f "$BT_START_REFUSED"
# A later boot that retries the queue is still the machine's own first setup.
root=$(bt_image retry)
mv "$root/var/lib/omarchy/image/target" "$root/var/lib/omarchy/image/target.booted"
bt_first_boot "$root" || fail "a retried queue finishes"
[[ $(<"$SYSTEMCTL_CALLS") == "$started" ]] ||
fail "a retried queue starts Bluetooth like the first run" "$(<"$SYSTEMCTL_CALLS")"
pass "a retried queue starts Bluetooth like the first run"
# Hardware setup outside the first boot, even with the marker inherited.
rm -f "$SYSTEMCTL_CALLS" "$RUNS"
root=$(new_root bt-live)
OMARCHY_IMAGE_DEFERRED_HARDWARE=1 OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$bt_fixture" \
OMARCHY_INSTALL_LOG_FILE="$root/var/log/omarchy-install.log" PATH="$bt_path" "$apply_hardware" --defer-provisioning >/dev/null ||
fail "hardware setup without a manifest runs the Bluetooth leaf"
[[ $(<"$SYSTEMCTL_CALLS") == "enable bluetooth.service" && $(cat "$RUNS") == c ]] ||
fail "hardware setup outside the first boot only enables Bluetooth" "$(<"$SYSTEMCTL_CALLS")"
pass "hardware setup outside the first boot only enables Bluetooth, as an install always has"
# --- Root ignores the fixture root ------------------------------------------
# As namespaced root the fixture root in the environment is ignored: the
# fixture image is not built into, and hardware setup runs as on any live system.
if [[ -e /var/lib/omarchy/image ]]; then
pass "this machine keeps image state of its own; skipping the root override probe"
elif unshare --user --map-root-user true 2>/dev/null; then
root_runner=(unshare --user --map-root-user)
reset_logs
root=$(new_root root-build)
write_manifest "$root"
OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$fixture" OMARCHY_INSTALL_LOG_FILE="$root/var/log/omarchy-install.log" \
PATH="$base_path" "${root_runner[@]}" "$ROOT/bin/omarchy-apply-hardware" --defer-provisioning >/dev/null ||
fail "root runs hardware setup with a fixture root in its environment"
[[ $(cat "$RUNS") == $'a user= path='"$fixture"$'\nb\nc' && ! -e $root/var/lib/omarchy/image/deferred-steps ]] ||
fail "root ignores a fixture manifest named by its environment"
# Both commands take every path from the helper, which as root answers with
# the live ones whatever the environment says.
resolved=$(OMARCHY_IMAGE_ROOT="$root" "${root_runner[@]}" bash -c \
'source "$1"; omarchy_image_init && printf "%s\n" "$omarchy_image_manifest" "$omarchy_image_queue" "$omarchy_image_systemd_dir"' \
bash "$ROOT/install/helpers/image-target.sh") || fail "root resolves the image paths"
[[ $resolved == $'/var/lib/omarchy/image/target\n/var/lib/omarchy/image/deferred-steps\n/etc/systemd/system' ]] ||
fail "root resolves the live image paths whatever its environment names" "$resolved"
pass "root ignores fixture roots in its environment"
else
pass "no unprivileged user namespace; skipping the root override probe"
fi
+442
View File
@@ -0,0 +1,442 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# omarchy-hw-platform answers with the image-target manifest while a root is
# being built, and with the hardware once it boots. Each world below is what one
# detector run sees: image/ is the root, proc/ its /proc (the host's device tree
# and PID 1), bin/ a uname for the CPU it runs on.
detector="$ROOT/bin/omarchy-hw-platform"
umask 022
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
apple_manifest=$'format=1\nplatform=aarch64-apple\n'
# $1 world, $2 CPU, then the host's device-tree tokens (none: no device tree).
world() {
local dir="$test_tmp/worlds/$1" cpu=$2
shift 2
rm -rf "$dir"
mkdir -p "$dir/image" "$dir/proc" "$dir/bin" "$dir/host"
if (( $# )); then
mkdir -p "$dir/proc/device-tree"
printf '%s\0' "$@" >"$dir/proc/device-tree/compatible"
fi
cat >"$dir/bin/uname" <<SH
#!/bin/bash
[[ \${1:-} == -m ]] && { echo $cpu; exit 0; }
exec /usr/bin/uname "\$@"
SH
chmod +x "$dir/bin/uname"
}
# $1 world, $2 how PID 1 sees the root: chroot (PID 1 is the build host's),
# own (PID 1 runs in the root, as in a PID namespace or a booted system),
# hidden (PID 1 is visible but its root is not, as for a normal user); $3 what
# PID 1 is, systemd unless named. Without a call the world has no /proc/1, as
# in a root without /proc or behind hidepid.
pid1() {
local dir="$test_tmp/worlds/$1"
mkdir -p "$dir/proc/1"
echo "${3:-systemd}" >"$dir/proc/1/comm"
case $2 in
chroot) ln -s ../../host "$dir/proc/1/root" ;;
own) ln -s ../../image "$dir/proc/1/root" ;;
hidden) ;;
esac
}
systemd_runs() {
mkdir -p "$test_tmp/worlds/$1/image/run/systemd/system"
}
# $1 world, $2 manifest body.
manifest() {
local image_dir="$test_tmp/worlds/$1/image/var/lib/omarchy/image"
mkdir -p "$image_dir"
chmod 0755 "$image_dir"
printf '%s' "$2" >"$image_dir/target"
chmod 0644 "$image_dir/target"
}
in_world() {
local dir="$test_tmp/worlds/$1"
shift
OMARCHY_PROC_ROOT="$dir/proc" OMARCHY_IMAGE_ROOT="$dir/image" PATH="$dir/bin:$ROOT/bin:$PATH" "$@"
}
detect() {
in_world "$1" "$detector" 2>"$test_tmp/error"
}
expect() {
local name=$1 expected=$2 description=$3 actual
actual=$(detect "$name") || fail "$description" "$(cat "$test_tmp/error")"
[[ $actual == "$expected" ]] || fail "$description" "expected: $expected
actual: $actual"
pass "$description"
}
expect_refused() {
local name=$1 message=$2 description=$3
if detect "$name" >/dev/null; then
fail "$description"
fi
grep -Fq "$message" "$test_tmp/error" || fail "$description explains itself" "$(cat "$test_tmp/error")"
pass "$description"
}
# --- Root reads only the live system -----------------------------------------
root_runner=()
if (( EUID != 0 )); then
root_runner=(unshare --user --map-root-user)
fi
if (( EUID == 0 )) || unshare --user --map-root-user true 2>/dev/null; then
live=$("${root_runner[@]}" "$detector") || fail "root detects the live platform"
apple_live=0
"${root_runner[@]}" "$ROOT/bin/omarchy-hw-aarch64-apple" || apple_live=$?
# A world that would make any environment-led detector answer with another
# platform than the live one.
if [[ $live == "aarch64-apple" ]]; then
hostile=aarch64
hostile_tokens=("raspberrypi,5-model-b" "brcm,bcm2712")
else
hostile=aarch64-apple
hostile_tokens=("apple,j416c" "apple,t6021" "apple,arm-platform")
fi
world hostile aarch64 "${hostile_tokens[@]}"
pid1 hostile chroot
manifest hostile $'format=1\nplatform='"$hostile"$'\n'
hostile_dir="$test_tmp/worlds/hostile"
printf 'echo %s\nexit 0\n' "$hostile" >"$test_tmp/bash-env"
# Exported functions shadow commands and even builtins in a plain bash, and
# BASH_ENV runs before its first line; --clean-environment is what the
# detector passes itself once it has restarted.
hostile_env=(
OMARCHY_PROC_ROOT="$hostile_dir/proc" OMARCHY_SYS_ROOT="$hostile_dir/sys" OMARCHY_IMAGE_ROOT="$hostile_dir/image"
PATH="$hostile_dir/bin:$ROOT/bin:$PATH" BASH_ENV="$test_tmp/bash-env"
'BASH_FUNC_uname%%=() { echo aarch64; }'
'BASH_FUNC_mapfile%%=() { tokens=('"${hostile_tokens[0]}"'); }'
'BASH_FUNC_stat%%=() { echo 0 644; }'
'BASH_FUNC_dirname%%=() { echo '"$hostile_dir/bin"'; }'
)
printf '#!/bin/bash\necho %s\n' "$hostile" >"$hostile_dir/bin/omarchy-hw-platform"
chmod +x "$hostile_dir/bin/omarchy-hw-platform"
for flag in "" --clean-environment; do
args=()
[[ -z $flag ]] || args=("$flag")
overridden=$(env "${hostile_env[@]}" "${root_runner[@]}" "$detector" "${args[@]}") ||
fail "root detects the live platform with a hostile environment${flag:+ and $flag}"
[[ $overridden == "$live" ]] ||
fail "root ignores fixture roots, PATH, BASH_ENV and exported functions${flag:+ with $flag}" "live: $live
hostile: $overridden"
done
apple_hostile=0
env "${hostile_env[@]}" "${root_runner[@]}" "$ROOT/bin/omarchy-hw-aarch64-apple" || apple_hostile=$?
(( apple_hostile == apple_live )) || fail "the Apple predicate ignores a hostile root environment"
pass "root ignores fixture roots, PATH, BASH_ENV and exported functions"
else
skip "no unprivileged user namespace; skipping the root environment probe"
fi
# Root with a manifest at the live path: a private mount namespace lays a
# fixture over /var/lib and /run, and in a PID namespace PID 1 is a copy of bash
# named for what it plays, the build or systemd, and runs the detector.
mkdir -p "$test_tmp/init"
cp "$BASH" "$test_tmp/init/bash"
cp "$BASH" "$test_tmp/init/systemd"
if (( EUID != 0 )) && "$test_tmp/init/bash" -c true 2>/dev/null &&
unshare --user --map-root-user --mount --pid --fork --mount-proc true 2>/dev/null; then
# $1 fixture, $2 PID 1 (bash or systemd), $3 a file to lay over the manifest.
live_root() {
local fixture=$1 init=$2 owner_file=${3:-}
unshare --user --map-root-user --mount --pid --fork --mount-proc "$test_tmp/init/$init" -c '
mount --bind "$1/var/lib" /var/lib && mount --bind "$1/run" /run || exit 99
if [[ -n $3 ]]; then mount --bind "$3" /var/lib/omarchy/image/target || exit 99; fi
"$2"
status=$?
exit "$status"
' "$init" "$fixture" "$detector" "$owner_file" 2>"$test_tmp/error"
}
baseline=$(unshare --user --map-root-user --mount --pid --fork --mount-proc "$detector") ||
fail "root detects the live platform in a PID namespace"
# A target the hardware would not give, so only the manifest can name it.
target=aarch64-apple
[[ $baseline != "aarch64-apple" ]] || target=aarch64
fixture="$test_tmp/live-root"
mkdir -p "$fixture/var/lib/omarchy/image" "$fixture/run"
printf 'format=1\nplatform=%s\n' "$target" >"$fixture/var/lib/omarchy/image/target"
expect_live_manifest() {
local description=$1 actual
if [[ $(uname -m) == aarch64 ]]; then
actual=$(live_root "$fixture" bash) || fail "$description" "$(cat "$test_tmp/error")"
[[ $actual == "$target" ]] || fail "$description" "expected: $target
actual: $actual"
else
if live_root "$fixture" bash >/dev/null; then
fail "$description, and a $target target on $(uname -m) contradicts it"
fi
grep -Fq "/var/lib/omarchy/image/target names $target hardware but the CPU is" "$test_tmp/error" ||
fail "$description" "$(cat "$test_tmp/error")"
fi
pass "$description"
}
expect_live_manifest "root answers with the live manifest in a build with a /run of its own"
mkdir -p "$fixture/run/systemd/system"
expect_live_manifest "root answers with the live manifest in a build whose PID 1 is not systemd, with systemd in /run"
actual=$(live_root "$fixture" systemd) || fail "a booted root ignores the live manifest" "$(cat "$test_tmp/error")"
[[ $actual == "$baseline" ]] || fail "a booted root ignores the live manifest" "expected: $baseline
actual: $actual"
pass "a booted root answers with its hardware despite a manifest"
rmdir "$fixture/run/systemd/system"
# A file the namespace's root does not own: the real root's, unmapped here.
if live_root "$fixture" bash /etc/os-release >/dev/null; then
fail "root refuses a manifest another user owns"
fi
grep -Fq "is not a root-owned regular file" "$test_tmp/error" || fail "root refuses a manifest another user owns" "$(cat "$test_tmp/error")"
pass "root refuses a manifest another user owns"
else
skip "no unprivileged user, mount and PID namespaces, or an executable temporary directory; skipping the live manifest probe"
fi
require_platform_fixtures "the image-target fixtures"
# --- Image builds -------------------------------------------------------------
# An Apple image built on an x86 host: the chroot runs under aarch64 emulation,
# the host has no device tree, and PID 1 is the host's.
world x86-host aarch64
pid1 x86-host chroot
manifest x86-host "$apple_manifest"
expect x86-host aarch64-apple "an Apple image built in a chroot on an x86 host is Apple Silicon"
# The same with the host's /run bound into the chroot, systemd's directory and all.
systemd_runs x86-host
expect x86-host aarch64-apple "a chroot build ignores the host's systemd in a bound /run"
# On a plain aarch64 host, whose own device tree the chroot can see.
world arm-host aarch64 linux,dummy-virt
pid1 arm-host chroot
manifest arm-host "$apple_manifest"
expect arm-host aarch64-apple "an Apple image built in a chroot on a plain aarch64 host is Apple Silicon"
# The image builder's isolated chroot: a PID namespace whose PID 1 is the build.
world builder aarch64 raspberrypi,5-model-b brcm,bcm2712
pid1 builder own bash
manifest builder "$apple_manifest"
expect builder aarch64-apple "an Apple image built in its own PID namespace is Apple Silicon"
# The same with the host's /run bound in: PID 1 is still not systemd.
systemd_runs builder
expect builder aarch64-apple "a PID namespace build ignores the host's systemd in a bound /run"
# A root without /proc, with a /run of its own.
world no-proc aarch64
manifest no-proc "$apple_manifest"
expect no-proc aarch64-apple "an Apple image built without /proc is Apple Silicon"
# The host never decides the target, even when it is a Mac or its tree is
# contradictory.
world on-a-mac aarch64 apple,j416c apple,t6021 apple,arm-platform
pid1 on-a-mac chroot
manifest on-a-mac $'format=1\nplatform=aarch64\n'
expect on-a-mac aarch64 "a plain aarch64 image built on a Mac is plain aarch64"
world odd-host aarch64 apple,j416c qcom,x1e80100
pid1 odd-host chroot
manifest odd-host $'format=1\nplatform=aarch64\n'
expect odd-host aarch64 "a build never reads the host's device tree"
# Image builders written before the platform names settled write the old ones.
world legacy-apple aarch64
pid1 legacy-apple chroot
manifest legacy-apple $'format=1\nplatform=apple-silicon\n'
expect legacy-apple aarch64-apple "an old builder's apple-silicon manifest reads as aarch64-apple"
world legacy-arm aarch64
pid1 legacy-arm chroot
manifest legacy-arm $'format=1\nplatform=generic-aarch64\n'
expect legacy-arm aarch64 "an old builder's generic-aarch64 manifest reads as aarch64"
world legacy-x86 x86_64
pid1 legacy-x86 chroot
manifest legacy-x86 $'format=1\nplatform=generic\n'
expect legacy-x86 x86 "an old builder's generic manifest reads as x86"
# Comments and keys a later format adds are ignored.
world commented aarch64
pid1 commented chroot
manifest commented $'# written by the image builder\nformat=1\nbuilder=ci\nplatform=aarch64-apple\n'
expect commented aarch64-apple "comments and unknown manifest keys are ignored"
# --- Booted systems -----------------------------------------------------------
# A booted Mac whose manifest outlived its first boot: the hardware decides, and
# not even a broken manifest stops it.
world booted-mac aarch64 apple,j416c apple,t6021 apple,arm-platform
pid1 booted-mac own
systemd_runs booted-mac
manifest booted-mac $'format=1\nplatform=aarch64\n'
expect booted-mac aarch64-apple "a booted Mac with a stale manifest is Apple Silicon"
manifest booted-mac "not a manifest"
expect booted-mac aarch64-apple "a booted Mac ignores a malformed stale manifest"
world booted-qualcomm aarch64 lenovo,yoga-slim7x qcom,x1e80100
pid1 booted-qualcomm own
systemd_runs booted-qualcomm
manifest booted-qualcomm "$apple_manifest"
expect booted-qualcomm aarch64 "a booted Snapdragon laptop with an Apple manifest is plain aarch64"
world booted-x86 x86_64
pid1 booted-x86 own
systemd_runs booted-x86
manifest booted-x86 "$apple_manifest"
expect booted-x86 x86 "a booted x86 machine with an Apple manifest is x86"
# An Apple image under VM acceptance, booted before its first-boot setup ran.
world booted-vm aarch64 linux,dummy-virt
pid1 booted-vm own
systemd_runs booted-vm
manifest booted-vm "$apple_manifest"
expect booted-vm aarch64 "a booted VM with an Apple manifest is plain aarch64"
# What a normal user cannot see never makes a build: PID 1's root, or PID 1
# itself behind hidepid.
world user-view aarch64 apple,j416c apple,t6021 apple,arm-platform
pid1 user-view hidden
systemd_runs user-view
manifest user-view $'format=1\nplatform=aarch64\n'
expect user-view aarch64-apple "a booted system whose PID 1 root cannot be compared uses its hardware"
world hidepid aarch64 apple,j416c apple,t6021 apple,arm-platform
systemd_runs hidepid
manifest hidepid "not a manifest"
expect hidepid aarch64-apple "a booted system whose PID 1 is hidden uses its hardware"
# --- No manifest --------------------------------------------------------------
# An installer on the target machine chroots into the new root with no
# manifest: the hardware it runs on is the target.
world installer aarch64 apple,j314s apple,t6000 apple,arm-platform
pid1 installer chroot
expect installer aarch64-apple "a chroot without a manifest uses the hardware"
# A manifest retired by the first boot no longer names anything.
world retired aarch64 linux,dummy-virt
pid1 retired chroot
manifest retired "$apple_manifest"
mv "$test_tmp/worlds/retired/image/var/lib/omarchy/image/target" "$test_tmp/worlds/retired/image/var/lib/omarchy/image/target.booted"
expect retired aarch64 "a retired manifest is not read"
# --- Invalid manifests stop a build -------------------------------------------
invalid() {
local name=$1 body=$2 message=$3 description=$4
world "$name" aarch64 linux,dummy-virt
pid1 "$name" chroot
manifest "$name" "$body"
expect_refused "$name" "$message" "$description"
}
invalid no-format $'platform=aarch64-apple\n' "is not format=1" "a manifest without a format is refused"
invalid format-2 $'format=2\nplatform=aarch64-apple\n' "is not format=1" "a manifest of another format is refused"
invalid empty "" "is not format=1" "an empty manifest is refused"
invalid unknown $'format=1\nplatform=intel-mac\n' "names no known platform: intel-mac" "a manifest naming an unknown platform is refused"
invalid no-platform $'format=1\n' "names no known platform: none" "a manifest naming no platform is refused"
invalid bare-line $'format=1\naarch64-apple\n' "is malformed: aarch64-apple" "a manifest line without a key is refused"
world x86-cpu x86_64
pid1 x86-cpu chroot
manifest x86-cpu "$apple_manifest"
expect_refused x86-cpu "names aarch64-apple hardware but the CPU is x86_64" "an Apple manifest on an x86 CPU contradicts it"
world arm-generic aarch64
pid1 arm-generic chroot
manifest arm-generic $'format=1\nplatform=x86\n'
expect_refused arm-generic "names x86 hardware but the CPU is aarch64" "an x86 manifest on an aarch64 CPU contradicts it"
untrusted="is not a root-owned regular file"
world symlink aarch64
pid1 symlink chroot
manifest symlink "$apple_manifest"
image_dir="$test_tmp/worlds/symlink/image/var/lib/omarchy/image"
mv "$image_dir/target" "$image_dir/real-target"
ln -s real-target "$image_dir/target"
expect_refused symlink "$untrusted" "a symlinked manifest is refused"
world dangling aarch64
pid1 dangling chroot
mkdir -p "$test_tmp/worlds/dangling/image/var/lib/omarchy/image"
ln -s missing "$test_tmp/worlds/dangling/image/var/lib/omarchy/image/target"
expect_refused dangling "$untrusted" "a dangling manifest symlink is refused"
world directory aarch64
pid1 directory chroot
mkdir -p "$test_tmp/worlds/directory/image/var/lib/omarchy/image/target"
expect_refused directory "$untrusted" "a manifest that is a directory is refused"
world writable aarch64
pid1 writable chroot
manifest writable "$apple_manifest"
chmod 0664 "$test_tmp/worlds/writable/image/var/lib/omarchy/image/target"
expect_refused writable "$untrusted" "a group-writable manifest is refused"
world writable-dir aarch64
pid1 writable-dir chroot
manifest writable-dir "$apple_manifest"
chmod 0777 "$test_tmp/worlds/writable-dir/image/var/lib/omarchy/image"
expect_refused writable-dir "$untrusted" "a manifest in a world-writable directory is refused"
# Another owner: a fixture is trusted when the test's own user owns it, so a
# stat reporting root stands in for a file this user did not write.
world other-owner aarch64
pid1 other-owner chroot
manifest other-owner "$apple_manifest"
real_stat=$(command -v stat)
cat >"$test_tmp/worlds/other-owner/bin/stat" <<SH
#!/bin/bash
[[ \${!#} == */var/lib/omarchy/image/target ]] && { echo "0 644"; exit 0; }
exec $real_stat "\$@"
SH
chmod +x "$test_tmp/worlds/other-owner/bin/stat"
expect_refused other-owner "$untrusted" "a manifest another user owns is refused"
# --- What a build sets up from it ---------------------------------------------
# The Apple predicate of an Apple image built on an x86 host, and of a generic
# image built on a Mac.
in_world x86-host "$ROOT/bin/omarchy-hw-aarch64-apple" || fail "the Apple predicate accepts an Apple image build"
if in_world on-a-mac "$ROOT/bin/omarchy-hw-aarch64-apple"; then
fail "the Apple predicate rejects a plain aarch64 image built on a Mac"
fi
pass "the Apple predicate follows the image target"
# omarchy-mac's packages and units still call the predicate by its old name.
in_world x86-host "$ROOT/bin/omarchy-hw-apple-silicon" || fail "the old Apple predicate name accepts an Apple image build"
if in_world on-a-mac "$ROOT/bin/omarchy-hw-apple-silicon"; then
fail "the old Apple predicate name rejects a plain aarch64 image built on a Mac"
fi
pass "the old Apple predicate name answers as omarchy-hw-aarch64-apple"
packages=$(in_world x86-host env OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults") || fail "an Apple image build composes its packages"
[[ $packages == "$(OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults" aarch64-apple)" ]] ||
fail "an Apple image build installs the Apple Silicon package set"
packages=$(in_world on-a-mac env OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults") || fail "a plain aarch64 image build composes its packages"
[[ $packages == "$(OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults" aarch64)" ]] ||
fail "a plain aarch64 image built on a Mac installs the plain aarch64 package set"
packages=$(in_world odd-host env OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults") || fail "an image built on a contradictory host composes its packages"
[[ $packages == "$(OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults" aarch64)" ]] ||
fail "an image built on a contradictory host installs its target's package set"
pass "the default package set follows the image target"
# A caller in a private PID namespace looks like a build, so no unit may use one.
units=$(grep -rlE '^[[:space:]]*PrivatePIDs=' "$ROOT" --include='*.service' --include='*.conf' --exclude-dir=.git || true)
[[ -z $units ]] || fail "no unit runs in a private PID namespace" "$units"
pass "no unit runs in a private PID namespace"
+16 -1
View File
@@ -14,6 +14,10 @@ stub_bin="$tmpdir/bin"
mkdir -p "$home/.config" "$stub_bin"
cp -r "$ROOT/config/hypr" "$home/.config/hypr"
# No platform package: neither its key names nor its binds.
menu="$tmpdir/omarchy-menu-keybindings"
platform_root_copy "$ROOT/bin/omarchy-menu-keybindings" "$menu" "$tmpdir/no-platform"
# The menu reads binds from Hyprland, which is not running here, so stand in for
# it. A Lua bind reports dispatcher __lua and no arg, and the menu recovers both
# from the Lua source; an exec bind carries its own command. Both shapes matter:
@@ -43,7 +47,7 @@ stub_hyprctl() {
keybindings() {
env -i PATH="$stub_bin:$ROOT/bin:$PATH" HOME="$home" \
XDG_CACHE_HOME="$tmpdir/cache" OMARCHY_PATH="$ROOT" \
bash "$ROOT/bin/omarchy-menu-keybindings" --print
bash "$menu" --print
}
# Closing a window and toggling the scratchpad are two of the actions Omarchy
@@ -65,6 +69,17 @@ grep -q 'SUPER + F *→ Full screen' <<<"$rendered" ||
fail "a chord with no alternative renders on its own" "$rendered"
pass "the keybindings menu renders its entries"
# The menu's own scan of the config runs lua -E, so no LUA_INIT or LUA_PATH from
# the environment reaches it and nothing there can move the platform root.
printf "io.open('%s', 'w'):close()\n" "$tmpdir/init-ran" >"$tmpdir/init.lua"
LUA_INIT="@$tmpdir/init.lua" lua -e '' && [[ -e $tmpdir/init-ran ]] || fail "the LUA_INIT probe runs in a plain lua"
rm -f "$tmpdir/init-ran"
env -i PATH="$stub_bin:$ROOT/bin:$PATH" HOME="$home" XDG_CACHE_HOME="$tmpdir/cache-init" OMARCHY_PATH="$ROOT" \
LUA_INIT="@$tmpdir/init.lua" LUA_INIT_5_5="@$tmpdir/init.lua" LUA_INIT_5_4="@$tmpdir/init.lua" LUA_PATH="$tmpdir/?.lua" \
bash "$ROOT/bin/omarchy-menu-keybindings" --print >/dev/null
[[ ! -e $tmpdir/init-ran ]] || fail "the menu's config scan ignores LUA_INIT from the environment"
pass "the menu's config scan ignores LUA_INIT and LUA_PATH from the environment"
(( $(grep -c '→ Close window$' <<<"$rendered") == 1 )) ||
fail "an alternative chord joins the row of the first one" "$rendered"
grep -q 'SUPER + W / SUPER + Q *→ Close window' <<<"$rendered" ||
+398
View File
@@ -0,0 +1,398 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# The watcher half opens its event stream with {fd}> redirection.
(( BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1] >= 401 )) || fail "this test runs under bash 4.1 or newer" "$BASH_VERSION"
lid_inhibit="$ROOT/bin/omarchy-system-lid-inhibit"
test_tmp=$(mktemp -d)
watch_pid=""
events_fd=""
# The watcher forks clamshell retries and a poll; orphaning them would leave
# them syncing against a deleted test tree.
stop_watcher() {
local child descendants
if [[ -n $watch_pid ]]; then
descendants=$(pgrep -P "$watch_pid" 2>/dev/null || true)
for child in $descendants; do
descendants+=" $(pgrep -P "$child" 2>/dev/null || true)"
done
kill -KILL "$watch_pid" 2>/dev/null || true
wait "$watch_pid" 2>/dev/null || true
for child in $descendants; do
kill -KILL "$child" 2>/dev/null || true
done
watch_pid=""
fi
if [[ -n $events_fd ]]; then
exec {events_fd}>&-
events_fd=""
fi
return 0
}
cleanup() {
stop_watcher
rm -rf "$test_tmp"
}
trap cleanup EXIT
fake_bin="$test_tmp/bin"
drm_path="$test_tmp/drm"
unit_state="$test_tmp/unit-active"
call_log="$test_tmp/calls"
monitors="$test_tmp/monitors.json"
hyprctl_fail="$test_tmp/hyprctl-fails"
busctl_fail="$test_tmp/busctl-fails"
cgroup="$test_tmp/cgroup"
events="$test_tmp/events"
mkdir -p "$fake_bin"
# The user manager, reduced to the one unit. Like the real one, it refuses to
# start a unit name that is already loaded.
cat >"$fake_bin/systemd-run" <<'SH'
#!/bin/bash
if [[ -e $OMARCHY_TEST_UNIT_STATE ]]; then
printf 'refused\n' >>"$OMARCHY_TEST_CALL_LOG"
echo "Unit already loaded" >&2
exit 1
fi
printf 'start %s\n' "$*" >>"$OMARCHY_TEST_CALL_LOG"
touch "$OMARCHY_TEST_UNIT_STATE"
SH
cat >"$fake_bin/systemctl" <<'SH'
#!/bin/bash
case "$*" in
"--user is-active --quiet omarchy-lid-inhibit.service")
[[ -e $OMARCHY_TEST_UNIT_STATE ]]
;;
"--user stop omarchy-lid-inhibit.service")
printf 'stop\n' >>"$OMARCHY_TEST_CALL_LOG"
rm -f "$OMARCHY_TEST_UNIT_STATE"
;;
*)
exit 2
;;
esac
SH
cat >"$fake_bin/busctl" <<'SH'
#!/bin/bash
[[ $* == *" HandleLidSwitchDocked" ]] || exit 1
[[ -f $OMARCHY_TEST_BUSCTL_FAIL ]] && exit 1
printf 's "%s"\n' "$OMARCHY_TEST_DOCKED_ACTION"
SH
cat >"$fake_bin/omarchy-hw-laptop" <<'SH'
#!/bin/bash
[[ $OMARCHY_TEST_LAPTOP == 1 ]]
SH
cat >"$fake_bin/hyprctl" <<'SH'
#!/bin/bash
[[ $* == "monitors all -j" ]] || exit 0
[[ -f $OMARCHY_TEST_HYPRCTL_FAIL ]] && exit 1
cat "$OMARCHY_TEST_MONITORS"
SH
chmod +x "$fake_bin"/*
export OMARCHY_DRM_PATH="$drm_path"
export OMARCHY_TEST_UNIT_STATE="$unit_state"
export OMARCHY_TEST_CALL_LOG="$call_log"
export OMARCHY_TEST_MONITORS="$monitors"
export OMARCHY_TEST_HYPRCTL_FAIL="$hyprctl_fail"
export OMARCHY_TEST_BUSCTL_FAIL="$busctl_fail"
export OMARCHY_CGROUP_PATH="$cgroup"
export OMARCHY_TEST_DOCKED_ACTION=ignore
export OMARCHY_TEST_LAPTOP=1
# Arguments come in pairs: connector and status. The card itself sits beside
# its connectors, as in sysfs.
write_connectors() {
rm -rf "$drm_path"
mkdir -p "$drm_path/card2"
while (( $# )); do
mkdir -p "$drm_path/$1"
printf '%s\n' "$2" >"$drm_path/$1/status"
shift 2
done
}
# What Hyprland reports: every output it knows, with whether it is disabled.
write_monitors() {
printf '%s\n' "$1" >"$monitors"
}
reset_unit() {
rm -f "$unit_state" "$hyprctl_fail" "$busctl_fail"
: >"$call_log"
}
# Where the caller runs: an SSH login's session scope, outside the user manager.
printf '0::/user.slice/user-1000.slice/session-3.scope\n' >"$cgroup"
clamshell='[{"name":"eDP-1","disabled":true,"dpmsStatus":true},{"name":"USB-2","disabled":false,"dpmsStatus":true}]'
sync_inhibit() {
PATH="$fake_bin:$PATH" "$lid_inhibit"
}
held() {
[[ -e $unit_state ]]
}
count_calls() {
grep -c "^$1" "$call_log" || true
}
# Apple Silicon in clamshell: the panel on eDP, a USB-C display on a "USB"
# connector that logind does not count as external.
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors "$clamshell"
sync_inhibit
held || fail "a USB-C display on a USB connector takes the lid inhibitor"
start=$(grep '^start' "$call_log")
for arg in --no-ask-password --what=handle-lid-switch --mode=block PartOf=graphical-session.target "sleep infinity"; do
[[ $start == *"$arg"* ]] || fail "the inhibitor unit is started with $arg" "$start"
done
pass "a USB-C display on a USB connector takes a blocking lid inhibitor without prompting"
[[ $start != *BindsTo=* ]] || fail "a caller outside the user manager binds the inhibitor to nothing" "$start"
pass "a caller outside the user manager binds the inhibitor to nothing"
# The watcher runs in a scope of the user manager; the inhibitor stops with it.
reset_unit
watcher_scope='app-Hyprland-omarchy\x2dhyprland\x2dmonitor\x2dwatch-f1fda151.scope'
printf '0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-graphical.slice/%s\n' "$watcher_scope" >"$cgroup"
sync_inhibit
start=$(grep '^start' "$call_log")
# systemd-run unquotes property values: a single backslash would name a unit
# that does not exist, and the inhibitor would never start.
bound_scope='app-Hyprland-omarchy\\x2dhyprland\\x2dmonitor\\x2dwatch-f1fda151.scope'
[[ $start == *"--property=BindsTo=$bound_scope --property=After=$bound_scope "* ]] ||
fail "the inhibitor is bound to the watcher's unit" "$start"
printf '0::/user.slice/user-1000.slice/session-3.scope\n' >"$cgroup"
pass "the inhibitor is bound to the watcher's unit, so it cannot outlive it"
sync_inhibit
sync_inhibit
(( $(count_calls start) == 1 && $(count_calls stop) == 0 && $(count_calls refused) == 0 )) ||
fail "repeated syncs keep a single inhibitor" "$(<"$call_log")"
pass "repeated syncs keep a single inhibitor"
# Some drivers report a connector disabled while DPMS blanks it; Hyprland keeps
# the monitor enabled. Releasing here would suspend a closed laptop the moment
# its external display went idle.
write_monitors '[{"name":"eDP-1","disabled":true,"dpmsStatus":false},{"name":"USB-2","disabled":false,"dpmsStatus":false}]'
sync_inhibit
held || fail "a USB-C display blanked by DPMS keeps the inhibitor" "$(<"$call_log")"
pass "a USB-C display blanked by DPMS keeps the inhibitor"
# Hyprland misses queries while it reconfigures outputs, which is exactly what
# a lid close sets off. Releasing on a missed answer would suspend the machine.
touch "$hyprctl_fail"
sync_inhibit || fail "an unanswered compositor query is not an error"
held || fail "an unanswered compositor query keeps the inhibitor"
rm -f "$hyprctl_fail"
write_monitors ''
sync_inhibit || fail "an empty compositor answer is not an error"
held || fail "an empty compositor answer keeps the inhibitor"
write_monitors 'HYPRLAND_INSTANCE_SIGNATURE not set!'
sync_inhibit || fail "a compositor answer that is not JSON is not an error"
held || fail "a compositor answer that is not JSON keeps the inhibitor"
write_monitors "$clamshell"
pass "a compositor that does not answer changes nothing"
# logind can miss a query too, and the release would suspend the machine just the same.
touch "$busctl_fail"
sync_inhibit || fail "an unanswered logind query is not an error"
held || fail "an unanswered logind query keeps the inhibitor"
rm -f "$busctl_fail"
pass "logind not answering changes nothing"
# Unplugging is read from the connector, so it releases even when the compositor
# cannot be asked.
write_connectors card2-eDP-1 connected card2-USB-2 disconnected
touch "$hyprctl_fail"
sync_inhibit
! held || fail "unplugging the USB-C display releases the inhibitor"
(( $(count_calls stop) == 1 )) || fail "the inhibitor is released once" "$(<"$call_log")"
pass "unplugging the USB-C display releases the inhibitor"
sync_inhibit
(( $(count_calls stop) == 1 && $(count_calls start) == 1 )) ||
fail "a sync with nothing held changes nothing" "$(<"$call_log")"
pass "a sync with nothing held changes nothing"
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
touch "$hyprctl_fail"
sync_inhibit || fail "an unanswered compositor query is not an error"
touch "$busctl_fail"
rm -f "$hyprctl_fail"
sync_inhibit || fail "an unanswered logind query is not an error"
[[ ! -s $call_log ]] || fail "an unanswered query takes no inhibitor" "$(<"$call_log")"
pass "an unanswered query takes no inhibitor either"
# A display left plugged in but switched off in the monitor config is not in use,
# and a closed lid should suspend as it would with that display on HDMI.
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors '[{"name":"eDP-1","disabled":false},{"name":"USB-2","disabled":true}]'
sync_inhibit
[[ ! -s $call_log ]] || fail "a display switched off in the monitor config takes no inhibitor" "$(<"$call_log")"
pass "a USB-C display switched off in the monitor config takes no inhibitor"
# Machines logind already handles must behave exactly as before.
reset_unit
write_connectors card2-eDP-1 connected
write_monitors '[{"name":"eDP-1","disabled":false}]'
sync_inhibit
[[ ! -s $call_log ]] || fail "the built-in panel alone takes no inhibitor" "$(<"$call_log")"
pass "the built-in panel alone takes no inhibitor"
reset_unit
write_connectors card0-eDP-1 connected card0-HDMI-A-1 connected card0-DP-1 connected
write_monitors '[{"name":"eDP-1","disabled":true},{"name":"HDMI-A-1","disabled":false},{"name":"DP-1","disabled":false}]'
sync_inhibit
[[ ! -s $call_log ]] || fail "displays logind counts take no inhibitor" "$(<"$call_log")"
pass "HDMI and DisplayPort displays, which logind counts, take no inhibitor"
reset_unit
write_connectors card2-USB-2 connected
write_monitors "$clamshell"
OMARCHY_TEST_LAPTOP=0 sync_inhibit
[[ ! -s $call_log ]] || fail "a machine without a lid takes no inhibitor" "$(<"$call_log")"
pass "a machine without a lid takes no inhibitor"
# Only where logind would ignore a docked lid is there anything to stand in for.
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors "$clamshell"
OMARCHY_TEST_DOCKED_ACTION=suspend sync_inhibit
[[ ! -s $call_log ]] || fail "a docked lid set to suspend takes no inhibitor" "$(<"$call_log")"
sync_inhibit
held || fail "the inhibitor is taken under the default docked action"
OMARCHY_TEST_DOCKED_ACTION=suspend sync_inhibit
! held || fail "changing the docked lid action away from ignore releases the inhibitor"
pass "the inhibitor follows logind's docked lid action"
# logind lists DVI-A and Composite, but a missing comma fuses them into one
# entry, so they are uncounted too.
for connector in DVI-A-1 Composite-1; do
reset_unit
write_connectors card0-LVDS-1 connected "card0-$connector" connected
write_monitors "[{\"name\":\"LVDS-1\",\"disabled\":true},{\"name\":\"$connector\",\"disabled\":false}]"
sync_inhibit
held || fail "$connector takes the inhibitor"
done
pass "every external connector type logind misses takes the inhibitor"
# Held alongside a counted display too, or unplugging that one would let logind
# suspend before the watcher could take over.
reset_unit
write_connectors card2-eDP-1 connected card2-HDMI-A-1 connected card2-USB-2 connected
write_monitors '[{"name":"eDP-1","disabled":true},{"name":"HDMI-A-1","disabled":false},{"name":"USB-2","disabled":false}]'
sync_inhibit
held || fail "a USB-C display beside an HDMI one still holds the inhibitor"
pass "a USB-C display beside an HDMI one still holds the inhibitor"
# The monitor watcher is what keeps the inhibitor in step as displays come and go.
cat >"$fake_bin/socat" <<'SH'
#!/bin/bash
exec cat "$OMARCHY_TEST_EVENTS"
SH
for command in omarchy-hyprland-monitor-clamshell omarchy-hyprland-monitor-external-active; do
cat >"$fake_bin/$command" <<'SH'
#!/bin/bash
exit 0
SH
done
cat >"$fake_bin/omarchy-hyprland-monitor-modeless" <<'SH'
#!/bin/bash
exit 1
SH
chmod +x "$fake_bin"/*
ln -s "$lid_inhibit" "$fake_bin/omarchy-system-lid-inhibit"
start_watcher() {
rm -f "$events"
mkfifo "$events"
PATH="$fake_bin:$PATH" \
XDG_RUNTIME_DIR="$test_tmp" \
HYPRLAND_INSTANCE_SIGNATURE=test \
OMARCHY_TEST_EVENTS="$events" \
"$ROOT/bin/omarchy-hyprland-monitor-watch" &
watch_pid=$!
exec {events_fd}>"$events"
}
await() {
local waited
for (( waited = 0; waited < 100; waited++ )); do
"$@" && return 0
sleep 0.05
done
return 1
}
released() {
! held
}
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors "$clamshell"
start_watcher
await held || fail "the watcher takes the inhibitor at startup with a USB-C display attached"
pass "the watcher takes the inhibitor at startup with a USB-C display attached"
write_connectors card2-eDP-1 connected card2-USB-2 disconnected
write_monitors '[{"name":"eDP-1","disabled":false}]'
printf 'monitorremovedv2>>1,USB-2,Display\n' >&"$events_fd"
await released || fail "the watcher releases the inhibitor when the USB-C display is removed"
pass "the watcher releases the inhibitor when the USB-C display is removed"
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors "$clamshell"
printf 'monitoraddedv2>>2,USB-2,Display\n' >&"$events_fd"
await held || fail "the watcher takes the inhibitor again when the USB-C display returns"
# Across the delayed retries and the docked poll, one unit stays the only one.
sleep 3.5
(( $(count_calls start) == 2 && $(count_calls stop) == 1 && $(count_calls refused) == 0 )) ||
fail "the watcher never stacks inhibitors across its retries and poll" "$(<"$call_log")"
pass "the watcher keeps one inhibitor across hotplugs, retries and its poll"
stop_watcher
+346
View File
@@ -0,0 +1,346 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
dispatch="$ROOT/bin/omarchy-lifecycle-dispatch"
tmp=$(mktemp -d)
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 aarch64-apple aarch64 x86; do
fake_platform "$tmp/$platform" "$platform"
done
mkdir -p "$tmp/contradiction/proc/device-tree"
printf '%s\0' apple,j416c qcom,x1e80100 >"$tmp/contradiction/proc/device-tree/compatible"
cp -r "$tmp/aarch64-apple/bin" "$tmp/contradiction/bin"
# A root-owned directory of entrypoints stands in for the boot package; in a
# fixture root the caller's own files count as root's.
implementation=usr/lib/omarchy/mac-boot
install_implementation() {
local root=$1 operation
rm -rf "$root"
mkdir -p "$root/$implementation"
for operation in "${operations[@]}"; do
cat >"$root/$implementation/$operation" <<SH
#!/bin/bash
{ printf '%s' "$operation"; printf ' %q' "\$@"; echo; } >>"$tmp/ran"
env >"$tmp/env"
[[ ! -e $tmp/fail-with ]] || exit "\$(cat "$tmp/fail-with")"
SH
chmod 755 "$root/$implementation/$operation"
done
chmod -R go-w "$root"
}
# Runs the dispatcher on a platform fixture: $1 platform, $2 lifecycle root.
on() {
local platform=$1 root=$2
shift 2
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$root" PATH="$tmp/$platform/bin:$PATH" \
"$dispatch" "$@"
}
# -p in the shebang is what keeps a root caller's exported functions and
# BASH_ENV out, so an ordinary Bash launch with a decoy -p argument is refused
# before it resolves anything.
status=0
output=$(/usr/bin/bash "$dispatch" -p 2>&1) || status=$?
(( status == 126 )) && [[ $output == "Refusing an unsafe Bash startup." ]] ||
fail "an ordinary Bash launch with a decoy -p is refused" "status $status: $output"
pass "an ordinary Bash launch with a decoy -p is refused"
require_platform_fixtures "lifecycle dispatch on platform fixtures"
full=$tmp/full
install_implementation "$full"
empty=$tmp/empty
mkdir -p "$empty"
# x86 and plain aarch64 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 x86 aarch64; 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"
[[ -z $output && ! -e $tmp/ran ]] || fail "$platform: $operation runs nothing and says nothing" "$output"
output=$(on "$platform" "$full" --resolve "$operation" 2>&1) || fail "$platform: $operation resolves" "$output"
[[ -z $output ]] || fail "$platform: $operation resolves to nothing" "$output"
done
pass "$platform: every operation is a no-op and no Mac entrypoint runs"
done
rm -f "$tmp/fail-with"
# Apple with omarchy-mac-boot: each operation runs its entrypoint with the
# caller's arguments, a cleared environment and a fixed PATH.
export CALLER_SECRET=leak
for operation in "${operations[@]}"; do
rm -f "$tmp/ran"
on aarch64-apple "$full" "$operation" first "second arg" || fail "apple: $operation runs its entrypoint"
[[ $(cat "$tmp/ran") == "$operation first second\\ arg" ]] || fail "apple: $operation passes its arguments" "$(cat "$tmp/ran")"
! grep -q CALLER_SECRET "$tmp/env" || fail "apple: $operation does not pass the caller's environment" "$(cat "$tmp/env")"
grep -qx 'PATH=/usr/local/sbin:/usr/local/bin:/usr/bin' "$tmp/env" || fail "apple: $operation runs with a fixed PATH" "$(cat "$tmp/env")"
resolved=$(on aarch64-apple "$full" --resolve "$operation") || fail "apple: $operation resolves"
[[ $resolved == "$full/$implementation/$operation" ]] || fail "apple: $operation resolves to its entrypoint" "$resolved"
done
unset CALLER_SECRET
echo 7 >"$tmp/fail-with"
status=0
on aarch64-apple "$full" provision-commit || status=$?
(( status == 7 )) || fail "apple: the entrypoint's exit status is the dispatcher's" "status: $status"
rm -f "$tmp/fail-with"
pass "apple: each operation runs the boot package's entrypoint with its arguments and status"
# Apple without omarchy-mac-boot: required operations fail naming the package
# and entrypoint; optional ones are no-ops.
for operation in "${operations[@]}"; do
status=0
output=$(on aarch64-apple "$empty" "$operation" 2>&1) || status=$?
if [[ " ${apple_optional[*]} " == *" $operation "* ]]; then
(( status == 0 )) && [[ -z $output ]] || fail "apple: optional $operation is a no-op without the boot package" "$output"
output=$(on aarch64-apple "$empty" --resolve "$operation" 2>&1) && [[ -z $output ]] ||
fail "apple: optional $operation resolves to nothing without the boot package" "$output"
else
(( status == 3 )) || fail "apple: required $operation fails with status 3 without the boot package" "status: $status"
[[ $output == "Error: $operation on aarch64-apple needs omarchy-mac-boot, which provides /usr/lib/omarchy/mac-boot/$operation; it is not installed" ]] ||
fail "apple: required $operation names the missing package and entrypoint" "$output"
status=0
on aarch64-apple "$empty" --resolve "$operation" >/dev/null 2>&1 || status=$?
(( status == 3 )) || fail "apple: required $operation does not resolve without the boot package" "status: $status"
fi
done
pass "apple: without omarchy-mac-boot required operations fail with a clear message and optional ones are no-ops"
# An installed omarchy-mac-boot that predates an operation is named with its
# version, as an update away rather than missing.
older=$tmp/older
mkdir -p "$older/usr/lib/omarchy/mac-boot" "$older/var/lib/pacman/local/omarchy-mac-boot-20260921-10"
for operation in "${operations[@]}"; do
[[ " ${apple_optional[*]} " == *" $operation "* ]] && continue
status=0
output=$(on aarch64-apple "$older" "$operation" 2>&1) || status=$?
(( status == 1 )) && [[ $output == "Error: $operation on aarch64-apple needs /usr/lib/omarchy/mac-boot/$operation, which omarchy-mac-boot 20260921-10 does not provide; update omarchy-mac-boot" ]] ||
fail "apple: an omarchy-mac-boot without $operation is named with its version" "status $status: $output"
done
pass "apple: an installed omarchy-mac-boot that lacks a required operation fails asking for its update"
# An entrypoint anyone but root could have changed never runs, optional or not.
untrusted() {
local description=$1 operation=$2
rm -f "$tmp/ran"
if output=$(on aarch64-apple "$full" "$operation" 2>&1); then
fail "apple: $description is refused"
fi
[[ ! -e $tmp/ran ]] || fail "apple: $description never runs"
[[ $output == *"refusing /usr/lib/omarchy/mac-boot/$operation"* ]] || fail "apple: $description is named" "$output"
if on aarch64-apple "$full" --resolve "$operation" >/dev/null 2>&1; then
fail "apple: $description does not resolve"
fi
}
install_implementation "$full"
chmod g+w "$full/$implementation/reset-commit"
untrusted "a group-writable entrypoint" reset-commit
install_implementation "$full"
chmod o+w "$full/$implementation/provision-commit"
untrusted "a world-writable entrypoint" provision-commit
install_implementation "$full"
chmod o+w "$full/usr/lib/omarchy"
untrusted "an entrypoint in a world-writable directory" provision-verify
install_implementation "$full"
mv "$full/$implementation/provision-prepare" "$tmp/elsewhere"
ln -s "$tmp/elsewhere" "$full/$implementation/provision-prepare"
untrusted "a symlinked entrypoint" provision-prepare
install_implementation "$full"
chmod 644 "$full/$implementation/update-verify"
untrusted "a non-executable entrypoint" update-verify
install_implementation "$full"
rm "$full/$implementation/reset-verify"
mkdir "$full/$implementation/reset-verify"
chmod 755 "$full/$implementation/reset-verify"
untrusted "a directory in place of an entrypoint" reset-verify
pass "apple: an entrypoint that is not a root-owned file in root-owned directories never runs"
install_implementation "$full"
for arguments in "" "unknown-operation" "--resolve" "--resolve unknown-operation"; do
rm -f "$tmp/ran"
status=0
output=$(on aarch64-apple "$full" $arguments 2>&1) || status=$?
(( status == 2 )) && [[ $output == Usage:* ]] || fail "'$arguments' is a usage error" "status $status: $output"
[[ ! -e $tmp/ran ]] || fail "'$arguments' runs nothing"
done
status=0
output=$(on aarch64-apple "$full" "provision-prepare provision-commit" 2>&1) || status=$?
(( status == 2 )) && [[ ! -e $tmp/ran ]] || fail "two operation names in one argument are a usage error" "status $status: $output"
pass "an operation outside the fixed set is a usage error"
rm -f "$tmp/ran"
if output=$(on contradiction "$full" provision-commit 2>&1); then
fail "a platform the detector cannot settle fails the operation"
fi
[[ ! -e $tmp/ran && $output == *"cannot determine the hardware platform for provision-commit"* ]] ||
fail "an undetermined platform runs nothing and says why" "$output"
pass "an undetermined platform fails closed"
rm -f "$tmp/ran"
if output=$(cd "$tmp" && on aarch64-apple full provision-commit 2>&1); then
fail "a relative fixture root is refused"
fi
[[ ! -e $tmp/ran && $output == "Error: OMARCHY_LIFECYCLE_ROOT must be an absolute path" ]] ||
fail "a relative fixture root runs nothing and says why" "$output"
pass "a relative fixture root is refused"
# Root resolves only the fixed /usr/lib path, whatever fixture root its
# environment names. A copy beside a detector that always answers Apple keeps
# the live platform out of the way.
if unshare --user --map-root-user true 2>/dev/null; then
mkdir -p "$tmp/rootbin"
cp "$dispatch" "$tmp/rootbin/"
printf '#!/bin/bash\necho aarch64-apple\n' >"$tmp/rootbin/omarchy-hw-platform"
chmod +x "$tmp/rootbin/omarchy-hw-platform"
rm -f "$tmp/ran"
status=0
output=$(OMARCHY_LIFECYCLE_ROOT="$full" unshare --user --map-root-user "$tmp/rootbin/omarchy-lifecycle-dispatch" reset-prepare 2>&1) ||
status=$?
(( status != 0 )) && [[ ! -e $tmp/ran && $output != *"$tmp"* && $output == *" /usr/lib/omarchy/mac-boot/reset-prepare"* ]] ||
fail "root ignores a fixture root in its environment" "status $status: $output"
pass "root ignores fixture roots when resolving an operation"
printf 'touch %q\n' "$tmp/bash-env-ran" >"$tmp/bash-env"
dirname() { touch "$tmp/function-ran"; echo /nonexistent; }
export -f dirname
BASH_ENV="$tmp/bash-env" unshare --user --map-root-user "$dispatch" --resolve provision-commit >/dev/null 2>&1 || true
unset -f dirname
[[ ! -e $tmp/bash-env-ran && ! -e $tmp/function-ran ]] || fail "root runs no code from BASH_ENV or exported functions"
pass "root runs no code from BASH_ENV or exported functions"
else
skip "no unprivileged user namespace; skipping the root override probe"
fi
# ── setup and app-install operations ─────────────────────────────────────────
# System and user setup and the app-install hooks resolve in omarchy-mac's
# directory on a Mac, never in omarchy-mac-boot's, and are optional everywhere.
setup_operations=(setup-system setup-user post-install pre-remove)
user_operations=(setup-user post-install pre-remove)
setup_implementation=usr/lib/omarchy/mac
install_setup() {
local root=$1 dir=$2 operation
mkdir -p "$root/$dir"
for operation in "${setup_operations[@]}"; do
cat >"$root/$dir/$operation" <<SH
#!/bin/bash
{ printf '%s' "$operation"; (( \$# == 0 )) || printf ' %q' "\$@"; echo; } >>"$tmp/ran"
env >"$tmp/env"
[[ ! -e $tmp/fail-with ]] || exit "\$(cat "$tmp/fail-with")"
SH
chmod 755 "$root/$dir/$operation"
done
chmod -R go-w "$root"
}
with_mac=$tmp/with-mac
rm -rf "$with_mac"
install_setup "$with_mac" "$setup_implementation"
boot_only=$tmp/boot-only
rm -rf "$boot_only"
install_setup "$boot_only" "$implementation"
echo 9 >"$tmp/fail-with"
for platform in x86 aarch64; do
for operation in "${setup_operations[@]}"; do
rm -f "$tmp/ran"
output=$(on "$platform" "$with_mac" "$operation" 2>&1) && [[ -z $output && ! -e $tmp/ran ]] ||
fail "$platform: $operation is a no-op" "$output"
output=$(on "$platform" "$with_mac" --resolve "$operation" 2>&1) && [[ -z $output ]] ||
fail "$platform: $operation resolves to nothing" "$output"
done
done
rm -f "$tmp/fail-with"
pass "x86 and plain aarch64: setup and app-install operations are no-ops, even with Mac entrypoints on disk"
for operation in "${setup_operations[@]}"; do
rm -f "$tmp/ran"
resolved=$(on aarch64-apple "$with_mac" --resolve "$operation") && [[ $resolved == "$with_mac/$setup_implementation/$operation" ]] ||
fail "apple: $operation resolves to omarchy-mac's entrypoint" "$resolved"
output=$(on aarch64-apple "$empty" "$operation" 2>&1) && [[ -z $output ]] ||
fail "apple: $operation is a no-op without omarchy-mac" "$output"
output=$(on aarch64-apple "$boot_only" "$operation" 2>&1) && [[ -z $output && ! -e $tmp/ran ]] ||
fail "apple: $operation never runs from omarchy-mac-boot's directory" "$output"
done
pass "apple: setup and the app-install hooks resolve in omarchy-mac's directory, and are no-ops without omarchy-mac"
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"
env -u WAYLAND_DISPLAY "${session[@]}" OMARCHY_PROC_ROOT="$tmp/aarch64-apple/proc" OMARCHY_LIFECYCLE_ROOT="$with_mac" \
PATH="$tmp/aarch64-apple/bin:$PATH" "$dispatch" setup-system image-first-boot || fail "apple: setup-system runs"
[[ $(cat "$tmp/ran") == "setup-system image-first-boot" ]] || fail "apple: setup-system gets its argument" "$(cat "$tmp/ran")"
[[ $(grep -Ev '^(_|PWD|OLDPWD|SHLVL)=' "$tmp/env" | sort) == "PATH=/usr/local/sbin:/usr/local/bin:/usr/bin" ]] ||
fail "apple: setup-system gets PATH alone" "$(cat "$tmp/env")"
expected=$(printf '%s\n' DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus HOME=/home/owner OMARCHY_PATH=/usr/share/omarchy \
PATH=/usr/local/sbin:/usr/local/bin:/usr/bin USER=owner XDG_CONFIG_HOME=/home/owner/.cfg XDG_RUNTIME_DIR=/run/user/1000 XDG_STATE_HOME=/home/owner/.st)
for invocation in "setup-user" "post-install steam" "pre-remove steam"; do
rm -f "$tmp/ran"
env -u WAYLAND_DISPLAY "${session[@]}" OMARCHY_PROC_ROOT="$tmp/aarch64-apple/proc" OMARCHY_LIFECYCLE_ROOT="$with_mac" \
PATH="$tmp/aarch64-apple/bin:$PATH" "$dispatch" $invocation || fail "apple: $invocation runs"
[[ $(cat "$tmp/ran") == "$invocation" ]] || fail "apple: $invocation runs its entrypoint with its argument" "$(cat "$tmp/ran")"
[[ $(grep -Ev '^(_|PWD|OLDPWD|SHLVL)=' "$tmp/env" | sort) == "$expected" ]] ||
fail "apple: $invocation gets the user's home and session, and nothing else" "$(cat "$tmp/env")"
done
echo 3 >"$tmp/fail-with"
status=0
on aarch64-apple "$with_mac" setup-user || status=$?
(( status == 3 )) || fail "apple: a setup entrypoint's own status 3 passes through" "status $status"
rm -f "$tmp/fail-with"
pass "apple: setup-system gets PATH alone and the user operations the user's home and session, with the entrypoint's status"
chmod o+w "$with_mac/$setup_implementation/setup-user"
rm -f "$tmp/ran"
if output=$(on aarch64-apple "$with_mac" setup-user 2>&1); then
fail "apple: a world-writable setup entrypoint is refused"
fi
[[ ! -e $tmp/ran && $output == *"refusing /usr/lib/omarchy/mac/setup-user"* ]] ||
fail "apple: a world-writable setup entrypoint never runs" "$output"
chmod o-w "$with_mac/$setup_implementation/setup-user"
pass "apple: a setup entrypoint that fails the trust rules never runs"
# Root is refused a user operation only where the platform registers it, so an
# 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 aarch64-apple x86 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"
chmod +x "$tmp/root-$platform/omarchy-hw-platform"
for operation in "${user_operations[@]}"; do
for arguments in "$operation" "--resolve $operation" "$operation steam"; do
rm -f "$tmp/ran"
status=0
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 == "aarch64-apple" ]]; then
(( status == 1 )) && [[ $output == "Error: $operation runs as the user, never as root" ]] ||
fail "apple: 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"
else
skip "no unprivileged user namespace; skipping the root refusal of the user operations"
fi
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
leaf="$ROOT/install/config/locale.sh"
[[ -f $leaf ]] || fail "the locale step ships"
grep -Fq 'config/locale.sh' "$ROOT/install/config/all.sh" ||
fail "config phase sets a UTF-8 locale"
pass "config phase sets a UTF-8 locale"
# Root always writes the real locale files: the overrides below are for
# unprivileged runs, so as root the step would change this machine's locale.
if (( EUID == 0 )); then
skip "running as root, where the locale step ignores its fixture; skipping"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls.log"
locale_conf="$test_tmp/etc/locale.conf"
locale_gen="$test_tmp/etc/locale.gen"
mkdir -p "$stub_bin" "$test_tmp/etc"
# GENERATED_LOCALES stands in for what the machine already has built.
cat >"$stub_bin/locale" <<'SH'
#!/bin/bash
[[ ${1:-} == "-a" ]] || exit 0
printf '%s\n' ${GENERATED_LOCALES:-C}
SH
cat >"$stub_bin/locale-gen" <<'SH'
#!/bin/bash
printf 'locale-gen\n' >>"$TEST_LOG"
SH
chmod +x "$stub_bin"/*
run_leaf() {
: >"$calls"
GENERATED_LOCALES="${1:-C}" TEST_LOG="$calls" PATH="$stub_bin:$PATH" \
OMARCHY_LOCALE_CONF="$locale_conf" OMARCHY_LOCALE_GEN="$locale_gen" \
bash -euo pipefail -c 'source "$1"' bash "$leaf" >/dev/null
}
# A root from a distribution tarball: LANG=C, nothing enabled in locale.gen.
printf 'LANG=C\n' >"$locale_conf"
printf '#en_US.UTF-8 UTF-8\n#de_DE.UTF-8 UTF-8\n' >"$locale_gen"
run_leaf C
/usr/bin/grep -qx 'LANG=en_US.UTF-8' "$locale_conf" || fail "a C locale is replaced with UTF-8" "$(cat "$locale_conf")"
/usr/bin/grep -qx 'en_US.UTF-8 UTF-8' "$locale_gen" || fail "en_US.UTF-8 is enabled in locale.gen" "$(cat "$locale_gen")"
/usr/bin/grep -qx '#de_DE.UTF-8 UTF-8' "$locale_gen" || fail "other locales are left commented" "$(cat "$locale_gen")"
/usr/bin/grep -qx 'locale-gen' "$calls" || fail "the locale is generated" "$(cat "$calls")"
pass "a stock LANG=C machine gets en_US.UTF-8"
# Only the stock state is repaired. Every named locale is somebody's choice --
# including C.UTF-8, and including one that is not UTF-8 at all.
for chosen in en_DK.UTF-8 C.UTF-8 en_US.ISO-8859-1; do
printf 'LANG=%s\n' "$chosen" >"$locale_conf"
run_leaf "C en_DK.utf8 C.utf8"
/usr/bin/grep -qx "LANG=$chosen" "$locale_conf" || fail "a chosen locale is left alone" "$chosen -> $(cat "$locale_conf")"
[[ ! -s $calls ]] || fail "a chosen locale is not regenerated" "$chosen: $(cat "$calls")"
done
pass "a machine with a chosen locale is left alone"
# POSIX is the same stock state under another name, and a missing file means
# the system never had one set.
printf 'LANG=POSIX\n' >"$locale_conf"
run_leaf C
/usr/bin/grep -qx 'LANG=en_US.UTF-8' "$locale_conf" || fail "a POSIX locale is repaired" "$(cat "$locale_conf")"
rm -f "$locale_conf"
run_leaf C
/usr/bin/grep -qx 'LANG=en_US.UTF-8' "$locale_conf" || fail "a machine with no locale.conf is repaired" "$(cat "$locale_conf" 2>&1)"
pass "POSIX and an absent locale.conf count as stock"
# Already generated, just not selected: set it without a rebuild.
printf 'LANG=C\n' >"$locale_conf"
run_leaf "C en_US.utf8"
/usr/bin/grep -qx 'LANG=en_US.UTF-8' "$locale_conf" || fail "an available locale is selected" "$(cat "$locale_conf")"
[[ ! -s $calls ]] || fail "a generated locale is not regenerated" "$(cat "$calls")"
pass "a generated locale is selected without rebuilding"
# Other locale variables somebody set stay as they are.
printf 'LANG=C\nLC_TIME=en_DK.UTF-8\nLC_PAPER=de_DE.UTF-8\n' >"$locale_conf"
run_leaf "C en_US.utf8"
[[ $(cat "$locale_conf") == $'LANG=en_US.UTF-8\nLC_TIME=en_DK.UTF-8\nLC_PAPER=de_DE.UTF-8' ]] ||
fail "only LANG is repaired; LC_* settings stay" "$(cat "$locale_conf")"
printf 'LC_TIME=en_DK.UTF-8\n' >"$locale_conf"
run_leaf "C en_US.utf8"
[[ $(cat "$locale_conf") == $'LC_TIME=en_DK.UTF-8\nLANG=en_US.UTF-8' ]] ||
fail "an unset LANG is added beside the LC_* settings" "$(cat "$locale_conf")"
pass "only LANG is repaired; LC_* settings stay"
printf 'LANG=C\n' >"$locale_conf"
run_leaf "C en_US.utf8"
# Second pass over a repaired machine changes nothing.
run_leaf "C en_US.utf8"
(( $(/usr/bin/grep -c . "$locale_conf") == 1 )) || fail "the locale step is idempotent" "$(cat "$locale_conf")"
pass "the locale step is idempotent"
# The x86 ISO writes the locale the owner picked before this runs, so there the
# step changes nothing.
printf 'LANG=en_US.UTF-8\n' >"$locale_conf"
printf '#en_US.UTF-8 UTF-8\n' >"$locale_gen"
: >"$calls"
output=$(GENERATED_LOCALES="C en_US.utf8" TEST_LOG="$calls" PATH="$stub_bin:$PATH" \
OMARCHY_LOCALE_CONF="$locale_conf" OMARCHY_LOCALE_GEN="$locale_gen" \
bash -euo pipefail -c 'source "$1"' bash "$leaf")
[[ $output == "Leaving the locale as en_US.UTF-8" && $(cat "$locale_conf") == "LANG=en_US.UTF-8" ]] ||
fail "an ISO install's locale is left as the ISO wrote it" "$output"
[[ $(cat "$locale_gen") == "#en_US.UTF-8 UTF-8" && ! -s $calls ]] || fail "an ISO install's locale.gen is not touched"
pass "an ISO install's locale is left as the ISO wrote it"
@@ -0,0 +1,71 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
findmnt() {
[[ $* == "-no SOURCE /" ]] || return 99
printf '%s\n' "$root_source"
}
lsblk() {
[[ ${*: -1} == "/dev/mapper/root" ]] || return 98
printf '%s\n' "$*" >>"$test_tmp/lsblk-calls"
[[ $disk_state != "failure" ]] || return 1
if [[ $disk_state == "lvm" ]]; then
printf '/dev/mapper/root btrfs\n/dev/mapper/cryptlvm LVM2_member\n/dev/nvme0n1p2 crypto_LUKS\n/dev/nvme0n1 \n'
elif [[ $disk_state == "encrypted" ]]; then
if [[ $1 == -*r* ]]; then
printf '/dev/mapper/root btrfs\n/dev/nvme0n1p6 crypto_LUKS\n/dev/nvme0n1 \n'
else
printf '/dev/mapper/root btrfs\n└─/dev/nvme0n1p6 crypto_LUKS\n └─/dev/nvme0n1 \n'
fi
else
printf '/dev/mapper/root btrfs\n/dev/nvme0n1p6 ext4\n'
fi
}
for script in omarchy-provision-owner omarchy-system-factory-reset; do
# Load only this read-only helper, never the provisioning/reset entrypoint.
sed -n '/^luks_device() {/,/^}/p' "$ROOT/bin/$script" |
sed "s|/proc/cmdline|$test_tmp/cmdline|g" >"$test_tmp/helper"
[[ -s $test_tmp/helper ]] || fail "$script exposes its LUKS lookup"
source "$test_tmp/helper"
printf 'root=/dev/mapper/root rd.luks.name=fixture=root\n' >"$test_tmp/cmdline"
root_source='/dev/mapper/root[/@]'
disk_state=encrypted
actual=$(luks_device) || fail "$script resolves a crypttab root"
[[ $actual == "/dev/nvme0n1p6" ]] || fail "$script returns the raw partition path" "$actual"
pass "$script strips the btrfs subvolume and returns the LUKS parent without tree glyphs"
disk_state=lvm
actual=$(luks_device) || fail "$script resolves a root on LVM on LUKS"
[[ $actual == "/dev/nvme0n1p2" ]] || fail "$script walks past the LVM layer to the LUKS partition" "$actual"
pass "$script finds the LUKS partition beneath an LVM volume group"
for disk_state in plain failure; do
if actual=$(luks_device); then
fail "$script rejects $disk_state ancestry" "$actual"
fi
[[ -z $actual ]] || fail "$script emits no device on $disk_state ancestry"
done
root_source=""
if luks_device; then fail "$script rejects a missing root source"; fi
for spec in UUID=fixture PARTUUID=fixture LABEL=fixture PARTLABEL=fixture /dev/nvme0n1p6; do
case $spec in
UUID=*) expected=/dev/disk/by-uuid/fixture ;;
PARTUUID=*) expected=/dev/disk/by-partuuid/fixture ;;
LABEL=*) expected=/dev/disk/by-label/fixture ;;
PARTLABEL=*) expected=/dev/disk/by-partlabel/fixture ;;
*) expected=$spec ;;
esac
: >"$test_tmp/lsblk-calls"
printf 'cryptdevice=%s:root root=/dev/mapper/root\n' "$spec" >"$test_tmp/cmdline"
actual=$(luks_device) || fail "$script preserves $spec lookup"
[[ $actual == "$expected" && ! -s $test_tmp/lsblk-calls ]] || fail "$script prefers the explicit cryptdevice"
done
pass "$script preserves explicit cryptdevice lookup and rejects unencrypted or missing roots"
done
+904
View File
@@ -0,0 +1,904 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Owner provisioning's LUKS re-key, killed after every durable step and rerun.
# Each attempt is its own process, like a reboot: omarchy-provision-owner's
# setup attempt from the accepted owner form on, the shared re-key, and a
# cryptsetup that is either a slot-table fake or the real binary on a
# file-backed LUKS2 or LUKS1 volume. The callbacks reach the platform through
# the real omarchy-lifecycle-dispatch: on an x86 fixture it is a no-op and the
# Limine UKI path runs; on an Apple fixture a fake boot package with
# provisioning and luks-slots entrypoints owns an unlock kept on a boot
# partition and the kernel command line and records the kept slot.
tmp=$(mktemp -d)
token_key=""
trap 'rm -rf "$tmp"; [[ -z $token_key ]] || keyctl unlink "$token_key" @s >/dev/null 2>&1 || true' EXIT
fake_platform "$tmp/x86" x86
fake_platform "$tmp/apple" aarch64-apple
platform=x86
# Root's dispatcher ignores the fixtures and sees this machine, which stands in
# for x86 only where no boot package is registered.
platforms=(x86 apple)
if (( EUID == 0 )); then
if [[ $("$ROOT/bin/omarchy-hw-platform") == "aarch64-apple" ]]; then
skip "running as root on Apple Silicon, where dispatch ignores fixtures; skipping"
exit 0
fi
platforms=(x86)
skip "running as root, where dispatch ignores fixtures; skipping the Apple runs"
fi
runtime=$ROOT
mac_boot=$tmp/lifecycle/usr/lib/omarchy/mac-boot
mkdir -p "$mac_boot"
cat >"$tmp/mac-boot-lib.sh" <<'SH'
ran() { echo "$1" >>"$TMP/mac-boot-ran"; }
crash() {
local count
count=$(( $(cat "$TMP/steps") + 1 ))
echo "$count" >"$TMP/steps"
printf '%s %s\n' "$count" "$1" >>"$TMP/trace"
if (( count == $(cat "$TMP/crash-at") )); then
kill -9 "$PPID"
kill -9 $$
fi
}
SH
mac_boot_entrypoint() {
{ printf '#!/bin/bash\nset -euo pipefail\nTMP=%q\nsource "$TMP/mac-boot-lib.sh"\nran %s\n' "$tmp" "$1"; cat; } >"$mac_boot/$1"
}
mac_boot_entrypoint provision-prepare <<'SH'
if [[ -e $TMP/prepare-fail ]]; then
echo "The boot partition is not mounted." >&2
exit 1
fi
SH
mac_boot_entrypoint provision-commit <<'SH'
[[ ! -e $TMP/rebuild-fail ]] || exit 1
rm -f "$TMP/boot/omarchy/luks-key"
crash "boot-partition key removed"
sed -i 's/ rd\.luks\.key=[^" ]*//' "$TMP/etc/default/grub"
echo rebuild >>"$TMP/rebuilds"
crash "boot rebuilt"
SH
mac_boot_entrypoint provision-verify <<'SH'
[[ ! -e $TMP/boot/omarchy/luks-key ]] && ! grep -q 'rd\.luks\.key=' "$TMP/etc/default/grub"
SH
mac_boot_entrypoint luks-slots <<'SH'
[[ ! -e $TMP/record-fail ]] || exit 1
printf '%s\n' "$@" >"$TMP/slot-record"
crash "slots recorded"
SH
chmod 755 "$mac_boot"/*
chmod -R go-w "$tmp/lifecycle"
staged_key=staged-install-key
seller_key=previous-owner-key
owner_password=owner-password
sed -n '/^PROVISIONING_UNLOCK_FILES=(/,/^)/p; /^UNLOCK_OWNER=/p; /^limine_auto_unlock_present() {/,/^}/p; /^limine_auto_unlock_drop() {/,/^}/p
/^unlock_owner() {/,/^}/p; /^luks_auto_unlock_present() {/,/^}/p; /^luks_auto_unlock_drop() {/,/^}/p
/^luks_record_slots() {/,/^}/p' \
"$ROOT/bin/omarchy-provision-owner" | sed "s|/etc/|$tmp/etc/|g" >"$tmp/unlock.sh"
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
/^platform_ready() {/,/^}/p; /^run_setup() {/,/^}/p
/^rekey_accepts_password() {/,/^}/p' \
"$ROOT/bin/omarchy-provision-owner" |
sed -e "s|/etc/|$tmp/etc/|g" -e "s|/usr/bin/omarchy-thunderbolt-authorization-admin|omarchy-thunderbolt-authorization-admin|" >"$tmp/provision.sh"
grep -q '^run_provisioning() {' "$tmp/provision.sh" && grep -q '^run_setup() {' "$tmp/provision.sh" ||
fail "omarchy-provision-owner defines its setup and provisioning worker"
cat >"$tmp/attempt.sh" <<'SH'
set -euo pipefail
source "$ROOT/install/provisioning/luks-rekey.sh"
source "$TMP/unlock.sh"
source <(sed -n '/^luks_boot_layout() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" | sed "s|/etc/|$TMP/etc/|g")
declare -F luks_boot_layout >/dev/null
PROVISIONING_DIR=$TMP/provisioning
REKEY_STATE=$PROVISIONING_DIR/luks-rekey.state
LOG_FILE=$TMP/log
password=$PASSWORD
log_step() { printf '%s\n' "$*" >>"$LOG_FILE"; }
say() { :; }
crash_point() {
local count
count=$(( $(cat "$TMP/steps") + 1 ))
echo "$count" >"$TMP/steps"
printf '%s %s\n' "$count" "$1" >>"$TMP/trace"
if (( count == ${CRASH_AT:-0} )); then
kill -9 $$
kill -9 "$BASHPID"
fi
}
eval "$(declare -f rekey_state_put | sed '1s/^rekey_state_put /journal_write /')"
rekey_state_put() {
journal_write "$@" || return 1
crash_point "journal $*"
}
reset_limine_config() {
echo reset >>"$TMP/limine-ran"
crash_point "auto-unlock files removed"
}
limine-update() {
echo update >>"$TMP/limine-ran"
echo rebuild >>"$TMP/rebuilds"
[[ ! -e $TMP/rebuild-fail ]] || return 1
crash_point "boot rebuilt"
}
shred() {
command rm -f -- "${@: -1}"
crash_point "staged key destroyed"
}
fake_cryptsetup() {
local op=$1 key_file="" token_type="" target="" extra="" requested="" material slot next
shift
while (( $# )); do
case $1 in
--key-file) key_file=$2; shift 2 ;;
--key-slot) requested=$2; shift 2 ;;
--token-type) token_type=$2; shift 2 ;;
-*) shift ;;
*)
if [[ -z $target ]]; then target=$1; else extra=$1; fi
shift
;;
esac
done
[[ $target == "$DEVICE" ]] || return 4
if [[ $op == "luksDump" ]]; then
echo "Keyslots:"
awk '{ printf " %s: luks2\n", $1 }' "$TMP/slots"
if [[ -s $TMP/token-slot ]]; then
printf 'Tokens:\n 0: luks2-keyring\n\tKeyslot: %s\n' "$(cat "$TMP/token-slot")"
fi
printf 'Digests:\n 0: pbkdf2\n'
return 0
fi
if [[ $op == "luksKillSlot" ]]; then
[[ ! -e $TMP/kill-noop ]] || return 0
awk -v s="$extra" '$1 != s' "$TMP/slots" >"$TMP/slots.next"
command mv -f "$TMP/slots.next" "$TMP/slots"
return 0
fi
# An enrolled token unlocks its slot whatever key is given, unless the
# allowed token types exclude it, as with cryptsetup.
if [[ $op == "open" && -z $token_type && -s $TMP/token-slot ]]; then
echo "Key slot $(cat "$TMP/token-slot") unlocked."
return 0
fi
material=$(cat "$key_file")
slot=$(awk -v m="$material" '$2 == m { print $1; exit }' "$TMP/slots")
if [[ -z $slot ]]; then
echo "No key available with this passphrase."
return 2
fi
case $op in
open) echo "Key slot $slot unlocked." ;;
luksAddKey)
next=$requested
if [[ -z $next ]]; then
for (( next = 0; next < 32; next++ )); do
awk '{ print $1 }' "$TMP/slots" | grep -qx "$next" || break
done
fi
! awk '{ print $1 }' "$TMP/slots" | grep -qx "$next" || return 1
printf '%s %s\n' "$next" "$(cat "$extra")" >>"$TMP/slots"
;;
*) return 1 ;;
esac
}
cryptsetup() {
local status=0
if [[ $BACKEND == "fake" ]]; then
fake_cryptsetup "$@" || status=$?
elif [[ $1 == "luksAddKey" ]]; then
command cryptsetup luksAddKey --pbkdf pbkdf2 --pbkdf-force-iterations 1000 "${@:2}" || status=$?
else
command cryptsetup "$@" || status=$?
fi
(( status == 0 )) || return "$status"
case $1 in
luksAddKey)
echo owner >>"$TMP/adds"
crash_point "owner key added"
;;
luksKillSlot) crash_point "slot ${*: -1} killed" ;;
esac
}
# omarchy-provision-owner's setup functions, with account and boot setup and
# the screen stubbed.
setup_functions() {
source "$TMP/provision.sh"
STATE_FILE=$TMP/state
FINALIZE_WARNING_FLAG=$TMP/finalize-warning
username=owner hostname="" timezone=""
create_user() { :; }
install_authorized_keys() { :; }
configure_login() { :; }
configure_hostname() { :; }
configure_timezone() { :; }
finalize_user() { :; }
# Accessory enrollment, recorded with whether the staged unlock was gone.
omarchy-pkg-present() { return 1; }
omarchy-thunderbolt-authorization-admin() {
if luks_staged_unlock_remains; then echo "$* early" >>"$TMP/accessories"; else echo "$*" >>"$TMP/accessories"; fi
}
limine_entries_stale() {
crash_point "re-key returned"
[[ -e $TMP/stale ]]
}
luks_device() { echo "$DEVICE"; }
systemctl() { :; }
clear_logo() { :; }
sleep() { :; }
say() { printf '%s\n' "$*" >>"$TMP/screen"; }
}
# The first-boot worker around the re-key.
provision() {
setup_functions
run_provisioning
}
# A whole setup attempt once the owner form is filled in: the password check,
# then the worker.
attempt() {
setup_functions
keyboard_form() { :; }
user_form() { :; }
confirm_form() { :; }
notice() {
printf 'refused: %s\n' "$1" >>"$TMP/screen"
exit 3
}
set_now() { NOW=0; }
render_setup_static() { :; }
render_setup_dynamic() { :; }
SHOW_CURSOR=""
run_setup
}
# First-boot setup up to the owner form, with the screen captured.
setup() {
source "$TMP/provision.sh"
clear_logo() { :; }
sleep() { :; }
say() { printf '%s\n' "$*" >>"$TMP/screen"; }
keyboard_form() {
echo "owner form" >>"$TMP/screen"
exit 0
}
run_setup
}
case $MODE in
provision) provision ;;
attempt) attempt ;;
setup) setup ;;
rekey) luks_rekey "$DEVICE" ;;
accepts) luks_rekey_accepts_password "$DEVICE" ;;
remains) luks_staged_unlock_remains ;;
esac
SH
run() {
local mode=$1 password=$2 crash_at=${3:-0}
echo "$crash_at" >"$tmp/crash-at"
{
ROOT=$ROOT TMP=$tmp BACKEND=$backend DEVICE=$device MODE=$mode PASSWORD=$password CRASH_AT=$crash_at \
OMARCHY_PATH=$runtime OMARCHY_PROC_ROOT=$tmp/$platform/proc OMARCHY_LIFECYCLE_ROOT=$tmp/lifecycle \
PATH="$tmp/$platform/bin:$PATH" bash "$tmp/attempt.sh"
} >>"$tmp/output" 2>&1
}
# The slot the key opens on the volume, or nothing. Tokens never answer.
opens() {
if [[ $backend == "fake" ]]; then
awk -v m="$1" '$2 == m { print $1; exit }' "$tmp/slots"
else
LC_ALL=C cryptsetup open --test-passphrase --verbose --token-type passphrase-only --key-file <(printf '%s' "$1") "$device" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' || true
fi
}
slot_count() {
if [[ $backend == "fake" ]]; then
wc -l <"$tmp/slots"
else
cryptsetup luksDump "$device" 2>/dev/null | grep -cE '^ +[0-9]+: luks2$|^Key Slot [0-9]+: ENABLED'
fi
}
fixture() {
local format=${1:-luks2}
rm -rf "$tmp/provisioning" "$tmp/etc" "$tmp/boot" "$tmp/log" "$tmp/output" "$tmp/trace" "$tmp/rebuilds" "$tmp/adds" "$tmp/rebuild-fail" "$tmp/kill-noop" \
"$tmp/prepare-fail" "$tmp/screen" "$tmp/stale" "$tmp/token-slot" "$tmp/slot-record" "$tmp/record-fail" "$tmp/accessories"
mkdir -p "$tmp/provisioning"
chmod 755 "$tmp/provisioning"
touch "$tmp/provisioning/pending"
printf '%s' "$staged_key" >"$tmp/provisioning/luks-key"
if [[ $platform == "apple" ]]; then
mkdir -p "$tmp/boot/omarchy" "$tmp/etc/default"
printf '%s' "$staged_key" >"$tmp/boot/omarchy/luks-key"
echo 'GRUB_CMDLINE_LINUX="rd.luks.name=root-uuid=root rd.luks.key=root-uuid=/omarchy/luks-key:UUID=boot-uuid"' >"$tmp/etc/default/grub"
else
mkdir -p "$tmp/etc/omarchy" "$tmp/etc/limine-entry-tool.d" "$tmp/etc/mkinitcpio.conf.d"
printf '%s' "$staged_key" >"$tmp/etc/omarchy/provisioning.key"
echo 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"' \
>"$tmp/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf"
echo 'FILES+=(/etc/omarchy/provisioning.key)' >"$tmp/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf"
fi
echo 0 >"$tmp/steps"
: >"$tmp/log"
# A reset leaves the previous owner's slot next to the staged key.
if [[ $backend == "fake" ]]; then
device=$tmp/volume
: >"$device"
printf '0 %s\n1 %s\n' "$staged_key" "$seller_key" >"$tmp/slots"
else
device=$tmp/volume.img
rm -f "$device"
truncate -s 32M "$device"
local pbkdf=(--pbkdf pbkdf2 --pbkdf-force-iterations 1000)
[[ $format == "luks1" ]] && pbkdf=(--pbkdf-force-iterations 1000)
cryptsetup luksFormat -q --type "$format" "${pbkdf[@]}" "$device" <(printf '%s' "$staged_key")
cryptsetup luksAddKey "${pbkdf[@]}" --key-file <(printf '%s' "$staged_key") "$device" <(printf '%s' "$seller_key")
fi
}
# What a previous owner can leave on the real volume: a systemd-tpm2 token and
# a luks2-keyring token on slot $1, the second answered from the session keyring
# with $2. Fails when the kernel keyring is out of reach (no keyctl, or a
# container's seccomp profile), so that a bare cryptsetup open does not take the
# token in place of any key.
enroll_tokens() {
local slot=$1 key=$2 description=omarchy-test-token-$$
command -v keyctl >/dev/null || return 1
token_key=$(printf '%s' "$key" | keyctl padd user "$description" @s 2>/dev/null) || return 1
keyctl timeout "$token_key" 600 >/dev/null 2>&1 || true
printf '{"type":"systemd-tpm2","keyslots":["%s"],"tpm2-blob":"AA==","tpm2-pcrs":[7],"tpm2-pcr-bank":"sha256","tpm2-primary-alg":"ecc","tpm2-policy-hash":"00","tpm2-pin":false}' "$slot" |
cryptsetup token import --disable-external-tokens "$device" || return 1
cryptsetup token add --key-description "$description" --key-slot "$slot" "$device" >/dev/null || return 1
LC_ALL=C cryptsetup open --test-passphrase --verbose --key-file <(printf 'not-a-key') "$device" 2>&1 | grep -qx "Key slot $slot unlocked."
}
unlock_files_present() {
[[ -e $tmp/etc/omarchy/provisioning.key || -e $tmp/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf ||
-e $tmp/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf || -e $tmp/boot/omarchy/luks-key ]] ||
grep -qs 'rd\.luks\.key=' "$tmp/etc/default/grub"
}
no_secrets_in() {
local file
local -a keys=(-e "$staged_key" -e "$seller_key" -e "$owner_password" -e "other-password")
for file in "$@"; do
[[ -e $file ]] || continue
! grep -Fq "${keys[@]}" "$file" ||
fail "$backend: $(basename "$file") holds no key material" "$(cat "$file")"
done
}
# Between attempts the disk must still open at the next boot: unattended while
# the auto-unlock remains, otherwise with a password the owner typed.
assert_recoverable() {
local context=$1 password=$2
if unlock_files_present; then
[[ -n $(opens "$staged_key") ]] || fail "$backend: $context: the auto-unlock key still opens the disk"
else
[[ -n $(opens "$password") ]] || fail "$backend: $context: the owner's password opens the disk once auto-unlock is gone"
fi
if [[ -n $(opens "$staged_key") && ! -f $tmp/provisioning/luks-key ]]; then
fail "$backend: $context: the staged key file is kept while it still unlocks the volume"
fi
if [[ -f $tmp/provisioning/luks-rekey.state ]]; then
[[ $(stat -c %a "$tmp/provisioning/luks-rekey.state") == "600" ]] || fail "$backend: $context: the journal is private"
fi
[[ $(stat -c %a "$tmp/provisioning") == "755" ]] || fail "$backend: $context: the provisioning directory stays readable for the new user"
no_secrets_in "$tmp/provisioning/luks-rekey.state" "$tmp/log"
}
# The re-key finished: the owner's slot is all the volume holds.
assert_finished() {
local context=$1 password=$2 max_adds=${3:-1} adds=0 owner
owner=$(opens "$password")
[[ $(slot_count) == "1" && -n $owner ]] || fail "$backend: $context: only the owner's slot remains" "$(cat "$tmp/log")"
[[ -z $(opens "$staged_key") ]] || fail "$backend: $context: the staged install key no longer unlocks the volume"
[[ -z $(opens "$seller_key") ]] || fail "$backend: $context: the previous owner's key no longer unlocks the volume"
[[ ! -e $tmp/provisioning/luks-key ]] || fail "$backend: $context: the staged key file is destroyed"
! unlock_files_present || fail "$backend: $context: no boot-time auto-unlock remains"
[[ -f $tmp/adds ]] && adds=$(grep -c '^owner' "$tmp/adds" || true)
(( adds <= max_adds )) || fail "$backend: $context: the owner's key is added at most $max_adds time(s)"
! run remains "$password" || fail "$backend: $context: nothing of the staged unlock remains"
if [[ $platform == "apple" ]]; then
[[ $(cat "$tmp/slot-record" 2>/dev/null) == "owner=$owner"$'\n'"recovery=" ]] ||
fail "$backend: $context: the boot package records the owner's slot and no recovery slot" "$(cat "$tmp/slot-record" 2>/dev/null)"
else
[[ ! -e $tmp/slot-record ]] || fail "$backend: $context: x86 records no slots"
fi
no_secrets_in "$tmp/provisioning/luks-rekey.state" "$tmp/log" "$tmp/output" "$tmp/slot-record"
}
# Setup completed: the disk opens with the password the account got, and the
# provisioning state is gone.
assert_provisioned() {
assert_finished "$@"
[[ ! -e $tmp/provisioning/pending && ! -e $tmp/provisioning/luks-rekey.state ]] ||
fail "$backend: $1: setup drops pending and the journal together"
}
backends=(fake)
if command -v cryptsetup >/dev/null; then
backends+=(luks2 luks1)
else
skip "cryptsetup is not installed; skipping the file-backed volume runs"
fi
# The crash matrix runs whole setup attempts on every backend, for x86 and for
# Apple.
matrix=()
for backend in "${backends[@]}"; do
matrix+=("x86 $backend")
[[ " ${platforms[*]} " != *" apple "* ]] || matrix+=("apple $backend")
done
for run_spec in "${matrix[@]}"; do
read -r platform backend <<<"$run_spec"
rm -f "$tmp/mac-boot-ran" "$tmp/limine-ran"
format=$backend
[[ $backend == "fake" ]] && format=luks2
fixture "$format"
run attempt "$owner_password" || fail "$backend: uninterrupted setup completes" "$(cat "$tmp/log" "$tmp/output")"
assert_provisioned "uninterrupted" "$owner_password"
[[ $(cat "$tmp/accessories" 2>/dev/null) == "owner" ]] ||
fail "$backend: accessories are enrolled once, after the staged unlock is gone" "$(cat "$tmp/accessories" 2>/dev/null)"
total_steps=$(cat "$tmp/steps")
(( total_steps >= 11 )) || fail "$backend: every durable step is a crash point" "$(cat "$tmp/trace")"
if [[ $platform == "apple" ]]; then
pass "apple $backend: uninterrupted setup leaves only the owner's slot, records it and destroys the staged key"
else
pass "x86 $backend: uninterrupted setup leaves only the owner's slot and destroys the staged key"
fi
# After each kill the owner reboots and answers the form again, with the same
# password or a new one. The account takes whatever the form accepts, so the
# disk must open with that password when setup finishes.
for (( step = 1; step <= total_steps; step++ )); do
for retry_password in "$owner_password" other-password; do
fixture "$format"
if run attempt "$owner_password" "$step"; then
fail "$backend: setup is killed at step $step"
fi
point=$(sed -n "${step}p" "$tmp/trace")
assert_recoverable "killed after '$point'" "$owner_password"
[[ -e $tmp/provisioning/pending ]] || fail "$backend: killed after '$point', setup runs again"
staged_alive=$([[ -n $(opens "$staged_key") ]] && echo 1 || echo 0)
if run accepts "$retry_password"; then
[[ $retry_password == "$owner_password" ]] || (( staged_alive )) ||
fail "$backend: after '$point' a new password is refused once the staged key is retired"
run attempt "$retry_password" || fail "$backend: after '$point' setup completes" "$(cat "$tmp/log")"
if [[ $retry_password == "$owner_password" ]]; then
assert_provisioned "rerun after '$point'" "$retry_password"
if head -n "$step" "$tmp/trace" | grep -q 'journal phase boot'; then
[[ $(wc -l <"$tmp/rebuilds") == "1" ]] || fail "$backend: rerun after '$point' does not rebuild boot again"
fi
else
assert_provisioned "new password after '$point'" "$retry_password" 2
[[ -z $(opens "$owner_password") ]] || fail "$backend: after '$point' the abandoned password is retired"
fi
else
[[ $retry_password != "$owner_password" ]] || fail "$backend: after '$point' the original password is always accepted"
(( ! staged_alive )) || fail "$backend: after '$point' a new password is accepted while the staged key works"
if run attempt "$retry_password"; then fail "$backend: after '$point' setup refuses a new password"; fi
assert_recoverable "refused new password after '$point'" "$owner_password"
[[ -e $tmp/provisioning/pending ]] || fail "$backend: after '$point' a refused password keeps setup pending"
run attempt "$owner_password" || fail "$backend: after '$point' the original password completes" "$(cat "$tmp/log")"
assert_provisioned "original password after '$point'" "$owner_password"
fi
done
done
if [[ $platform == "apple" ]]; then
pass "apple $backend: killed after each of $total_steps steps, setup resumes to a disk that opens with the account's password, with its slot recorded"
else
pass "x86 $backend: killed after each of $total_steps steps, setup resumes to a disk that opens with the account's password"
fi
if [[ $platform == "x86" ]]; then
[[ ! -e $tmp/mac-boot-ran ]] || fail "x86 $backend: no Mac boot entrypoint runs" "$(cat "$tmp/mac-boot-ran")"
pass "x86 $backend: dispatch is a no-op and the Limine UKI path runs unchanged"
else
[[ ! -e $tmp/limine-ran ]] || fail "apple $backend: the Limine UKI path never runs on Apple"
grep -qx provision-commit "$tmp/mac-boot-ran" && grep -qx provision-verify "$tmp/mac-boot-ran" ||
fail "apple $backend: the boot package commits and verifies the unlock"
pass "apple $backend: the boot package owns the boot-time unlock through dispatch"
fi
done
platform=x86
backend=fake
fixture
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "a failed boot rebuild fails the attempt"; fi
unlock_files_present || fail "a failed boot rebuild restores the auto-unlock"
[[ $(cat "$tmp/etc/omarchy/provisioning.key") == "$staged_key" ]] || fail "the restored keyfile is the staged key"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "a failed boot rebuild retires no slot"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "the retry after a failed rebuild completes" "$(cat "$tmp/log")"
assert_finished "retry after a failed rebuild" "$owner_password"
pass "a failed boot rebuild keeps the unattended unlock and every slot for the retry"
fixture
printf '2 %s\n' tpm-sealed-key >>"$tmp/slots"
echo 2 >"$tmp/token-slot"
run provision "$owner_password" || fail "setup completes beside an enrolled token" "$(cat "$tmp/log")"
assert_provisioned "beside the previous owner's token" "$owner_password"
pass "a token the previous owner enrolled never answers for a key and is retired with its slot"
if [[ " ${backends[*]} " == *" luks2 "* ]]; then
backend=luks2
fixture luks2
if enroll_tokens 1 "$seller_key"; then
run provision "$owner_password" || fail "luks2: setup completes beside live tokens" "$(cat "$tmp/log" "$tmp/output")"
assert_provisioned "beside the previous owner's live tokens" "$owner_password"
pass "luks2: with a live keyring token and a TPM2 token on the previous owner's slot, only real keys count"
else
skip "the kernel keyring is out of reach; skipping the live token run"
fi
backend=fake
fi
fixture
touch "$tmp/kill-noop"
if run rekey "$owner_password"; then fail "a slot kill that changes nothing fails the re-key"; fi
[[ -f $tmp/provisioning/luks-key && -f $tmp/provisioning/luks-rekey.state ]] || fail "an unverified retirement keeps the staged key and journal"
rm "$tmp/kill-noop"
run rekey "$owner_password" || fail "the retry after an ineffective slot kill completes" "$(cat "$tmp/log")"
assert_finished "retry after an ineffective slot kill" "$owner_password"
pass "the staged key is destroyed only after the volume proves it no longer opens"
fixture
printf '0 %s\n1 %s\n2 %s\n' "$staged_key" "$seller_key" "$owner_password" >"$tmp/slots"
printf 'staged_slot=0\nowner_slot=2\nphase=boot\n' >"$tmp/provisioning/luks-rekey.state"
run rekey "$owner_password" || fail "a leftover auto-unlock after the boot step is removed" "$(cat "$tmp/log")"
assert_finished "leftover auto-unlock after the boot step" "$owner_password"
[[ $(wc -l <"$tmp/rebuilds") == "1" ]] || fail "the leftover auto-unlock triggers a boot rebuild"
pass "an auto-unlock that outlives the boot step is rebuilt away rather than blocking forever"
# The boot files ask for the disk password in the layout vconsole.conf held
# when they were built. A retry after the owner picked another layout rebuilds
# them once, at the boot step and after done; the same layout rebuilds nothing,
# and a journal from before the fingerprint rebuilds once.
choose_layout() {
mkdir -p "$tmp/etc"
printf 'KEYMAP=%s\nXKBLAYOUT=%s\nXKBMODEL=pc105\n' "$1" "$2" >"$tmp/etc/vconsole.conf"
}
rebuild_count() {
wc -l <"$tmp/rebuilds" 2>/dev/null || echo 0
}
assert_fingerprint() {
local context=$1 journal=$tmp/provisioning/luks-rekey.state
grep -Eqx 'boot_layout=[0-9a-f]{64}' "$journal" || fail "$context: the journal records the layout as a hash" "$(cat "$journal")"
! grep -Eqw 'dk|us' "$journal" "$tmp/log" "$tmp/trace" || fail "$context: the journal and log never name the layout"
}
boot_layout_runs() {
local name=$1 stop step refresh before
fixture
choose_layout us us
run rekey "$owner_password" || fail "$name: the re-key completes with a layout" "$(cat "$tmp/log")"
assert_fingerprint "$name: uninterrupted"
cp "$tmp/trace" "$tmp/layout-trace"
for stop in boot done; do
step=$(grep -m1 "^[0-9]* journal phase $stop" "$tmp/layout-trace" | cut -d' ' -f1)
[[ -n $step ]] || fail "$name: the trace names the $stop journal write" "$(cat "$tmp/layout-trace")"
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
[[ $(rebuild_count) == 1 ]] || fail "$name: killed at $stop after one rebuild"
run rekey "$owner_password" || fail "$name: the same layout completes after $stop" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an unchanged layout after $stop rebuilds nothing"
assert_finished "$name: unchanged layout after $stop" "$owner_password"
# The owner picks another layout on the retry right after the interruption.
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
before=$(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state")
choose_layout dk dk
run rekey "$owner_password" || fail "$name: a changed layout after $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: a changed layout after $stop rebuilds the boot files"
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") != "$before" ]] ||
fail "$name: the journal takes the new layout's fingerprint after $stop"
[[ $(grep -c '^[0-9]* journal boot_layout ' "$tmp/trace") == 1 && $(grep -c "^[0-9]* journal phase $stop" "$tmp/trace") == 1 ]] ||
fail "$name: the rebuild after $stop records only the fingerprint, keeping the phase" "$(cat "$tmp/trace")"
refresh=$(awk -v s="$step" '$1 > s && / boot rebuilt$/ { print $1; exit }' "$tmp/trace")
run rekey "$owner_password" || fail "$name: a rerun after the rebuild completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: the changed layout after $stop rebuilds only once"
assert_finished "$name: changed layout after $stop" "$owner_password"
assert_fingerprint "$name: changed layout after $stop"
# Killed between that rebuild and its fingerprint: the retry rebuilds again.
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
choose_layout dk dk
if run rekey "$owner_password" "$refresh"; then fail "$name: the re-key is killed after the layout rebuild at $stop"; fi
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") == "$before" ]] ||
fail "$name: a layout rebuild killed before its journal write keeps the old fingerprint"
run rekey "$owner_password" || fail "$name: the retry after a killed layout rebuild at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 3 ]] || fail "$name: a layout rebuild killed at $stop runs again"
assert_finished "$name: killed layout rebuild at $stop" "$owner_password"
assert_fingerprint "$name: killed layout rebuild at $stop"
done
# Interrupted at the boot step, the owner picks another layout and a new
# password while the staged key still opens the disk.
step=$(grep -m1 "^[0-9]* journal phase boot" "$tmp/layout-trace" | cut -d' ' -f1)
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the boot journal write"; fi
choose_layout dk dk
run rekey other-password || fail "$name: a new layout and password after the boot step complete" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: a new layout and password after the boot step rebuild once"
assert_finished "$name: new layout and password after the boot step" other-password 2
assert_fingerprint "$name: new layout and password after the boot step"
# A journal from before the fingerprint, at the boot step with the auto-unlock
# already gone and after done: rebuilt once, then recorded.
for stop in boot done; do
fixture
choose_layout dk dk
run rekey "$owner_password" || fail "$name: the re-key completes" "$(cat "$tmp/log")"
step=$(grep -m1 -n "^[0-9]* journal phase $stop" "$tmp/trace" | cut -d: -f1)
fixture
choose_layout dk dk
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
sed -i '/^boot_layout=/d' "$tmp/provisioning/luks-rekey.state"
rm -f "$tmp/rebuilds"
run rekey "$owner_password" || fail "$name: an older journal at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an older journal at $stop rebuilds the boot files once"
run rekey "$owner_password" || fail "$name: a rerun of an older journal at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an older journal at $stop rebuilds only once"
assert_finished "$name: older journal at $stop" "$owner_password"
assert_fingerprint "$name: older journal at $stop"
done
# After done a failed rebuild keeps the old fingerprint, so the retry rebuilds.
fixture
choose_layout us us
run rekey "$owner_password" || fail "$name: the re-key completes" "$(cat "$tmp/log")"
before=$(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state")
choose_layout dk dk
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "$name: a failed layout rebuild after done fails the attempt"; fi
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") == "$before" ]] ||
fail "$name: a failed layout rebuild keeps the old fingerprint"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "$name: the retry after a failed layout rebuild completes" "$(cat "$tmp/log")"
assert_finished "$name: retry after a failed layout rebuild" "$owner_password"
assert_fingerprint "$name: retry after a failed layout rebuild"
}
boot_layout_runs x86
pass "x86: a changed keyboard layout rebuilds the boot files once, an unchanged one never, and an older journal once"
fixture
printf 'wrong-key' >"$tmp/provisioning/luks-key"
if run rekey "$owner_password"; then fail "a staged key that opens nothing fails the re-key"; fi
[[ ! -e $tmp/provisioning/luks-rekey.state && $(slot_count) == "2" ]] || fail "a dead staged key changes nothing"
pass "a fresh re-key refuses a staged key that no longer unlocks the volume"
fixture
rm "$tmp/provisioning/luks-key"
run remains "$owner_password" || fail "a leftover auto-unlock counts as the staged unlock remaining"
if run rekey "$owner_password"; then fail "a leftover auto-unlock without its staged key fails closed"; fi
unlock_files_present || fail "the leftover auto-unlock is not silently accepted"
pass "a boot-time auto-unlock without its staged key keeps provisioning from finishing"
fixture
if run rekey "$staged_key"; then fail "the staged key is refused as the owner's password"; fi
[[ $(slot_count) == "2" ]] && unlock_files_present || fail "refusing the staged key as a password changes nothing"
run rekey "$owner_password" || fail "a retry with a real password completes" "$(cat "$tmp/log")"
assert_finished "retry after refusing the staged key" "$owner_password"
pass "the owner's password must differ from the staged install key"
fixture
printf 'phase=owner\nstaged_slot=0\nowner_slot=2\nrecovery_slot=5\n' >"$tmp/provisioning/luks-rekey.state"
chmod 600 "$tmp/provisioning/luks-rekey.state"
(
source "$ROOT/install/provisioning/luks-rekey.sh"
REKEY_STATE=$tmp/provisioning/luks-rekey.state
rekey_state_put phase boot
)
[[ $(grep -c . "$tmp/provisioning/luks-rekey.state") == "4" ]] && grep -qx 'phase=boot' "$tmp/provisioning/luks-rekey.state" &&
grep -qx 'recovery_slot=5' "$tmp/provisioning/luks-rekey.state" || fail "a journal write keeps every other recorded key" "$(cat "$tmp/provisioning/luks-rekey.state")"
pass "journal writes replace only their own keys"
fixture
rm "$tmp/provisioning/luks-key"
if run provision "$owner_password"; then fail "provisioning with a leftover auto-unlock and no staged key fails"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "provisioning keeps its state while the auto-unlock remains"
pass "first-boot provisioning never finishes with the boot-time auto-unlock still configured"
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/etc"
run provision "$owner_password" || fail "unencrypted provisioning finishes" "$(cat "$tmp/log" "$tmp/output")"
[[ ! -e $tmp/provisioning/pending && ! -e $tmp/provisioning/luks-rekey.state && $(slot_count) == "2" ]] ||
fail "unencrypted provisioning skips the re-key"
pass "provisioning without a staged key or auto-unlock skips the re-key"
if [[ " ${platforms[*]} " == *" apple "* ]]; then
# Apple: a failed boot-package commit keeps the unattended unlock and every slot.
platform=apple
fixture
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "apple: a failed boot-package commit fails the attempt"; fi
unlock_files_present || fail "apple: a failed commit keeps the boot-time unlock"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "apple: a failed commit retires no slot"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "apple: the retry after a failed commit completes" "$(cat "$tmp/log")"
assert_finished "apple: retry after a failed commit" "$owner_password"
pass "apple: a failed boot-package commit keeps the unattended unlock and every slot for the retry"
# Apple: an inherited decision never replaces resolving who owns the unlock.
fixture
rm -f "$tmp/limine-ran"
export UNLOCK_OWNER=limine
run provision "$owner_password" || fail "apple: setup completes with UNLOCK_OWNER in its environment" "$(cat "$tmp/log")"
unset UNLOCK_OWNER
assert_provisioned "apple: UNLOCK_OWNER inherited" "$owner_password"
[[ ! -e $tmp/limine-ran ]] || fail "apple: an inherited UNLOCK_OWNER does not select the Limine path"
pass "apple: an inherited UNLOCK_OWNER is ignored"
# Apple: the boot package's commit rebuilds for a changed layout, after done too.
boot_layout_runs apple
pass "apple: a changed keyboard layout reruns the boot package's commit once, an unchanged one never, and an older journal once"
fi
# After a factory reset left entries for another machine identity, the menu
# starts over and limine-update rebuilds, on Apple too, and setup still
# finishes only once the boot package verifies nothing of the staged unlock
# remains.
for platform in "${platforms[@]}"; do
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/etc" "$tmp/boot" "$tmp/limine-ran" "$tmp/mac-boot-ran"
touch "$tmp/stale"
run provision "$owner_password" || fail "$platform: stale boot entries are refreshed" "$(cat "$tmp/log" "$tmp/output")"
[[ ! -e $tmp/provisioning/pending && $(cat "$tmp/limine-ran") == $'reset\nupdate' ]] ||
fail "$platform: the Limine menu is reset and limine-update rebuilds" "$(cat "$tmp/limine-ran")"
if [[ $platform == "x86" ]]; then
[[ ! -e $tmp/mac-boot-ran ]] || fail "x86: no Mac entrypoint runs"
else
grep -qx provision-verify "$tmp/mac-boot-ran" || fail "apple: the boot package still has the last word"
fi
done
if [[ " ${platforms[*]} " == *" apple "* ]]; then
platform=apple
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/limine-ran"
touch "$tmp/stale"
if run provision "$owner_password"; then fail "apple: a leftover boot-partition unlock still stops setup after the refresh"; fi
[[ -e $tmp/provisioning/pending ]] || fail "apple: setup stays pending"
fi
pass "stale boot entries are rebuilt by limine-update, behind the boot package's verify on Apple"
# Before the owner form: a no-op on x86 even with Mac entrypoints on disk, the
# boot package's own answer on Apple.
platform=x86
fixture
rm -f "$tmp/mac-boot-ran"
touch "$tmp/prepare-fail"
run setup "$owner_password" || fail "x86: setup reaches the owner form without a boot package" "$(cat "$tmp/output")"
[[ $(cat "$tmp/screen") == "owner form" && ! -e $tmp/mac-boot-ran ]] ||
fail "x86: no Mac boot entrypoint runs before the owner form" "$(cat "$tmp/screen")"
pass "x86: the platform check before the owner form is a no-op"
if [[ " ${platforms[*]} " == *" apple "* ]]; then
platform=apple
fixture
run setup "$owner_password" && grep -qx 'owner form' "$tmp/screen" ||
fail "apple: setup reaches the owner form with the boot package ready" "$(cat "$tmp/output")"
fixture
touch "$tmp/prepare-fail"
if run setup "$owner_password"; then fail "apple: setup stops when the boot package is not ready"; fi
! grep -qx 'owner form' "$tmp/screen" || fail "apple: the owner is asked nothing when the boot package is not ready"
grep -q 'The boot partition is not mounted.' "$tmp/screen" && grep -q 'The boot partition is not mounted.' "$tmp/log" ||
fail "apple: the boot package's reason reaches the screen and the log" "$(cat "$tmp/screen" "$tmp/log" 2>/dev/null)"
pass "apple: setup stops before the owner form when the boot package is not ready"
# Apple without omarchy-mac-boot, or with one older than its provisioning
# entrypoints: setup stops before the owner form naming the package, and a
# worker that got past it anyway never finishes while any part of the staged
# unlock remains.
mv "$mac_boot" "$tmp/mac-boot.off"
for package in missing old; do
# An older package can already ship other files in that directory.
if [[ $package == old ]]; then
install -d -m 755 "$mac_boot"
install -m 755 /dev/null "$mac_boot/boot-hook"
fi
fixture
rm -f "$tmp/limine-ran" "$tmp/mac-boot-ran"
if run setup "$owner_password"; then fail "apple ($package package): setup refuses without provisioning entrypoints"; fi
! grep -qx 'owner form' "$tmp/screen" || fail "apple ($package package): the owner is asked nothing"
grep -q 'provision-prepare on aarch64-apple needs omarchy-mac-boot' "$tmp/screen" ||
fail "apple ($package package): the boot package is named on the screen" "$(cat "$tmp/screen" 2>/dev/null)"
grep -q '/usr/lib/omarchy/mac-boot/provision-prepare' "$tmp/log" || fail "apple ($package package): the log names the entrypoint" "$(cat "$tmp/log")"
if run provision "$owner_password"; then fail "apple ($package package): provisioning fails"; fi
[[ -e $tmp/provisioning/pending && -f $tmp/provisioning/luks-key ]] && unlock_files_present ||
fail "apple ($package package): provisioning keeps its state and the staged unlock"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "apple ($package package): no slot is retired"
fixture
rm "$tmp/provisioning/luks-key"
if run provision "$owner_password"; then fail "apple ($package package): a leftover boot-partition unlock fails provisioning"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "apple ($package package): a leftover boot-partition unlock keeps provisioning pending"
[[ ! -e $tmp/limine-ran && ! -e $tmp/mac-boot-ran ]] || fail "apple ($package package): the Limine UKI path is no fallback"
[[ $package == missing ]] || rm -r "$mac_boot"
done
mv "$tmp/mac-boot.off" "$mac_boot"
pass "apple: without omarchy-mac-boot's provisioning entrypoints setup stops naming the package, and the worker fails closed"
fi
# A boot package that implements only one of the commit/verify pair owns
# nothing: provisioning fails closed rather than mixing it with the Limine path.
mkdir -p "$tmp/half/bin"
cat >"$tmp/half/bin/omarchy-lifecycle-dispatch" <<SH
#!/bin/bash
echo "\$*" >>"$tmp/half-ran"
[[ \$* != "--resolve provision-commit" ]] || echo /usr/lib/omarchy/mac-boot/provision-commit
SH
chmod +x "$tmp/half/bin/omarchy-lifecycle-dispatch"
runtime=$tmp/half
fixture
rm -f "$tmp/limine-ran" "$tmp/half-ran"
if run provision "$owner_password"; then fail "a half-implemented unlock fails provisioning"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "a half-implemented unlock keeps provisioning pending"
[[ -s $tmp/half-ran && ! -e $tmp/limine-ran ]] && ! grep -qv -- '--resolve' "$tmp/half-ran" ||
fail "a half-implemented unlock runs neither the platform nor the Limine path" "$(cat "$tmp/half-ran")"
runtime=$ROOT
pass "a boot package implementing only half of the unlock pair fails closed"
# Any slot but the owner's is retired, a recovery key an earlier setup added
# and recorded in the journal included.
platform=x86
backend=fake
fixture
printf '2 recovery-key\n' >>"$tmp/slots"
printf 'recovery_slot=2\nrecovery_shown=1\n' >"$tmp/provisioning/luks-rekey.state"
chmod 600 "$tmp/provisioning/luks-rekey.state"
run rekey "$owner_password" || fail "the re-key beside an earlier recovery slot completes" "$(cat "$tmp/log")"
[[ -z $(opens "$staged_key") && -z $(opens "$seller_key") && -z $(opens recovery-key) && -n $(opens "$owner_password") && $(slot_count) == 1 ]] ||
fail "every slot but the owner's is retired" "$(cat "$tmp/slots")"
pass "the re-key retires every slot but the owner's, an earlier recovery slot included"
@@ -0,0 +1,155 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# A platform package names keys the way its keyboards print them. On a MacBook
# XF86MonBrightnessUp/Down are the F2/F1 keys, so without the names the menu
# shows rows like "SHIFT + XF86MonBrightnessUp". A cache written before the
# names were installed must not win over them.
mock_bin=$(mktemp -d)
cache_dir=$(mktemp -d)
platform=$(mktemp -d)
cleanup() {
rm -rf "$mock_bin" "$cache_dir" "$platform"
}
trap cleanup EXIT
cat >"$mock_bin/hyprctl" <<'SH'
#!/bin/bash
if [[ $1 == "devices" ]]; then
echo "Keyboards:"
echo " active keymap: Apple Keyboard (apple_vndr)"
exit 0
fi
cat <<'BINDS'
bindl
modmask: 1
key: XF86MonBrightnessUp
keycode: 0
description: Keyboard brightness up
dispatcher: exec
arg: omarchy-brightness-keyboard up
bindl
modmask: 1
key: XF86MonBrightnessDown
keycode: 0
description: Keyboard brightness down
dispatcher: exec
arg: omarchy-brightness-keyboard down
bindi
modmask: 8
key: XF86MonBrightnessUp
keycode: 0
description: Brightness up precise
dispatcher: exec
arg: omarchy-brightness-display +1%
bind
modmask: 64
key: F5
keycode: 0
description: Press brightness up
dispatcher: exec
arg: wtype -k XF86MonBrightnessUp
bindm
modmask: 64
key: Q
keycode: 0
description: Close window
dispatcher: killactive
arg:
BINDS
SH
chmod +x "$mock_bin/hyprctl"
# Minimal valid keymap so keycode resolution has something to chew on.
cat >"$mock_bin/xkbcli" <<'SH'
#!/bin/bash
cat <<'KEYMAP'
xkb_keycodes {
<AD01> = 24;
};
xkb_symbols {
key <AD01> { [ q ] };
};
KEYMAP
SH
chmod +x "$mock_bin/xkbcli"
cat >"$mock_bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
exit 1
SH
chmod +x "$mock_bin/omarchy-cmd-present"
# The menu reads key names from the fixed platform root; copies of it read a
# fixture root with key names, and one without.
mkdir -p "$platform/root" "$platform/bin"
printf '%s\n' 'XF86MonBrightnessUp F2' 'XF86MonBrightnessDown F1' 'not a line' 'Bad-Sym F3' >"$platform/root/key-names"
platform_root_copy "$ROOT/bin/omarchy-menu-keybindings" "$platform/bin/with-names" "$platform/root"
platform_root_copy "$ROOT/bin/omarchy-menu-keybindings" "$platform/bin/without-names" "$platform/none"
# Pre-seed the exact cache file that v13, which knew no key names, would read
# for these mocked inputs. Cached records use the rendered row as field 1, followed by
# the dispatcher and argument fields.
mkdir -p "$cache_dir/omarchy"
v13_key=$({
printf 'v13\n'
"$mock_bin/hyprctl" devices 2>/dev/null | grep -F 'active keymap:'
"$mock_bin/hyprctl" binds 2>/dev/null
} | sha256sum | awk '{ print $1 }')
cat >"$cache_dir/omarchy/keybindings-$v13_key.records" <<'EOF'
SHIFT + XF86MonBrightnessUp → Keyboard brightness up exec omarchy-brightness-keyboard up
SHIFT + XF86MonBrightnessDown → Keyboard brightness down exec omarchy-brightness-keyboard down
ALT + XF86MonBrightnessUp → Brightness up precise exec omarchy-brightness-display +1%
SUPER + Q → Close window killactive
EOF
output=$(PATH="$mock_bin:$PATH" XDG_CACHE_HOME="$cache_dir" "$platform/bin/with-names" --print)
tr -s ' ' <<<"$output" | grep -qF 'SHIFT + F2 → Keyboard brightness up' || \
fail "SHIFT + XF86MonBrightnessUp renders as SHIFT + F2 with its description" "$output"
tr -s ' ' <<<"$output" | grep -qF 'SHIFT + F1 → Keyboard brightness down' || \
fail "SHIFT + XF86MonBrightnessDown renders as SHIFT + F1 with its description" "$output"
tr -s ' ' <<<"$output" | grep -qF 'ALT + F2 → Brightness up precise' || \
fail "modifier combos keep the F-key name (ALT + F2)" "$output"
grep -qF 'XF86MonBrightness' <<<"$output" && \
fail "raw XF86MonBrightness symbols no longer leak into the menu" "$output"
tr -s ' ' <<<"$output" | grep -qF 'SUPER + Q → Close window' || \
fail "unrelated bindings render unchanged (SUPER + Q)" "$output"
# The menu runs a chosen row from its cached record, which carries what the
# binding runs.
records=$(cat "$cache_dir"/omarchy/keybindings-*.records)
grep -qF 'wtype -k XF86MonBrightnessUp' <<<"$records" && ! grep -qF 'wtype -k F2' <<<"$records" ||
fail "a key name never changes what a binding runs" "$records"
pass "keybindings menu shows a platform's key names (F1/F2), stale caches ignored"
# A key-names file saved with CRLF line ends, or with blanks around a line,
# reads the same, as displays.conf does.
mkdir -p "$platform/crlf"
printf '%s\r\n' 'XF86MonBrightnessUp F2' ' XF86MonBrightnessDown F1 ' >"$platform/crlf/key-names"
platform_root_copy "$ROOT/bin/omarchy-menu-keybindings" "$platform/bin/crlf-names" "$platform/crlf"
output=$(PATH="$mock_bin:$PATH" XDG_CACHE_HOME="$cache_dir" "$platform/bin/crlf-names" --print)
tr -s ' ' <<<"$output" | grep -qF 'SHIFT + F2 → Keyboard brightness up' && tr -s ' ' <<<"$output" | grep -qF 'SHIFT + F1 → Keyboard brightness down' ||
fail "key names saved with CRLF line ends and blanks around them still apply" "$output"
pass "keybindings menu reads key names saved with CRLF line ends"
# Without a platform package the keys keep their own names, and a cache made
# with the names is not reused.
output=$(PATH="$mock_bin:$PATH" XDG_CACHE_HOME="$cache_dir" "$platform/bin/without-names" --print)
tr -s ' ' <<<"$output" | grep -qF 'SHIFT + XF86MonBrightnessUp → Keyboard brightness up' ||
fail "without key names the brightness key keeps its own name" "$output"
grep -qF 'SHIFT + F2' <<<"$output" && fail "without key names nothing is renamed F2" "$output"
pass "keybindings menu keeps keysym names without a platform package"
+19 -5
View File
@@ -286,18 +286,27 @@ assert(
assert(!defaultById['install.ai.crush'], 'menu removes Crush from Install > AI')
// Software you already have keeps its place in Install, dimmed rather than
// dropped, so the list reads as a catalog of what Omarchy can install.
// Chromium Account is the sole Install row with anything left to hide for, so
// any other `when:` here is a row that went back to vanishing once installed.
// Chromium Account is the sole Install row with anything left to hide for. The
// Windows VM, whose guest can't run elsewhere, and the installers whose vendors
// ship Linux builds for x86_64 alone hide only off x86_64, so any other `when:`
// here is a row that went back to vanishing once installed.
const windowsGuard = 'omarchy-hw-x86'
const x86OnlyInstalls = ['install.windows', 'install.browser.edge', 'install.service.dropbox', 'install.service.spotify', 'install.gaming.minecraft', 'install.gaming.heroic']
assertDeepEqual(
defaultItems
.filter(item => item.id.startsWith('install.') && item.action && item.when)
.map(item => item.id),
['install.service.chromium-account'],
.map(item => item.id)
.sort(),
[...x86OnlyInstalls, 'install.service.chromium-account'].sort(),
'menu never hides an Install row because the software is already there'
)
assert(
x86OnlyInstalls.every(id => defaultById[id].when === windowsGuard),
'menu hides the Windows VM and the x86_64-only installers only off x86_64'
)
assert(
['install.browser.zen', 'install.editor.vscode', 'install.gaming.steam', 'install.development.rust', 'install.windows'].every(
id => defaultById[id].disabled && !defaultById[id].when
id => defaultById[id].disabled && (!defaultById[id].when || defaultById[id].when === windowsGuard)
),
'menu dims the Install rows for software that is already installed'
)
@@ -474,6 +483,11 @@ assertEqual(
'omarchy-hw-webcam',
'menu only shows webcam screen recording when a webcam is available'
)
assertEqual(
defaultById['trigger.capture.screenrecord.stop'].when,
'omarchy-capture-screenrecording --status',
'menu shows Stop Screenrecording only when its stop has a recording to end'
)
assert(
/font\.family: row\.iconFont\.length > 0 \? row\.iconFont : root\.fontFamily/.test(menuQml),
'menu rows support per-icon font families'
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Apple Silicon's boot package ships mkinitcpio drop-ins 90- to 94- that sort
# after the HOOKS baseline and add the Asahi firmware and systemd unlock hooks
# to it. The fixtures are the drop-ins omarchy-mac-boot 20260926-1.45 installs,
# as read from an encrypted M1 Pro, byte for byte the same as
# omarchy-mac-boot/files/etc/mkinitcpio.conf.d in omacom/omarchy-mac-pkgs at
# 4399105 (MIT). The encryption drop-in leaves a line carrying
# busybox encrypt alone, since that is a Mac unlocked through cryptdevice=, so
# the baseline must give a Mac the systemd line.
require_platform_fixtures "the Apple boot drop-ins on the HOOKS baseline"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
fake_platform "$tmp/platform" aarch64-apple
# No kernel modules here: the HID drop-in adds only what modinfo finds.
printf '#!/bin/bash\nexit 1\n' >"$tmp/platform/bin/modinfo"
chmod +x "$tmp/platform/bin/modinfo"
# $1 is the HOOKS line in mkinitcpio.conf. The drop-ins read /etc/vconsole.conf,
# so point them at a fixture.
compose() {
local config=$tmp/buildconfig conf
local -a conf_files=()
rm -rf "$tmp/conf.d"
mkdir -p "$tmp/conf.d"
cp "$ROOT"/etc/mkinitcpio.conf.d/*.conf "$ROOT"/test/shell.d/fixtures/omarchy-mac-boot-mkinitcpio/*.conf "$tmp/conf.d/"
sed -i "s|/etc/vconsole.conf|$tmp/vconsole.conf|g" "$tmp/conf.d"/*.conf
printf 'MODULES=()\nBINARIES=()\nFILES=()\nHOOKS=(%s)\n' "$1" >"$config"
mapfile -d '' conf_files < <(LC_ALL=C.UTF-8 find "$tmp/conf.d" -maxdepth 1 -xtype f -name '*.conf' -print0 |
sed -z 's/.*\///' | LC_ALL=C.UTF-8 sort -zVu)
for conf in "${conf_files[@]}"; do
cat -- "$tmp/conf.d/$conf" >>"$config"
done
OMARCHY_PROC_ROOT="$tmp/platform/proc" PATH="$tmp/platform/bin:$ROOT/bin:$PATH" "$BASH" -c '
. "$1" || exit 1
printf "HOOKS=%s\nFILES=%s\n" "${HOOKS[*]}" "${FILES[*]}"
' -- "$config"
}
assert_hooks() {
local description="$1" stock="$2" expected="$3" out
out=$(compose "$stock") || fail "$description" "the drop-ins do not source cleanly"
[[ $(sed -n 's/^HOOKS=//p' <<<"$out") == "$expected" ]] ||
fail "$description" "expected: HOOKS=$expected"$'\n'"actual: $out"
pass "$description"
}
# The HOOKS the M1 Pro builds its encrypted image from today.
apple="base systemd plymouth autodetect microcode modconf kms keyboard sd-vconsole block asahi omarchy-vendorfw omarchy-mac-encrypt sd-encrypt filesystems fsck"
alarm_systemd="base systemd autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck"
alarm_busybox="base udev autodetect microcode modconf kms keyboard keymap consolefont block filesystems fsck"
printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$tmp/vconsole.conf"
assert_hooks "a Mac keeps its firmware and systemd unlock hooks" "$alarm_systemd" "$apple"
assert_hooks "a Mac whose mkinitcpio.conf has the busybox line gets the same hooks" "$alarm_busybox" "$apple"
out=$(compose "$alarm_systemd")
[[ $(sed -n 's/^FILES=//p' <<<"$out") == *"$tmp/vconsole.conf"* ]] ||
fail "a Latin layout reaches the image" "$out"
pass "a Latin layout reaches the image"
printf 'KEYMAP=ru\nXKBLAYOUT=ru\n' >"$tmp/vconsole.conf"
assert_hooks "a non-Latin layout keeps the prompt on the US map" "$alarm_systemd" "${apple/ sd-vconsole / }"
out=$(compose "$alarm_systemd")
[[ $(sed -n 's/^FILES=//p' <<<"$out") != *vconsole* ]] ||
fail "a non-Latin layout stays out of the image" "$out"
pass "a non-Latin layout stays out of the image"
# A Mac that unlocks through cryptdevice= keeps its busybox line, drop-ins and all.
printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$tmp/vconsole.conf"
legacy="base udev plymouth autodetect microcode modconf kms keyboard keymap consolefont block encrypt asahi filesystems fsck"
assert_hooks "a busybox encrypt Mac keeps its own line" "$legacy" "$legacy"
+264
View File
@@ -0,0 +1,264 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_platform_fixtures "the composed mkinitcpio HOOKS"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
for platform in aarch64-apple aarch64 x86; do
fake_platform "$test_tmp/platforms/$platform" "$platform"
done
# omarchy-settings without the runtime package: no detector on PATH.
mkdir -p "$test_tmp/platforms/no-detector/bin"
# A device tree naming both Apple and Qualcomm, which the detector refuses.
fake_platform "$test_tmp/platforms/contradiction" aarch64-apple
printf '%s\0' apple,j416c qcom,x1e80100 >"$test_tmp/platforms/contradiction/proc/device-tree/compatible"
etc="$test_tmp/etc"
devices="$test_tmp/devices"
omarchy_hooks="base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs"
omarchy_hooks_without_kms=${omarchy_hooks/ kms / }
apple_hooks="base systemd plymouth autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck"
nvidia_modules="nvidia nvidia_modeset nvidia_uvm nvidia_drm"
# The kernel being built, as modinfo answers for it: KERNEL_MODULES names its
# modules (thunderbolt, as on x86 and Aurora, unless a case says otherwise) and
# KERNEL_BUILTINS the ones built in.
cat >"$test_tmp/modinfo" <<'SH'
#!/bin/bash
[[ $1 == -k && $2 == "$KERNELVERSION" && $3 == -F && $4 == filename ]] || exit 1
if [[ " ${KERNEL_MODULES-thunderbolt} " == *" $5 "* ]]; then
printf '/usr/lib/modules/%s/kernel/%s.ko.zst\n' "$KERNELVERSION" "$5"
elif [[ " ${KERNEL_BUILTINS-} " == *" $5 "* ]]; then
printf '(builtin)\n'
else
exit 1
fi
SH
chmod +x "$test_tmp/modinfo"
for fixture in "$test_tmp"/platforms/*; do
cp "$test_tmp/modinfo" "$fixture/bin/"
done
# mkinitcpio's stock mkinitcpio.conf, which the baseline replaces, plus every
# drop-in Omarchy ships, on a machine with no PCI devices.
new_etc() {
rm -rf "$etc" "$devices"
mkdir -p "$etc/mkinitcpio.conf.d" "$devices"
cat >"$etc/mkinitcpio.conf" <<'CONF'
MODULES=()
BINARIES=()
FILES=()
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck)
CONF
cp "$ROOT"/etc/mkinitcpio.conf.d/*.conf "$etc/mkinitcpio.conf.d/"
}
# $1 names the drop-in; stdin is its content.
drop_in() {
cat >"$etc/mkinitcpio.conf.d/$1"
}
# Each argument is a PCI device as "vendor:class", in sysfs's own format.
pci_devices() {
local index=0 spec slot
for spec in "$@"; do
slot=$(printf '0000:%02x:00.0' "$index")
mkdir -p "$devices/$slot"
printf '%s\n' "${spec%%:*}" >"$devices/$slot/vendor"
printf '%s\n' "${spec##*:}" >"$devices/$slot/class"
index=$((index + 1))
done
}
# mkinitcpio appends the drop-ins to mkinitcpio.conf in its own order and
# sources the result once. Do the same on a platform fixture and print what the
# image is built from. The vconsole.conf entry comes from the host's own file,
# so it is left out of FILES.
compose() {
local fixture="$test_tmp/platforms/$1" config="$test_tmp/buildconfig" conf
local -a conf_files=()
mapfile -d '' conf_files < <(LC_ALL=C.UTF-8 find "$etc/mkinitcpio.conf.d" -maxdepth 1 -xtype f -name '*.conf' -print0 |
sed -z 's/.*\///' | LC_ALL=C.UTF-8 sort -zVu)
cat -- "$etc/mkinitcpio.conf" >"$config"
for conf in "${conf_files[@]}"; do
cat -- "$etc/mkinitcpio.conf.d/$conf" >>"$config"
done
local path="$fixture/bin:$ROOT/bin:$PATH"
[[ $1 != "no-detector" ]] || path="$fixture/bin"
KERNELVERSION=6.99.0-test OMARCHY_PROC_ROOT="$fixture/proc" OMARCHY_PCI_DEVICES_PATH="$devices" PATH="$path" "$BASH" -c '
. "$1" || exit 1
files=()
for file in "${FILES[@]}"; do
[[ $file == /etc/vconsole.conf ]] || files+=("$file")
done
printf "HOOKS=%s\nMODULES=%s\nFILES=%s\n" "${HOOKS[*]}" "${MODULES[*]}" "${files[*]}"
' -- "$config"
}
assert_composed() {
local description="$1" platform="$2" hooks="$3" modules="$4" files="$5"
local expected actual
expected=$(printf 'HOOKS=%s\nMODULES=%s\nFILES=%s\n' "$hooks" "$modules" "$files")
actual=$(compose "$platform") || fail "$description" "the drop-ins do not source cleanly"
[[ $actual == "$expected" ]] ||
fail "$description" "expected:"$'\n'"$expected"$'\n'"actual:"$'\n'"$actual"
pass "$description"
}
assert_hooks() {
local description="$1" platform="$2" expected="$3" actual
actual=$(compose "$platform") || fail "$description" "the drop-ins do not source cleanly"
actual=$(sed -n 's/^HOOKS=//p' <<<"$actual")
[[ $actual == "$expected" ]] ||
fail "$description" "expected: $expected"$'\n'"actual: $actual"
pass "$description"
}
# Each platform starts from its own baseline, whatever mkinitcpio.conf says.
new_etc
assert_hooks "Apple Silicon starts from the systemd baseline" aarch64-apple "$apple_hooks"
assert_hooks "plain aarch64 starts from the Omarchy baseline" aarch64 "$omarchy_hooks"
assert_hooks "x86 starts from the Omarchy baseline" x86 "$omarchy_hooks"
assert_hooks "without the detector the Omarchy baseline stays" no-detector "$omarchy_hooks"
if composed=$(compose contradiction 2>"$test_tmp/contradiction.err"); then
fail "a platform the detector cannot place stops the build" "composed: $composed"
fi
grep -q "contradictory platform identity" "$test_tmp/contradiction.err" ||
fail "a platform the detector cannot place stops the build" "stderr: $(<"$test_tmp/contradiction.err")"
pass "a platform the detector cannot place stops the build"
# A Mac that set up its own initramfs before the Apple boot package keeps it:
# busybox encrypt unlocks through cryptdevice=, which sd-encrypt cannot parse,
# and the asahi hook carries the firmware. The asahi hook marks such a root off
# a Mac too. Busybox encrypt alone means nothing off Apple Silicon.
legacy_encrypted="base udev plymouth autodetect microcode modconf kms keyboard keymap consolefont block encrypt asahi filesystems fsck"
legacy_plain="base udev autodetect microcode modconf kms keyboard keymap consolefont block asahi filesystems fsck"
for legacy in "$legacy_encrypted" "$legacy_plain" "${legacy_encrypted/ asahi / }"; do
new_etc
sed -i "s/^HOOKS=.*/HOOKS=($legacy)/" "$etc/mkinitcpio.conf"
assert_hooks "a Mac keeps its own HOOKS=($legacy)" aarch64-apple "$legacy"
done
new_etc
sed -i "s/^HOOKS=.*/HOOKS=($legacy_plain)/" "$etc/mkinitcpio.conf"
assert_hooks "an asahi root keeps its HOOKS off a Mac" aarch64 "$legacy_plain"
# A legacy GRUB Mac has the asahi hook right after base, where Asahi's images
# put it, and may run a runtime older than the detector.
legacy_grub="base asahi udev plymouth autodetect microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck"
for platform in aarch64-apple no-detector; do
for legacy in "$legacy_grub" "${legacy_grub/ encrypt / }"; do
new_etc
sed -i "s/^HOOKS=.*/HOOKS=($legacy)/" "$etc/mkinitcpio.conf"
assert_hooks "a legacy GRUB Mac keeps HOOKS=($legacy) ($platform)" "$platform" "$legacy"
done
done
new_etc
sed -i "s/^HOOKS=.*/HOOKS=(base udev autodetect modconf block encrypt filesystems fsck)/" "$etc/mkinitcpio.conf"
assert_hooks "busybox encrypt off Apple Silicon still gets the Omarchy baseline" x86 "$omarchy_hooks"
new_etc
sed -i "s/^HOOKS=.*/HOOKS=(base udev autodetect modconf kms keyboard keymap consolefont block filesystems fsck)/" "$etc/mkinitcpio.conf"
assert_hooks "a Mac with a stock busybox line and no asahi hook gets the systemd baseline" aarch64-apple "$apple_hooks"
# A platform fragment sorts after the baseline and before omarchy_hooks.conf,
# as omarchy-mac-boot's 90- drop-ins do. Its hooks must reach the image.
new_etc
drop_in 90-platform-fragment.conf <<'CONF'
_fragment_hooks=()
for _fragment_hook in "${HOOKS[@]}"; do
[[ $_fragment_hook == "filesystems" ]] && _fragment_hooks+=(platform-firmware)
_fragment_hooks+=("$_fragment_hook")
done
HOOKS=("${_fragment_hooks[@]}" platform-late)
unset _fragment_hooks _fragment_hook
CONF
assert_hooks "a platform fragment's hooks survive on Apple Silicon" aarch64-apple \
"base systemd plymouth autodetect microcode modconf kms keyboard sd-vconsole block platform-firmware filesystems fsck platform-late"
assert_hooks "a platform fragment's hooks survive on plain aarch64" aarch64 \
"base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt platform-firmware filesystems fsck btrfs-overlayfs platform-late"
assert_hooks "a platform fragment's hooks survive on x86" x86 \
"base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt platform-firmware filesystems fsck btrfs-overlayfs platform-late"
# The NVIDIA filter still runs after the fragment and removes only kms.
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
pci_devices 0x10de:0x030000
assert_hooks "the NVIDIA filter keeps a platform fragment's hooks" x86 \
"base udev plymouth keyboard autodetect microcode modconf keymap consolefont block encrypt platform-firmware filesystems fsck btrfs-overlayfs platform-late"
# Existing x86 configurations build the image they built before the baseline
# moved: the same HOOKS, MODULES and FILES.
new_etc
assert_composed "a machine without hardware drop-ins is unchanged" x86 \
"$omarchy_hooks" "thunderbolt" ""
new_etc
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
pci_devices 0x10de:0x030000
assert_composed "NVIDIA-only drops only kms" x86 \
"$omarchy_hooks_without_kms" "$nvidia_modules thunderbolt" ""
new_etc
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
pci_devices 0x8086:0x030000 0x10de:0x030200
assert_composed "hybrid graphics keeps kms for the iGPU" x86 \
"$omarchy_hooks" "$nvidia_modules thunderbolt" ""
new_etc
pci_devices 0x10de:0x030000
assert_composed "NVIDIA-only without early nvidia_drm keeps kms" x86 \
"$omarchy_hooks" "thunderbolt" ""
new_etc
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
drop_in omarchy_resume.conf <<<"HOOKS+=(resume)"
drop_in 99-omarchy-provisioning-key.conf <<<"FILES+=(/etc/omarchy/provisioning.key)"
pci_devices 0x10de:0x030000
assert_composed "NVIDIA-only with hibernation and a provisioning key is unchanged" x86 \
"$omarchy_hooks_without_kms resume" "$nvidia_modules thunderbolt" "/etc/omarchy/provisioning.key"
new_etc
drop_in apple-t2.conf <<<"MODULES+=(t2bce_vhci usbhid hid_apple hid_generic xhci_pci xhci_hcd)"
assert_composed "a T2 Mac is unchanged" x86 \
"$omarchy_hooks" "t2bce_vhci usbhid hid_apple hid_generic xhci_pci xhci_hcd thunderbolt" ""
new_etc
drop_in macbook_spi_modules.conf <<<"MODULES=(applespi intel_lpss_pci spi_pxa2xx_platform)"
assert_composed "an SPI keyboard MacBook is unchanged" x86 \
"$omarchy_hooks" "applespi intel_lpss_pci spi_pxa2xx_platform thunderbolt" ""
new_etc
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
drop_in surface_device_modules.conf <<<"MODULES=(pinctrl_tigerlake surface_aggregator surface_aggregator_registry surface_aggregator_hub surface_hid_core surface_hid surface_kbd intel_lpss_pci 8250_dw)"
pci_devices 0x8086:0x030000
assert_composed "a Surface is unchanged" x86 \
"$omarchy_hooks" "pinctrl_tigerlake surface_aggregator surface_aggregator_registry surface_aggregator_hub surface_hid_core surface_hid surface_kbd intel_lpss_pci 8250_dw thunderbolt" ""
# Snapdragon and other aarch64 machines build the same image as x86, whatever
# systemd line their mkinitcpio.conf starts from.
for platform in aarch64; do
new_etc
drop_in omarchy_resume.conf <<<"HOOKS+=(resume)"
drop_in 99-omarchy-provisioning-key.conf <<<"FILES+=(/etc/omarchy/provisioning.key)"
assert_composed "$platform with hibernation and a provisioning key is unchanged" "$platform" \
"$omarchy_hooks resume" "thunderbolt" "/etc/omarchy/provisioning.key"
done
# thunderbolt is early-loaded only where the kernel builds it as a module. A
# systemd initramfs writes MODULES into modules-load.d as they are, so neither
# an optional thunderbolt? nor a module the kernel lacks may reach it.
new_etc
assert_composed "Apple Silicon early-loads the Aurora kernel's thunderbolt module" aarch64-apple \
"$apple_hooks" "thunderbolt" ""
new_etc
KERNEL_MODULES="" assert_composed "a plain aarch64 kernel without thunderbolt leaves it out" aarch64 \
"$omarchy_hooks" "" ""
new_etc
KERNEL_MODULES="" KERNEL_BUILTINS=thunderbolt assert_composed "a kernel with thunderbolt built in leaves it out" x86 \
"$omarchy_hooks" "" ""
+2
View File
@@ -97,6 +97,8 @@ cat >"$fake_bin/omarchy-hyprland-monitor-clamshell" <<'SH'
exit 0
SH
cp "$fake_bin/omarchy-hyprland-monitor-clamshell" "$fake_bin/omarchy-system-lid-inhibit"
chmod +x "$fake_bin"/*
ln -s "$ROOT/bin/omarchy-hyprland-monitor-modeless" "$fake_bin/omarchy-hyprland-monitor-modeless"
+6 -1
View File
@@ -60,7 +60,8 @@ grep -F 'hyprctl monitors all -j' "$ROOT/bin/omarchy-hyprland-monitor-modeless"
pass "modeless helper sees mirrors and ignores monitors disabled on purpose"
grep -F 'omarchy-hw-laptop-closed && omarchy-hw-external-monitors' "$hw_clamshell" >/dev/null
grep -F '/proc/acpi/button/lid/*/state' "$hw_laptop_closed" >/dev/null
grep -F 'org.freedesktop.login1.Manager LidClosed' "$hw_laptop_closed" >/dev/null
grep -F 'OMARCHY_ACPI_LID_PATH:-/proc/acpi/button/lid' "$hw_laptop_closed" >/dev/null
pass "clamshell helper detects closed-lid external monitor state"
# A mirrored external is absent from plain `monitors`, so asking without `all`
@@ -99,6 +100,10 @@ pass "internal mirror helper recovers when no active external display remains"
grep -F 'switch:on:Lid Switch", nil, "omarchy-system-lid-close"' "$utilities" >/dev/null
grep -F 'switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell"' "$utilities" >/dev/null
grep -F 'switch:on:Apple SMC power/lid events", nil, "omarchy-system-lid-close"' "$utilities" >/dev/null ||
fail "closing an Apple Silicon lid runs the same lid-close as any laptop"
grep -F 'switch:off:Apple SMC power/lid events", nil, "omarchy-hyprland-monitor-clamshell"' "$utilities" >/dev/null ||
fail "opening an Apple Silicon lid restores the panel like any laptop"
pass "lid switch bindings lock on close and reconcile clamshell display state"
grep -F 'omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true' "$system_wake" >/dev/null
+17
View File
@@ -259,6 +259,18 @@ assertDeepEqual(
'notifications ignore a left bar for popup placement'
)
// A toast clears the bar as thick as it is on the toast's own screen: a notch
// floor can make a top bar thicker on one screen only. A bar without
// per-screen sizes (a plugin bar) counts its configured size, and a hidden or
// missing bar the default size, as before.
const notchBar = { barHidden: false, barSize: 26, barSizeFor: name => (name === 'eDP-1' ? 40 : 26) }
assertEqual(notifications.barClearance(notchBar, 'eDP-1', 26, 6), 46, 'a toast clears a notch-floored bar on its screen')
assertEqual(notifications.barClearance(notchBar, 'DP-1', 26, 6), 32, 'a toast on another screen clears the configured size')
assertEqual(notifications.barClearance({ barHidden: false, barSize: 30 }, 'eDP-1', 26, 6), 36, 'a bar without per-screen sizes counts its configured size')
assertEqual(notifications.barClearance({ barHidden: true, barSize: 30, barSizeFor: () => 40 }, 'eDP-1', 26, 6), 32, 'a hidden bar counts the default size')
assertEqual(notifications.barClearance(null, 'eDP-1', 28, 6), 34, 'a missing bar counts the default size')
assertEqual(notifications.barClearance({ barHidden: false, barSize: undefined }, 'eDP-1', 26, 6), 6, 'an unreadable size counts as none')
const notification = {
id: 12,
appName: 'Mail',
@@ -595,6 +607,11 @@ assert(!('exec' in legacyRestored), 'a restored legacy popup drops the old exec
assertEqual(notifications.parseExecArgv(legacyRestored.execArgv || ''), null, 'a restored legacy popup has no runnable click action')
const serviceQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/Service.qml'), 'utf8')
assert(
/function barClearanceFor\(screenName\) \{\s*return NotificationLogic\.barClearance\(shell \? shell\.bar : null, screenName, defaultBarSize, Style\.gapsOut\)\s*\}/.test(serviceQml) &&
/NotificationLogic\.popupPlacement\(\s*service\.barPosition, service\.barClearanceFor\(modelData \? modelData\.name : ""\), Style\.gapsOut\)/.test(serviceQml),
'each screen\'s toasts clear the bar as thick as it is on that screen'
)
assert(
/readonly property int historyLimit: 10/.test(serviceQml),
'notifications service keeps the last ten notifications in history'
+32 -3
View File
@@ -7,7 +7,9 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
baseline_conf="$ROOT/etc/mkinitcpio.conf.d/00-omarchy-hooks.conf"
hooks_conf="$ROOT/etc/mkinitcpio.conf.d/omarchy_hooks.conf"
fake_platform "$tmp_dir/generic" x86
# Each argument is a PCI device as "vendor:class", in sysfs's own format.
write_pci_devices() {
@@ -26,8 +28,8 @@ write_pci_devices() {
done
}
# Sources the hook config the way mkinitcpio does — with earlier drop-ins
# already applied — and prints the resulting HOOKS. mkinitcpio does not run
# Sources the hook config the way mkinitcpio does — after the baseline and
# the earlier drop-ins — and prints the resulting HOOKS. mkinitcpio does not run
# under set -u, but the config must survive it, so source under it anyway.
# "unset" leaves MODULES undeclared entirely.
resolved_hooks() {
@@ -37,9 +39,11 @@ resolved_hooks() {
# The vconsole block sources the host's /etc/vconsole.conf, which may set
# only KEYMAP; predefine XKBLAYOUT so its expansion survives set -u and the
# test stays independent of the machine it runs on.
OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices" bash -uc "
OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices" OMARCHY_PROC_ROOT="$tmp_dir/generic/proc" \
PATH="$tmp_dir/generic/bin:$ROOT/bin:$PATH" bash -uc "
FILES=()
XKBLAYOUT=us
source '$baseline_conf'
$modules_decl
source '$hooks_conf'
echo \"\${HOOKS[*]}\"
@@ -101,3 +105,28 @@ write_pci_devices 0x10de:0x030000
mkdir -p "$tmp_dir/devices/0000:01:00.0"
assert_hooks "unreadable device beside an NVIDIA GPU keeps kms" \
"$nvidia_modules" "$with_kms"
mkdir -p "$tmp_dir/conf" "$tmp_dir/bin"
cp "$baseline_conf" "$tmp_dir/conf/00-omarchy-hooks.conf"
cp "$hooks_conf" "$tmp_dir/conf/omarchy_hooks.conf"
printf 'MODULES=(%s)\n' "$nvidia_modules" >"$tmp_dir/conf/nvidia.conf"
printf '#!/bin/bash\nexec "$@"\n' >"$tmp_dir/bin/sudo"
printf '#!/bin/bash\necho rebuild >>"$TEST_REBUILD_LOG"\n' >"$tmp_dir/bin/limine-mkinitcpio"
chmod +x "$tmp_dir/bin/"*
export PATH="$tmp_dir/bin:$tmp_dir/generic/bin:$ROOT/bin:$PATH"
export OMARCHY_PROC_ROOT="$tmp_dir/generic/proc" OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices"
export OMARCHY_MKINITCPIO_HOOKS_CONF="$tmp_dir/conf/omarchy_hooks.conf"
export OMARCHY_MKINITCPIO_NVIDIA_CONF="$tmp_dir/conf/nvidia.conf"
export OMARCHY_KMS_REBUILD_MARKER="$tmp_dir/rebuilt" TEST_REBUILD_LOG="$tmp_dir/rebuild-log"
write_pci_devices 0x1002:0x030000 0x10de:0x030200
bash -euo pipefail "$ROOT/migrations/1786605598.sh" >/dev/null
[[ ! -e $TEST_REBUILD_LOG && ! -e $OMARCHY_KMS_REBUILD_MARKER ]] || fail "the NVIDIA migration skips hybrid graphics with the split hook baseline"
pass "the NVIDIA migration reads the new baseline and skips hybrid graphics"
write_pci_devices 0x10de:0x030000
bash -euo pipefail "$ROOT/migrations/1786605598.sh" >/dev/null
[[ $(<"$TEST_REBUILD_LOG") == "rebuild" && -e $OMARCHY_KMS_REBUILD_MARKER ]] || fail "the NVIDIA migration still rebuilds NVIDIA-only graphics"
bash -euo pipefail "$ROOT/migrations/1786605598.sh" >/dev/null
[[ $(<"$TEST_REBUILD_LOG") == "rebuild" ]] || fail "the NVIDIA migration respects its completed marker"
pass "the NVIDIA migration still rebuilds NVIDIA-only graphics once"
+173
View File
@@ -0,0 +1,173 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command python3
require_command pacman-conf
# Every platform's repositories are a pacman.conf and mirrorlist per channel,
# copied into place whole on a channel change and at install finalization, as
# x86_64's always were.
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
export OMARCHY_PATH="$ROOT"
source "$ROOT/install/helpers/pacman.sh"
platforms="x86 aarch64 aarch64-apple"
# aarch64 has edge alone: stable and rc there would install the release line,
# which has no aarch64 support.
channels_for() {
if [[ $1 == "x86" ]]; then echo "stable rc edge"; else echo "edge"; fi
}
# ── the templates ────────────────────────────────────────────────────────────
[[ $(omarchy_pacman_templates x86) == "$ROOT/default/pacman" ]] || fail "x86 keeps its templates where they were"
[[ $(omarchy_pacman_templates aarch64) == "$ROOT/default/pacman/aarch64" ]] || fail "plain aarch64 uses the aarch64 templates"
[[ $(omarchy_pacman_templates aarch64-apple) == "$ROOT/default/pacman/aarch64-apple" ]] || 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 x86) == stable ]] || fail "x86 defaults to stable"
for platform in aarch64 aarch64-apple; 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"
pass "x86 defaults to stable and every aarch64 platform to edge"
# pacman reads each repository list from the template, with its mirrorlist
# standing in for /etc/pacman.d/mirrorlist.
repos() {
local templates=$1 channel=$2
sed "s|/etc/pacman.d/mirrorlist|$templates/mirrorlist-$channel|" "$templates/pacman-$channel.conf" >"$work/pacman.conf"
pacman-conf --config "$work/pacman.conf" --repo-list | tr '\n' ' '
}
for platform in $platforms; do
templates=$(omarchy_pacman_templates "$platform")
for channel in stable rc; do
[[ $platform == "x86" ]] && continue
[[ ! -e $templates/pacman-$channel.conf && ! -e $templates/mirrorlist-$channel ]] ||
fail "$platform has no $channel template or mirrorlist"
done
for channel in $(channels_for "$platform"); do
[[ -f $templates/pacman-$channel.conf && -f $templates/mirrorlist-$channel ]] ||
fail "$platform has a $channel template and mirrorlist"
list=$(repos "$templates" "$channel") || fail "$platform $channel: pacman reads the template"
case $platform in
x86) expected="omarchy core extra multilib " ;;
aarch64) expected="omarchy core extra alarm aur " ;;
aarch64-apple) expected="omarchy asahi-alarm core extra alarm aur " ;;
esac
[[ $list == "$expected" ]] || fail "$platform $channel: repositories in order" "$list"
# $arch stays literal: pacman fills it in on the machine.
server=$(sed -n '/^\[omarchy\]/,/^\[/s/^Server = //p' "$templates/pacman-$channel.conf")
[[ $server == "https://pkgs.omarchy.org/$channel/\$arch" ]] ||
fail "$platform $channel: Omarchy's $channel repository" "$server"
done
done
pass "x86 has templates for stable, rc and edge and aarch64 for edge alone, each with its repositories in order"
# ── install finalization ─────────────────────────────────────────────────────
finalize_bin=$work/finalize-bin
mkdir -p "$finalize_bin"
printf '#!/bin/bash\necho "$PLATFORM"\n' >"$finalize_bin/omarchy-hw-platform"
for command in omarchy-pkg-add pacman-key; do
printf '#!/bin/bash\nexit 0\n' >"$finalize_bin/$command"
done
chmod +x "$finalize_bin"/*
sed "s|/etc/pacman|$work/etc/pacman|g" "$ROOT/install/post-install/pacman.sh" >"$work/finalize.sh"
mkdir -p "$work/install/hardware"
: >"$work/install/hardware/pacman.sh"
# Finalization asks the image helper whether this is an image build, which root
# answers from the real /var/lib/omarchy; everyone else from the test's root.
if (( EUID == 0 )); then
skip "install finalization copies the platform's channel template and mirrorlist (refuses to run as root)"
else
for platform in $platforms; do
templates=$(omarchy_pacman_templates "$platform")
# An install built for a channel the platform has no templates for, or for
# none, gets the platform's default channel.
for channel in stable rc edge ""; do
expected=$channel
[[ -n $expected && -f $templates/pacman-$expected.conf ]] || expected=$(omarchy_pacman_default_channel "$platform")
rm -rf "$work/etc"
mkdir -p "$work/etc/pacman.d"
printf 'offline\n' | tee "$work/etc/pacman.conf" >"$work/etc/pacman.d/mirrorlist"
OMARCHY_IMAGE_ROOT=$work OMARCHY_MIRROR=$channel PLATFORM=$platform OMARCHY_INSTALL="$work/install" PATH="$finalize_bin:$PATH" \
bash -e -c 'source "$1"' bash "$work/finalize.sh" >/dev/null || fail "$platform finalization on '$channel'"
cmp -s "$work/etc/pacman.conf" "$templates/pacman-$expected.conf" || fail "$platform '$channel': finalization copies the $expected template"
cmp -s "$work/etc/pacman.d/mirrorlist" "$templates/mirrorlist-$expected" || fail "$platform '$channel': finalization copies the $expected mirrorlist"
done
done
pass "install finalization copies the platform's channel template and mirrorlist, or its default channel's when it has none for that channel"
fi
# ── refresh through the command, with every privileged step a stand-in ───────
rm -rf "$work"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-refresh-pacman
refresh() {
"$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" "$@" >"$boundary_tmp/output" 2>&1
}
events() {
grep -vE '^sudo (-h|-k)$' "$SUDO_TEST_LOG" || true
}
for platform in $platforms; do
case $platform in
x86) templates=$SUDO_TEST_ROOT/default/pacman ;;
aarch64) templates=$SUDO_TEST_ROOT/default/pacman/aarch64 ;;
aarch64-apple) templates=$SUDO_TEST_ROOT/default/pacman/aarch64-apple ;;
esac
# No channel named refreshes to the platform's default one.
for channel in $(channels_for "$platform") ""; do
reset_boundary
SUDO_TEST_PLATFORM=$platform refresh $channel || fail "$platform refreshes to '$channel'" "$(cat "$boundary_tmp/output")"
[[ -n $channel ]] || channel=$(omarchy_pacman_default_channel "$platform")
python3 - "$SUDO_TEST_LOG" "$templates" "$channel" <<'PY'
import sys
events = [e for e in open(sys.argv[1]).read().splitlines() if e not in ('sudo -h', 'sudo -k')]
templates, channel = sys.argv[2:]
expected = [
'step:cp -f /etc/pacman.conf /etc/pacman.conf.bak',
'step:cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak',
f'step:cp -f {templates}/pacman-{channel}.conf /etc/pacman.conf',
f'step:cp -f {templates}/mirrorlist-{channel} /etc/pacman.d/mirrorlist',
]
copies = [e for e in events if e.startswith('step:cp ')]
assert copies == expected, events
hook = events.index('step:omarchy-hook pre-refresh-pacman')
transaction = events.index('step:pacman -Syyuu --noconfirm')
assert events.index(expected[-1]) < hook < transaction, events
PY
assert_boundary_cold "$platform $channel"
done
done
pass "a refresh backs up and copies the platform's channel template and mirrorlist, or its default channel's when none is named, then runs the cold hook and the upgrade"
# A channel the platform has no template for (stable or rc on aarch64), or a
# machine whose platform can't be told, stops before anything changes.
for platform in aarch64 aarch64-apple; do
for channel in stable rc; do
reset_boundary
if SUDO_TEST_PLATFORM=$platform refresh "$channel"; then fail "$platform: $channel is refused"; fi
[[ -z $(events) ]] || fail "$platform: $channel stops before anything" "$(events)"
grep -q "Omarchy has no $channel channel for $platform" "$boundary_tmp/output" || fail "the refusal says why" "$(cat "$boundary_tmp/output")"
assert_boundary_cold "$platform $channel"
done
done
cat >"$SUDO_TEST_ROOT/bin/omarchy-hw-platform" <<'STUB'
#!/bin/bash
exit 1
STUB
reset_boundary
if refresh stable; then fail "an unknown platform is refused"; fi
[[ -z $(events) ]] || fail "an unknown platform stops before anything" "$(events)"
assert_boundary_cold "unknown platform"
pass "a channel without a template, or a machine whose platform can't be told, changes nothing"
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Browser and Steam installs, and Steam's removal, call the platform's own
# hooks through the real dispatcher: a Mac fixture with omarchy-mac's
# entrypoints, and x86.
require_platform_fixtures "app-install hooks on platform fixtures"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
fake_platform "$tmp/apple" aarch64-apple
fake_platform "$tmp/x86" x86
# The hooks record what they were called with and which flags files the
# browser install had written by then.
lifecycle=$tmp/lifecycle
mkdir -p "$lifecycle/usr/lib/omarchy/mac"
for operation in post-install pre-remove; do
cat >"$lifecycle/usr/lib/omarchy/mac/$operation" <<SH
#!/bin/bash
printf '%s %s\n' "$operation" "\$*" >>"$tmp/calls"
ls "\$HOME/.config" 2>/dev/null | grep -- '-flags.conf$' >"$tmp/flags-at-hook" || true
[[ ! -e $tmp/fail ]]
SH
chmod 755 "$lifecycle/usr/lib/omarchy/mac/$operation"
done
chmod -R go-w "$lifecycle"
stub_bin=$tmp/bin
omarchy_path=$tmp/omarchy
mkdir -p "$stub_bin" "$omarchy_path/install/helpers" "$omarchy_path/config"
printf 'browser_policy_setup_dir() { :; }\nbrowser_policy_setup_firefox_distribution() { :; }\nas_root() { "$@"; }\n' \
>"$omarchy_path/install/helpers/browser-policy.sh"
cp "$ROOT/config/chromium-flags.conf" "$omarchy_path/config/"
for command in omarchy-pkg-add omarchy-pkg-aur-add omarchy-pkg-drop omarchy-install-gaming-gpu-lib32; do
printf '#!/bin/bash\nprintf "%%s %%s\\n" "%s" "$*" >>"%s"\n' "$command" "$tmp/calls" >"$stub_bin/$command"
done
for command in omarchy-install-chromium-copy-url omarchy-install-chromium-ytdlp omarchy-theme-set-browser setsid; do
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/$command"
done
chmod +x "$stub_bin"/*
run() {
local platform=$1
shift
rm -f "$tmp/calls" "$tmp/flags-at-hook"
rm -rf "$tmp/home"
mkdir -p "$tmp/home"
HOME="$tmp/home" OMARCHY_PATH="$omarchy_path" OMARCHY_OPT_PATH="$tmp/opt" \
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$lifecycle" \
PATH="$stub_bin:$tmp/$platform/bin:$ROOT/bin:$PATH" bash "$@" >"$tmp/output" 2>&1
}
# ── browsers ─────────────────────────────────────────────────────────────────
declare -A flags=(
[chromium]=chromium-flags.conf [chrome]=chrome-flags.conf [edge]=microsoft-edge-stable-flags.conf
[brave]=brave-flags.conf [brave-origin]=brave-origin-flags.conf [firefox]="" [zen]=""
)
for browser in chromium chrome edge brave brave-origin firefox zen; do
run apple "$ROOT/bin/omarchy-install-browser" "$browser" || fail "apple: $browser installs" "$(cat "$tmp/output")"
[[ $(grep -c '^post-install ' "$tmp/calls") == 1 ]] && grep -qx "post-install $browser" "$tmp/calls" ||
fail "apple: a $browser install runs post-install $browser once" "$(cat "$tmp/calls")"
[[ $(cat "$tmp/flags-at-hook") == "${flags[$browser]}" ]] ||
fail "apple: the $browser hook runs once its flags file is written" "$(cat "$tmp/flags-at-hook")"
[[ $(grep -n . "$tmp/calls" | grep -m1 'post-install' | cut -d: -f1) -gt 1 ]] ||
fail "apple: the $browser hook runs after its package step" "$(cat "$tmp/calls")"
done
pass "apple: every browser install runs post-install <browser> after its package and flags"
touch "$tmp/fail"
for browser in chromium brave; do
if run apple "$ROOT/bin/omarchy-install-browser" "$browser"; then
fail "apple: a failed $browser hook fails the install"
fi
! grep -q 'browser installed' "$tmp/output" || fail "apple: a failed $browser hook is not announced as installed" "$(cat "$tmp/output")"
done
pass "apple: a failed browser hook fails the install"
for browser in chromium brave-origin zen; do
run x86 "$ROOT/bin/omarchy-install-browser" "$browser" || fail "x86: $browser installs" "$(cat "$tmp/output")"
! grep -q '^post-install' "$tmp/calls" || fail "x86: a $browser install runs no hook" "$(cat "$tmp/calls")"
done
rm -f "$tmp/fail"
pass "x86: browser installs run no hook, even with Mac entrypoints on disk"
# ── Steam ────────────────────────────────────────────────────────────────────
run apple "$ROOT/bin/omarchy-install-gaming-steam" || fail "apple: Steam installs" "$(cat "$tmp/output")"
[[ $(cat "$tmp/calls") == $'omarchy-install-gaming-gpu-lib32 \nomarchy-pkg-add steam\npost-install steam' ]] ||
fail "apple: Steam's hook runs after the 32-bit drivers and the package" "$(cat "$tmp/calls")"
run apple "$ROOT/bin/omarchy-remove-gaming-steam" || fail "apple: Steam is removed" "$(cat "$tmp/output")"
[[ $(head -n 2 "$tmp/calls") == $'pre-remove steam\nomarchy-pkg-drop steam' ]] ||
fail "apple: Steam's pre-remove hook runs before the package goes" "$(cat "$tmp/calls")"
touch "$tmp/fail"
if run apple "$ROOT/bin/omarchy-install-gaming-steam"; then
fail "apple: a failed Steam hook fails the install"
fi
if run apple "$ROOT/bin/omarchy-remove-gaming-steam"; then
fail "apple: a failed pre-remove hook stops the removal"
fi
! grep -q '^omarchy-pkg-drop' "$tmp/calls" || fail "apple: a failed pre-remove hook removes nothing" "$(cat "$tmp/calls")"
pass "apple: Steam's install and removal run their hooks in order, and fail with them"
run x86 "$ROOT/bin/omarchy-install-gaming-steam" || fail "x86: Steam installs" "$(cat "$tmp/output")"
[[ $(cat "$tmp/calls") == $'omarchy-install-gaming-gpu-lib32 \nomarchy-pkg-add steam' ]] ||
fail "x86: Steam installs as before" "$(cat "$tmp/calls")"
run x86 "$ROOT/bin/omarchy-remove-gaming-steam" || fail "x86: Steam is removed" "$(cat "$tmp/output")"
[[ $(cat "$tmp/calls") == "omarchy-pkg-drop steam" ]] || fail "x86: Steam is removed as before" "$(cat "$tmp/calls")"
rm -f "$tmp/fail"
pass "x86: Steam installs and is removed as before, running no hook"
# ── 32-bit drivers ───────────────────────────────────────────────────────────
# Without an Intel, AMD or NVIDIA GPU there is nothing to add, which must not
# fail the Steam install that asked.
gpu_bin=$tmp/gpu-bin
mkdir -p "$gpu_bin"
printf '#!/bin/bash\necho "00:02.0 VGA compatible controller: %s"\n' '${GPU:-Apple Inc. AGX}' >"$gpu_bin/lspci"
printf '#!/bin/bash\nexit 1\n' >"$gpu_bin/omarchy-hw-nvidia-gsp"
printf '#!/bin/bash\nexit 1\n' >"$gpu_bin/omarchy-hw-nvidia-without-gsp"
printf '#!/bin/bash\necho "omarchy-pkg-add $*" >>"%s"\n' "$tmp/calls" >"$gpu_bin/omarchy-pkg-add"
chmod +x "$gpu_bin"/*
rm -f "$tmp/calls"
PATH="$gpu_bin:$PATH" bash "$ROOT/bin/omarchy-install-gaming-gpu-lib32" >"$tmp/output" 2>&1 ||
fail "no supported GPU is not a failure" "$(cat "$tmp/output")"
[[ ! -e $tmp/calls ]] || fail "no supported GPU installs nothing" "$(cat "$tmp/calls")"
GPU="Advanced Micro Devices, Inc. [AMD/ATI] Navi 31" PATH="$gpu_bin:$PATH" bash "$ROOT/bin/omarchy-install-gaming-gpu-lib32" >/dev/null 2>&1 ||
fail "an AMD GPU installs its 32-bit driver"
[[ $(cat "$tmp/calls") == "omarchy-pkg-add lib32-vulkan-radeon" ]] || fail "an AMD GPU gets lib32-vulkan-radeon" "$(cat "$tmp/calls")"
pass "32-bit drivers: a machine with no supported GPU adds nothing without failing"
+160
View File
@@ -0,0 +1,160 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_platform_fixtures "platform package lists"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
export OMARCHY_PATH="$ROOT"
export PATH="$ROOT/bin:$PATH"
names() {
sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' "$@"
}
base=$(names "$ROOT/install/omarchy-base.packages")
aarch64=$(names "$ROOT/install/omarchy-aarch64.packages")
for platform in aarch64-apple aarch64 x86; 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")" ]] ||
fail "$platform: the detected platform selects its own list"
printf '%s\n' "$defaults" >"$work/$platform.packages"
[[ -z $(sort "$work/$platform.packages" | uniq -d) ]] || fail "$platform: each package is listed once"
# aarch64 platforms leave out the base packages only x86_64 builds.
expected_base=$base
[[ $platform == "x86" ]] || expected_base=$(grep -vxF -f <(names "$ROOT/install/omarchy-x86_64-only.packages") <<<"$base")
[[ $(head -n "$(wc -l <<<"$expected_base")" "$work/$platform.packages") == "$expected_base" ]] ||
fail "$platform: the base list comes first, unchanged"
case $platform in
x86)
[[ $defaults == "$base" ]] || fail "x86_64 installs exactly the base list"
[[ $defaults == "$(grep -v '^#' "$ROOT/install/omarchy-base.packages" | grep -v '^$')" ]] ||
fail "x86_64 installs what omarchy-reinstall-pkgs read from the base list before"
;;
*)
while IFS= read -r package; do
grep -Fxq "$package" "$work/$platform.packages" || fail "$platform: aarch64 addition $package"
done <<<"$aarch64"
;;
esac
# 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
if [[ $platform == "aarch64-apple" ]]; then
grep -Fxq "$package" "$work/$platform.packages" || fail "Apple Silicon adds $package"
else
! grep -Fxq "$package" "$work/$platform.packages" || fail "$platform: no $package"
fi
done
done
pass "each platform composes the base, architecture and platform lists"
# A hardware family's own list joins only that family's set, after the base and
# architecture lists; plain aarch64 composes the others alone.
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-aarch64-apple.packages"
for platform in aarch64-apple aarch64 x86; do
defaults=$(OMARCHY_PATH="$tree" omarchy-pkg-defaults "$platform")
if [[ $platform == "aarch64-apple" ]]; then
[[ $(tail -n 1 <<<"$defaults") == "example-board-support" ]] || fail "a platform list is added after the others" "$defaults"
(( $(grep -cx zram-generator <<<"$defaults") == 1 )) || fail "a name in two lists is installed once" "$defaults"
else
! grep -Fxq example-board-support <<<"$defaults" || fail "$platform: another platform's list stays out"
fi
done
pass "a platform list joins only its own platform's set"
# Image builders written before the platform names settled pass the old names.
for legacy in apple-silicon:aarch64-apple generic-aarch64:aarch64 generic:x86; do
[[ $(omarchy-pkg-defaults "${legacy%%:*}") == "$(omarchy-pkg-defaults "${legacy#*:}")" ]] ||
fail "the old name ${legacy%%:*} composes the ${legacy#*:} set"
done
pass "the old platform names compose the same sets"
# Base packages that only x86_64 builds stay out of every aarch64 set.
for platform in aarch64-apple aarch64 x86; do
defaults=$(OMARCHY_PATH="$ROOT" omarchy-pkg-defaults "$platform")
while read -r name; do
[[ -n $name && $name != \#* ]] || continue
if [[ $platform == "x86" ]]; then
grep -Fxq "$name" <<<"$defaults" || fail "x86_64 keeps $name"
else
! grep -Fxq "$name" <<<"$defaults" || fail "$platform leaves out the x86_64-only $name"
fi
done <"$ROOT/install/omarchy-x86_64-only.packages"
done
grep -Fxq superwhisper-bin "$ROOT/install/omarchy-x86_64-only.packages" || fail "superwhisper-bin is listed as x86_64-only"
pass "base packages only x86_64 builds stay out of every aarch64 set"
! omarchy-pkg-defaults riscv 2>/dev/null || fail "an unknown platform is refused"
failing_bin="$work/failing-bin"
mkdir -p "$failing_bin"
printf '#!/bin/bash\nexit 1\n' >"$failing_bin/omarchy-hw-platform"
chmod +x "$failing_bin/omarchy-hw-platform"
! PATH="$failing_bin:$ROOT/bin:$PATH" omarchy-pkg-defaults >/dev/null 2>&1 || fail "a platform the detector cannot tell is refused"
pass "an unknown or undetectable platform is refused"
# omarchy-reinstall-pkgs installs the detected platform's set.
mkdir -p "$work/stubs"
cat >"$work/stubs/omarchy-update-pacman" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$STUB_LOG"
SH
cat >"$work/stubs/omarchy-refresh-pacman" <<'SH'
#!/bin/bash
printf 'refresh %s\n' "$*" >>"$STUB_LOG"
[[ -z ${REFRESH_FAILS:-} ]]
SH
chmod +x "$work/stubs/"*
for platform in aarch64 x86; do
export STUB_LOG="$work/$platform.log"
: >"$STUB_LOG"
OMARCHY_PROC_ROOT="$work/$platform/proc" PATH="$work/stubs:$work/$platform/bin:$ROOT/bin:$PATH" \
omarchy-reinstall-pkgs
expected="-Syu --noconfirm --needed $(tr '\n' ' ' <"$work/$platform.packages")"
[[ $(tail -n 1 "$STUB_LOG") == "${expected% }" ]] ||
fail "$platform: reinstall installs the platform's default set" "$(tail -n 1 "$STUB_LOG")"
done
pass "omarchy-reinstall-pkgs installs the platform's default set"
# Every platform resets to its stable templates, as x86_64 always has; a failed
# refresh stops the reinstall.
reinstall_on() {
local platform=$1
export STUB_LOG="$work/channel.log"
: >"$STUB_LOG"
OMARCHY_PROC_ROOT="$work/$platform/proc" PATH="$work/stubs:$work/$platform/bin:$ROOT/bin:$PATH" \
omarchy-reinstall-pkgs >"$work/channel.out" 2>&1
}
for platform in x86 aarch64 aarch64-apple; do
reinstall_on "$platform" || fail "$platform: reinstall completes" "$(cat "$work/channel.out")"
[[ $(head -n 1 "$STUB_LOG") == "refresh " ]] || fail "$platform: reinstall refreshes on stable" "$(cat "$STUB_LOG")"
expected="-Syu --noconfirm --needed $(tr '\n' ' ' <"$work/$platform.packages")"
[[ $(tail -n 1 "$STUB_LOG") == "${expected% }" ]] || fail "$platform: reinstall installs the platform's set"
done
pass "omarchy-reinstall-pkgs refreshes on stable on every platform"
if REFRESH_FAILS=1 reinstall_on aarch64; then
fail "reinstall stops when the refresh fails"
fi
[[ $(cat "$STUB_LOG") == "refresh " ]] || fail "reinstall runs nothing after a failed refresh" "$(cat "$STUB_LOG")"
pass "omarchy-reinstall-pkgs stops when the refresh fails"
export STUB_LOG="$work/undetected.log"
: >"$STUB_LOG"
if PATH="$failing_bin:$work/stubs:$ROOT/bin:$PATH" omarchy-reinstall-pkgs >/dev/null 2>&1; then
fail "reinstall stops when the platform cannot be told"
fi
! grep -q -- '--needed' "$STUB_LOG" || fail "reinstall installs nothing when the platform cannot be told" "$(cat "$STUB_LOG")"
pass "omarchy-reinstall-pkgs stops when the platform cannot be told"
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# The two setup leaves and first run call the platform's own setup through the
# real dispatcher: a Mac fixture with omarchy-mac's entrypoints, and x86.
hardware_leaf="$ROOT/install/hardware/platform-setup.sh"
user_leaf="$ROOT/install/user/platform-setup.sh"
[[ $(grep '^run_logged' "$ROOT/install/hardware/all.sh" | tail -n 1) == 'run_logged "$OMARCHY_INSTALL/hardware/platform-setup.sh"' ]] ||
fail "the platform's system setup is the last hardware leaf"
[[ $(grep '^run_logged' "$ROOT/install/user/all.sh" | tail -n 1) == 'run_logged "$OMARCHY_INSTALL/user/platform-setup.sh"' ]] ||
fail "the platform's user setup is the last user leaf"
pass "the platform's setup is the last leaf of hardware and of user setup"
require_platform_fixtures "platform setup on platform fixtures"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
fake_platform "$tmp/apple" aarch64-apple
fake_platform "$tmp/x86" x86
lifecycle=$tmp/lifecycle
mkdir -p "$lifecycle/usr/lib/omarchy/mac" "$lifecycle/usr/lib/omarchy/mac-boot"
for entry in mac/setup-system mac/setup-user mac-boot/setup-boot; do
operation=${entry#*/}
cat >"$lifecycle/usr/lib/omarchy/$entry" <<SH
#!/bin/bash
{ printf '%s' "$operation"; (( \$# == 0 )) || printf ' %s' "\$@"; echo; } >>"$tmp/ran"
[[ ! -e $tmp/fail && ! -e $tmp/fail-$operation ]]
SH
chmod 755 "$lifecycle/usr/lib/omarchy/$entry"
done
chmod -R go-w "$lifecycle"
# Sources a leaf as run_logged does, on a platform fixture.
leaf() {
local platform=$1 script=$2
shift 2
rm -f "$tmp/ran"
env "$@" OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$lifecycle" \
PATH="$tmp/$platform/bin:$ROOT/bin:$PATH" bash -eE -c 'source "$1"' bash "$script" >"$tmp/output" 2>&1
}
# ── system setup ─────────────────────────────────────────────────────────────
leaf apple "$hardware_leaf" || fail "apple: the system setup leaf runs" "$(cat "$tmp/output")"
[[ $(cat "$tmp/ran") == $'setup-boot\nsetup-system' ]] || fail "apple: an install or rerun runs setup-boot, then setup-system, with no argument" "$(cat "$tmp/ran")"
leaf apple "$hardware_leaf" OMARCHY_IMAGE_DEFERRED_HARDWARE=1 || fail "apple: the system setup leaf runs on an image's first boot"
[[ $(cat "$tmp/ran") == $'setup-boot image-first-boot\nsetup-system image-first-boot' ]] ||
fail "apple: an image's first boot tells setup-boot and setup-system so" "$(cat "$tmp/ran")"
touch "$tmp/fail-setup-system"
if leaf apple "$hardware_leaf"; then
fail "apple: a failed setup-system fails the leaf"
fi
[[ $(cat "$tmp/ran") == $'setup-boot\nsetup-system' ]] || fail "apple: setup-system ran after the boot setup and failed" "$(cat "$tmp/ran")"
rm -f "$tmp/fail-setup-system"
touch "$tmp/fail-setup-boot"
if leaf apple "$hardware_leaf"; then
fail "apple: a failed setup-boot fails the leaf"
fi
[[ $(cat "$tmp/ran") == "setup-boot" ]] || fail "apple: setup-system waits for a boot setup that failed" "$(cat "$tmp/ran")"
rm -f "$tmp/fail-setup-boot"
mv "$lifecycle/usr/lib/omarchy/mac-boot/setup-boot" "$tmp/setup-boot.saved"
if leaf apple "$hardware_leaf"; then
fail "apple: a Mac whose boot package lacks setup-boot fails the leaf instead of skipping its boot setup"
fi
grep -q "setup-boot on aarch64-apple needs omarchy-mac-boot" "$tmp/output" || fail "apple: the failure names the boot package" "$(cat "$tmp/output")"
mv "$tmp/setup-boot.saved" "$lifecycle/usr/lib/omarchy/mac-boot/setup-boot"
pass "apple: the system setup leaf runs omarchy-mac-boot's setup-boot, then omarchy-mac's setup-system, says when it is an image's first boot, and fails with either"
touch "$tmp/fail"
leaf x86 "$hardware_leaf" OMARCHY_IMAGE_DEFERRED_HARDWARE=1 || fail "x86: the system setup leaf is a no-op" "$(cat "$tmp/output")"
[[ ! -e $tmp/ran && ! -s $tmp/output ]] || fail "x86: the system setup leaf runs nothing and says nothing" "$(cat "$tmp/output")"
rm -f "$tmp/fail"
pass "x86: the system setup leaf is a no-op, even with Mac entrypoints on disk"
# ── user setup ───────────────────────────────────────────────────────────────
leaf apple "$user_leaf" OMARCHY_IMAGE_ROOT="$tmp/booted" || fail "apple: the user setup leaf runs" "$(cat "$tmp/output")"
[[ $(cat "$tmp/ran") == "setup-user" ]] || fail "apple: the user setup leaf runs setup-user" "$(cat "$tmp/ran" 2>/dev/null)"
touch "$tmp/fail"
if leaf apple "$user_leaf" OMARCHY_IMAGE_ROOT="$tmp/booted"; then
fail "apple: a failed setup-user fails finalization's leaf"
fi
rm -f "$tmp/fail"
mkdir -p "$tmp/image/var/lib/omarchy/image"
: >"$tmp/image/var/lib/omarchy/image/target"
leaf apple "$user_leaf" OMARCHY_IMAGE_ROOT="$tmp/image" || fail "apple: the user setup leaf succeeds in an image build"
[[ ! -e $tmp/ran && $(cat "$tmp/output") == *"waits for first run on the machine"* ]] ||
fail "apple: an image build leaves the platform's user setup to first run" "$(cat "$tmp/output")"
pass "apple: the user setup leaf runs omarchy-mac's setup-user, fails with it, and waits for the machine in an image build"
touch "$tmp/fail"
leaf x86 "$user_leaf" OMARCHY_IMAGE_ROOT="$tmp/booted" || fail "x86: the user setup leaf is a no-op" "$(cat "$tmp/output")"
[[ ! -e $tmp/ran && ! -s $tmp/output ]] || fail "x86: the user setup leaf runs nothing and says nothing" "$(cat "$tmp/output")"
rm -f "$tmp/fail"
pass "x86: the user setup leaf is a no-op, even with Mac entrypoints on disk"
# ── first run ────────────────────────────────────────────────────────────────
# First run on the machine runs setup-user once the session is up. Its failure
# leaves first run pending for the next login, as a failed finalization does.
first_bin=$tmp/first-bin
first_root=$tmp/omarchy
mkdir -p "$first_bin" "$first_root/install/user/first-run" "$tmp/home"
cat >"$first_bin/omarchy-done" <<'SH'
#!/bin/bash
case "$1:$2" in
check:first-run-user) exit 1 ;;
mark:first-run-user) touch "$FIRST_RUN_MARKER" ;;
esac
SH
for helper in omarchy-provision-user omarchy-hook-install omarchy-notification-wait; do
printf '#!/bin/bash\nexit 0\n' >"$first_bin/$helper"
done
chmod +x "$first_bin"/*
for script in welcome.sh wifi.sh enable-user-units.sh gnome-theme.sh gtk-primary-paste.sh audio-tuning.sh; do
printf 'exit 0\n' >"$first_root/install/user/first-run/$script"
done
first_run() {
local platform=$1
rm -f "$tmp/ran" "$tmp/first-run-marker" "$tmp/home/.local/state/omarchy/first-run.log"
HOME="$tmp/home" OMARCHY_PATH="$first_root" FIRST_RUN_MARKER="$tmp/first-run-marker" \
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$lifecycle" \
PATH="$first_bin:$tmp/$platform/bin:$ROOT/bin:$PATH" bash "$ROOT/bin/omarchy-provision-first-run" >"$tmp/output" 2>&1
}
first_run apple || fail "apple: first run finishes" "$(cat "$tmp/output")"
[[ $(cat "$tmp/ran") == "setup-user" && -e $tmp/first-run-marker ]] ||
fail "apple: first run runs setup-user and completes" "$(cat "$tmp/ran" 2>/dev/null)"
touch "$tmp/fail"
first_run apple || fail "apple: first run with a failing setup-user still runs its other steps"
[[ ! -e $tmp/first-run-marker ]] || fail "apple: a failed setup-user keeps first run pending"
grep -q "Failed: run the platform's user setup" "$tmp/home/.local/state/omarchy/first-run.log" ||
fail "apple: first run logs the failed platform setup" "$(cat "$tmp/home/.local/state/omarchy/first-run.log")"
first_run x86 || fail "x86: first run finishes"
[[ ! -e $tmp/ran && -e $tmp/first-run-marker ]] || fail "x86: first run runs no platform setup and completes"
rm -f "$tmp/fail"
pass "first run runs the platform's user setup, and a failure keeps first run pending"
@@ -0,0 +1,313 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# First-boot setup's keyboard step. The owner types the disk password at boot in
# the layout /etc/vconsole.conf names, so a layout that did not load, did not
# persist, or does not read back must be asked for again before the password
# form. keyboard_form and apply_keyboard run as omarchy-provision-owner defines
# them, against a fixture root, with loadkeys, localectl and systemd-firstboot
# faked.
tmp=$(cd -- "$(mktemp -d)" && pwd -P)
trap 'rm -rf "$tmp"' EXIT
root=$tmp/root
stub_bin=$tmp/bin
conf=$root/etc/vconsole.conf
mkdir -p "$stub_bin"
# Each fake takes its next behaviour from a queue file, and behaves normally
# once the queue is empty.
cat >"$stub_bin/next-mode" <<'SH'
#!/bin/bash
queue=$1
[[ -s $queue ]] || { echo normal; exit 0; }
head -n1 "$queue"
sed -i 1d "$queue"
SH
cat >"$stub_bin/tty" <<SH
#!/bin/bash
[[ -e "$tmp/no-tty" ]] && { echo "not a tty"; exit 1; }
echo /dev/tty1
SH
cat >"$stub_bin/loadkeys" <<SH
#!/bin/bash
echo "loadkeys \$*" >>"$tmp/calls"
[[ \$("$stub_bin/next-mode" "$tmp/loadkeys-modes") == normal ]] || { echo "cannot open file \$1" >&2; exit 1; }
SH
cat >"$stub_bin/localectl" <<SH
#!/bin/bash
args=("\$@")
[[ \${args[0]} == --no-pager ]] && args=("\${args[@]:1}")
case \${args[0]} in
list-keymaps)
[[ \$("$stub_bin/next-mode" "$tmp/list-modes") == normal ]] || { echo "Failed to connect to bus" >&2; exit 1; }
printf '%s\n' bg-cp1251 colemak cz de-latin1 dk fr pl ua us
;;
set-keymap)
echo "localectl set-keymap \${args[1]}" >>"$tmp/calls"
[[ \$("$stub_bin/next-mode" "$tmp/localectl-modes") == normal ]] || exit 1
printf 'KEYMAP=%s\nXKBLAYOUT=%s\n' "\${args[1]}" "\${args[1]}" >"$conf"
;;
*) exit 1 ;;
esac
SH
cat >"$stub_bin/systemd-firstboot" <<SH
#!/bin/bash
keymap=\${1#--keymap=}
echo "systemd-firstboot \$*" >>"$tmp/calls"
case \$("$stub_bin/next-mode" "$tmp/firstboot-modes") in
normal) printf 'KEYMAP=%s\nXKBLAYOUT=%s\nXKBMODEL=pc105\n' "\$keymap" "\$keymap" >"$conf" ;;
nothing) ;;
mismatch) printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$conf" ;;
no-xkb) printf 'KEYMAP=%s\n' "\$keymap" >"$conf" ;;
fail) echo "Failed to write vconsole.conf" >&2; exit 1 ;;
esac
SH
chmod +x "$stub_bin"/*
export PATH="$stub_bin:$PATH"
sed -n '/^keyboard_form() {/,/^}/p; /^keyboard_xkb_settings() {/,/^}/p; /^apply_keyboard() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" |
sed "s|/etc/|$root/etc/|g" >"$tmp/keyboard.sh"
grep -q '^keyboard_form() {' "$tmp/keyboard.sh" && grep -q '^apply_keyboard() {' "$tmp/keyboard.sh" ||
fail "omarchy-provision-owner defines the keyboard step"
# The owner's answers, one "label|keymap" per prompt. Running out means the step
# asked once more than the test answered.
cat >"$tmp/form.sh" <<SH
set -euo pipefail
source "$tmp/keyboard.sh"
LOG_FILE=$tmp/log
OMARCHY_FORM_BACK=2
step() { echo "prompt" >>"$tmp/screen"; }
notice() { echo "notice: \$1" >>"$tmp/screen"; }
log_step() { echo "\$1" >>"\$LOG_FILE"; }
confirm_reboot() { exit 5; }
omarchy_prompt_keyboard() {
local answer
answer=\$("$stub_bin/next-mode" "$tmp/answers")
[[ \$answer != normal ]] || { echo "asked again" >>"$tmp/screen"; exit 9; }
keyboard_label=\${answer%%|*}
keyboard=\${answer#*|}
}
keyboard_form
echo "password form" >>"$tmp/screen"
SH
# A fresh deferred install: the placeholder US layout from the image build.
fresh_root() {
rm -rf "$root" "$tmp"/*-modes "$tmp/answers" "$tmp/calls" "$tmp/screen" "$tmp/log" "$tmp/no-tty"
mkdir -p "$root/etc"
printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$conf"
: >"$tmp/calls"
: >"$tmp/log"
}
answers() {
printf '%s\n' "$@" >"$tmp/answers"
}
form() {
local status=0
bash "$tmp/form.sh" || status=$?
return "$status"
}
prompts() {
grep -cx prompt "$tmp/screen" || true
}
assert_layout() {
grep -qx "KEYMAP=$1" "$conf" && grep -q '^XKBLAYOUT=.' "$conf" || fail "$2: vconsole.conf holds $1" "$(cat "$conf")"
}
# Asked once more after the failure, with the reason on the screen and in the
# log, and only the second answer reaches the password form.
assert_asked_again() {
local context=$1 log_line=$2
[[ $(prompts) == 2 ]] || fail "$context: the keyboard is asked for again" "$(cat "$tmp/screen")"
[[ $(sed -n 2p "$tmp/screen") == "notice: Could not set the Danish keyboard layout. Choose it again." ]] ||
fail "$context: the owner is told the layout did not take" "$(cat "$tmp/screen")"
[[ $(tail -n1 "$tmp/screen") == "password form" && $(grep -c 'password form' "$tmp/screen") == 1 ]] ||
fail "$context: the password form comes only after a layout took" "$(cat "$tmp/screen")"
grep -q "$log_line" "$tmp/log" || fail "$context: the log records the failure" "$(cat "$tmp/log")"
}
fresh_root
answers 'Danish|dk'
form || fail "a known layout is set" "$(cat "$tmp/screen" "$tmp/log")"
[[ $(prompts) == 1 ]] && ! grep -q '^notice' "$tmp/screen" || fail "a layout that takes is asked for once" "$(cat "$tmp/screen")"
grep -qx 'loadkeys dk' "$tmp/calls" && grep -qx 'systemd-firstboot --keymap=dk --force' "$tmp/calls" ||
fail "the layout is loaded on the console and persisted" "$(cat "$tmp/calls")"
assert_layout dk "a known layout"
pass "a layout that loads, persists and reads back goes straight to the password form"
# systemd-firstboot writes only KEYMAP= for a keymap kbd-model-map lacks, as it
# does for Polish, Ukrainian and Colemak. Setup supplies the XKB layout.
fresh_root
answers 'Polish|pl'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "a keymap firstboot has no XKB layout for is set" "$(cat "$tmp/screen" "$tmp/log")"
[[ $(prompts) == 1 ]] && ! grep -q '^notice' "$tmp/screen" || fail "Polish is asked for once" "$(cat "$tmp/screen")"
grep -qx 'XKBLAYOUT=pl' "$conf" || fail "Polish gets the pl XKB layout" "$(cat "$conf")"
fresh_root
answers 'Ukrainian|ua'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "Ukrainian is set" "$(cat "$tmp/screen" "$tmp/log")"
grep -qx 'KEYMAP=ua' "$conf" && grep -qx 'XKBLAYOUT=ua,us' "$conf" &&
grep -qx 'XKBOPTIONS=terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll' "$conf" ||
fail "a non-Latin layout keeps a US layout to switch to" "$(cat "$conf")"
fresh_root
answers 'English (US, Colemak)|colemak'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "Colemak is set" "$(cat "$tmp/screen" "$tmp/log")"
grep -qx 'XKBLAYOUT=us' "$conf" && grep -qx 'XKBVARIANT=colemak' "$conf" ||
fail "Colemak is the us layout's colemak variant" "$(cat "$conf")"
# The desktop must type what the console does, or the shared password differs:
# kbd's cz is QWERTY and bg-cp1251 phonetic, unlike XKB's defaults.
fresh_root
answers 'Czech|cz'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "Czech is set" "$(cat "$tmp/screen" "$tmp/log")"
grep -qx 'XKBLAYOUT=cz,us' "$conf" && grep -qx 'XKBVARIANT=qwerty,' "$conf" ||
fail "Czech is QWERTY on the desktop as on the console" "$(cat "$conf")"
fresh_root
answers 'Bulgarian|bg-cp1251'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "Bulgarian is set" "$(cat "$tmp/screen" "$tmp/log")"
grep -qx 'XKBLAYOUT=bg,us' "$conf" && grep -qx 'XKBVARIANT=phonetic,' "$conf" ||
fail "Bulgarian is phonetic on the desktop as on the console" "$(cat "$conf")"
pass "a keymap systemd-firstboot has no XKB layout for gets one from setup"
# Every layout the form offers must end with an XKB layout: kbd-model-map's, or
# setup's own. A keymap in neither could never finish the keyboard step.
model_map=/usr/share/systemd/kbd-model-map
if [[ -r $model_map ]]; then
source "$ROOT/install/provisioning/setup-form.sh"
source "$tmp/keyboard.sh"
while IFS='|' read -r label keymap; do
awk -v k="$keymap" '$1 == k { found = 1 } END { exit !found }' "$model_map" ||
{ declare -F keyboard_xkb_settings >/dev/null && keyboard_xkb_settings "$keymap" >/dev/null; } ||
fail "the $label layout ($keymap) gets an XKB layout"
done <<<"$OMARCHY_KEYBOARD_LAYOUTS"
pass "every layout the form offers gets an XKB layout"
else
echo "ok - # SKIP no $model_map to check the form's layouts against"
fi
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo fail >"$tmp/loadkeys-modes"
form || fail "loadkeys failing once, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "loadkeys fails" "loadkeys could not load keymap dk"
[[ $(grep -c '^systemd-firstboot' "$tmp/calls") == 1 ]] || fail "a layout the console refused is not persisted" "$(cat "$tmp/calls")"
assert_layout dk "after loadkeys failed once"
pass "a layout loadkeys refuses is asked for again"
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo nothing >"$tmp/firstboot-modes"
form || fail "firstboot writing nothing, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "firstboot writes nothing" "does not read back as keymap dk"
assert_layout dk "after firstboot wrote nothing once"
pass "a systemd-firstboot that writes nothing is asked for again"
for mode in mismatch no-xkb; do
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo "$mode" >"$tmp/firstboot-modes"
form || fail "readback $mode, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "readback $mode" "does not read back as keymap dk"
assert_layout dk "after readback $mode"
done
pass "a vconsole.conf that reads back another keymap, or no XKB layout, is asked for again"
# The readback runs in a clean shell: values this process exports never stand
# in for the file.
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo nothing >"$tmp/firstboot-modes"
: >"$conf"
KEYMAP=dk XKBLAYOUT=dk form || fail "an exported layout, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "exported KEYMAP" "does not read back as keymap dk"
pass "the readback ignores KEYMAP and XKBLAYOUT in the environment"
fresh_root
answers 'Danish|xx' 'Danish|dk'
form || fail "an unknown keymap, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "unknown keymap" "keymap xx unknown to localectl"
! grep -q 'keymap=xx\|set-keymap xx' "$tmp/calls" || fail "an unknown keymap is never persisted" "$(cat "$tmp/calls")"
assert_layout dk "after an unknown keymap"
pass "a keymap localectl does not know is asked for again rather than keeping the default"
fresh_root
answers 'Danish|DK' 'Danish|dk'
form || fail "a keymap in the wrong case, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "keymap case" "keymap DK unknown to localectl"
assert_layout dk "after a keymap in the wrong case"
pass "a keymap matches localectl's list exactly, case included"
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo fail >"$tmp/list-modes"
form || fail "localectl failing to list keymaps, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "localectl list fails" "localectl could not list keymaps to check dk"
[[ $(grep -c '^systemd-firstboot' "$tmp/calls") == 1 ]] || fail "a keymap that could not be checked is not persisted" "$(cat "$tmp/calls")"
pass "a keymap list localectl cannot produce is a failure, logged apart from an unknown keymap"
fresh_root
answers 'Danish|dk'
echo fail >"$tmp/firstboot-modes"
form || fail "localectl persists when firstboot fails" "$(cat "$tmp/screen" "$tmp/log")"
[[ $(prompts) == 1 ]] && grep -qx 'localectl set-keymap dk' "$tmp/calls" || fail "the localectl fallback persists the layout" "$(cat "$tmp/calls")"
assert_layout dk "the localectl fallback"
pass "a failed systemd-firstboot falls back to localectl, checked the same way"
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo fail >"$tmp/firstboot-modes"
echo fail >"$tmp/localectl-modes"
form || fail "both persisting paths failing, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "firstboot and localectl fail" "could not persist keymap dk"
pass "a layout neither systemd-firstboot nor localectl persists is asked for again"
# A layout that never takes keeps the owner at the keyboard step.
fresh_root
answers 'Danish|dk' 'Danish|dk'
printf 'fail\nfail\n' >"$tmp/loadkeys-modes"
if form; then fail "a layout that never loads does not end the keyboard step"; fi
[[ $(tail -n1 "$tmp/screen") == "asked again" ]] && ! grep -q 'password form' "$tmp/screen" ||
fail "the password form is never reached while the layout fails" "$(cat "$tmp/screen")"
grep -qx 'KEYMAP=us' "$conf" || fail "the install's layout is left alone" "$(cat "$conf")"
pass "while the layout fails the owner never reaches the password form"
# A machine that can't set any layout (localectl gone for good) must not keep the
# owner at the keyboard step: after three failures in a row the attempt fails,
# and setup's own retry-or-console screen takes over.
fresh_root
answers 'Danish|dk' 'German|de-latin1' 'Danish|dk' 'Danish|dk'
printf 'fail\nfail\nfail\nfail\n' >"$tmp/list-modes"
status=0
form || status=$?
(( status == 1 )) || fail "a layout that can never be set fails the attempt" "status $status: $(cat "$tmp/screen")"
[[ $(prompts) == 3 ]] && [[ $(tail -n1 "$tmp/screen") == "notice: Setup could not set a keyboard layout on this machine." ]] ||
fail "the keyboard step gives up after three failures, and says so" "$(cat "$tmp/screen")"
! grep -q 'password form' "$tmp/screen" || fail "a layout that can never be set never reaches the password form" "$(cat "$tmp/screen")"
grep -q 'no keyboard layout could be set after 3 attempts' "$tmp/log" || fail "the log records giving up" "$(cat "$tmp/log")"
sed -n '/^run_setup() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" | grep -qx ' keyboard_form || return 1' ||
fail "a failed keyboard step fails the setup attempt, which offers retry or a console"
pass "a machine that can't set any layout fails the attempt after three tries, reaching setup's retry-or-console screen"
fresh_root
answers 'Danish|dk'
touch "$tmp/no-tty"
form || fail "off a console the layout is only persisted" "$(cat "$tmp/screen" "$tmp/log")"
! grep -q '^loadkeys' "$tmp/calls" || fail "loadkeys runs only on a virtual console" "$(cat "$tmp/calls")"
assert_layout dk "off a console"
pass "off a virtual console the layout is persisted and checked without loadkeys"
+128
View File
@@ -0,0 +1,128 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# A deferred install (an Apple Silicon image, an OEM ISO install) runs its
# hardware steps on the UTC placeholder, so the wireless regulatory domain can
# only come from the timezone the owner picks in first-boot setup. The setup
# step runs against a fixture root: the real install step with its paths moved
# there, and wireless-regdb's boot helper as it ships, with iw recording calls.
tmp=$(cd -- "$(mktemp -d)" && pwd -P)
trap 'rm -rf "$tmp"' EXIT
root=$tmp/root
omarchy=$tmp/omarchy
stub_bin=$tmp/bin
conf=$root/etc/conf.d/wireless-regdom
calls=$tmp/calls
mkdir -p "$stub_bin" "$omarchy/bin" "$omarchy/install/hardware"
ln -s "$ROOT/bin/omarchy-cmd-present" "$omarchy/bin/omarchy-cmd-present"
sed -e "s|/etc/|$root/etc/|g" -e "s|/usr/share/zoneinfo|$root/usr/share/zoneinfo|g" \
"$ROOT/install/hardware/set-wireless-regdom.sh" >"$omarchy/install/hardware/set-wireless-regdom.sh"
cat >"$stub_bin/timedatectl" <<SH
#!/bin/bash
[[ \$1 == set-timezone && -f "$root/usr/share/zoneinfo/\$2" ]] || exit 1
ln -sfn "../usr/share/zoneinfo/\$2" "$root/etc/localtime"
SH
printf '#!/bin/bash\nexec "$@"\n' >"$stub_bin/sudo"
cat >"$stub_bin/set-wireless-regdom" <<SH
#!/bin/bash
unset WIRELESS_REGDOM
. "$conf"
[ -n "\${WIRELESS_REGDOM}" ] && iw reg set \${WIRELESS_REGDOM}
SH
cat >"$stub_bin/iw" <<SH
#!/bin/bash
echo "iw \$*" >>"$calls"
[[ ! -e "$tmp/no-wifi" ]] || { echo "nl80211 not found." >&2; exit 1; }
SH
chmod +x "$stub_bin"/*
# A fresh deferred install: wireless-regdb's commented list, the UTC placeholder.
fresh_root() {
rm -rf "$root" "$calls" "$tmp/no-wifi"
mkdir -p "$root/etc/conf.d" "$root/usr/share/zoneinfo/Australia" "$root/usr/share/zoneinfo/America"
printf '#\n# Wireless regulatory domain configuration\n#\n#WIRELESS_REGDOM="AU"\n#WIRELESS_REGDOM="US"\n' >"$conf"
: >"$root/usr/share/zoneinfo/UTC"
: >"$root/usr/share/zoneinfo/Australia/Brisbane"
: >"$root/usr/share/zoneinfo/America/New_York"
printf 'AU\t-2728+15302\tAustralia/Brisbane\tQueensland (most areas)\nUS\t+404251-0740023\tAmerica/New_York\tEastern (most areas)\n' \
>"$root/usr/share/zoneinfo/zone.tab"
ln -s ../usr/share/zoneinfo/UTC "$root/etc/localtime"
: >"$calls"
}
export PATH="$stub_bin:$omarchy/bin:$PATH"
export OMARCHY_PATH=$omarchy
# The two setup functions as omarchy-provision-owner defines them, with its
# fixed paths moved into the fixture root.
sed -n '/^configure_timezone() {/,/^}/p; /^configure_wireless_regdom() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" |
sed -e "s|/usr/share/zoneinfo|$root/usr/share/zoneinfo|g" -e "s|/etc/|$root/etc/|g" >"$tmp/setup.sh"
grep -q '^configure_wireless_regdom() {' "$tmp/setup.sh" || fail "omarchy-provision-owner sets the regulatory domain in setup"
# shellcheck disable=SC1091
source "$tmp/setup.sh"
regdom_lines() {
grep '^WIRELESS_REGDOM=' "$conf" || true
}
# The install step itself stays as an up-front install runs it: persisted only,
# the radios untouched until the reboot that follows an install.
fresh_root
ln -sfn ../usr/share/zoneinfo/America/New_York "$root/etc/localtime"
bash "$omarchy/install/hardware/set-wireless-regdom.sh"
[[ $(regdom_lines) == 'WIRELESS_REGDOM="US"' ]] || fail "an up-front install persists its timezone's country" "$(cat "$conf")"
[[ ! -s $calls ]] || fail "an up-front install leaves the radios alone" "$(cat "$calls")"
pass "an up-front install persists its timezone's country and leaves the radios to the reboot"
fresh_root
bash "$omarchy/install/hardware/set-wireless-regdom.sh"
[[ -z $(regdom_lines) ]] || fail "the UTC placeholder names no country" "$(cat "$conf")"
pass "a deferred install's hardware step sets nothing from the UTC placeholder"
timezone=Australia/Brisbane
configure_timezone >/dev/null
[[ $(readlink "$root/etc/localtime") == ../usr/share/zoneinfo/Australia/Brisbane ]] ||
fail "setup applies the owner's timezone"
[[ $(regdom_lines) == 'WIRELESS_REGDOM="AU"' ]] || fail "setup persists the owner's country" "$(cat "$conf")"
[[ $(cat "$calls") == "iw reg set AU" ]] || fail "setup applies the owner's country to the radios" "$(cat "$calls")"
pass "setup persists the owner's timezone's country and applies it without a reboot"
configure_timezone >/dev/null
[[ $(regdom_lines) == 'WIRELESS_REGDOM="AU"' ]] || fail "a retried setup writes the domain once" "$(cat "$conf")"
pass "a retried setup keeps one regulatory domain line"
fresh_root
printf 'WIRELESS_REGDOM="DE"\n' >>"$conf"
timezone=America/New_York
configure_timezone >/dev/null
[[ $(regdom_lines) == 'WIRELESS_REGDOM="DE"' ]] || fail "setup keeps a domain already chosen" "$(cat "$conf")"
[[ $(cat "$calls") == "iw reg set DE" ]] || fail "setup applies the domain already chosen" "$(cat "$calls")"
pass "a regulatory domain already chosen is kept"
fresh_root
timezone=UTC
configure_timezone >/dev/null
[[ -z $(regdom_lines) ]] || fail "a timezone without a country sets no domain" "$(cat "$conf")"
[[ ! -s $calls ]] || fail "a timezone without a country leaves the radios alone" "$(cat "$calls")"
pass "a timezone without a country sets no regulatory domain"
fresh_root
: >"$tmp/no-wifi"
timezone=America/New_York
configure_timezone >/dev/null || fail "setup goes on on a machine without Wi-Fi"
[[ $(regdom_lines) == 'WIRELESS_REGDOM="US"' ]] || fail "a machine without Wi-Fi still gets the domain for later" "$(cat "$conf")"
pass "a machine without Wi-Fi finishes setup with the domain persisted for its next boot"
fresh_root
timezone=Omarchy/Nowhere
configure_timezone >/dev/null || fail "setup goes on after a timezone it cannot apply"
[[ -z $(regdom_lines) && ! -s $calls ]] || fail "a timezone that did not apply sets no domain" "$(cat "$conf" "$calls")"
pass "a timezone that could not be applied sets no regulatory domain"
+228
View File
@@ -0,0 +1,228 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
tmp=$(mktemp -d)
cleanup() {
for child in $(jobs -pr); do kill "$child" 2>/dev/null || true; done
if [[ -f $tmp/recorder-pid ]]; then kill "$(<"$tmp/recorder-pid")" 2>/dev/null || true; fi
rm -rf "$tmp"
}
trap cleanup EXIT
mkdir -p "$tmp/bin" "$tmp/runtime" "$tmp/recordings"
mkfifo "$tmp/release"
cat >"$tmp/bin/omarchy-capture-screenrecording-process" <<'SH'
#!/bin/bash
if [[ ${1:-} != "--pid" || $2 == "123" ]]; then
if [[ ${TEST_ROLE:-} == "old-status" && ${2:-} == "123" ]]; then
touch "$TEST_STATE/checked"
read -r _ <"$TEST_STATE/release"
fi
exit 1
fi
[[ -f $TEST_STATE/recorder-pid && $2 == "$(<"$TEST_STATE/recorder-pid")" ]] || exit 1
if [[ ${3:-} == "--signal" ]]; then kill -TERM "$2"; else kill -0 "$2" 2>/dev/null; fi
SH
cat >"$tmp/bin/wf-recorder" <<'SH'
#!/bin/bash
echo "$$" >"$TEST_STATE/recorder-pid"
for arg in "$@"; do
if [[ ${next:-} == 1 ]]; then [[ -n ${TEST_NEVER_READY:-} ]] || touch "$arg"; break; fi
[[ $arg == "-f" ]] && next=1
done
if [[ -n ${TEST_IGNORE_TERM:-} ]]; then trap '' TERM; else trap 'exit 0' TERM INT; fi
while true; do sleep 0.05; done
SH
cat >"$tmp/bin/pactl" <<'SH'
#!/bin/bash
case $1 in
get-default-sink) echo desktop ;;
get-default-source) echo microphone ;;
load-module)
id=$(<"$TEST_STATE/next-module")
echo $(( id + 1 )) >"$TEST_STATE/next-module"
touch "$TEST_STATE/module-$id"
echo "$id"
;;
unload-module) rm -f "$TEST_STATE/module-$2" ;;
esac
SH
cat >"$tmp/bin/flock" <<'SH'
#!/bin/bash
[[ ${TEST_ROLE:-} == "new-start" ]] && touch "$TEST_STATE/lock-attempted"
exec /usr/bin/flock "$@"
SH
cat >"$tmp/bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
[[ $1 == "wf-recorder" ]]
SH
cat >"$tmp/bin/omarchy-hyprland-monitor-focused" <<'SH'
#!/bin/bash
echo Virtual-1
SH
cat >"$tmp/bin/ffmpeg" <<'SH'
#!/bin/bash
if [[ ${TEST_ROLE:-} == "old-stop" && ! -e $TEST_STATE/checked ]]; then
touch "$TEST_STATE/checked"
read -r _ <"$TEST_STATE/release"
fi
touch "${@: -3:1}"
SH
for command in omarchy-shell omarchy-notification-send ffprobe pkill; do
printf '#!/bin/bash\nexit 0\n' >"$tmp/bin/$command"
done
cat >"$tmp/bin/omarchy-shell" <<'SH'
#!/bin/bash
# A stop that finds no start pending while the start is finishing.
if [[ -n ${TEST_LATE_STOP:-} && ! -e $TEST_STATE/late-stop-sent ]]; then
touch "$TEST_STATE/late-stop-sent"
rm -f "$XDG_RUNTIME_DIR/omarchy-screenrecord-starting"
TEST_LATE_STOP= "$TEST_RECORD" --stop-recording 9>&- >/dev/null 2>&1 &
echo $! >"$TEST_STATE/late-stop-pid"
until [[ -e $XDG_RUNTIME_DIR/omarchy-screenrecord-cancel ]]; do sleep 0.05; done
fi
if [[ -n ${TEST_PAUSE_INDICATOR:-} && ! -e $TEST_STATE/indicator-paused ]]; then
touch "$TEST_STATE/indicator-paused"
read -r _ <"$TEST_STATE/release"
fi
exit 0
SH
chmod +x "$tmp/bin/"*
export PATH="$tmp/bin:$ROOT/bin:$PATH" TEST_STATE="$tmp" XDG_RUNTIME_DIR="$tmp/runtime"
export OMARCHY_SCREENRECORD_DIR="$tmp/recordings"
record="$ROOT/bin/omarchy-capture-screenrecording"
export TEST_RECORD=$record
wait_for() {
for _ in {1..200}; do
for file in "$@"; do [[ ! -e $file ]] || return 0; done
sleep 0.01
done
fail "concurrent recording command reached its barrier" "$*"
}
assert_recording() {
[[ -s $XDG_RUNTIME_DIR/omarchy-screenrecord-pid && -s $XDG_RUNTIME_DIR/omarchy-screenrecord-filename ]] ||
fail "the new recording retains its saved state"
[[ -f $XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules ]] || fail "the new recording retains its audio mix"
for module in $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules"); do
[[ -f $tmp/module-$module ]] || fail "the new recording's audio module stays loaded" "$module"
done
}
echo 123 >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pid"
printf '10\n11\n12\n' >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules"
touch "$tmp/module-10" "$tmp/module-11" "$tmp/module-12"
echo 20 >"$tmp/next-module"
TEST_ROLE=old-status "$record" --status >/dev/null 2>&1 & old=$!
wait_for "$tmp/checked"
TEST_ROLE=new-start "$record" --fullscreen --resolution=1280x800 --with-desktop-audio --with-microphone-audio >/dev/null 2>&1 & new=$!
wait_for "$tmp/lock-attempted" "$tmp/recorder-pid"
if [[ ! -e $tmp/lock-attempted ]]; then
wait_for "$XDG_RUNTIME_DIR/omarchy-screenrecord-pid"
for _ in {1..200}; do
[[ $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-pid") == 123 ]] || break
sleep 0.01
done
[[ $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-pid") != 123 ]] || fail "the concurrent recording saves its PID"
fi
echo release >"$tmp/release"
status=0
wait "$old" || status=$?
(( status == 1 )) || fail "the old status reports its exited recorder" "$status"
wait "$new" || fail "the new recording starts"
assert_recording
pass "stale status cleanup cannot unload a concurrent new recording's audio"
rm -f "$tmp/checked" "$tmp/lock-attempted"
TEST_ROLE=old-stop "$record" --stop-recording >/dev/null 2>&1 & old=$!
wait_for "$tmp/checked"
rm "$tmp/recorder-pid"
stop_locked=0
if ! /usr/bin/flock -n "$XDG_RUNTIME_DIR/omarchy-screenrecord.lock" true; then stop_locked=1; fi
TEST_ROLE=new-start "$record" --fullscreen --resolution=1280x800 --with-desktop-audio --with-microphone-audio >/dev/null 2>&1 & new=$!
wait_for "$tmp/lock-attempted" "$tmp/recorder-pid"
if (( ! stop_locked )); then
wait "$new" || fail "the concurrent recording starts while the old stop is paused"
new=""
fi
echo release >"$tmp/release"
wait "$old" || fail "the old recording stops"
if [[ -n $new ]]; then
wait "$new" || fail "another recording starts after stop"
fi
assert_recording
pass "a finishing stop cannot remove a concurrent new recording's state"
"$record" --stop-recording >/dev/null 2>&1 || fail "the earlier recording stops"
# A recorder that never creates its file keeps the start waiting. Another
# toggle, or a stop, cancels that start instead of queueing a new one behind it.
for second in toggle stop; do
# Names are per second, so an earlier recording's file would look ready.
rm -f "$tmp/recorder-pid" "$tmp/recordings/"*
# The stop's recorder ignores TERM, so ending it needs the KILL that follows.
ignore_term=""
[[ $second == "stop" ]] && ignore_term=1
TEST_IGNORE_TERM=$ignore_term TEST_NEVER_READY=1 "$record" --fullscreen --resolution=1280x800 --with-desktop-audio --with-microphone-audio >/dev/null 2>&1 & pending=$!
wait_for "$tmp/recorder-pid"
waiting=$(<"$tmp/recorder-pid")
args=(--fullscreen --resolution=1280x800)
[[ $second == "stop" ]] && args=(--stop-recording)
timeout 5 "$record" "${args[@]}" >/dev/null 2>&1 || fail "a $second during a pending start returns at once"
timeout 5 tail --pid="$pending" -f /dev/null || fail "the pending start ends once a $second cancels it"
wait "$pending" || true
! kill -0 "$waiting" 2>/dev/null || fail "a cancelled start stops its recorder"
[[ $(<"$tmp/recorder-pid") == "$waiting" ]] || fail "a $second during a pending start does not begin another recording"
for state in pid filename pa-modules starting cancel; do
[[ ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-$state ]] || fail "a cancelled start leaves no $state behind"
done
pass "a $second during a start that is still waiting on its recorder cancels it"
done
# A stop that arrives while the start is saving its recording as started still
# ends that recording.
rm -f "$tmp/recorder-pid" "$tmp/recordings/"* "$tmp/indicator-paused"
TEST_PAUSE_INDICATOR=1 "$record" --fullscreen --resolution=1280x800 --with-desktop-audio >/dev/null 2>&1 & pending=$!
wait_for "$tmp/indicator-paused"
committed=$(<"$tmp/recorder-pid")
timeout 5 "$record" --stop-recording >/dev/null 2>&1 || fail "a stop during the start's last step returns"
echo release >"$tmp/release"
timeout 5 tail --pid="$pending" -f /dev/null || fail "the start finishes after its last step"
wait "$pending" || true
! kill -0 "$committed" 2>/dev/null || fail "a stop during the start's last step ends the recording"
for state in pid filename pa-modules starting cancel; do
[[ ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-$state ]] || fail "a stop during the start's last step leaves no $state behind"
done
pass "a stop while the start saves its recording as started still ends it"
# A stop that finds no start pending while the start is still finishing waits
# for the lock, and the recording ends either way.
rm -f "$tmp/recorder-pid" "$tmp/recordings/"* "$tmp/late-stop-sent" "$tmp/late-stop-pid"
TEST_LATE_STOP=1 "$record" --fullscreen --resolution=1280x800 >/dev/null 2>&1 || fail "the start with a late stop finishes"
started=$(<"$tmp/recorder-pid")
timeout 10 tail --pid="$(<"$tmp/late-stop-pid")" -f /dev/null || fail "the late stop finishes"
! kill -0 "$started" 2>/dev/null || fail "a stop waiting behind a finishing start still ends the recording"
[[ ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-pid && ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-cancel ]] ||
fail "a late stop leaves no state behind"
pass "a stop that waits behind a finishing start still ends the recording"
"$record" --stop-recording >/dev/null 2>&1 || true
# A start that was killed leaves its marker behind. A toggle that waits on the
# lock (held here as a stop would) does not take that stale marker for a start
# still in progress: it waits its turn and starts the recording.
rm -f "$tmp/recorder-pid" "$tmp/recordings/"* "$XDG_RUNTIME_DIR/omarchy-screenrecord-cancel"
true & dead=$!
wait "$dead"
echo "$dead" >"$XDG_RUNTIME_DIR/omarchy-screenrecord-starting"
/usr/bin/flock "$XDG_RUNTIME_DIR/omarchy-screenrecord.lock" sleep 2 & holder=$!
sleep 0.3
timeout 15 "$record" --fullscreen --resolution=1280x800 >/dev/null 2>&1 || fail "a toggle behind a stale start marker finishes"
wait "$holder" || true
[[ -s $XDG_RUNTIME_DIR/omarchy-screenrecord-pid ]] && kill -0 "$(<"$tmp/recorder-pid")" 2>/dev/null ||
fail "a toggle behind a stale start marker starts the recording instead of cancelling a start that is gone"
"$record" --stop-recording >/dev/null 2>&1 || true
pass "a killed start's leftover marker does not swallow a later toggle"
+197
View File
@@ -0,0 +1,197 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
SCRIPT="$ROOT/bin/omarchy-capture-screenrecording-process"
tmp=$(mktemp -d)
cleanup() {
local file
for file in "$tmp"/*.pid; do
[[ -f $file ]] && kill -KILL "$(<"$file")" 2>/dev/null || true
done
rm -rf "$tmp"
}
trap cleanup EXIT
mkdir -p "$tmp/bin"
# pgrep reports the pids listed in $TEST_STATE/running, as a recorder search would.
cat >"$tmp/bin/pgrep" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >"$TEST_STATE/pgrep-args"
[[ -s $TEST_STATE/running ]] || exit 1
cat "$TEST_STATE/running"
SH
chmod +x "$tmp/bin/pgrep"
export TEST_STATE="$tmp"
# A fake /proc gives each pid the executable link only Linux provides.
exe() {
mkdir -p "$tmp/proc/$1"
ln -sfn "$2" "$tmp/proc/$1/exe"
}
recorders() {
local pid
printf '%s\n' "$@" >"$tmp/running"
for pid; do
[[ -L $tmp/proc/$pid/exe ]] || exe "$pid" /usr/bin/gpu-screen-recorder
done
}
run() {
local status=0
OMARCHY_PROC_ROOT="$tmp/proc" PATH="$tmp/bin:$PATH" "$SCRIPT" "$@" 2>"$tmp/stderr" || status=$?
echo "$status"
}
# Job control keeps SIGINT deliverable to background jobs, which a
# non-interactive shell would otherwise start with it ignored.
set -m
spawn() {
local name=$1
bash -c 'trap "echo INT >\"$0\"; exit 0" INT; while :; do sleep 0.05; done' "$tmp/$name.signal" &
echo $! >"$tmp/$name.pid"
disown
}
got_int() {
local name=$1 tries=0
while [[ ! -s $tmp/$name.signal ]] && ((tries < 100)); do
sleep 0.02
tries=$((tries + 1))
done
[[ -s $tmp/$name.signal ]]
}
alive() {
kill -0 "$1" 2>/dev/null
}
wait_dead() {
local tries=0
while alive "$1" && ((tries < 100)); do
sleep 0.02
tries=$((tries + 1))
done
! alive "$1"
}
: >"$tmp/running"
[[ $(run) == 1 ]] || fail "no recorder running exits 1"
[[ $(run --pid 123) == 1 ]] || fail "a pid with no recorder running exits 1"
pass "no recorder running exits 1"
recorders 4242
[[ $(run) == 0 ]] || fail "a running recorder exits 0"
[[ $(<"$tmp/pgrep-args") == "-u $UID -x gpu-screen-reco(rder)?|wf-recorder" ]] ||
fail "recorders are found by the current user's process name" "$(<"$tmp/pgrep-args")"
pass "a running recorder exits 0"
recorders 4242 4343
[[ $(run --pid 4343) == 0 ]] || fail "--pid of a recorder exits 0"
[[ $(run --pid 04343) == 0 ]] || fail "--pid ignores leading zeros"
[[ $(run --pid 0000000000000004343) == 0 ]] || fail "--pid ignores leading zeros past ten digits"
[[ $(run --pid 4194304) == 1 ]] || fail "--pid accepts the largest Linux pid"
[[ $(run --pid 999) == 1 ]] || fail "--pid of a non-recorder exits 1"
[[ $(run --pid 434) == 1 ]] || fail "--pid matches whole pids only"
[[ $(run --pid 999 --pid 4242) == 0 ]] || fail "repeated --pid matches any of them"
pass "--pid restricts the check to the given recorder pids"
spawn a
spawn b
spawn other
recorders "$(<"$tmp/a.pid")" "$(<"$tmp/b.pid")"
[[ $(run --signal INT) == 0 ]] || fail "--signal INT with recorders exits 0"
got_int a && got_int b || fail "--signal INT interrupts every recorder"
alive "$(<"$tmp/other.pid")" && [[ ! -s $tmp/other.signal ]] || fail "--signal INT leaves other processes alone"
pass "--signal INT interrupts every recorder and nothing else"
spawn c
spawn d
recorders "$(<"$tmp/c.pid")" "$(<"$tmp/d.pid")"
[[ $(run --pid "$(<"$tmp/c.pid")" --signal INT) == 0 ]] || fail "--pid with --signal INT exits 0"
got_int c || fail "--pid with --signal INT interrupts the given recorder"
[[ ! -s $tmp/d.signal ]] && alive "$(<"$tmp/d.pid")" || fail "--pid with --signal INT leaves other recorders alone"
[[ $(run --pid "$(<"$tmp/d.pid")" --signal KILL) == 0 ]] || fail "--signal KILL exits 0"
wait_dead "$(<"$tmp/d.pid")" || fail "--signal KILL kills the given recorder"
[[ ! -s $tmp/d.signal ]] || fail "--signal KILL does not interrupt first"
alive "$(<"$tmp/other.pid")" || fail "--signal KILL leaves other processes alone"
[[ $(run --pid 999 --signal KILL) == 1 ]] || fail "--signal with no matching recorder exits 1"
alive "$(<"$tmp/other.pid")" || fail "--signal with no matching recorder signals nothing"
pass "--pid with --signal INT or KILL sends that signal to only the given recorder"
spawn e
gone=$(<"$tmp/d.pid")
recorders "$gone" "$(<"$tmp/e.pid")"
[[ $(run --signal INT) == 0 ]] || fail "a recorder exiting before its signal is not an error"
got_int e || fail "a recorder exiting before its signal does not stop the others being signalled"
pass "a recorder exiting before its signal is not an error"
exe 5001 /usr/bin/gpu-screen-recorder-helper
exe 5002 "/usr/bin/wf-recorder (deleted)"
recorders 5001
[[ $(run) == 1 ]] || fail "a process named like a recorder but running another executable is not a recorder"
[[ $(run --pid 5001) == 1 ]] || fail "--pid of a recorder-prefixed helper exits 1"
recorders 5003
rm -rf "$tmp/proc/5003"
[[ $(run) == 1 ]] || fail "a candidate whose executable cannot be read is not a recorder"
recorders 5002
[[ $(run) == 0 ]] || fail "a recorder whose executable an upgrade replaced is still a recorder"
spawn helper
exe "$(<"$tmp/helper.pid")" /usr/bin/gpu-screen-recorder-helper
recorders "$(<"$tmp/helper.pid")"
[[ $(run --signal INT) == 1 ]] || fail "--signal with only a recorder-prefixed helper exits 1"
alive "$(<"$tmp/helper.pid")" && [[ ! -s $tmp/helper.signal ]] || fail "--signal leaves a recorder-prefixed helper alone"
pass "the executable, not the truncated process name, decides what is a recorder"
true &
dead1=$!
wait "$dead1"
true &
dead2=$!
wait "$dead2"
recorders "$dead1" "$dead2"
[[ $(run --signal INT) == 1 ]] || fail "--signal exits 1 when every recorder exits before its signal"
pass "--signal exits 1 when no signal was delivered"
for args in "--pid" "--pid abc" "--pid -1" "--pid 12x" "--signal" "--signal TERM" "--signal int" "--bogus" "123" "--pid 1 extra" \
"--pid 4194305" "--pid 12345678901" "--pid 18446744073709555959" "--pid 99999999999999999999"; do
# shellcheck disable=SC2086
[[ $(run $args) == 2 ]] || fail "invalid arguments exit 2: $args"
grep -q '^Usage: omarchy-capture-screenrecording-process ' "$tmp/stderr" || fail "invalid arguments print usage: $args"
done
pass "invalid arguments print usage and exit 2"
# The real pgrep sees Linux's 15-character process names, which cut
# gpu-screen-recorder short.
if [[ -r /proc/self/comm ]]; then
mkdir -p "$tmp/real"
cp "$(command -v sleep)" "$tmp/real/gpu-screen-recorder"
cp "$(command -v sleep)" "$tmp/real/wf-recorder"
"$tmp/real/gpu-screen-recorder" 30 &
gpu=$!
echo "$gpu" >"$tmp/gpu.pid"
"$tmp/real/wf-recorder" 30 &
wf=$!
echo "$wf" >"$tmp/wf.pid"
disown -a
sleep 0.2
"$SCRIPT" --pid "$gpu" || fail "the real pgrep finds gpu-screen-recorder"
"$SCRIPT" --pid "$wf" || fail "the real pgrep finds wf-recorder"
"$SCRIPT" --pid "$gpu" --pid "$wf" --signal KILL || fail "the real pgrep signals both recorders"
wait_dead "$gpu" && wait_dead "$wf" || fail "the real recorders are killed"
pass "the real pgrep finds and signals both recorders by name"
else
skip "the real pgrep finds and signals both recorders by name (needs Linux /proc)"
fi
grep -Fq '["omarchy-capture-screenrecording", "--status"]' "$ROOT/shell/plugins/bar/indicators/ScreenRecording.qml" ||
fail "the bar recording indicator asks the recorder script, which stop also uses"
! grep -Fq 'gpu-screen-recorder' "$ROOT/shell/plugins/bar/indicators/ScreenRecording.qml" ||
fail "the bar recording indicator still greps gpu-screen-recorder"
grep -Fq '"when":"omarchy-capture-screenrecording --status"' "$ROOT/default/omarchy/omarchy-menu.jsonc" ||
fail "Stop Screenrecording asks the recorder script, which its action also uses"
! grep -Fq "pgrep -f '^gpu-screen-recorder'" "$ROOT/default/omarchy/omarchy-menu.jsonc" ||
fail "Stop Screenrecording still greps gpu-screen-recorder"
pass "the bar indicator and the menu ask the recorder script whether its stop has a recording to end"
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
require_command ffmpeg
require_command ffprobe
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
# loudnorm declares 192 kHz output, so without a pinned rate the finalize pass
# writes 96 kHz AAC from a 48 kHz recording. Many editors decode such a track as
# silence, and on Apple Silicon playing it switches the speakers to 96 kHz,
# which their safety daemon answers by locking the amps.
source <(sed -n '/^finalize_recording() {/,/^}/p' "$ROOT/bin/omarchy-capture-screenrecording")
recording="$work/recording.mp4"
ffmpeg -loglevel error -f lavfi -i testsrc=size=64x64:rate=30 -f lavfi -i sine=frequency=440:sample_rate=48000 \
-t 1 -c:v libx264 -c:a aac -ar 48000 "$recording"
RECORDING_FILE="$work/latest"
echo "$recording" >"$RECORDING_FILE"
finalize_recording
# The pass trims the first 0.1 s, so a shorter file proves it ran.
duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$recording")
awk -v d="$duration" 'BEGIN { exit !(d < 0.95) }' || fail "the loudness pass processed the recording" "$duration"
rate=$(ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate -of csv=p=0 "$recording")
[[ $rate == "48000" ]] || fail "the finalized recording keeps 48 kHz audio" "$rate"
pass "the finalized recording keeps 48 kHz audio"
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/bin" "$tmp/runtime" "$tmp/recordings"
cat >"$tmp/bin/omarchy-capture-screenrecording-process" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_HELPER_CALLS"
[[ ${1:-} == "--pid" && ${2:-} == "123" && ${TEST_RECORDER_ACTIVE:-0} == "1" ]]
SH
cat >"$tmp/bin/pactl" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_AUDIO_CALLS"
SH
chmod +x "$tmp/bin/omarchy-capture-screenrecording-process" "$tmp/bin/pactl"
export PATH="$tmp/bin:$ROOT/bin:$PATH"
export XDG_RUNTIME_DIR="$tmp/runtime"
export OMARCHY_SCREENRECORD_DIR="$tmp/recordings"
export TEST_HELPER_CALLS="$tmp/helper-calls"
export TEST_AUDIO_CALLS="$tmp/audio-calls"
echo 123 >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pid"
for action in --status --stop-recording; do
printf '10\n11\n12\n' >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules"
: >"$TEST_HELPER_CALLS"
: >"$TEST_AUDIO_CALLS"
status=0
"$ROOT/bin/omarchy-capture-screenrecording" "$action" >/dev/null 2>&1 || status=$?
(( status == 1 )) || fail "$action reports no recorder after it exits" "$status"
[[ $(<"$TEST_AUDIO_CALLS") == $'unload-module 12\nunload-module 11\nunload-module 10' ]] ||
fail "$action releases the exited recorder's audio loopbacks before its sink" "$(<"$TEST_AUDIO_CALLS")"
[[ ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules ]] || fail "$action removes the stale audio module list"
! grep -q -- '--signal' "$TEST_HELPER_CALLS" || fail "$action signals no other recorder"
pass "$action releases audio capture after the saved recorder exits"
done
printf '10\n11\n12\n' >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules"
: >"$TEST_AUDIO_CALLS"
TEST_RECORDER_ACTIVE=1 "$ROOT/bin/omarchy-capture-screenrecording" --status || fail "status recognizes the active recorder"
[[ ! -s $TEST_AUDIO_CALLS && -e $XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules ]] ||
fail "status leaves an active recording's audio modules loaded"
pass "status preserves the active recorder's audio capture"
rm "$XDG_RUNTIME_DIR/omarchy-screenrecord-pid"
: >"$TEST_AUDIO_CALLS"
status=0
"$ROOT/bin/omarchy-capture-screenrecording" --status >/dev/null 2>&1 || status=$?
(( status == 1 )) || fail "status reports inactive before the recorder starts"
[[ ! -s $TEST_AUDIO_CALLS && -e $XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules ]] ||
fail "status preserves the audio mix created before the recorder starts"
pass "status leaves the starting recorder's audio mix alone"
+131 -2
View File
@@ -307,7 +307,8 @@ pass "webcam size rules place the initial window in its final corner"
recording_dir="$tmp_dir/recordings"
mkdir -p "$recording_dir"
cat >"$stub_bin/pgrep" <<'SH'
# Nothing is recording yet, whatever else runs on the machine.
cat >"$stub_bin/omarchy-capture-screenrecording-process" <<'SH'
#!/bin/bash
exit 1
SH
@@ -331,7 +332,7 @@ cat >"$stub_bin/omarchy-shell" <<'SH'
exit 0
SH
chmod +x "$stub_bin"/pgrep "$stub_bin"/omarchy-hyprland-monitor-focused \
chmod +x "$stub_bin"/omarchy-capture-screenrecording-process "$stub_bin"/omarchy-hyprland-monitor-focused \
"$stub_bin"/gpu-screen-recorder "$stub_bin"/omarchy-shell
# Compare that name across the run rather than demanding it be absent: the
@@ -410,3 +411,131 @@ pass "without a runtime dir the webcam anchors to the recorded region"
mode=$(stat -c '%a' "$state_home/omarchy" 2>/dev/null || stat -f '%Lp' "$state_home/omarchy")
[[ $mode == "700" ]] || fail "fallback directory is private even when it already existed" "mode: $mode"
pass "fallback directory is private even when it already existed"
# gpu-screen-recorder aborts before recording on a GPU it does not know, such
# as Apple Silicon's. wf-recorder takes over when it is installed, on the same
# monitor, and its pid is the one stop and status act on.
cat >"$stub_bin/gpu-screen-recorder" <<'SH'
#!/bin/bash
echo "gsr error: unknown gpu vendor" >&2
exit 1
SH
cat >"$stub_bin/wf-recorder" <<'SH'
#!/bin/bash
printf '%s\n' "$@" >"$OMARCHY_TEST_WF_ARGS"
echo "$$" >"$OMARCHY_TEST_WF_PID"
for i in "$@"; do
[[ -n ${take_next:-} ]] && { : >"$i"; break; }
[[ $i == "-f" ]] && take_next=1
done
sleep 5
SH
chmod +x "$stub_bin/gpu-screen-recorder" "$stub_bin/wf-recorder"
# Fresh directories: a recording named for the same second must not exist yet.
wf_runtime="$tmp_dir/wf-runtime"
wf_recordings="$tmp_dir/wf-recordings"
mkdir -p "$wf_runtime" "$wf_recordings"
wf_args="$tmp_dir/wf-args"
wf_pid="$tmp_dir/wf-pid"
XDG_RUNTIME_DIR="$wf_runtime" OMARCHY_TEST_WF_ARGS="$wf_args" OMARCHY_TEST_WF_PID="$wf_pid" \
OMARCHY_SCREENRECORD_DIR="$wf_recordings" \
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1
pkill -f "$stub_bin/wf-recorder" 2>/dev/null || true
[[ -s $wf_args ]] || fail "wf-recorder records when gpu-screen-recorder cannot start"
grep -Fxq -- '-o' "$wf_args" && grep -Fxq DP-1 "$wf_args" && grep -Fxq 48000 "$wf_args" ||
fail "wf-recorder records the focused monitor at 48 kHz" "$(<"$wf_args")"
[[ $(<"$wf_runtime/omarchy-screenrecord-pid") == "$(<"$wf_pid")" ]] ||
fail "the recorder pid is the wf-recorder that is recording" "$(ls -a "$wf_runtime")"
[[ $(<"$wf_runtime/omarchy-screenrecord-filename") == "$wf_recordings"/* ]] ||
fail "the wf-recorder recording is the one recorded as started"
pass "wf-recorder records when gpu-screen-recorder cannot start"
# Without wf-recorder, a recorder that cannot start leaves nothing behind.
rm "$stub_bin/wf-recorder"
none_runtime="$tmp_dir/none-runtime"
none_recordings="$tmp_dir/none-recordings"
mkdir -p "$none_runtime" "$none_recordings"
echo 424242 >"$none_runtime/omarchy-screenrecord-pid"
XDG_RUNTIME_DIR="$none_runtime" OMARCHY_SCREENRECORD_DIR="$none_recordings" \
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1 || true
[[ ! -e $none_runtime/omarchy-screenrecord-pid && ! -e $none_runtime/omarchy-screenrecord-filename ]] ||
fail "a recorder that cannot start records no state, and clears a stale pid" "$(ls -a "$none_runtime")"
pass "a recorder that cannot start records no state, and clears a stale pid"
# Another recorder runs in both cases below: the helper answers for any
# selection but a pid, and a stop by name ends it.
helper_calls="$tmp_dir/helper-calls"
recording_flag="$tmp_dir/recording"
cat >"$stub_bin/omarchy-capture-screenrecording-process" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_HELPER_CALLS"
if [[ $1 == "--pid" ]]; then
[[ $2 == "${OMARCHY_TEST_LIVE_PID:-}" ]]
exit
fi
if [[ $* == *"--signal INT" ]]; then
rm -f "$OMARCHY_TEST_RECORDING"
exit 0
fi
[[ -e $OMARCHY_TEST_RECORDING ]]
SH
chmod +x "$stub_bin/omarchy-capture-screenrecording-process"
# A saved pid that is no longer a recorder is a recording that ended without a
# stop: nothing of ours records, so stop has nothing to do and signals no other
# recorder the user runs.
stale_runtime="$tmp_dir/stale-runtime"
mkdir -p "$stale_runtime"
echo 424242 >"$stale_runtime/omarchy-screenrecord-pid"
touch "$recording_flag"
: >"$helper_calls"
if XDG_RUNTIME_DIR="$stale_runtime" OMARCHY_TEST_HELPER_CALLS="$helper_calls" OMARCHY_TEST_RECORDING="$recording_flag" \
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
"$ROOT/bin/omarchy-capture-screenrecording" --stop-recording >/dev/null 2>&1; then
fail "stop finds no recording of ours behind a stale pid" "$(<"$helper_calls")"
fi
! grep -q -- '--signal' "$helper_calls" ||
fail "stop signals nothing when the saved pid is stale" "$(<"$helper_calls")"
[[ -e $recording_flag ]] || fail "the other recorder keeps recording"
pass "a stale saved pid leaves every other recorder alone"
# Without a saved pid (a recording started before the pid was saved), status
# and stop select every recorder the user runs, as the bar and the menu do.
legacy_runtime="$tmp_dir/legacy-runtime"
mkdir -p "$legacy_runtime"
touch "$recording_flag"
: >"$helper_calls"
XDG_RUNTIME_DIR="$legacy_runtime" OMARCHY_TEST_HELPER_CALLS="$helper_calls" OMARCHY_TEST_RECORDING="$recording_flag" \
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
"$ROOT/bin/omarchy-capture-screenrecording" --stop-recording >/dev/null 2>&1 ||
fail "stop finds a recording started before the pid was saved" "$(<"$helper_calls")"
grep -Fxq -- '--signal INT' "$helper_calls" ||
fail "stop signals the user's recorders when no pid was saved" "$(<"$helper_calls")"
! grep -Fq -- '--signal KILL' "$helper_calls" ||
fail "a recorder that stops on INT is not killed" "$(<"$helper_calls")"
pass "without a saved pid, status and stop select the user's recorders"
# The bar indicator and the menu's Stop row ask --status, so they show a stop
# exactly when the toggle has a recording of ours to end. It needs no
# recordings directory, never notifies and never signals.
status() {
XDG_RUNTIME_DIR="$1" OMARCHY_TEST_HELPER_CALLS="$helper_calls" OMARCHY_TEST_RECORDING="$recording_flag" \
OMARCHY_TEST_LIVE_PID="${2:-}" OMARCHY_SCREENRECORD_DIR="$tmp_dir/no-such-recordings" \
"$ROOT/bin/omarchy-capture-screenrecording" --status >/dev/null 2>&1
}
touch "$recording_flag"
: >"$helper_calls"
rm -f "$OMARCHY_TEST_NOTIFICATION_ARGS"
echo 424242 >"$stale_runtime/omarchy-screenrecord-pid"
if status "$stale_runtime"; then fail "--status reports nothing of ours behind a stale pid"; fi
status "$legacy_runtime" || fail "--status reports a recording started before the pid was saved"
status "$stale_runtime" 424242 || fail "--status reports the recorder its saved pid names"
rm -f "$recording_flag"
if status "$legacy_runtime"; then fail "--status reports nothing when no recorder runs"; fi
! grep -q -- '--signal' "$helper_calls" || fail "--status signals nothing" "$(<"$helper_calls")"
[[ ! -e $OMARCHY_TEST_NOTIFICATION_ARGS ]] || fail "--status never notifies, even without a recordings directory" "$(<"$OMARCHY_TEST_NOTIFICATION_ARGS")"
pass "--status answers what stop would act on, without a recordings directory, notifications or signals"
@@ -0,0 +1,110 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
# wf-recorder takes one audio device, so desktop and microphone audio together
# go through a throwaway null sink. A source whose loopback won't load is left
# out with a notification naming it, never recorded as silence.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
awk '
/^(wf_audio_device|wf_audio_unavailable|wf_audio_cleanup)\(\) \{/ { copying = 1 }
copying { print }
/^}/ { copying = 0 }
' "$ROOT/bin/omarchy-capture-screenrecording" >"$tmp/functions"
mkdir -p "$tmp/bin"
# Modules number from 10 up; a load fails when its arguments name a source
# listed in $tmp/refuse.
cat >"$tmp/bin/pactl" <<'SH'
#!/bin/bash
echo "pactl $*" >>"$TMP/calls"
case $1 in
get-default-sink) [[ -e $TMP/no-sink ]] || echo speakers ;;
get-default-source) [[ -e $TMP/no-source ]] || echo mic ;;
load-module)
for refused in $(cat "$TMP/refuse" 2>/dev/null); do
[[ " $* " != *" $refused "* ]] || exit 1
done
n=$(( $(cat "$TMP/next" 2>/dev/null || echo 10) ))
echo $(( n + 1 )) >"$TMP/next"
echo "$n"
;;
esac
SH
cat >"$tmp/bin/omarchy-notification-send" <<'SH'
#!/bin/bash
echo "notify $*" >>"$TMP/notes"
echo "noise on stdout"
SH
chmod +x "$tmp/bin"/*
device() {
rm -f "$tmp/calls" "$tmp/notes" "$tmp/next" "$tmp/modules"
TMP=$tmp PATH="$tmp/bin:$PATH" DESKTOP_AUDIO=$1 MICROPHONE_AUDIO=$2 PA_MODULES_FILE=$tmp/modules \
bash -c 'source "$TMP/functions"; wf_audio_device' >"$tmp/out"
}
loaded() {
grep -c 'load-module' "$tmp/calls" || true
}
unloaded() {
grep 'unload-module' "$tmp/calls" | awk '{ print $3 }' | tr '\n' ' '
}
: >"$tmp/refuse"
device true true
[[ $(cat "$tmp/out") == omarchy_screenrec.monitor ]] || fail "both sources record through the null sink" "$(cat "$tmp/out")"
[[ $(cat "$tmp/modules") == $'10\n11\n12' ]] || fail "all three module ids are kept for stop" "$(cat "$tmp/modules")"
[[ ! -e $tmp/notes ]] || fail "a full mix notifies nothing" "$(cat "$tmp/notes")"
pass "desktop and microphone audio record together through the null sink"
echo "source=mic" >"$tmp/refuse"
device true true
[[ $(cat "$tmp/out") == speakers.monitor ]] || fail "a microphone that won't mix leaves desktop audio" "$(cat "$tmp/out")"
[[ $(unloaded) == "11 10 " && ! -e $tmp/modules ]] || fail "the modules that loaded are unloaded" "$(cat "$tmp/calls")"
grep -q 'notify .*microphone audio' "$tmp/notes" && ! grep -q 'desktop' "$tmp/notes" ||
fail "the dropped microphone is named" "$(cat "$tmp/notes" 2>/dev/null)"
pass "a microphone that won't mix is dropped, named, and desktop audio records alone"
echo "source=speakers.monitor" >"$tmp/refuse"
device true true
[[ $(cat "$tmp/out") == mic ]] || fail "desktop audio that won't mix leaves the microphone" "$(cat "$tmp/out")"
[[ $(unloaded) == "11 10 " ]] || fail "the modules that loaded are unloaded" "$(cat "$tmp/calls")"
grep -q 'notify .*desktop audio' "$tmp/notes" && ! grep -q 'microphone' "$tmp/notes" ||
fail "the dropped desktop audio is named" "$(cat "$tmp/notes" 2>/dev/null)"
pass "desktop audio that won't mix is dropped, named, and the microphone records alone"
echo "source=speakers.monitor source=mic" >"$tmp/refuse"
device true true
[[ ! -s $tmp/out ]] || fail "with neither source there is no audio device" "$(cat "$tmp/out")"
[[ $(grep -c '^notify' "$tmp/notes") == 2 ]] || fail "both dropped sources are named" "$(cat "$tmp/notes")"
pass "with neither loopback the recording has no audio track rather than a silent one, and says so"
echo "module-null-sink" >"$tmp/refuse"
device true true
[[ $(cat "$tmp/out") == speakers.monitor && $(loaded) == 1 ]] || fail "without the null sink desktop audio records alone" "$(cat "$tmp/out")"
grep -q 'notify .*microphone audio' "$tmp/notes" || fail "without the null sink the microphone is named" "$(cat "$tmp/notes" 2>/dev/null)"
pass "without the null sink, desktop audio records alone and the microphone is named"
: >"$tmp/refuse"
touch "$tmp/no-source"
device true true
[[ $(cat "$tmp/out") == speakers.monitor && $(loaded) == 0 ]] || fail "without a default source desktop audio records alone" "$(cat "$tmp/out")"
grep -q 'notify .*microphone audio' "$tmp/notes" || fail "a missing default source is named" "$(cat "$tmp/notes" 2>/dev/null)"
device false true
[[ ! -s $tmp/out ]] && grep -q 'notify .*microphone audio' "$tmp/notes" || fail "a microphone-only recording without a source says so" "$(cat "$tmp/out")"
rm "$tmp/no-source"
pass "a requested source with no default device is named"
device true false
[[ $(cat "$tmp/out") == speakers.monitor && ! -e $tmp/notes ]] || fail "desktop audio alone records the sink monitor" "$(cat "$tmp/out")"
device false true
[[ $(cat "$tmp/out") == mic && ! -e $tmp/notes ]] || fail "microphone audio alone records the source" "$(cat "$tmp/out")"
device false false
[[ ! -s $tmp/out && ! -e $tmp/calls ]] || fail "no audio asks pactl nothing" "$(cat "$tmp/calls")"
pass "a single source records directly and no audio asks for nothing"
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# The omarchy-settings recipe ships the same files on every architecture when
# the source has default/settings-runtime-profile, which lists the files that
# decide at runtime whether they apply. Every file it lists must exist.
profile="$ROOT/default/settings-runtime-profile"
[[ -f $profile ]] || fail "the source marks itself for the omarchy-settings recipe"
listed=$(grep -oE '(^|[[:space:]])(etc|default)/[^ ,]*' "$profile" | sed 's/^[[:space:]]*//' | sort -u)
[[ -n $listed ]] || fail "the runtime profile lists its files"
while IFS= read -r path; do
[[ -e $ROOT/${path%/} ]] || fail "the runtime profile lists an existing file: $path"
done <<<"$listed"
pass "every file the runtime profile lists exists in the source"
# The files that decide at runtime, and what they decide it from.
grep -q 'omarchy-hw-platform' "$ROOT/etc/mkinitcpio.conf.d/00-omarchy-hooks.conf" ||
fail "the HOOKS baseline asks the platform"
grep -q 'modinfo' "$ROOT/etc/mkinitcpio.conf.d/thunderbolt_module.conf" ||
fail "thunderbolt is added only where the kernel has the module"
grep -qx zram-generator "$ROOT/install/omarchy-aarch64.packages" ||
fail "aarch64 installs the zram generator the zram drop-in needs"
pass "the profile's runtime decisions are in place"
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir "$work/bin"
cat >"$work/bin/cat" <<'STUB'
#!/bin/bash
if [[ $1 == /sys/class/dmi/id/product_name ]]; then
[[ ${DMI_STATUS:-0} == 0 ]] || exit "$DMI_STATUS"
printf '%s\n' "${DMI_MODEL:-Unknown}"
else
/usr/bin/cat "$@"
fi
STUB
cat >"$work/bin/omarchy-pkg-add" <<'STUB'
#!/bin/bash
echo "$*" >>"$SPI_CALLS"
STUB
cat >"$work/bin/sudo" <<'STUB'
#!/bin/bash
# No host filesystem or kernel mutations in this fixture.
exit 0
STUB
chmod +x "$work/bin/"*
export SPI_CALLS="$work/calls"
for failure in 1 13; do
DMI_STATUS=$failure PATH="$work/bin:$PATH" bash -eE "$ROOT/install/hardware/apple/fix-spi-keyboard.sh"
[[ ! -f $SPI_CALLS ]] || fail 'missing/unreadable DMI must not install SPI drivers'
done
DMI_MODEL=MacBookPro13,2 PATH="$work/bin:$PATH" bash -eE "$ROOT/install/hardware/apple/fix-spi-keyboard.sh"
grep -Fxq macbook12-spi-driver-dkms "$SPI_CALLS" || fail 'matching Intel Mac retains SPI driver'
pass 'missing/unreadable DMI survives errexit and matching Intel Macs retain drivers'
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
# omarchy update's boot check through the real omarchy-update-boot, dispatcher
# and platform detector, inside the sudo boundary fixture: every other update
# step is a stub that records itself, and sudo records its arguments. A fake
# boot package's update-verify records itself and exits with the status it is
# given.
require_platform_fixtures "omarchy update's boot check on platform fixtures"
copy_boundary_file bin/omarchy-update
rm "$SUDO_TEST_ROOT/bin/omarchy-update-boot"
for command in omarchy-update-boot omarchy-lifecycle-dispatch omarchy-hw-platform; do
ln -sfn "$ROOT/bin/$command" "$SUDO_TEST_ROOT/bin/$command"
done
export OMARCHY_UPDATE_LOGGED=1
tmp=$boundary_tmp/boot
mkdir -p "$tmp"
for platform in aarch64-apple aarch64 x86; do
fake_platform "$tmp/$platform" "$platform"
done
# A lifecycle root whose update-verify exits with $2.
boot_package() {
local lifecycle=$1 verify=$2
rm -rf "$lifecycle"
mkdir -p "$lifecycle/usr/lib/omarchy/mac-boot"
printf '#!/bin/bash\necho update-verify >>%q\necho "the boot files do not match" >&2\nexit %s\n' "$tmp/boot-ran" "$verify" \
>"$lifecycle/usr/lib/omarchy/mac-boot/update-verify"
chmod 755 "$lifecycle"/usr/lib/omarchy/mac-boot/*
chmod -R go-w "$lifecycle"
}
boot_package "$tmp/passing" 0
boot_package "$tmp/failing" 9
boot_package "$tmp/unverified" 1
mkdir -p "$tmp/none" "$tmp/older/usr/lib/omarchy/mac-boot" "$tmp/older/var/lib/pacman/local/omarchy-mac-boot-20260921-10"
# omarchy update -y on platform $1 with the boot package in lifecycle root $2.
# The update's own PATH is fixed, so the platform's uname goes beside the
# stubbed steps.
run_update() {
local platform=$1 lifecycle=$2
reset_boundary
rm -f "$tmp/boot-ran"
cp "$tmp/$platform/bin/uname" "$SUDO_TEST_ROOT/bin/uname"
status=0
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$lifecycle" \
"$SUDO_TEST_ROOT/bin/omarchy-update" -y >"$tmp/out" 2>"$tmp/err" || status=$?
}
# The line number of the first event in the sudo log that starts with $1.
at() {
grep -n -m 1 -F -- "$1" "$SUDO_TEST_LOG" | cut -d: -f1
}
reboot_offered() {
grep -q '^step:omarchy-update-restart --reboot-only' "$SUDO_TEST_LOG"
}
# x86 and plain aarch64: the boot check is a no-op, even with a
# failing boot-package entrypoint on disk, and nothing asks for root.
for platform in x86 aarch64; 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")"
! grep -q 'omarchy-lifecycle-dispatch' "$SUDO_TEST_LOG" || fail "$platform: the boot check asks for no root" "$(cat "$SUDO_TEST_LOG")"
[[ ! -s $tmp/err ]] || fail "$platform: the boot check prints nothing" "$(cat "$tmp/err")"
reboot_offered || fail "$platform: the reboot is offered" "$(cat "$SUDO_TEST_LOG")"
assert_boundary_cold "$platform update"
done
pass "x86 and plain aarch64 updates are unchanged: no boot check runs, nothing asks for root and the reboot is offered"
# Apple: verify runs once the last package step, AUR, is done, cold through
# the no-update wrapper, before the reboot offer.
run_update aarch64-apple "$tmp/passing"
(( status == 0 )) || fail "apple: an update whose boot check passes reports success" "status $status: $(cat "$tmp/err")"
[[ $(cat "$tmp/boot-ran") == "update-verify" ]] || fail "apple: verify runs once" "$(cat "$tmp/boot-ran")"
aur=$(at 'step:omarchy-update-aur-pkgs') verify=$(at 'sudo -N omarchy-lifecycle-dispatch update-verify') hook=$(at 'step:omarchy-hook post-update')
[[ -n $aur && -n $verify && -n $hook ]] && (( hook < aur && aur < verify )) ||
fail "apple: verify follows AUR through the no-update wrapper" "$(cat "$SUDO_TEST_LOG")"
! grep -q '^sudo omarchy-lifecycle-dispatch update-verify' "$SUDO_TEST_LOG" || fail "apple: verification never uses a reusable timestamp" "$(cat "$SUDO_TEST_LOG")"
[[ $(sed -n "$((verify - 1))p;$((verify + 1))p" "$SUDO_TEST_LOG") == $'sudo -k\nsudo -k' ]] ||
fail "apple: verification starts cold after AUR and sudo is revoked right after it" "$(cat "$SUDO_TEST_LOG")"
reboot_offered || fail "apple: a verified update offers the reboot"
assert_boundary_cold "apple update"
pass "apple: verify runs as root through the boot package, and a verified update offers the reboot"
# omarchy-update-boot takes no operation, so a caller still asking for a
# preflight is a usage error rather than a verification.
status=0
"$SUDO_TEST_ROOT/bin/omarchy-update-boot" preflight >/dev/null 2>&1 || status=$?
(( status == 2 )) || fail "omarchy-update-boot refuses an argument" "status $status"
pass "omarchy-update-boot refuses an argument"
# A failed verification lets the update finish its remaining steps, then fails
# it without offering the reboot.
run_update aarch64-apple "$tmp/unverified"
(( status == 1 )) || fail "apple: a failed verification fails the update" "status $status: $(cat "$tmp/err")"
! reboot_offered || fail "apple: a failed verification offers no reboot"
grep -q '^step:omarchy-update-stay-awake stop' "$SUDO_TEST_LOG" ||
fail "apple: after a failed verification Stay Awake is released" "$(cat "$SUDO_TEST_LOG")"
grep -q 'the boot files do not match' "$tmp/err" && grep -q 'The update is not finished' "$tmp/err" ||
fail "apple: a failed verification says why, and that the update is not finished" "$(cat "$tmp/err")"
! grep -q 'Something went wrong during the update' "$tmp/out" "$tmp/err" || fail "apple: a failed verification is not reported as a crash"
assert_boundary_cold "apple failed verification"
pass "apple: a failed verification fails the update, explained, with no reboot offered"
# A Mac without the boot package at all predates it: the update warns that its
# boot files were not verified and finishes, asking for no root.
run_update aarch64-apple "$tmp/none"
(( status == 0 )) && reboot_offered || fail "apple without the boot package: the update finishes and offers the reboot" "status $status: $(cat "$tmp/err")"
grep -q 'update-verify on aarch64-apple needs omarchy-mac-boot' "$tmp/err" && grep -q 'The boot files were not verified' "$tmp/err" ||
fail "apple without the boot package: the update says the boot files were not verified" "$(cat "$tmp/err")"
! grep -q 'omarchy-lifecycle-dispatch' "$SUDO_TEST_LOG" || fail "apple without the boot package: nothing asks for root"
pass "apple: without the boot package the update warns that the boot files were not verified"
# A boot package from before update-verify is one package update away.
run_update aarch64-apple "$tmp/older"
(( status == 1 )) && ! reboot_offered || fail "apple: a boot package without update-verify fails the update" "status $status: $(cat "$tmp/err")"
grep -q 'which omarchy-mac-boot 20260921-10 does not provide; update omarchy-mac-boot' "$tmp/err" ||
fail "apple: a boot package without update-verify is named with its version" "$(cat "$tmp/err")"
pass "apple: a boot package without update-verify fails the update and asks for its update"
+1
View File
@@ -79,6 +79,7 @@ for command in \
omarchy-update-aur-pkgs \
omarchy-update-mise \
omarchy-update-orphan-pkgs \
omarchy-update-boot \
omarchy-hook \
omarchy-update-analyze-logs \
omarchy-shell \
+82 -1
View File
@@ -52,14 +52,32 @@ chmod +x "$stub_bin/sudo" "$stub_bin/systemd-run" "$stub_bin/pacman"
replaced="$test_tmp/replaced"
# Every case says which platform it runs on (x86 unless it sets PLATFORM), and
# which boot package entrypoints are installed (none unless it sets
# LIFECYCLE_ROOT), rather than inherit the machine running the suite.
for platform in x86 aarch64-apple; do
fake_platform "$test_tmp/$platform" "$platform"
done
mkdir -p "$test_tmp/no-boot-package"
# Root ignores both fixtures and would ask the machine's own boot package, so
# as root the cases before the platform ones get a dispatcher that registers
# nothing, and the platform ones are skipped.
if (( EUID == 0 )); then
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/omarchy-lifecycle-dispatch"
chmod +x "$stub_bin/omarchy-lifecycle-dispatch"
fi
run_update() {
local platform=${PLATFORM:-x86}
OMARCHY_REPLACED_DIR="$replaced" \
RETRY_FAILS="${RETRY_FAILS:-}" \
RETRY_INSTALLS="${RETRY_INSTALLS:-}" \
PACMAN_ATTEMPTS="$test_tmp/attempts" \
CONFLICT_REPORT="$test_tmp/report" \
OWNED_PATHS="${OWNED_PATHS:-}" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
OMARCHY_PROC_ROOT="$test_tmp/$platform/proc" \
OMARCHY_LIFECYCLE_ROOT="${LIFECYCLE_ROOT:-$test_tmp/no-boot-package}" \
PATH="$stub_bin:$test_tmp/$platform/bin:$ROOT/bin:$PATH" \
bash "$ROOT/bin/omarchy-update-system-pkgs"
}
@@ -282,3 +300,66 @@ run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
[[ $(cat "$test_tmp/attempts") == 2 ]] ||
fail "a clean upgrade runs more than one pacman transaction"
pass "a clean upgrade runs a single pacman transaction"
# A platform whose boot package owns files no package does vouches for a
# takeover before anything moves. Only a boot package's answer lets it through.
require_platform_fixtures "the platform's say over a takeover"
boot_package="$test_tmp/boot-package"
mkdir -p "$boot_package/usr/lib/omarchy/mac-boot"
cat >"$boot_package/usr/lib/omarchy/mac-boot/update-takeover" <<SH
#!/bin/bash
printf '%s\\n' "\$@" >"$test_tmp/vouched"
exit "\$(cat "$test_tmp/takeover-status")"
SH
chmod 755 "$boot_package/usr/lib/omarchy/mac-boot/update-takeover"
chmod -R go-w "$boot_package"
fresh_work
echo "ours" >"$stray"
write_report omarchy-settings "$stray"
echo 0 >"$test_tmp/takeover-status"
rm -f "$test_tmp/vouched"
PLATFORM=aarch64-apple LIFECYCLE_ROOT="$boot_package" run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
fail "a takeover the boot package vouches for goes ahead" "$(cat "$test_tmp/err")"
[[ ! -e $stray && -f $replaced$stray && $(cat "$test_tmp/vouched") == "$stray" ]] ||
fail "the boot package is asked about exactly the files that move"
pass "apple: a takeover the boot package vouches for goes ahead"
fresh_work
echo "ours" >"$stray"
write_report omarchy-settings "$stray"
echo 1 >"$test_tmp/takeover-status"
if PLATFORM=aarch64-apple LIFECYCLE_ROOT="$boot_package" run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
fail "a takeover the boot package refuses stops the upgrade"
fi
[[ -f $stray && ! -e $replaced$stray && $(cat "$test_tmp/attempts") == 1 ]] ||
fail "a refused takeover moves nothing and retries nothing"
grep -Fq "$stray" "$test_tmp/err" || fail "a refused takeover names the files it keeps" "$(cat "$test_tmp/err")"
pass "apple: a takeover the boot package refuses moves nothing"
# A Mac without its boot package, or with one too old to answer, keeps the files.
older="$test_tmp/older-boot-package"
mkdir -p "$older/usr/lib/omarchy/mac-boot" "$older/var/lib/pacman/local/omarchy-mac-boot-20260921-10"
for root in "$test_tmp/no-boot-package" "$older"; do
fresh_work
echo "ours" >"$stray"
write_report omarchy-settings "$stray"
if PLATFORM=aarch64-apple LIFECYCLE_ROOT="$root" run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
fail "a Mac whose boot package can't answer keeps the files" "$root"
fi
[[ -f $stray && ! -e $replaced$stray ]] || fail "a Mac whose boot package can't answer moves nothing" "$root"
grep -Fq omarchy-mac-boot "$test_tmp/err" || fail "the refusal names the boot package" "$(cat "$test_tmp/err")"
done
pass "apple: a Mac whose boot package can't answer keeps the files"
# x86 with Mac entrypoints on disk still takes over as before, asking no one.
fresh_work
echo "ours" >"$stray"
write_report omarchy-settings "$stray"
echo 1 >"$test_tmp/takeover-status"
rm -f "$test_tmp/vouched"
LIFECYCLE_ROOT="$boot_package" run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
fail "x86 takes over as before" "$(cat "$test_tmp/err")"
[[ ! -e $stray && ! -e $test_tmp/vouched ]] || fail "x86 asks no boot package"
pass "x86 takes over as before, asking no boot package"
+65 -1
View File
@@ -58,6 +58,22 @@ exit "${KEYRING_TEST_PKG_MISSING:-1}"
SH
chmod +x "$stub_bin/omarchy-pkg-missing"
# KEYRING_TEST_ARM=1: Arch Linux ARM's keyring is installed.
# KEYRING_TEST_PLATFORM: the platform (default x86).
cat >"$stub_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ($* == "archlinuxarm-keyring" && ${KEYRING_TEST_ARM:-0} == 1) || " ${KEYRING_TEST_INSTALLED:-} " == *" $* "* ]]
SH
chmod +x "$stub_bin/omarchy-pkg-present"
cat >"$stub_bin/omarchy-hw-aarch64" <<'SH'
#!/bin/bash
[[ ${KEYRING_TEST_PLATFORM:-x86} == aarch64* ]]
SH
chmod +x "$stub_bin/omarchy-hw-aarch64"
cat >"$stub_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
@@ -66,11 +82,17 @@ exit 0
SH
chmod +x "$stub_bin/omarchy-pkg-add"
# The platform's keyrings list is read at a fixed path; the test runs a copy
# rewritten to read a fixture there.
platform_keyrings="$test_tmp/platform-keyrings"
sed "s|/usr/share/omarchy-platform/keyrings|$platform_keyrings|" "$ROOT/bin/omarchy-update-keyring" >"$test_tmp/omarchy-update-keyring"
chmod +x "$test_tmp/omarchy-update-keyring"
run_keyring() {
KEYRING_TEST_LOG="$log_file" \
KEYRING_TEST_DIR="$test_tmp" \
PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-update-keyring" "$@"
"$test_tmp/omarchy-update-keyring" "$@"
}
# Everything healthy: the key and package are present, the reinstall works.
@@ -86,6 +108,48 @@ grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring$' "$log_file" ||
fail "update-keyring still reinstalls archlinux-keyring" "$(cat "$log_file")"
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 aarch64-apple aarch64; do
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_ARM=1 KEYRING_TEST_PLATFORM=$platform run_keyring >"$test_tmp/arm.out"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring\tarchlinuxarm-keyring$' "$log_file" ||
fail "update-keyring also reinstalls Arch Linux ARM's keyring on $platform" "$(cat "$log_file")"
done
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_ARM=1 run_keyring >"$test_tmp/x86.out"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring$' "$log_file" ||
fail "x86 reinstalls only Arch's keyring, even with Arch Linux ARM's installed" "$(cat "$log_file")"
pass "aarch64 machines also reinstall Arch Linux ARM's keyring where it is installed, x86 never"
# The platform package's list adds the installed keyrings it names: Apple
# Silicon's [asahi-alarm], with no platform branch here.
printf '%s\n' '# Asahi' '' asahi-alarm-keyring archlinuxarm-keyring other-keyring asahi-alarm-keyring >"$platform_keyrings"
printf 'asahi-alarm-keyring' >>"$platform_keyrings"
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_ARM=1 KEYRING_TEST_PLATFORM=aarch64-apple KEYRING_TEST_INSTALLED=asahi-alarm-keyring run_keyring >"$test_tmp/platform.out"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring\tarchlinuxarm-keyring\tasahi-alarm-keyring$' "$log_file" ||
fail "update-keyring reinstalls the installed keyrings the platform names, once each" "$(cat "$log_file")"
pass "update-keyring reinstalls the installed keyrings the platform names, once each"
for listed in 'asahi-alarm-keyring;reboot' '../asahi-alarm-keyring' 'asahi-alarm' '-Syu-keyring' 'asahi-alarm-keyring extra'; do
printf '%s\n%s\n' "$listed" asahi-alarm-keyring >"$platform_keyrings"
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_INSTALLED=asahi-alarm-keyring run_keyring >"$test_tmp/invalid.out" 2>&1 ||
fail "update-keyring carries on past a platform list line naming $listed" "$(cat "$test_tmp/invalid.out")"
grep -qF "skipping $listed" "$test_tmp/invalid.out" ||
fail "update-keyring warns about $listed" "$(cat "$test_tmp/invalid.out")"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring\tasahi-alarm-keyring$' "$log_file" ||
fail "update-keyring skips $listed and still reinstalls the valid keyrings, and nothing else" "$(cat "$log_file")"
done
rm -f "$platform_keyrings"
pass "update-keyring skips a platform list line naming anything but a keyring package, with a warning"
# Key and package missing: the full populate path runs and verifies at the end.
: >"$log_file"
rm -f "$test_tmp/list-calls"
+1
View File
@@ -68,6 +68,7 @@ for command in \
omarchy-update-aur-pkgs \
omarchy-update-mise \
omarchy-update-orphan-pkgs \
omarchy-update-boot \
omarchy-hook \
omarchy-update-analyze-logs \
omarchy-shell \
+2
View File
@@ -18,6 +18,7 @@ steps=(
omarchy-snapshot
omarchy-update-stay-awake
omarchy-update-dev
omarchy-update-boot
omarchy-update-keyring
omarchy-update-system-pkgs
omarchy-migrate
@@ -79,6 +80,7 @@ expected_steps() {
omarchy-hook \
omarchy-update-mise \
omarchy-update-aur-pkgs \
omarchy-update-boot \
omarchy-update-stay-awake \
omarchy-update-restart
}
+35
View File
@@ -965,3 +965,38 @@ touch "$TEST_GUARD_ACTIVE"
bash -euo pipefail "$ROOT/migrations/1790608617.sh" >/dev/null
grep -Fqx 'migrate-setup <--yes>' "$calls" || fail "migration repairs an active daemon without a watcher"
pass "USB migration repairs enabled protection and preserves opt-out"
# Boot-time denial writes Limine's own configuration, which a Mac's boot
# package owns: it is refused there before anything changes. Root's platform
# check reads this machine, never the fixture, so this runs unprivileged only.
if (( EUID == 0 )); then
skip "running as root, where the platform check ignores its fixture; skipping the Apple Silicon refusal"
else
fake_platform "$scratch/apple" aarch64-apple
mkdir -p "$scratch/apple-stubs"
printf '#!/bin/bash\necho "sudo $*" >>"%s"\n' "$scratch/apple-sudo" >"$scratch/apple-stubs/sudo"
chmod +x "$scratch/apple-stubs/sudo"
status=0
OMARCHY_PROC_ROOT="$scratch/apple/proc" PATH="$scratch/apple/bin:$scratch/apple-stubs:$ROOT/bin:$PATH" \
"$scratch/setup" --boot --yes >"$scratch/apple-out" 2>&1 || status=$?
(( status == 2 )) || fail "Apple Silicon refuses USB authorization from boot" "$(cat "$scratch/apple-out")"
grep -Fq "not available on Apple Silicon" "$scratch/apple-out" || fail "the refusal says why" "$(cat "$scratch/apple-out")"
[[ ! -e $scratch/apple-sudo ]] || fail "the refusal changes nothing" "$(cat "$scratch/apple-sudo")"
pass "USB authorization from boot is refused on Apple Silicon before anything changes"
fi
# On Apple Silicon the boot helper never touches Limine: enabling is refused,
# disabling what is not configured succeeds (so USB authorization can still be
# turned off), and a configuration from elsewhere is left with removal steps.
(
drop_in=$scratch/apple-boot/usb-authorization.conf
limine_defaults=$scratch/apple-boot/limine
mkdir -p "$scratch/apple-boot"
status=0; usb_authorization_apple_boot enable 2>/dev/null || status=$?
(( status == 2 )) || fail "Apple Silicon refuses boot-time USB authorization in the helper"
usb_authorization_apple_boot disable || fail "a clean Mac disables boot-time USB authorization as a no-op"
echo "$setting" >"$drop_in"
if usb_authorization_apple_boot disable 2>"$scratch/apple-boot/err"; then fail "an inherited boot-time setting is not claimed removed"; fi
grep -Fq "Remove it by hand" "$scratch/apple-boot/err" || fail "the inherited setting comes with removal steps"
)
pass "the boot helper leaves a Mac's boot files alone and still lets USB authorization be turned off"
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
lib_path="$test_tmp/lib/voxtype"
calls="$test_tmp/calls.log"
mkdir -p "$stub_bin" "$lib_path"
for command in gum omarchy-pkg-drop omarchy-pkg-add hyprctl omarchy-restart-shell omarchy-notification-send systemctl; do
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/$command"
done
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/omarchy-pkg-present"
# Voxtype is already installed, so the install asks nothing and only sets it up.
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/omarchy-cmd-missing"
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/omarchy-dictation-use"
cat >"$stub_bin/omarchy-hw-vulkan" <<'SH'
#!/bin/bash
(( ${VULKAN:-1} == 1 ))
SH
cat >"$stub_bin/voxtype" <<'SH'
#!/bin/bash
printf 'voxtype %s\n' "$*" >>"$TEST_LOG"
SH
chmod +x "$stub_bin"/*
run_install() {
local vulkan=$1 backend=$2
: >"$calls"
rm -rf "$test_tmp/home" "$lib_path/voxtype-vulkan"
mkdir -p "$test_tmp/home"
case $backend in
executable)
printf '#!/bin/bash\n' >"$lib_path/voxtype-vulkan"
chmod +x "$lib_path/voxtype-vulkan"
;;
unusable)
: >"$lib_path/voxtype-vulkan"
;;
esac
HOME="$test_tmp/home" OMARCHY_PATH="$ROOT" OMARCHY_VOXTYPE_LIB_PATH="$lib_path" \
VULKAN="$vulkan" TEST_LOG="$calls" PATH="$stub_bin:$PATH" \
XDG_CONFIG_HOME="$test_tmp/home/.config" bash "$ROOT/bin/omarchy-install-dictation-voxtype" >/dev/null ||
fail "Voxtype install completes (vulkan=$vulkan backend=$backend)"
grep -Fx 'voxtype setup systemd' "$calls" >/dev/null ||
fail "Voxtype install sets up the user service (vulkan=$vulkan backend=$backend)"
}
run_install 1 executable
grep -Fx 'voxtype setup gpu --enable' "$calls" >/dev/null ||
fail "Voxtype enables the GPU backend when Vulkan and the backend binary are present"
pass "Voxtype enables the GPU backend when Vulkan and the backend binary are present"
run_install 1 missing
! grep -q 'setup gpu' "$calls" ||
fail "Voxtype does not enable a Vulkan backend the package did not ship"
pass "Voxtype does not enable a Vulkan backend the package did not ship"
run_install 1 unusable
! grep -q 'setup gpu' "$calls" ||
fail "Voxtype does not enable a Vulkan backend that is not executable"
pass "Voxtype does not enable a Vulkan backend that is not executable"
run_install 0 executable
! grep -q 'setup gpu' "$calls" ||
fail "Voxtype does not enable the GPU backend without a Vulkan runtime"
pass "Voxtype does not enable the GPU backend without a Vulkan runtime"
+9
View File
@@ -20,6 +20,15 @@ export OMARCHY_WINDOWS_DIR="$TMPDIR/win"
export HOME="$TMPDIR/home"
mkdir -p "$HOME"
# The command refuses any CPU but x86_64 before defining anything, and sourcing
# it there would exit this test with it. What is under test is the x86_64 path,
# so omarchy-hw-x86 answers as one.
STUB_BIN="$TMPDIR/bin"
mkdir -p "$STUB_BIN"
printf '#!/bin/bash\nexit 0\n' >"$STUB_BIN/omarchy-hw-x86"
chmod +x "$STUB_BIN/omarchy-hw-x86"
PATH="$STUB_BIN:$PATH"
set -- help
source "$ROOT/bin/omarchy-windows-vm" >/dev/null 2>&1
COMPOSE="$OMARCHY_WINDOWS_DIR/docker-compose.yml"
@@ -30,6 +30,13 @@ mount -t tmpfs -o uid=0,gid=0,mode=0710,size=1g home-alice /home/alice
export HOME=/home/alice
unset OMARCHY_WINDOWS_DIR
# The command refuses any CPU but x86_64 before defining anything, and sourcing
# it there would exit this test with it. What is under test is the x86_64 path,
# so omarchy-hw-x86 answers as one.
mkdir -p "$test_tmp/bin"
printf '#!/bin/bash\nexit 0\n' >"$test_tmp/bin/omarchy-hw-x86"
chmod +x "$test_tmp/bin/omarchy-hw-x86"
PATH="$test_tmp/bin:$PATH"
set -- help
source "$test_tmp/omarchy-windows-vm" >/dev/null 2>&1