Compare commits

...
144 Commits
Author SHA1 Message Date
David Heinemeier HanssonandClaude Opus 5 0631c0530a Stop shipping a second kernel nobody boots
The live ISO boots one kernel for every machine, and after the preceding commit
that kernel is linux-t2. Stock linux is then a second kernel nobody boots, plus
its own archiso initramfs, copied into both the ISO tree and the
size-constrained FAT EFI image — about 147MB.

It could not simply be deleted: releng's broadcom-wl hard-depends on linux and
is the only releng package that does, so pacman would pull the kernel back in.

broadcom-wl is a prebuilt module for stock linux and cannot load on linux-t2.
It has kept working until now only because of the bug the preceding commit
fixes — the ISO has been booting stock all along — so it is booting T2 that
takes it away, not this commit. Dropping the package only stops shipping
something that can no longer load. Nothing in the offline install needs Wi-Fi
in the live environment.

Both packages stay in the offline mirror and this changes nothing about
installed systems: linux comes from builder/archinstall.packages and remains
what configurator's detect_kernel picks for every machine that is not a T2 Mac,
and broadcom-wl comes from omarchy-other.packages, so install/hardware/
fix-bcm43xx.sh can still install it offline on BCM4360/BCM4331 MacBooks.

The removal is anchored so linux-t2, linux-firmware, linux-firmware-marvell and
linux-atm are untouched; verified against releng's real list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:27:53 -07:00
David Heinemeier HanssonandClaude Opus 5 ed710a3b41 Boot the T2 kernel the ISO says it boots
Every boot entry has named vmlinuz-linux-t2 since 0638855, but the file has
contained stock Arch. Verified against the shipped ISO:

  arch/boot/x86_64/vmlinuz-linux-t2:
    bzImage, version 7.1.5-arch1-1 (linux@archlinux)

while the mirror carries linux-t2-7.1.4-2. T2 MacBooks have been installing
with no keyboard or trackpad in the live environment.

Kernel packages ship nothing in /boot — only usr/lib/modules/<kver>/vmlinuz.
install_kernel() in /usr/share/libalpm/scripts/mkinitcpio sources
/etc/mkinitcpio.d/<pkgbase>.preset and copies THAT package's kernel to whatever
ALL_kver names, whenever is_kernelcopy() sees an unowned absolute path. Our file
was named linux.preset, so it overrode releng's copy of the same name and
pointed the STOCK kernel at the T2 filenames. linux-t2's own generated preset
writes those same two paths, and stock won.

c23e0e7 introduced it a week after 0638855 added T2 support, so the feature
worked for exactly seven days: before the preset existed, releng's linux.preset
kept the stock kernel on its own filenames and linux-t2's generated preset
produced the real thing.

The fix is the filename. releng's linux.preset takes over the stock kernel
again, which now writes /boot/vmlinuz-linux and initramfs-linux.img instead of
squatting on the T2 names — so the ISO grows by one kernel and one archiso
initramfs. That cost is not free and is worth revisiting: the stock kernel is
in the live root only because releng's broadcom-wl hard-depends on it, and
broadcom-wl is a prebuilt module for that kernel which cannot load on the T2
one we now boot. Dropping both is ~147MB and old Broadcom Macs lose live Wi-Fi.

Boot the ISO on a T2 Mac before trusting this; the version string is only
evidence that the right kernel ships, not that it works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 22:20:15 -07:00
David Heinemeier HanssonandClaude Opus 5 89245f44ec Warm the offline mirror while the user answers the wizard
The install reads ~3GB out of the offline mirror, and pacman consumes it at
only ~33MB/s (3.14GB of packages across the 95s the package phase is calibrated
at), so on any medium slower than that the install is read-bound. The ISO medium
is idle for the minutes the wizard takes, and the target disk written to later
is a different device, so reading ahead costs nothing that is otherwise in use.

Warms largest-first under a MemAvailable/2 budget. Deliberately no dependency
resolution: asking pacman exactly which files the install will read would copy
the package-set knowledge in phases_impl.py — the early list, the runtime
target, the kernel from the wizard's JSON — into a shell script that would drift
silently. The install reads roughly 69% of the mirror, so warming blindly is
close enough for a cache hint.

The page cache this fills is clean and reclaimable, so the failure mode under
memory pressure is wasted bandwidth rather than OOM. The budget stops small
machines evicting what was just warmed. The job ends when that budget is spent
rather than when the wizard exits, because the partitioning and format window
afterwards is also free medium time.

OMARCHY_NO_PREFETCH=1 turns it off so a single ISO can measure both arms against
the Installing Arch + Omarchy entry in /var/log/omarchy-install-timing.json.
Not yet measured on real USB media, and expected to be worth nothing in the VM
harness, where the ISO is backed by the host's page cache.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:16:16 -07:00
David Heinemeier HanssonandClaude Opus 5 e47ce379ad Drop gum from the orchestrator's progress lines
The dashboard owns the visible UI: it captures the orchestrator's stdout into
the support log and strips CSI sequences on the way in, so the colouring was
never seen by anyone. gum charged a Go process start per line for it, on every
phase and every package group. Plain writes keep the same indented shape.

Flushing stays: pacstrap and arch-chroot inherit this fd, so buffering would
reorder our lines against theirs in the log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:24:35 -07:00
David Heinemeier HanssonandClaude Opus 5 a8c3a671ea Compress the live root with zstd instead of xz
Squashfs decompresses on the page-fault path through a single stream
(CONFIG_SQUASHFS_DECOMP_SINGLE), where xz manages roughly a tenth of zstd's
throughput, and the live root is read cold on every boot: kernel, plymouth,
systemd, python, archinstall, gum. The offline mirror is already stored
uncompressed, so this only covers the part that still gets decompressed.

Costs well under a percent of ISO size. Dropping the x86 BCJ filter also
removes one of the blockers listed in plans/aarch64-support.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:24:35 -07:00
David Heinemeier HanssonandClaude Opus 5 c2b93afe01 Defer the target's UKI rebuild until the final Limine build
The existing hook mask only covers pacstrap. pacman.conf(5) HookDir is
absolute, so it never applied to the pacman runs inside arch-chroot, and during
omarchy-setup-system the target's own hooks are live. limine-entry-tool's
90-mkinitcpio-install.hook triggers on usr/lib/firmware/*, usr/src/*/dkms.conf
and usr/lib/modules/*/pkgbase, and collect_pkgbase_files sets rebuild_all for
any trigger outside usr/lib/modules — so it rebuilds the initramfs and UKI for
every installed kernel. The hardware scripts hit that routinely: sof-firmware
on Intel audio, nvidia-open-dkms, linux-ptl on Panther Lake, linux-t2 on Macs.

Every one of those builds is discarded. finalize_limine_boot runs limine-update
immediately after, which pipes "rebuild" into the same script and rebuilds
unconditionally. They are stale as well as redundant: hardware/nvidia.sh writes
its mkinitcpio drop-in after installing the driver.

Mask that one hook inside the target for the duration of the phase. The
kernel-removal hooks stay live — they only prune Limine entries, which is what
ptl-kernel.sh's "pacman -Rdd linux" needs.

A mask that survived would be invisible until a later kernel update shipped a
UKI-less boot, so it is restored in a finally, again in main()'s finally, and
validate_boot repairs and then insists the real hook is back before reboot.
limine-mkinitcpio-hook is a hard dependency of the Omarchy runtime package, so
that file is always on disk before the mask goes up.

Worth several seconds per avoided build on real hardware. It is invisible in
the VM harness, which installs no hardware packages, so measure it on an Intel
laptop rather than in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:24:26 -07:00
David Heinemeier HanssonandClaude Opus 5 436f37f926 Explain why archinstall's zram-generator.conf gets dropped
omarchy-settings now ships the tuning as a vendor drop-in that outranks the
main config file, so setup_swap's /etc copy decides nothing and is removed only
to keep /etc free for the local admin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LjeyQZsNBxqyYy9z8KaKm7
2026-07-25 14:00:52 -07:00
David Heinemeier HanssonandClaude Opus 5 529001d566 Validate the UKI that was actually built
Hardware packages like omarchy-hw-intel-ptl swap the kernel mid-install,
so the UKI lands under a name the configurator never asked for and boot
validation halted a perfectly good install. Read the kernels back off
/usr/lib/modules/*/pkgbase instead of trusting the requested name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 13:05:22 -07:00
David Heinemeier HanssonandClaude Opus 4.8 78824fe084 Skip archinstall's pre-format countdown
archinstall's FilesystemHandler.perform_filesystem_operations defaults to
show_countdown=True, which runs a ~4.5s "5...4...3...2...1" warning
(_final_warning) before wiping the disk. That prompt is meant for
archinstall's interactive TUI; in our orchestrated/dashboard install it
renders nowhere and just sleeps silently on every full-disk install.

Pass show_countdown=False, guarded by an introspection check so it stays
safe on newer archinstall releases that dropped both the countdown and the
parameter. Generalize _method_accepts_users into _method_accepts for the
check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 09:41:45 -07:00
David Heinemeier HanssonandClaude Opus 4.8 8653d07e06 Drop archinstall's zram-generator.conf before pacstrap
archinstall's setup_swap writes /etc/systemd/zram-generator.conf directly
into the target, but omarchy-settings now ships that file as a packaged
config. pacstrap then aborts the transaction on a file conflict
("zram-generator.conf exists in filesystem"), failing the install.

Delete archinstall's copy after setup_swap so omarchy-settings' packaged
config lands cleanly. We still keep the zram-generator package and service
that setup_swap installs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 07:54:04 -07:00
David Heinemeier HanssonandClaude Fable 5 5e80c917ea Skip signature checks for the offline mirror
Fresh installs on real hardware failed pacstrap with "keyring is not
writable" and "required key missing from keyring". The offline repo's
SigLevel = Optional TrustAll was not the bypass it appeared to be:
Optional verifies whenever a .sig file exists — and every package in the
offline mirror ships one — while pacstrap's pacman -r relocates the
install root but not GpgDir, so verification runs against the LIVE
keyring. That silently made every install depend on archiso's boot-time
pacman-init.service having finished: fast in QEMU, minutes-or-never on
real hardware, where pacstrap then raced it and lost.

The mirror ships inside the ISO, whose integrity is already covered by
the airootfs sha512 and the ISO signature, so pacman-level signature
verification adds nothing. Set SigLevel = Never so installs never touch
the live keyring. The target still gets its own keyring via pacstrap -K
plus the archlinux-keyring/omarchy-keyring package hooks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:49:29 -07:00
David Heinemeier HanssonandClaude Fable 5 f47785a196 Drop the emulated serial port from omarchy-iso-boot
QEMU's default COM1 is advertised to the guest via ACPI SPCR, the boot
stub appends console=uart,io,0x3f8 to the kernel cmdline, and Plymouth
deliberately forces its plain-text details mode whenever a serial console
is active. That made every omarchy-iso-boot test show an unthemed LUKS
prompt that real hardware (no serial port) never shows. Remove the serial
device so manual QEMU tests match real-hardware appearance.

omarchy-iso-test keeps its -serial file: logging on purpose — the harness
reads the console log and types blindly, so details mode is harmless there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:18:39 -07:00
David Heinemeier HanssonandClaude Fable 5 6ac95226af Stop blocking installs on the live pacman keyring
prepare_live waited on archiso's pacman-init.service, stalling real-hardware
installs at 5% for minutes: the boot-time keyring init (gpg key generation
plus populating every keyring) is slow when reading from USB, and as a
Type=oneshot unit with no start timeout it can block systemctl start
indefinitely.

Nothing in the install consults the live keyring. The offline repo is
SigLevel = Optional TrustAll and archinstall bootstraps with pacstrap -K,
which gives the target its own independently initialized keyring. Drop the
wait and let pacman-init finish in the background for anyone using the live
environment interactively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:41:23 -07:00
David Heinemeier HanssonandClaude Fable 5 f97a664ea3 Unpack the live initramfs synchronously
Kernel 7.1 unpacks the initramfs asynchronously, which races /init: the
early /proc, /sys, /dev, and /run mounts fail with EPERM while unpacking
settles. In the live environment that kills Plymouth and leaves early
archiso mounts to luck. Force synchronous unpacking on every boot entry,
matching the fix shipped for installed systems in omarchy-defaults.conf.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:30:55 -07:00
David Heinemeier Hansson 338e687f32 Explain Super 2026-07-23 19:56:35 -07:00
David Heinemeier Hansson 4e8ee7da9b Switch to using tips 2026-07-23 19:39:54 -07:00
David Heinemeier Hansson ca79d0f4dc Prefer local packages in local-source ISOs 2026-07-23 17:41:15 -07:00
David Heinemeier Hansson 463f862b6c Speed up offline ISO installs 2026-07-23 17:21:13 -07:00
David Heinemeier Hansson 3544261e8c Retry stale offline package downloads 2026-07-23 16:20:36 -07:00
David Heinemeier Hansson a274a2c56e More whimsy 2026-07-23 16:15:29 -07:00
David Heinemeier Hansson fa54a8b2dc Expand ISO acceptance coverage 2026-07-22 15:31:05 -07:00
David Heinemeier Hansson a27385483f Replace stale local package signatures 2026-07-22 15:31:05 -07:00
David Heinemeier HanssonandClaude Fable 5 01a5203009 Pick the test ISO like omarchy-iso-boot does
Offer the same gum chooser over release/*.iso when no ISO is passed,
and fail plainly when there is no existing ISO to test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 09:47:46 -07:00
David Heinemeier Hansson a72a109e7a Add fallback mirror for arch-mact2 2026-07-22 09:46:08 -07:00
David Heinemeier HanssonandClaude Fable 5 3c98b0e10c Add omarchy-iso-test acceptance harness
Installs an ISO into a headless QEMU VM by driving the real interactive
install flow: each screen is read via QMP screendumps + OCR and answered
with virtual keystrokes, so the configurator wizard, install dashboard,
reboot prompt, and SDDM login are exercised exactly as a user would.
SSH access is bootstrapped by logging into a console TTY, the installed
disk is snapshotted for fast reruns, and a throwaway overlay then runs
the omarchy repo's in-guest acceptance suite over SSH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 18:29:49 -07:00
David Heinemeier Hansson 9d4b155e22 Language tweaks 2026-07-19 21:04:27 -07:00
David Heinemeier HanssonandClaude Fable 5 04aafcb781 Retry the partition commit when udev holds the disk
Full-disk installs flaked with "Partition(s) 1, 2 on /dev/vda have been
written, but we have been unable to inform the kernel of the change":
archinstall's disk wipe and parted's own partition registrations fire
udev probes, and when one is still holding the disk mid-commit, the
BLKPG sync fails and the install halts. The table is written correctly
when this happens — only the kernel's view is stale — so settle udev
before partitioning and redo the filesystem operations (wipe, partition,
format) when parted throws exactly this error. Recoveries are logged so
they show up in the support log.

Reproduced headlessly by booting the ISO on a serial console and driving
the real installer: stock failed 1 in 5 full-disk cycles at this exact
phase, with an audit watch confirming nothing else writes the disk. With
the fix, 13 of 13 cycles got cleanly through partitioning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 20:58:39 -07:00
David Heinemeier Hansson c3f9743847 Don't like the first one 2026-07-18 18:27:10 -07:00
David Heinemeier HanssonandClaude Fable 5 434db24b81 Keep the installer centered when console geometry shifts
Remeasure the terminal at every screen redraw instead of once at boot:
the framebuffer can settle after the configurator starts (VM windows
especially), which left stale padding for the whole flow. Pad the
confirm-screen warnings with say so every line shares the same left
edge. Boot the test VM with SDL instead of GTK, whose GL display path
crops the guest framebuffer on HiDPI Wayland desktops.

Verified in a live VM by driving the whole flow over QMP and reading
each screen back from /dev/vcs1: all screens center on the real console
width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 16:41:23 -07:00
David Heinemeier HanssonandClaude Fable 5 969d461eb9 Parse the free-space fields with a single read
Three echo-through-awk substitutions to split one line into three
variables is what read was made for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 10:55:49 -07:00
David Heinemeier HanssonandClaude Fable 5 4583a0e5e6 Extract the helpers shared by both install paths
The free-space and full-disk paths each carried verbatim copies of the
T2 Mac kernel detection, the credentials JSON heredoc, the identity/
encryption answer files, the dry-run print-and-cleanup block, and the
udev settle loops. Pull those into detect_kernel, write_user_files,
print_dry_run_files, and wait_for_device, which also removes a subtle
rm vs rm -f drift between the two dry-run cleanups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 10:55:32 -07:00
David Heinemeier HanssonandClaude Fable 5 f79578f2db Remove the dead shared-ESP path from the free-space install
We decided Omarchy always creates its own dedicated ESP and never adopts
a Windows one, so shared_esp was declared empty and never assigned —
every conditional on it was guaranteed to take the dedicated-ESP branch.
Collapse the whole dual-path structure into straight-line code and keep
detect_windows_esp only for the "leaving it untouched" message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 10:54:38 -07:00
David Heinemeier HanssonandClaude Fable 5 4e3e82922d Trust that tzupdate is on the ISO
build-iso.sh bakes tzupdate into the live environment, so the runtime
pacman fallback was dead there — and in a dry run on a dev machine it
would install a package as a side effect. tzupdate -p already fails
gracefully into the manual timezone filter when the tool is missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 10:53:57 -07:00
David Heinemeier HanssonandClaude Fable 5 59ffc3321e Simplify the configurator preamble
Drop the dead LOGO_PATH override, the redundant package-targets sourcing,
the unused TERM_HEIGHT and OMARCHY_NVIM_PACKAGE, the non-gum exports, and
the logo fallback branches. Trust that logo.txt is where OMARCHY_PATH says.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 10:40:33 -07:00
David Heinemeier Hansson 1463936de3 Simplify the free-space install selection 2026-07-16 14:41:18 -07:00
David Heinemeier Hansson f6775344f6 Offer easy way to test partitioned install 2026-07-16 09:48:21 -07:00
David Heinemeier Hansson 49dcd808aa Whimsy tweaks 2026-07-15 18:07:07 -07:00
David Heinemeier Hansson c2a58526f4 Replace installer preview Python with jq 2026-07-15 18:05:29 -07:00
David Heinemeier HanssonandClaude Fable 5 a5e605af4c Replace dashboard Python heredocs with jq + bash
The install dashboard shelled out to a full Python interpreter to read
four keys from the installer state JSON — including inside the render
loop, twice a second. jq is already on the ISO and does the same job
with plain bash arithmetic for the phase-weight interpolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 17:52:47 -07:00
David Heinemeier Hansson 8c1e143686 Hand write the whimsy 2026-07-15 17:40:24 -07:00
David Heinemeier Hansson 4ad8a22abe We don't need more than 8 cores for this 2026-07-15 17:35:37 -07:00
David Heinemeier Hansson 764a65f7ee Bring back gl acceleration 2026-07-15 17:35:19 -07:00
David Heinemeier Hansson c167bf9c1d Cleanup the look 2026-07-14 18:09:48 -07:00
David Heinemeier Hansson f87b08bba7 Merge branch 'main' into omarchy-4 2026-07-14 17:13:26 -07:00
David Heinemeier Hansson 588973b0cd Only offer a choice of installing alongside existing data if it's actually possible 2026-07-14 17:05:45 -07:00
David Heinemeier Hansson c77a7b555b Drop the tool offering during this initial install 2026-07-14 17:03:06 -07:00
David Heinemeier Hansson 08b7460900 Explain purpose 2026-07-14 16:53:10 -07:00
David Heinemeier Hansson 341bb84093 Use same naming as omarchy-iso-configurator 2026-07-14 16:53:06 -07:00
David Heinemeier Hansson c303a9f746 More aesthetic tweaks 2026-07-13 17:06:43 -07:00
David Heinemeier Hansson c8a0072403 Tweak the installer aesthetics 2026-07-13 15:55:08 -07:00
David Heinemeier Hansson ff6c9582ca Use the real flow during simulation so they don't drift 2026-07-13 14:44:42 -07:00
David Heinemeier Hansson d44bbc5e5f Fix archinstall 4.4 compatibility 2026-07-12 18:43:51 -07:00
David Heinemeier Hansson d51a280636 Let's go quattro 2026-07-12 18:31:10 -07:00
Ryan Hughes 8c77631f68 Delegate Snapper setup to Omarchy packages 2026-06-21 02:29:09 -04:00
Ryan Hughes 58221e5ed6 Fix bootstrap pacman database setup 2026-06-08 19:25:25 -04:00
Ryan Hughes b1c09fa1a8 Use dev Omarchy packages in dev ISO builds 2026-06-08 12:53:01 -04:00
Ryan Hughes ef3b0e4b61 Always create our own ESP 2026-05-30 01:38:24 -04:00
Ryan Hughes b280cbcc42 Patch t2 mirror for now 2026-05-27 23:06:03 -04:00
Ryan Hughes ce9e648ade Drop omarchy-limine package; settings now ships limine configs
Pairs with omarchy-pkgs' fold of omarchy-limine into omarchy-settings.

Changes:
- EARLY_BOOTSTRAP_PACKAGES in phases_impl.py drops omarchy-limine.
  The limine configs it shipped (mkinitcpio hooks, entry-tool drop-ins,
  snapper template, default/limine/, default/snapper/) now come with
  omarchy-settings, which is already in the early bootstrap.
- builder/build-iso.sh's offline-mirror seed list and post-mirror filter
  no longer reference omarchy-limine.
- builder/build-omarchy-packages.sh's build queue drops omarchy-limine.
- Test manifests (fresh-4.json, fresh-4-semantic.json) no longer expect
  omarchy-limine in the installed-packages list. The /etc/pacman.d/hooks/
  99-omarchy-limine.hook entry stays — that hook file is generated by the
  orchestrator regardless, and the filename is descriptive, not tied to
  the package name.
2026-05-27 19:09:05 -04:00
Ryan Hughes 239c8e7f64 Use omarchy-finalize-user in target chroot setup
Pairs with omarchy-installer's rename of bin/omarchy-setup-user to
bin/omarchy-finalize-user. The orchestrator now invokes
omarchy-finalize-user --force --first-install in the run_chroot_finalizer
phase, and the phase-ordering docstring is updated to match.
2026-05-27 19:05:38 -04:00
Ryan Hughes afa8293b2b Improve ISO installer flow and VM tooling 2026-05-25 19:54:12 -04:00
Ryan Hughes c50d0bc299 Unify ISO install orchestration
Drive installs through the common ISO pipeline, carry install intent in configurator JSON, and finalize Limine boot setup from the ISO.
2026-05-25 12:50:31 -04:00
Ryan Hughes 660edcb3af installer: defer boot hooks during pacstrap 2026-05-23 14:39:52 -04:00
Ryan Hughes 3a51e875b6 installer: move hibernation into orchestrator 2026-05-23 14:05:15 -04:00
Ryan Hughes c7b8d4b584 builder: prune stale offline package versions 2026-05-23 13:08:15 -04:00
Ryan Hughes 38f56103e1 builder: add debug install instrumentation 2026-05-23 12:45:12 -04:00
Ryan Hughes b16303f294 installer: always clean up finalizer log bind 2026-05-23 10:30:53 -04:00
Ryan Hughes 6232620806 installer: unify finalizer logging 2026-05-23 10:21:38 -04:00
Ryan Hughes 8d1d3c906d installer: harden dashboard finish transition 2026-05-23 10:02:36 -04:00
Ryan Hughes fd12ace8bc installer: tolerate missing duration on finish 2026-05-23 05:29:53 -04:00
Ryan Hughes 1d656af51d installer: make dashboard own install flow 2026-05-23 04:50:32 -04:00
Ryan Hughes af4679b153 installer: clear stale dashboard state 2026-05-23 03:43:24 -04:00
Ryan Hughes c43bad26b5 installer: stop dashboard before finish prompt 2026-05-23 03:35:55 -04:00
Ryan Hughes 7d7f6451a3 installer: track real dashboard pid 2026-05-23 02:55:44 -04:00
Ryan Hughes 0890472c9b installer: pass dashboard tty explicitly 2026-05-23 02:14:13 -04:00
Ryan Hughes 1162d111bd installer: force dashboard shutdown before finish 2026-05-22 23:10:44 -04:00
Ryan Hughes ca55321621 installer: animate dashboard and finish screens 2026-05-22 19:20:03 -04:00
Ryan Hughes 0734e982ca installer: harden dashboard handoff 2026-05-22 19:15:06 -04:00
Ryan Hughes 61f84a95cf installer: smooth dashboard progress 2026-05-22 18:09:00 -04:00
Ryan Hughes 63ba457034 builder: mark install dashboard executable 2026-05-22 17:46:55 -04:00
Ryan Hughes 337ba2e590 installer: show dashboard during ISO install 2026-05-22 17:34:01 -04:00
Ryan Hughes de0f013b5e orchestrator: converge free-space boot on finalizer 2026-05-22 16:33:11 -04:00
Ryan Hughes 948a53cc0a orchestrator: recover missing ESP mount in free-space installs 2026-05-22 15:00:19 -04:00
Ryan Hughes cab315bb8a configurator: count EFI inside free-space size 2026-05-22 14:47:57 -04:00
Ryan Hughes 2178c26a92 configurator: let free-space installs choose size 2026-05-22 14:05:28 -04:00
Ryan Hughes f3973abb4c configurator: use valid bootloader value for free-space installs 2026-05-22 13:51:40 -04:00
Ryan Hughes 3e0cd31c2b configurator: clarify free-space install semantics 2026-05-22 13:51:02 -04:00
Ryan Hughes 7e79355c71 configurator: add partition tool and recoverable partition flow 2026-05-22 13:29:10 -04:00
Ryan Hughes d7e4233042 orchestrator: defer full-disk mkinitcpio to final UKI build 2026-05-22 13:26:06 -04:00
Ryan Hughes 5ad90afbae orchestrator: use epoch for finalizer timing 2026-05-22 13:04:34 -04:00
Ryan Hughes c2396fb447 orchestrator: preserve offline finalizer timing 2026-05-22 12:26:51 -04:00
Ryan Hughes 9ad189ef82 orchestrator: configure snapper before offline finalizer 2026-05-22 12:13:00 -04:00
Ryan Hughes 1e26d18031 orchestrator: point finalizer at installed runtime assets 2026-05-22 11:23:46 -04:00
Ryan Hughes 6decb2f683 orchestrator: install omarchy-nvim before user creation 2026-05-22 10:58:26 -04:00
Ryan Hughes 3420bc9aa8 builder: include omarchy-nvim in local package builds 2026-05-22 10:46:56 -04:00
Ryan Hughes a21ab8bcb1 orchestrator: prepare writable offline finalizer log 2026-05-22 02:10:18 -04:00
Ryan Hughes c5baf2c791 orchestrator: keep finalizer payload out of tmp 2026-05-22 01:39:49 -04:00
Ryan Hughes ec218bade6 orchestrator: chown finalizer tooling inside target 2026-05-22 01:17:45 -04:00
Ryan Hughes c179d78814 configurator: emit explicit bootloader_config with removable=false
archinstall 4.3 maps the deprecated top-level 'bootloader': 'Limine'
to BootloaderConfiguration(bootloader=Limine, removable=True) (per
lib/args.py:185-190 and lib/models/bootloader.py:56-69). Our new
_install_limine_omarchy mirrors archinstall's removable handling: it
would copy BOOTX64.EFI to EFI/BOOT (fallback path) and SKIP
efibootmgr, neither of which is what we want.

Switch to the explicit bootloader_config form with removable=false
so we take the EFI/limine path + register the 'Limine' NVRAM entry
as intended.
2026-05-22 00:15:04 -04:00
Ryan Hughes 3a91cf780c orchestrator: install Limine ourselves; stop creating arch-limine entries to delete later
archinstall's add_bootloader creates EFI/arch-limine/{BOOTX64.EFI, BOOTIA32.EFI}, writes a limine.conf with an Arch Linux entry, and registers an efibootmgr label "Arch Linux Limine Bootloader" — only for limine-snapper.sh to delete it all and rebuild with Omarchy branding via limine-mkinitcpio-hook.

Replace the add_bootloader call with _install_limine_omarchy, which does the same work archinstall's _add_limine_bootloader does (UEFI EFI binary copy + efibootmgr register, or BIOS limine-bios.sys + limine bios-install) but with Omarchy-owned paths and labels from the start:

  - EFI subdir:    EFI/limine          (not EFI/arch-limine)

  - efibootmgr:    label "Limine"      (not "Arch Linux Limine Bootloader")

  - loader path:   \EFI\limine\BOOTX64.EFI

Compute the kernel cmdline directly from disk_config instead of harvesting it from a file archinstall wrote (whose path keeps drifting across versions). Then write /etc/default/limine and /etc/kernel/cmdline, and stage /boot/limine.conf from our styling template — limine-mkinitcpio-hook will add the actual boot entries when it deploys.

Adds limine + efibootmgr to EARLY_PACKAGES (archinstall used to pacstrap these as a side effect of add_bootloader).

Does not modify limine-snapper.sh — its cleanup code will simply no-op because there are no arch-limine paths or "Arch Linux Limine Bootloader" entries to find. That code can be pruned in a follow-up.
2026-05-21 21:57:50 -04:00
Ryan Hughes 39113a2d2b orchestrator: probe multiple paths for limine.conf after add_bootloader
archinstall 4.3 writes limine.conf to <target>/<esp_mount>/EFI/arch-limine/
limine.conf for UEFI non-removable installs (lib/installer.py:1487), not
to /mnt/boot/limine.conf as our code assumed. Older archinstall versions
used different paths.

Probe the 4.3 paths (arch-limine, BOOT, limine subdir) and the legacy
/boot/limine.conf in order; pick whichever exists. Error message now
lists everywhere it looked so future drift is easy to diagnose.
2026-05-21 20:56:58 -04:00
Ryan Hughes 16cc1058be automated_script: restore proven gum stream/env contract
Restore the env + redirect setup that the previously-working ISO
installer used (matching /home/ryan/Downloads/ISO scripts/.automated_script.sh):

  export COLUMNS=$(tput cols)
  export LINES=$(tput lines)
  exec > >(tee >(sed CSI-strip >> log) 2>/dev/null) 2>/dev/tty
  export CLICOLOR_FORCE=1
  export FORCE_COLOR=1

The cutover commit (064b5e2) silently dropped these. Subsequent
attempts to fix the TTY/gum rendering by reshaping the redirect were
all symptom-chasing — the proven shape was correct. The key insight:

  - gum draws its TUI on stderr, not stdout. 2>/dev/tty keeps that
    rendering on the real terminal.
  - stdout-as-pipe (via tee) is *desired*: it lets gum detect a
    non-TTY stdout and skip cursor-position probes (CSI 6n). Without
    that detection, the DSR replies leak as visible ^[[13;1R bytes.
  - CLICOLOR_FORCE/FORCE_COLOR force gum to still emit colour despite
    stdout being a pipe.

Also drops the configurator's _drain_tty workaround — unnecessary
once gum stops sending DSR queries in the first place.
2026-05-21 20:54:05 -04:00
Ryan Hughes 8734136908 configurator/shim: drain tty without hanging (use -t 0.05, not -t 0)
bash's `read -t 0` only TESTS whether input is available; it does not
consume the bytes. So the drain loop `while read -t 0 ...; do :; done`
spun forever the moment any bytes were pending (e.g., the DSR reply
sitting in the tty input buffer after gum style sent CSI 6n).

That hung the configurator at the first step() title screen — blank
prompt, blinking cursor, no keyboard form.

Use `read -t 0.05 -N 9999` once, which consumes up to 9999 bytes with
a 50ms timeout. Always returns after 50ms whether there's data or not.
Single call replaces the while loop.
2026-05-21 20:46:50 -04:00
Ryan Hughes a18d4651a3 orchestrator: pacstrap limine+efibootmgr in protected base install
configure_protected_boot used to arch-chroot pacman -S limine
efibootmgr, but at that phase the live ISO's offline mirror is not
bind-mounted into the target yet (run_chroot_finalizer does that
later) and the target's pacman.conf points at network mirrors that
aren't reachable during an offline install.

Pacstrap these packages alongside the runtime package list in
arch_install_base, where archinstall is still pacstrapping against
the live ISO's offline mirror. Drop the now-redundant arch-chroot
pacman call from configure_protected_boot.
2026-05-21 20:38:03 -04:00
Ryan Hughes a5a7110bea configurator: drain tty input after each step() title
gum's TUI startup queries the terminal (e.g., cursor position via
CSI 6n) and doesn't always read the reply. The reply bytes
("^[[13;1R") then surface visibly next to the next step title.
A one-time drain at script start (in .automated_script.sh) catches
the boot-time replies but not the gum-per-call ones, since the
artifact came back at every new step().

Add a small _drain_tty helper and call it from step() after the
title gum-style invocation so any pending DSR replies are consumed
before the next prompt runs.
2026-05-21 20:34:21 -04:00
Ryan Hughes 34c0a14766 archinstall_adapter: pass Path (not str) to Installer
archinstall 4.3's Installer.__init__ types target as Path and uses it
directly (self.target / 'boot', etc.). Passing str(mountpoint) makes
self.target a str, so every internal path operation raises
'unsupported operand type(s) for /: str and str' — most visibly during
set_mirrors, which constructs target / MIRRORLIST.relative_to_root().

Drop the str() cast; pass the Path through as-is.
2026-05-21 20:34:21 -04:00
Ryan Hughes be721e17f4 installer: route kernel/initramfs to ESP for encrypted protected installs (Limine can't read LUKS) 2026-05-21 20:09:50 -04:00
Ryan Hughes 21dbbcb917 ui: attach gum confirm to /dev/tty so it doesn't render through the install log tee 2026-05-21 20:08:38 -04:00
Ryan Hughes 260edc13c5 orchestrator: tear down protected-mode mounts and LUKS mapper on failure 2026-05-21 20:08:29 -04:00
Ryan Hughes e049f28bfa orchestrator: record bind mounts immediately to prevent leaks on partial failure 2026-05-21 20:08:06 -04:00
Ryan Hughes ed7c62febe omarchy-iso: drop obsolete archinstall patch helper (4.3 has the fixes upstream) 2026-05-21 20:07:55 -04:00
Ryan Hughes b568b0b796 archinstall_adapter: pass --config/--creds via synthetic sys.argv (env vars don't work)
ArchConfigHandler reads --config and --creds from sys.argv via argparse at
construction time; it ignores ARCHINSTALL_CONFIG/ARCHINSTALL_CREDS env
vars (confirmed in archinstall 4.3 lib/args.py:_parse_args). Setting those
env vars was a no-op, so handler.config.disk_config was always None when
the wrapper stripped our args off the real argv.

Fix: build a synthetic argv [prog, --config <path>, --creds <path>] and
swap it into sys.argv just for the ArchConfigHandler() call, then
restore. Drops the unused 'import os' now that the env var lines are
gone.
2026-05-21 19:53:27 -04:00
Ryan Hughes c1828b13ad automated_script: drain stale tty input + drop redundant stdin redirect
Fix the ^[[13;1R cursor-position-report artifacts visible next to the
first configurator step. archiso's boot init leaves DSR responses in the
kernel tty input buffer; the first gum invocation reads them as stray
input and echoes them as visible escape sequences.

- Drain stdin (with a short timeout) from /dev/tty before configurator
  runs so any pending replies are consumed.
- Drop the explicit </dev/tty stdin redirect — the parent script's
  stdin is already tty1 (inherited from archiso autostart), and the
  explicit redirect opened a new fd with subtly different termios.
2026-05-21 19:51:35 -04:00
Ryan Hughes 22f94e6da6 orchestrator: switch wrapper interface from CLI args to env vars to avoid archinstall argparse collision 2026-05-21 19:40:31 -04:00
Ryan Hughes 8dad5a7436 omarchy-iso-make: lint that executable airootfs files have file_permissions entries 2026-05-21 18:44:09 -04:00
Ryan Hughes 0477275d08 configurator: shorten install-mode labels to fit 80-col terminals 2026-05-21 18:44:09 -04:00
Ryan Hughes 13e0201e9b automated_script: keep configurator on real TTY; scope logging to install phase 2026-05-21 18:44:09 -04:00
Ryan Hughes 9c6d14a80b iso: add file_permissions entries for new omarchy-iso-* helpers (fix Permission denied) 2026-05-21 18:44:09 -04:00
Ryan 05a1c6f271 orchestrator: implement protected-mode boot (Limine in EFI/Omarchy, fstab/crypttab/mkinitcpio, efibootmgr with BootOrder preservation) 2026-05-21 18:18:50 -04:00
Ryan Hughes 26e6a78d3a configurator: add partition install mode with BitLocker/ESP guards and pre_mounted_config output 2026-05-21 18:12:55 -04:00
Ryan Hughes 7a56a9c1e6 orchestrator: mode-aware phase selection for full-disk vs protected 2026-05-21 18:06:55 -04:00
Ryan Hughes b36280f1a6 omarchy-iso-cleanup-disk: add --protected mode; adapter: doc pre-mount mount semantics 2026-05-21 18:06:52 -04:00
Ryan Hughes 064b5e2393 automated_script: collapse to configurator + omarchy-iso-install wrapper
All install-phase logic now lives in the Python orchestrator and the
omarchy-iso-* bash helpers. .automated_script.sh becomes a thin shim that:

  - sources install helpers (env for configurator)
  - sets the Tokyo Night palette on the live VT
  - tees stripped stdout/stderr to /var/log/omarchy-install.log
  - runs the configurator wizard
  - exec's omarchy-iso-install with absolute config paths

Absolute paths are required because the wrapper cd's into
/usr/share/omarchy-iso before invoking the orchestrator.

Removed install_base_system, install_omarchy, install_arch, install_disk,
cleanup_install_disk, configure_login_for_unencrypted_install, and
chroot_run_as_user — all replaced by Python phases or bash helpers.
2026-05-21 17:53:05 -04:00
Ryan Hughes 6d6b516221 orchestrator: always unwind bind mounts on exit
Bind mounts set up in run_chroot_finalizer used to be torn down in finish,
but finish doesn't run when a phase between the two raises. Move the unmount
loop into a dedicated cleanup_bind_mounts function and call it from main()'s
finally so it runs on success, failure, and KeyboardInterrupt.
2026-05-21 17:52:12 -04:00
Ryan Hughes 26f87f6789 orchestrator: shell out to bash helpers; patch archinstall before Python imports 2026-05-21 17:48:37 -04:00
Ryan Hughes 1742e6d997 Add omarchy-iso disk-cleanup and archinstall-patch bash helpers 2026-05-21 17:48:37 -04:00
Ryan Hughes 1daa755b70 gitignore python bytecode (orchestrator cache dirs) 2026-05-21 16:54:18 -04:00
Ryan Hughes 89cc55d559 Add Python install orchestrator (moved from omarchy-installer)
The orchestrator drives an offline install on the live ISO using
archinstall as a library subsystem. Lands under configs/airootfs/ so
mkarchiso bakes it into the live squashfs directly:

  /usr/share/omarchy-iso/orchestrator/   (the Python package)
  /usr/local/bin/omarchy-iso-install     (thin shell wrapper that
                                          execs python -m orchestrator.main)

Two corrections vs the previous omarchy-installer location:

1. omarchy-installer dropped from EARLY_PACKAGES \u2014 it's live-ISO-only
   install tooling, must never land on the target. /etc/skel is still
   populated pre-useradd via omarchy-settings.

2. run_chroot_finalizer now copies /usr/share/omarchy from the live
   ISO into /mnt/tmp/omarchy-install/ before arch-chroot. finalize.sh
   + install/ scripts run from /tmp inside the chroot; /tmp is wiped
   on first reboot, so the target boots with zero install detritus.
2026-05-21 16:53:13 -04:00
Ryan Hughes 0c63044066 chroot_run_as_user: unset XDG_RUNTIME_DIR
The live ISO runs as root with XDG_RUNTIME_DIR=/run/user/0; arch-chroot
-u ryan switches the UID but doesn't reset the env, so tools running as
ryan tried to write to root's runtime dir (dconf failed to commit,
omarchy-theme-set's flock target hit Permission denied). Drop the var
so tools fall back to no-runtime-dir behaviour, which works inside the
chroot.
2026-05-21 00:39:39 -04:00
Ryan Hughes f8010a355a Rebuild offline repo db from scratch each run
repo-add --new skips updating entries that are already in the db, so when
the omarchy-* packages are rebuilt in place (new content, same filename,
because pkgver is constant) the persistent cache's offline.db.tar.gz
kept the OLD size/checksum entries. Pacman then read the stale db size
and rejected the new file with 'Exceeded the maximum allowed file size'.

Delete the db files and let repo-add regenerate them fresh — slower but
deterministic, and 'offline.db.tar.gz' is small to rebuild even for the
full ~600 package set.
2026-05-21 00:28:32 -04:00
Ryan Hughes ca2b58fa4d Install omarchy-installer + omarchy-settings into the live ISO
Two motivations:

1. Plymouth: the omarchy plymouth theme + /etc/plymouth/plymouthd.conf
   ship from omarchy-settings (theme at /usr/share/plymouth/themes/omarchy,
   plymouthd.conf via the package's post_install etc-overrides hook).
   Without omarchy-settings on the live ISO, mkarchiso builds the initrd
   with plymouth pointing at the default theme instead of Omarchy's. Now
   it picks up the Omarchy theme on boot like the installed system does.

2. Drops the .pkg.tar.zst extraction hack in .automated_script.sh:
   OMARCHY_PATH=/usr/share/omarchy is the real package install path, so
   use_omarchy_helpers no longer needs to bsdtar pieces out of the
   offline mirror into /tmp/omarchy-iso.
2026-05-21 00:13:05 -04:00
Ryan Hughes 53bc2550f9 .automated_script.sh: seed user home from /etc/skel after archinstall
archinstall creates the user from user_credentials.json BEFORE installing
the 'additional packages' list, so when omarchy-settings populates
/etc/skel/.config/... later in the pacstrap, the user's home has already
been created from an empty skel and missed the seed (user boots into a
terminal with no Hyprland configs).

Copy /etc/skel into /home/$OMARCHY_USER after archinstall finishes (by
which point omarchy-settings has populated skel) and chown to the user.
cp -rT keeps it idempotent: existing files get overwritten, dirs merge.
2026-05-21 00:08:33 -04:00
Ryan Hughes 874e6cd24d Cache ISO mirror by channel instead of by date
The previous per-day cache (iso_YYYY-MM-DD) forced a fresh ~6GB download
on the first build of every day. Different channels (stable/edge/rc)
HAVE different package sets and shouldn't share a cache, but different
DAYS within a channel don't, so keying by channel gives us a stable
cache that persists indefinitely.

omarchy* packages always rebuild fresh inside the container regardless:
build-omarchy-packages.sh nukes /tmp/omarchy-pkg-build at the start
and the final mv overwrites the cached .pkg.tar.zst (pkgver is constant).
--no-cache still bypasses the mount entirely for a clean-slate run.
2026-05-20 23:43:37 -04:00
Ryan Hughes cf9f2745f3 .automated_script.sh: arch-chroot pacman -S omarchy-base.packages after pacstrap
omarchy's hard depends are now just the bricking set, not the full default
install. After archinstall pacstraps the meta packages (which populates
/etc/skel before user creation), install the rest of omarchy-base.packages
in the chroot before running omarchy-install.
2026-05-20 23:10:30 -04:00
Ryan Hughes 01b932023a configurator: pacstrap omarchy + omarchy-installer (not just installer)
With the cleaner dep graph, omarchy-installer no longer transitively
pulls omarchy + omarchy-settings + omarchy-limine. Pacstrap both
explicitly so /etc/skel (via omarchy -> omarchy-settings) is populated
before archinstall creates the user.
2026-05-20 22:27:38 -04:00
Ryan Hughes a4f5c1bf14 Pacstrap omarchy-installer via archinstall so /etc/skel is set up first
archinstall creates the user AFTER pacstrap. If omarchy-settings (which
ships /etc/skel/.config/**) isn't pacstrap'd, the user is created with
an empty skel and never gets the Omarchy default configs in their HOME.

Add omarchy-installer to the configurator's archinstall package list
(its depends pull omarchy + omarchy-settings + omarchy-limine), so all
the package files are in place before the user is created.

install_omarchy no longer needs the explicit 'pacman -S omarchy-installer'
step; it just runs 'omarchy-install' which is already at /usr/bin/.
2026-05-20 22:18:14 -04:00
Ryan Hughes 8982984f40 .automated_script.sh: also extract omarchy-settings into the helpers tree
presentation.sh reads $OMARCHY_PATH/logo.txt to compute LOGO_WIDTH and
LOGO_HEIGHT, which then drive max_line_width in logging.sh / errors.sh.
We were only extracting omarchy-installer into /tmp/, so logo.txt was
missing, LOGO_WIDTH=0, max_line_width=-4, and bash's substring expression
errored with 'substring expression < 0'.

Extract from omarchy-settings into the same /tmp tree so all the
presentation/logging helpers find what they expect. Renamed the dir
from omarchy-installer-iso to plain omarchy-iso since it now blends
content from two packages.
2026-05-20 22:16:34 -04:00
Ryan Hughes a74d20f8fb build-omarchy-packages.sh: pass --nodeps to makepkg
We're not installing the deps in the build container — just producing
.pkg.tar.zst files that pacman will resolve against the offline mirror
on the target. Installing limine-mkinitcpio-hook into the build
container also fires its pacman hook that errors about a missing ESP.
2026-05-20 21:42:22 -04:00
Ryan Hughes 1224531ce1 Add the [omarchy] repo to the build container's pacman.conf
makepkg in build-omarchy-packages.sh installs runtime build deps via
pacman, which uses the container's /etc/pacman.conf. That config only
had core/extra/multilib by default, so deps that live in the omarchy
repo (limine-snapper-sync, limine-mkinitcpio-hook) failed to resolve.

Append the [omarchy] block from the channel-specific online config to
/etc/pacman.conf right after omarchy-keyring is installed.
2026-05-20 21:39:58 -04:00
Ryan Hughes 8b0fd93011 chown the package build dir to the builder user
makepkg runs as the builder user and needs to write PKGDEST. /tmp/omarchy-pkg-build
is created by root, so without an explicit chown the build aborts with
'You do not have write permission for the directory $PKGDEST'.
2026-05-20 21:38:09 -04:00
Ryan Hughes b34f53629d Validate --local-source paths before any sudo work
Also includes the build-iso.sh cleanup that I prepared earlier but
forgot to stage (replacing the inline ugly $(...) conditional pipe
with a normal if/then-fi block).
2026-05-20 21:36:31 -04:00
Ryan Hughes e427304f29 Make --local-source require explicit checkout paths
Dropped the sibling-dir convention ($BUILD_ROOT/../omarchy-installer
etc.). Callers now pass paths explicitly:

  omarchy-iso-make --local-source <installer-checkout> <pkgs-checkout>

Errors clearly if either path is missing or doesn't exist.
2026-05-20 21:35:46 -04:00
Ryan Hughes b8df9d3f50 Make --local-source the opt-in; default to network omarchy mirror
The previous version made the local build the only path. Convention is
the opposite: omarchy-iso-make pulls released omarchy* packages from
the omacom-pkgs.org mirror by default, and only builds from local
checkouts when explicitly asked via --local-source.

- bin/omarchy-iso-make: only mount /omarchy-installer + /omarchy-pkgs
  when --local-source is set. Default has neither mount.
- builder/build-iso.sh: skip build-omarchy-packages.sh when no local
  source. Don't filter the omarchy* names out of the pacman -Syw list
  in network mode (so they get downloaded from the mirror). With
  --local-source, the four package names are stripped after assembly
  so we don't re-fetch what we just built.
- For the package-list source: with /omarchy-installer mounted, read
  omarchy-base.packages from the checkout. Without it, briefly pull
  the published omarchy package, extract the .packages files from it
  to /tmp, and use those.
2026-05-20 21:34:49 -04:00
Ryan Hughes 9f4e5feef0 Refactor ISO build for the package-backed install flow
The ISO no longer carries a cp -r'd copy of the omarchy git checkout
into /root/omarchy and the target /home/$user/.local/share/omarchy.
Instead it builds omarchy-installer + omarchy-settings + omarchy +
omarchy-limine from mounted source trees (${OMARCHY_INSTALLER_PATH:-
../omarchy-installer} and ${OMARCHY_PKGS_PATH:-../omarchy-pkgs}),
drops them in the offline mirror, and pacstraps omarchy-installer
during the target install — which deps-in the other three.

- builder/build-omarchy-packages.sh: new. Builds the four packages
  inside the ISO build container using each PKGBUILD's OMARCHY_SRC
  override (introduced in Chunk 2). Creates a 'builder' user because
  makepkg refuses to run as root. Drops the resulting .pkg.tar.zst
  files into the offline-mirror directory.

- builder/build-iso.sh: drop the git clone / -v omarchy mount path;
  call build-omarchy-packages.sh first; filter out the locally-built
  package names from the rest-of-mirror download so we don't try to
  fetch them from the network mirror.

- configs/airootfs/root/.automated_script.sh: replace the install_omarchy
  cp-and-source dance with 'arch-chroot pacman -S omarchy-installer'
  followed by 'OMARCHY_INSTALL_MODE=offline omarchy-install'. Drop the
  big chown/chmod block that was setting up the cloned tree.
  use_omarchy_helpers extracts the installer's helpers/ tree directly
  from the offline-mirror's omarchy-installer-*.pkg.tar.zst so the live
  ISO has access to gum-styled logging before pacstrap.

- bin/omarchy-iso-make: always mount omarchy-installer and omarchy-pkgs
  (paths configurable via OMARCHY_INSTALLER_PATH / OMARCHY_PKGS_PATH;
  default to ../omarchy-installer and ../omarchy-pkgs). --local-source
  is now a back-compat no-op.
2026-05-20 21:29:40 -04:00
41 changed files with 18556 additions and 518 deletions

No files matched your search

+7
View File
@@ -1,2 +1,9 @@
release/** release/**
vm-saves/** vm-saves/**
# Python bytecode (orchestrator)
__pycache__/
*.pyc
# Acceptance test harness artifacts
test-runs/**
+31 -11
View File
@@ -1,6 +1,6 @@
# Omarchy ISO # Omarchy ISO
The Omarchy ISO streamlines [the installation of Omarchy](https://learn.omacom.io/2/the-omarchy-manual/50/getting-started). It includes the Omarchy Configurator as a front-end to archinstall and automatically launches the [Omarchy Installer](https://github.com/basecamp/omarchy) after base arch has been setup. The Omarchy ISO is the only supported way to install Omarchy. It ships the Omarchy Configurator, installs Arch Linux, installs the Omarchy packages from the bundled mirror, runs target system setup in the chroot, creates the user, and runs `omarchy-setup-user` for that user.
## Downloading the latest ISO ## Downloading the latest ISO
@@ -8,31 +8,51 @@ See the ISO link on [omarchy.org](https://omarchy.org).
## Creating the ISO ## Creating the ISO
Run `./bin/omarchy-iso-make` and the output goes into `./release`. You can build from your local `$OMARCHY_PATH` for testing with `--local-source`, from the dev branch with `--dev`, or from the quattro branch with `--quattro`. The dev and quattro builds use the edge package mirror. Run `./bin/omarchy-iso-make`; output goes into `./release`. By default the ISO uses the Omarchy packages and tracks the `quattro` branch, from the stable mirror. Pass `--edge` to use `omarchy-dev` and `omarchy-settings-dev` from the edge mirror.
### Environment Variables For local development, build the ISO from sibling checkouts:
You can customize the repositories used during the build process by passing in variables:
- `OMARCHY_INSTALLER_REPO` - GitHub repository for the installer (default: `basecamp/omarchy`)
- `OMARCHY_INSTALLER_REF` - Git ref (branch/tag) for the installer (default: `master`)
Example usage:
```bash ```bash
OMARCHY_INSTALLER_REPO="myuser/omarchy-fork" OMARCHY_INSTALLER_REF="some-feature" ./bin/omarchy-iso-make ./bin/omarchy-iso-make --local-source ../omarchy-installer ../omarchy-pkgs
``` ```
Despite the local folder name, the first argument is the Omarchy source checkout (runtime commands, configs, setup scripts, themes, shell, migrations). The installer itself lives in this ISO repo.
Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror.
## Testing the ISO ## Testing the ISO
Run `./bin/omarchy-iso-boot [release/omarchy.iso]`. Run `./bin/omarchy-iso-boot [release/omarchy.iso]`.
To exercise installation alongside existing Windows-style partitions, run
`./bin/omarchy-iso-test-windows-disk [release/omarchy.iso]`. It creates a
synthetic disk in `/tmp` with an existing ESP and data partition plus ample
unallocated space, then offers to start an interactive installation on it. The
fixture exercises Windows partition preservation but does not contain Windows.
## Acceptance testing the ISO
Run `./bin/omarchy-iso-test [release/omarchy.iso]` to install the ISO into a headless VM by driving the real interactive install flow — the harness reads each screen via QMP screendumps + OCR and answers with virtual keystrokes, so the configurator wizard, install dashboard, reboot prompt, and SDDM login are all exercised exactly as a user would. It then boots the installed system, sends real VM keyboard shortcuts for the primary shell and window-management actions, and runs the in-guest acceptance suite (`test/acceptance` in the omarchy repo). The suite checks session and service health, the complete core-package manifest, user defaults, representative applications, menus, panels, live weather, launchers, visual selectors, notifications, clipboard, and other interactive shell behavior.
Visual checkpoints are saved as `success-<step>.png` or `failure-<step>.png` alongside the serial and install logs in `test-runs/<iso>/runs/<timestamp>/`. Independent test files and applications continue after a failure so one broken surface does not hide the rest of the report. The harness then stops the VM and opens the ordered screenshots in `imv` for quick visual review.
The harness syncs the acceptance suite from `$OMARCHY_PATH` when it is available. The install phase produces a reusable base image, so iterating against another checkout is fast:
```bash
./bin/omarchy-iso-test release/omarchy.iso --install-only # once per ISO
./bin/omarchy-iso-test release/omarchy.iso --reuse-base \
--sync-omarchy ../omarchy # fast loop against local tests
```
Pass `--encrypt` to drive the encrypted install flow (including typing the LUKS passphrase at boot) instead of the unencrypted one. Pass `--no-preview` to collect the same visual artifacts without opening them in `imv` when the run finishes.
## Signing the ISO ## Signing the ISO
Run `./bin/omarchy-iso-sign [release/omarchy.iso]`. The signing key is retrieved from the shared Omarchy vault with the 1Password CLI. Run `./bin/omarchy-iso-sign [release/omarchy.iso]`. The signing key is retrieved from the shared Omarchy vault with the 1Password CLI.
## Uploading the ISO ## Uploading the ISO
Run `./bin/omarchy-iso-upload [release/omarchy.iso]`. This requires you've configured rclone (use `rclone config`). Run `./bin/omarchy-iso-upload [release/omarchy.iso]`. This requires rclone configuration (`rclone config`).
## Full release of the ISO ## Full release of the ISO
+123 -15
View File
@@ -2,19 +2,113 @@
set -e set -e
ISO="$1" ISO=""
REUSE="$2" REUSE=""
SSH_HOST="${OMARCHY_VM_SSH_HOST:-127.0.0.1}"
SSH_PORT="${OMARCHY_VM_SSH_PORT:-2222}"
NETWORK="${OMARCHY_VM_NETWORK:-1}"
DISK="${OMARCHY_VM_DISK:-/tmp/omarchy-iso-boot.qcow2}"
OVMF_VARS="${OMARCHY_VM_OVMF_VARS:-/tmp/OVMF_VARS.4m.fd}"
EXTRA_QEMU_ARGS=()
if [ $# -eq 0 ]; then usage() {
cat <<USAGE
Usage: omarchy-iso-boot [options] [release/omarchy.iso] [reuse]
Options:
--reuse Reuse the existing VM disk
--ssh-port PORT Forward host PORT to guest port 22 (default: $SSH_PORT)
--ssh-host HOST Host address for SSH forwarding (default: $SSH_HOST)
--no-network Boot without a virtual NIC
--disk PATH VM disk path (default: $DISK)
--ovmf-vars PATH OVMF vars path (default: $OVMF_VARS)
-h, --help Show this help
-- Pass the remaining arguments directly to QEMU
SSH access after starting sshd in the guest:
ssh -p $SSH_PORT root@localhost
scp -P $SSH_PORT /path/to/system-manifest root@localhost:/tmp/
USAGE
}
while (( $# > 0 )); do
case "$1" in
--reuse|reuse)
REUSE="reuse"
shift
;;
--ssh-port)
SSH_PORT="${2:-}"
shift 2
;;
--ssh-port=*)
SSH_PORT="${1#*=}"
shift
;;
--ssh-host)
SSH_HOST="${2:-}"
shift 2
;;
--ssh-host=*)
SSH_HOST="${1#*=}"
shift
;;
--no-network|--no-net)
NETWORK=0
shift
;;
--disk)
DISK="${2:-}"
shift 2
;;
--disk=*)
DISK="${1#*=}"
shift
;;
--ovmf-vars)
OVMF_VARS="${2:-}"
shift 2
;;
--ovmf-vars=*)
OVMF_VARS="${1#*=}"
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
EXTRA_QEMU_ARGS+=("$@")
break
;;
*)
if [[ -z $ISO ]]; then
ISO="$1"
else
echo "Unexpected argument: $1" >&2
usage >&2
exit 1
fi
shift
;;
esac
done
if [[ -z $ISO && -t 0 ]]; then
ISO=$(ls -t release/*.iso 2>/dev/null | gum choose --header "Select ISO to boot") ISO=$(ls -t release/*.iso 2>/dev/null | gum choose --header "Select ISO to boot")
if gum confirm --default=false "Reuse existing disk?"; then if gum confirm --default=false "Reuse existing disk?"; then
REUSE="reuse" REUSE="reuse"
fi fi
fi fi
if [ -z "$ISO" ]; then if [[ -z $ISO ]]; then
echo "Usage: omarchy-iso-boot [release/omarchy.iso] [reuse]" usage
echo " SSH available at: ssh -p 2222 root@localhost (after installing openssh)" exit 1
fi
if [[ -z $DISK || -z $OVMF_VARS ]]; then
echo "Disk and OVMF vars paths must not be empty" >&2
exit 1 exit 1
fi fi
@@ -36,26 +130,40 @@ if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
exit 1 exit 1
fi fi
DISK="/tmp/omarchy-iso-boot.qcow2"
if [[ ! -f $DISK || $REUSE != "reuse" ]]; then if [[ ! -f $DISK || $REUSE != "reuse" ]]; then
qemu-img create -f qcow2 "$DISK" 40G qemu-img create -f qcow2 "$DISK" 40G
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd /tmp/OVMF_VARS.4m.fd cp /usr/share/edk2/x64/OVMF_VARS.4m.fd "$OVMF_VARS"
fi
NET_ARGS=()
if [[ $NETWORK != "0" && $NETWORK != "false" && $NETWORK != "no" ]]; then
if [[ -n $SSH_PORT && $SSH_PORT != "0" ]]; then
NET_ARGS=(-netdev "user,id=net0,hostfwd=tcp:${SSH_HOST}:${SSH_PORT}-:22" -device virtio-net-pci,netdev=net0)
echo "SSH forwarding enabled: ssh -p $SSH_PORT root@localhost"
echo "SCP example: scp -P $SSH_PORT /path/to/system-manifest root@localhost:/tmp/"
else
NET_ARGS=(-netdev user,id=net0 -device virtio-net-pci,netdev=net0)
echo "Networking enabled without SSH port forwarding."
fi
else
echo "Networking disabled."
fi fi
# Boot from ISO before we've installed, then boot from drive after # Boot from ISO before we've installed, then boot from drive after
qemu-system-x86_64 \ qemu-system-x86_64 \
-cpu host -enable-kvm -machine q35,accel=kvm \ -cpu host -enable-kvm -machine q35,accel=kvm \
-smp "$(nproc)" \ -smp "$(($(nproc) < 8 ? $(nproc) : 8))" \
-m 8192 \ -m 8192 \
-drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \ -drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
-drive if=pflash,format=raw,file=/tmp/OVMF_VARS.4m.fd \ -drive if=pflash,format=raw,file="$OVMF_VARS" \
-drive file="$DISK",format=qcow2,if=none,id=drive0 \ -drive file="$DISK",format=qcow2,if=none,id=drive0 \
-device virtio-blk-pci,drive=drive0,bootindex=1 \ -device virtio-blk-pci,drive=drive0,bootindex=1 \
-drive file="$ISO",media=cdrom,if=none,format=raw,id=cdrom0 \ -drive file="$ISO",media=cdrom,if=none,format=raw,id=cdrom0 \
-device ide-cd,drive=cdrom0,bootindex=2 \ -device ide-cd,drive=cdrom0,bootindex=2 \
-device virtio-vga \ -device virtio-vga-gl \
-display gtk \ -display sdl,gl=on \
-usb -device usb-tablet \ -usb -device usb-tablet \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \ -serial none \
-device virtio-net-pci,netdev=net0 \ "${NET_ARGS[@]}" \
-boot menu=on -boot menu=on \
"${EXTRA_QEMU_ARGS[@]}"
+8 -19
View File
@@ -3,25 +3,14 @@ set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cleanup() { if [[ -z "${OMARCHY_PATH:-}" ]]; then
[[ -n "${mock_omarchy_install:-}" ]] && rm -rf "$(dirname "$mock_omarchy_install")" for candidate in "$repo_root/../omarchy-installer" "$repo_root/../omarchy" /usr/share/omarchy; do
} if [[ -f "$candidate/logo.txt" ]]; then
trap cleanup EXIT export OMARCHY_PATH="$candidate"
break
if [[ -z "${OMARCHY_INSTALL:-}" ]]; then fi
if [[ -r /root/omarchy/install/helpers/all.sh ]]; then done
export OMARCHY_INSTALL=/root/omarchy/install
else
mock_root="$(mktemp -d)"
mock_omarchy_install="$mock_root/install"
mkdir -p "$mock_omarchy_install/helpers"
cat >"$mock_omarchy_install/helpers/all.sh" <<'EOF'
PADDING_LEFT=0
PADDING_LEFT_SPACES=""
clear_logo() { clear; }
EOF
export OMARCHY_INSTALL="$mock_omarchy_install"
fi
fi fi
# Do a dry-run of the configurator to show the flow without having to do a real installation
exec bash "$repo_root/configs/airootfs/root/configurator" dry "$@" exec bash "$repo_root/configs/airootfs/root/configurator" dry "$@"
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Preview the shipped Omarchy installer dashboard without building an ISO.
set -euo pipefail
write_simulation_state() {
local state_file="$1" started_at="$2" phase_started_at="$3" current_phase="$4"
local finished_at="${5:-null}" temporary="${state_file}.tmp"
jq -n \
--argjson started_at "$started_at" \
--argjson phase_started_at "$phase_started_at" \
--arg current_phase "$current_phase" \
--argjson finished_at "$finished_at" \
'{
started_at: $started_at,
phase_started_at: $phase_started_at,
current_phase: $current_phase
} + if $finished_at == null then {} else {finished_at: $finished_at} end' \
>"$temporary"
mv -f -- "$temporary" "$state_file"
}
simulate_install() {
local state_file="$1"
local started_at="$EPOCHREALTIME"
local phase phase_started_at finished_at duration i
local -a phases=(
"Starting installation" 0.5
"Preparing live environment" 0.8
"Installing Arch + Omarchy" 5.0
"Configuring system" 1.2
"Finalizing Limine boot" 1.0
"Finalizing user" 1.0
"Configuring login" 0.6
"Validating boot setup" 0.8
)
for ((i = 0; i < ${#phases[@]}; i += 2)); do
phase="${phases[i]}"
duration="${phases[i + 1]}"
phase_started_at="$EPOCHREALTIME"
write_simulation_state "$state_file" "$started_at" "$phase_started_at" "$phase"
sleep "$duration"
done
finished_at="$EPOCHREALTIME"
write_simulation_state "$state_file" "$started_at" "$finished_at" "Installation complete" "$finished_at"
sleep 0.3
}
if [[ ${1:-} == "--simulate-install" ]]; then
(( $# == 2 )) || exit 2
simulate_install "$2"
exit 0
fi
if (( $# > 0 )); then
echo "Usage: $0" >&2
exit 2
fi
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
dashboard="$repo_root/configs/airootfs/usr/local/bin/omarchy-install-dashboard"
if [[ ! -x $dashboard ]]; then
echo "Installer dashboard is not executable: $dashboard" >&2
exit 1
fi
if [[ -z ${OMARCHY_PATH:-} ]]; then
for candidate in "$repo_root/../omarchy" /usr/share/omarchy; do
if [[ -f $candidate/logo.txt ]]; then
export OMARCHY_PATH="$candidate"
break
fi
done
fi
if [[ -z ${OMARCHY_PATH:-} || ! -f $OMARCHY_PATH/logo.txt ]]; then
echo "Unable to find Omarchy logo.txt; set OMARCHY_PATH to an Omarchy checkout." >&2
exit 1
fi
preview_dir="$(mktemp -d)"
cleanup() {
rm -rf "$preview_dir"
}
trap cleanup EXIT
log_file="$preview_dir/install.log"
state_file="$preview_dir/state.json"
touch "$log_file"
OMARCHY_UI_AUTO_REBOOT=no \
"$dashboard" "$log_file" "$state_file" -- \
"$0" --simulate-install "$state_file"
+75 -26
View File
@@ -1,10 +1,10 @@
#!/bin/bash #!/bin/bash
# Abort if anything fails
set -e set -e
# Parse command line arguments
NO_CACHE="" NO_CACHE=""
LOCAL_OMARCHY_PATH=""
LOCAL_PKGS_PATH=""
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--no-cache) --no-cache)
@@ -15,13 +15,28 @@ while [[ $# -gt 0 ]]; do
NO_BOOT_OFFER=1 NO_BOOT_OFFER=1
shift shift
;; ;;
--local-source) --debug)
LOCAL_SOURCE=1 OMARCHY_INSTALL_DEBUG=1
shift
;;
--edge)
OMARCHY_ISO_REF=edge
OMARCHY_MIRROR=edge OMARCHY_MIRROR=edge
shift shift
;; ;;
--local-source)
LOCAL_OMARCHY_PATH="${2:-}"
LOCAL_PKGS_PATH="${3:-}"
if [[ -z $LOCAL_OMARCHY_PATH || -z $LOCAL_PKGS_PATH ]]; then
echo "Error: --local-source requires two args: <omarchy-checkout> <pkgs-checkout>" >&2
exit 1
fi
OMARCHY_MIRROR=edge
OMARCHY_ISO_REF=local
shift 3
;;
--dev) --dev)
OMARCHY_INSTALLER_REF=dev OMARCHY_ISO_REF=dev
OMARCHY_MIRROR=edge OMARCHY_MIRROR=edge
shift shift
;; ;;
@@ -31,40 +46,77 @@ while [[ $# -gt 0 ]]; do
shift shift
;; ;;
--rc) --rc)
OMARCHY_ISO_REF=rc
OMARCHY_MIRROR=rc OMARCHY_MIRROR=rc
OMARCHY_INSTALLER_REF=rc
shift shift
;; ;;
*) *)
echo "Unknown option: $1" echo "Unknown option: $1"
echo "Usage: $0 [--no-cache] [--no-boot-offer] [--local-source] [--dev] [--quattro] [--rc]" echo "Usage: $0 [--no-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--local-source <omarchy-checkout> <pkgs-checkout>]"
exit 1 exit 1
;; ;;
esac esac
done done
# Clear out old packages if [[ -n $LOCAL_OMARCHY_PATH ]]; then
sudo rm -rf /var/cache/pacman/pkg/* [[ -d $LOCAL_OMARCHY_PATH ]] || { echo "Error: Omarchy checkout not found at $LOCAL_OMARCHY_PATH" >&2; exit 1; }
[[ -d $LOCAL_PKGS_PATH ]] || { echo "Error: pkgs checkout not found at $LOCAL_PKGS_PATH" >&2; exit 1; }
LOCAL_OMARCHY_PATH=$(realpath "$LOCAL_OMARCHY_PATH")
LOCAL_PKGS_PATH=$(realpath "$LOCAL_PKGS_PATH")
fi
# Ensure we have the submodule checked out lint_file_permissions() {
local profiledef="configs/profiledef.sh"
local -A declared=()
local missing=()
while IFS= read -r path; do
[[ -n $path ]] && declared["$path"]=1
done < <(grep -oP '^\s*\["\K[^"]+(?="\])' "$profiledef" || true)
while IFS= read -r f; do
[[ -f $f ]] || continue
local rel="${f#configs/airootfs}"
local mode
mode=$(git ls-files --stage -- "$f" | awk '{print $1}')
if [[ $mode != 100755 && $(head -c 2 "$f") != "#!" ]]; then
continue
fi
if [[ -z ${declared[$rel]+x} ]]; then
missing+=("$rel")
fi
done < <(find configs/airootfs/usr/local/bin configs/airootfs/root -type f 2>/dev/null)
if (( ${#missing[@]} > 0 )); then
echo "ERROR: the following executable files are missing from $profiledef file_permissions:" >&2
printf ' %s\n' "${missing[@]}" >&2
echo "Add entries like [\"<path>\"]=\"0:0:755\" to profiledef.sh and retry." >&2
exit 1
fi
}
lint_file_permissions
sudo rm -rf /var/cache/pacman/pkg/*
git submodule update --init --recursive --jobs=8 git submodule update --init --recursive --jobs=8
# Prepare build repo
BUILD_SCRIPT="builder/build-iso.sh" BUILD_SCRIPT="builder/build-iso.sh"
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
BUILD_RELEASE_PATH="$BUILD_ROOT/release" BUILD_RELEASE_PATH="$BUILD_ROOT/release"
mkdir -p "$BUILD_RELEASE_PATH" mkdir -p "$BUILD_RELEASE_PATH"
OMARCHY_INSTALLER_REPO="${OMARCHY_INSTALLER_REPO:-basecamp/omarchy}" OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
OMARCHY_INSTALLER_REF="${OMARCHY_INSTALLER_REF:-master}"
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}" OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
DOCKER_ARGS=( DOCKER_ARGS=(
--rm --rm
--privileged --privileged
-e "OMARCHY_INSTALLER_REPO=$OMARCHY_INSTALLER_REPO" -e "OMARCHY_ISO_REF=$OMARCHY_ISO_REF"
-e "OMARCHY_INSTALLER_REF=$OMARCHY_INSTALLER_REF"
-e "OMARCHY_MIRROR=$OMARCHY_MIRROR" -e "OMARCHY_MIRROR=$OMARCHY_MIRROR"
-e "OMARCHY_INSTALL_DEBUG=${OMARCHY_INSTALL_DEBUG:-}"
-e "HOST_UID=$(id -u)" -e "HOST_UID=$(id -u)"
-e "HOST_GID=$(id -g)" -e "HOST_GID=$(id -g)"
-v "$BUILD_RELEASE_PATH/:/out/" -v "$BUILD_RELEASE_PATH/:/out/"
@@ -74,13 +126,14 @@ DOCKER_ARGS=(
-v "$BUILD_ROOT/configs:/configs:ro" -v "$BUILD_ROOT/configs:/configs:ro"
) )
# Use local source of Omarchy from $OMARCHY_PATH instead of git checkout if [[ -n $LOCAL_OMARCHY_PATH ]]; then
if [[ -n "${LOCAL_SOURCE-}" ]]; then DOCKER_ARGS+=(
DOCKER_ARGS+=(-v "$OMARCHY_PATH:/omarchy:ro") -v "$LOCAL_OMARCHY_PATH:/omarchy-source:ro"
-v "$LOCAL_PKGS_PATH:/omarchy-pkgs:ro"
)
fi fi
# Use local pacman cache if you already have one on host to speed up repeat runs if [[ -d /var/cache/pacman/pkg ]]; then
if [ -d "/var/cache/pacman/pkg" ]; then
DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg") DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg")
fi fi
@@ -89,20 +142,16 @@ fi
if [[ -z "$NO_CACHE" ]]; then if [[ -z "$NO_CACHE" ]]; then
OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${OMARCHY_MIRROR}/airootfs/var/cache/omarchy" OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${OMARCHY_MIRROR}/airootfs/var/cache/omarchy"
mkdir -p "$OFFLINE_REPO_BUILD_CACHE_DIR" mkdir -p "$OFFLINE_REPO_BUILD_CACHE_DIR"
# Mount to /var/cache/airootfs/var/cache/omarchy since that's where repo packages go in the ISO
DOCKER_ARGS+=(-v "$OFFLINE_REPO_BUILD_CACHE_DIR:/var/cache/airootfs/var/cache/omarchy") DOCKER_ARGS+=(-v "$OFFLINE_REPO_BUILD_CACHE_DIR:/var/cache/airootfs/var/cache/omarchy")
fi fi
# Run the builder with assembled args
docker run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT docker run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
# Move as install ref
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*.iso | head -n1) latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*.iso | head -n1)
iso_ref="${latest_iso%.*}-$OMARCHY_INSTALLER_REF.iso" iso_ref="${latest_iso%.*}-$OMARCHY_ISO_REF.iso"
mv -f "$latest_iso" "$iso_ref" mv -f "$latest_iso" "$iso_ref"
# Offer to boot the new build if [[ -z "${NO_BOOT_OFFER:-}" ]]; then
if [[ -z "$NO_BOOT_OFFER" ]]; then
echo echo
gum confirm "Boot $iso_ref?" && omarchy-iso-boot "$iso_ref" gum confirm "Boot $iso_ref?" && omarchy-iso-boot "$iso_ref"
fi fi
+6 -6
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
MAKE_ARGS=() MAKE_ARGS=()
INSTALLER_REF=master ISO_REF=quattro
RELEASE_SUFFIX="" RELEASE_SUFFIX=""
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
@@ -12,7 +12,7 @@ while [[ $# -gt 0 ]]; do
;; ;;
--rc) --rc)
MAKE_ARGS+=("$1") MAKE_ARGS+=("$1")
INSTALLER_REF=rc ISO_REF=rc
RELEASE_SUFFIX="-rc" RELEASE_SUFFIX="-rc"
shift shift
;; ;;
@@ -42,21 +42,21 @@ BUILD_RELEASE_PATH="$BUILD_ROOT/release"
# Make ISOs # Make ISOs
if [[ ! " ${MAKE_ARGS[*]} " =~ " --no-make " ]]; then if [[ ! " ${MAKE_ARGS[*]} " =~ " --no-make " ]]; then
echo -e "\e[32mMaking $INSTALLER_REF ISO\e[0m" echo -e "\e[32mMaking $ISO_REF ISO\e[0m"
omarchy-iso-make --no-cache --no-boot-offer "${MAKE_ARGS[@]}" omarchy-iso-make --no-cache --no-boot-offer "${MAKE_ARGS[@]}"
else else
echo -e "\e[31mSkipping remaking ISOs\e[0m" echo -e "\e[31mSkipping remaking ISOs\e[0m"
fi fi
# Sign and upload ISO # Sign and upload ISO
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-"$INSTALLER_REF".iso | head -n1) latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-"$ISO_REF".iso | head -n1)
release_iso="$BUILD_RELEASE_PATH/omarchy-$BUILD_VERSION$RELEASE_SUFFIX.iso" release_iso="$BUILD_RELEASE_PATH/omarchy-$BUILD_VERSION$RELEASE_SUFFIX.iso"
cp "$latest_iso" "$release_iso" cp "$latest_iso" "$release_iso"
echo -e "\e[32mSigning $INSTALLER_REF ISO\e[0m" echo -e "\e[32mSigning $ISO_REF ISO\e[0m"
omarchy-iso-sign "$release_iso" omarchy-iso-sign "$release_iso"
iso_sha=$(sha256sum "$release_iso") iso_sha=$(sha256sum "$release_iso")
echo -e "\e[32mSHA256 for $INSTALLER_REF ISO: $iso_sha\e[0m" echo -e "\e[32mSHA256 for $ISO_REF ISO: $iso_sha\e[0m"
omarchy-iso-upload "$release_iso" omarchy-iso-upload "$release_iso"
+914
View File
@@ -0,0 +1,914 @@
#!/bin/bash
#
# Acceptance test harness: boots an Omarchy ISO in a headless QEMU VM and
# drives the real interactive install flow the way a user would — reading
# each screen through QMP screendumps + OCR, and answering with virtual
# keystrokes. Nothing in the product is bypassed or stubbed: the configurator
# wizard, install dashboard, reboot prompt, and SDDM login are all exercised.
#
# After first boot it logs into a console TTY (typed, like everything else)
# to authorize an SSH key, snapshots the installed disk, then boots a
# throwaway overlay and runs the in-guest acceptance suite (test/acceptance
# in the omarchy repo) over SSH.
#
# Usage: omarchy-iso-test [release/omarchy.iso] [options]
#
# --encrypt Drive the encrypted install flow (default: unencrypted)
# --reuse-base Skip the install phase if a base image already exists
# --install-only Stop after producing the installed base image
# --sync-omarchy DIR Push DIR's test/acceptance into the guest before running
# (defaults to OMARCHY_PATH when the suite is present)
# --sync-all DIR Push all of DIR (bin/config/shell/test) into the guest
# --port PORT Host port forwarded to guest SSH (default 2222)
# --memory MB VM memory (default 8192)
# --timeout SECS Install timeout (default 2400)
# --keep-running Leave the acceptance VM running after the suite finishes
# --no-preview Do not open the collected screenshots in imv
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
ISO=""
SSH_PORT=2222
MEMORY=8192
INSTALL_TIMEOUT=2400
BOOT_TIMEOUT=600
ENCRYPT=false
REUSE_BASE=false
INSTALL_ONLY=false
KEEP_RUNNING=false
NO_PREVIEW=false
SYNC_DIR=""
SYNC_ALL=false
GUEST_USER="omarchy"
GUEST_PASSWORD="omarchy"
GUEST_HOSTNAME="omarchy-test"
while (($#)); do
case "$1" in
--encrypt) ENCRYPT=true ;;
--reuse-base) REUSE_BASE=true ;;
--install-only) INSTALL_ONLY=true ;;
--keep-running) KEEP_RUNNING=true ;;
--no-preview) NO_PREVIEW=true ;;
--sync-omarchy) SYNC_DIR="$2"; shift ;;
--sync-all) SYNC_DIR="$2"; SYNC_ALL=true; shift ;;
--port) SSH_PORT="$2"; shift ;;
--memory) MEMORY="$2"; shift ;;
--timeout) INSTALL_TIMEOUT="$2"; shift ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) ISO="$1" ;;
esac
shift
done
if [[ -z $SYNC_DIR && -f ${OMARCHY_PATH:-}/test/acceptance ]]; then
SYNC_DIR="$OMARCHY_PATH"
fi
if [[ -z $ISO && -t 0 ]]; then
ISO=$(ls -t "$REPO_DIR"/release/*.iso 2>/dev/null | gum choose --header "Select ISO to test")
fi
if [[ ! -f ${ISO:-} ]]; then
echo "No ISO to test. Pass one explicitly or put it in release/." >&2
echo "Usage: omarchy-iso-test [release/omarchy.iso] [options]" >&2
exit 1
fi
omarchy-pkg-add qemu-full edk2-ovmf socat imagemagick tesseract tesseract-data-eng
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
BASE_NAME="$(basename "$ISO" .iso)"
$ENCRYPT && BASE_NAME+="-encrypted"
BASE_DIR="$REPO_DIR/test-runs/$BASE_NAME"
RUN_DIR="$BASE_DIR/runs/$(date +%Y%m%d-%H%M%S)"
BASE_DISK="$BASE_DIR/base.qcow2"
BASE_OVMF="$BASE_DIR/OVMF_VARS.4m.fd"
SSH_KEY="$BASE_DIR/id_ed25519"
HTTP_PORT=$((SSH_PORT + 1))
HTTP_PID=""
mkdir -p "$BASE_DIR" "$RUN_DIR"
QMP_SOCK="$RUN_DIR/qmp.sock"
PIDFILE="$RUN_DIR/qemu.pid"
log() {
printf '\033[1;35m==> %s\033[0m\n' "$1"
}
# ---------------------------------------------------------------- vm control
vm_running() {
[[ -f $PIDFILE ]] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null
}
qmp() {
printf '{"execute":"qmp_capabilities"}\n{"execute":%s}\n' "$1" |
timeout 5 socat -t 2 - "UNIX-CONNECT:$QMP_SOCK" 2>/dev/null || true
}
screendump() {
qmp "\"screendump\", \"arguments\": {\"filename\": \"$1\"}" >/dev/null
}
capture_console() {
local name="$1" shot="$RUN_DIR/.capture.ppm"
sleep 1
screendump "$shot"
[[ -s $shot ]] || return 0
magick "$shot" "$RUN_DIR/$name.png" 2>/dev/null || true
rm -f "$shot"
}
stop_vm() {
vm_running || return 0
if ! ssh_guest "echo $GUEST_PASSWORD | sudo -S systemctl poweroff" >/dev/null 2>&1; then
qmp '"system_powerdown"' >/dev/null
fi
local waited=0
while vm_running && ((waited < 15)); do
sleep 1
((waited += 1))
done
if vm_running; then
qmp '"quit"' >/dev/null
sleep 1
fi
if vm_running; then
kill "$(cat "$PIDFILE")" 2>/dev/null || true
sleep 1
fi
}
open_screenshots() {
local entry
local -a screenshots=()
while IFS= read -r -d '' entry; do
screenshots+=("${entry#* }")
done < <(
find "$RUN_DIR" -type f \( -name "success-*.png" -o -name "failure-*.png" \) -printf '%T@ %p\0' |
sort -zn
)
(( ${#screenshots[@]} > 0 )) || return 0
log "Visual review: ${#screenshots[@]} screenshots in $RUN_DIR"
$NO_PREVIEW && return 0
if command -v imv >/dev/null && [[ -n ${WAYLAND_DISPLAY:-}${DISPLAY:-} ]]; then
setsid -f imv "${screenshots[@]}" >/dev/null 2>&1
fi
}
cleanup() {
local status=$?
[[ -n $HTTP_PID ]] && kill "$HTTP_PID" 2>/dev/null || true
if ! $KEEP_RUNNING; then
vm_running && log "Stopping test VM"
stop_vm
fi
rm -f "$RUN_DIR/.screen.ppm" "$RUN_DIR/.screen.png"
open_screenshots
return $status
}
trap cleanup EXIT
start_vm() {
local disk="$1" serial="$2"
shift 2
qemu-system-x86_64 \
-cpu host -enable-kvm -machine q35,accel=kvm \
-smp "$(nproc)" \
-m "$MEMORY" \
-drive if=pflash,format=raw,readonly=on,file="$OVMF_CODE" \
-drive if=pflash,format=raw,file="$BASE_OVMF" \
-drive file="$disk",format=qcow2,if=none,id=drive0 \
-device virtio-blk-pci,drive=drive0,bootindex=1 \
-device virtio-vga \
-display none \
-usb -device usb-tablet \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:$SSH_PORT-:22 \
-device virtio-net-pci,netdev=net0 \
-qmp "unix:$QMP_SOCK,server,nowait" \
-serial "file:$serial" \
-pidfile "$PIDFILE" \
-daemonize \
"$@"
}
# ------------------------------------------------------------ console driver
#
# The heart of the harness: read the VM's console with screendump + OCR, and
# type into it with QMP send-key. Together they make an expect-style loop
# that walks the same screens a user sees.
ocr_screen() {
local shot="$RUN_DIR/.screen.ppm" prepped="$RUN_DIR/.screen.png"
screendump "$shot"
[[ -s $shot ]] || return 0
magick "$shot" -colorspace gray -negate -resize 150% "$prepped" 2>/dev/null || return 0
tesseract "$prepped" - --psm 6 2>/dev/null || true
}
# Wait until the console shows the given text. Saves the offending screen as
# an artifact on timeout.
wait_for_screen() {
local text="$1" timeout="$2" waited=0 slug
until ocr_screen | grep -qi "$text"; do
if ! vm_running; then
echo "VM exited while waiting for screen: $text" >&2
return 1
fi
if ((waited >= timeout)); then
slug=${text,,}
slug=${slug// /-}
slug=${slug//[^a-z0-9-]/}
capture_console "failure-installer-waiting-for-$slug"
echo "Timed out after ${timeout}s waiting for screen: $text" >&2
return 1
fi
sleep 3
((waited += 3))
done
sleep 1 # let the screen settle before typing
}
# Press a key or chord, dash-separated: "ret", "ctrl-c", "ctrl-alt-f3".
press() {
local part json=""
local -a parts
IFS='-' read -ra parts <<<"$1"
for part in "${parts[@]}"; do
json+="{\"type\":\"qcode\",\"data\":\"$part\"},"
done
qmp "\"send-key\", \"arguments\": {\"keys\": [${json%,}]}" >/dev/null
}
type_text() {
local text="$1" ch i
for ((i = 0; i < ${#text}; i++)); do
ch=${text:i:1}
case "$ch" in
[a-z0-9]) press "$ch" ;;
[A-Z]) press "shift-${ch,,}" ;;
" ") press spc ;;
.) press dot ;;
,) press comma ;;
-) press minus ;;
_) press shift-minus ;;
/) press slash ;;
:) press shift-semicolon ;;
@) press shift-2 ;;
\|) press shift-backslash ;;
*) echo "type_text: unsupported character: $ch" >&2; return 1 ;;
esac
sleep 0.05
done
}
# --------------------------------------------------------------------- guest
ssh_guest() {
ssh -i "$SSH_KEY" -p "$SSH_PORT" \
-o BatchMode=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
-o LogLevel=ERROR \
"$GUEST_USER@127.0.0.1" "$@"
}
wait_for_ssh() {
local timeout="$1" failure_name="${2:-failure-session-ssh-timeout}" waited=0
while ! ssh_guest true 2>/dev/null; do
if ! vm_running; then
echo "VM exited while waiting for SSH" >&2
return 1
fi
if ((waited >= timeout)); then
capture_console "$failure_name"
echo "Timed out after ${timeout}s waiting for SSH" >&2
return 1
fi
sleep 5
((waited += 5))
done
}
session_started() {
ssh_guest "ls /run/user/1000/hypr 2>/dev/null | grep -q ." 2>/dev/null
}
ssh_session() {
local command="$1"
ssh_guest "export XDG_RUNTIME_DIR=/run/user/\$(id -u); \
export DBUS_SESSION_BUS_ADDRESS=unix:path=\$XDG_RUNTIME_DIR/bus; \
export LANG=\$(systemctl --user show-environment | sed -n 's/^LANG=//p' | head -1); \
export LANG=\${LANG:-C.UTF-8}; \
export HYPRLAND_INSTANCE_SIGNATURE=\$(ls -t \$XDG_RUNTIME_DIR/hypr | head -1); \
export WAYLAND_DISPLAY=\$(find \$XDG_RUNTIME_DIR -maxdepth 1 -name 'wayland-*' ! -name '*.lock' -printf '%f\\n' | head -1); \
export OMARCHY_PATH=/usr/share/omarchy; \
export PATH=\$OMARCHY_PATH/bin:\$PATH; \
$command" </dev/null
}
wait_for_guest_state() {
local description="$1" timeout="$2"
shift 2
local deadline=$((SECONDS + timeout))
until "$@" >/dev/null 2>&1; do
if ((SECONDS >= deadline)); then
printf 'not ok - %s\n' "$description" >&2
return 1
fi
sleep 1
done
printf 'ok - %s\n' "$description"
}
guest_layer_present() {
local namespace="$1"
ssh_session "hyprctl -j layers | jq -e --arg namespace '$namespace' \
'[.. | objects | select(.namespace? == \$namespace)] | length > 0'"
}
guest_layer_absent() {
! guest_layer_present "$1"
}
cleanup_shortcut_state() {
ssh_session 'for plugin in omarchy.launcher omarchy.menu omarchy.emojis omarchy.clipboard omarchy.audio; do
omarchy-shell shell hide "$plugin" >/dev/null 2>&1 || true
done
while read -r address; do
hyprctl dispatch "hl.dsp.window.close({ window = \"address:$address\" })" >/dev/null 2>&1 ||
hyprctl dispatch closewindow "address:$address" >/dev/null 2>&1 || true
done < <(hyprctl -j clients | jq -r '\''.[] | select(.class == "foot") | .address'\'')
hyprctl dispatch workspace 1 >/dev/null 2>&1 || true' >/dev/null 2>&1 || true
}
verify_surface_shortcut() {
local name="$1" chord="$2" namespace="$3" plugin="$4"
press "$chord"
if ! wait_for_guest_state "$name shortcut opens" 15 guest_layer_present "$namespace"; then
capture_console "failure-shortcut-$name-open"
ssh_session "omarchy-shell shell hide '$plugin' >/dev/null 2>&1 || true" || true
return 1
fi
capture_console "success-shortcut-$name"
press esc
if ! wait_for_guest_state "$name shortcut surface closes" 15 guest_layer_absent "$namespace"; then
capture_console "failure-shortcut-$name-close"
ssh_session "omarchy-shell shell hide '$plugin' >/dev/null 2>&1 || true" || true
return 1
fi
}
verify_desktop_shortcuts() {
cleanup_shortcut_state
press meta_l-ret
if ! wait_for_guest_state "terminal shortcut opens a window" 30 \
ssh_session "hyprctl -j clients | jq -e '[.[] | select(.class == \"foot\")] | length == 1'"; then
capture_console "failure-shortcut-terminal-open"
return 1
fi
press meta_l-ret
if ! wait_for_guest_state "terminal shortcut opens a second window" 30 \
ssh_session "hyprctl -j clients | jq -e '[.[] | select(.class == \"foot\")] | length == 2'"; then
capture_console "failure-shortcut-terminal-second-window"
return 1
fi
capture_console "success-shortcut-desktop-01-two-tiled-terminals"
press meta_l-t
if ! wait_for_guest_state "floating shortcut floats the active window" 15 \
ssh_session "hyprctl -j activewindow | jq -e '.class == \"foot\" and .floating == true'"; then
capture_console "failure-shortcut-window-float"
return 1
fi
capture_console "success-shortcut-desktop-02-floating-terminal"
press meta_l-t
if ! wait_for_guest_state "floating shortcut returns the window to tiling" 15 \
ssh_session "hyprctl -j activewindow | jq -e '.class == \"foot\" and .floating == false'"; then
capture_console "failure-shortcut-window-retile"
return 1
fi
press meta_l-shift-2
if ! wait_for_guest_state "move shortcut follows the window to workspace 2" 15 \
ssh_session "hyprctl -j activeworkspace | jq -e '.id == 2' && \
hyprctl -j activewindow | jq -e '.class == \"foot\" and .workspace.id == 2'"; then
capture_console "failure-shortcut-window-workspace-2"
return 1
fi
capture_console "success-shortcut-desktop-03-terminal-on-workspace-2"
press meta_l-1
if ! wait_for_guest_state "workspace shortcut returns to workspace 1" 15 \
ssh_session "hyprctl -j activeworkspace | jq -e '.id == 1'"; then
capture_console "failure-shortcut-workspace-1"
return 1
fi
capture_console "success-shortcut-desktop-04-workspace-1"
press meta_l-w
if ! wait_for_guest_state "close shortcut closes the workspace 1 terminal" 15 \
ssh_session "hyprctl -j clients | jq -e '[.[] | select(.class == \"foot\")] | length == 1'"; then
capture_console "failure-shortcut-close-workspace-1"
return 1
fi
press meta_l-2
if ! wait_for_guest_state "workspace shortcut visits workspace 2" 15 \
ssh_session "hyprctl -j activeworkspace | jq -e '.id == 2'"; then
capture_console "failure-shortcut-visit-workspace-2"
return 1
fi
press meta_l-w
if ! wait_for_guest_state "close shortcut closes the workspace 2 terminal" 15 \
ssh_session "hyprctl -j clients | jq -e '[.[] | select(.class == \"foot\")] | length == 0'"; then
capture_console "failure-shortcut-close-workspace-2"
return 1
fi
}
verify_universal_clipboard_shortcuts() {
local copy_token="OmarchyUniversalCopy$(date +%s)"
local paste_token="OmarchyUniversalPaste$(date +%s)"
local terminal_token="OmarchyTerminalPaste$(date +%s)"
local terminal_result="/tmp/omarchy-universal-terminal-paste"
cleanup_shortcut_state
ssh_session "setsid -f chromium --new-window about:blank >/dev/null 2>&1"
if ! wait_for_guest_state "universal clipboard test opens Chromium" 45 \
ssh_session "hyprctl -j clients | jq -e '[.[] | select(.class | test(\"(?i)chromium\"))] | length == 1'"; then
capture_console "failure-shortcut-universal-clipboard-browser"
return 1
fi
press ctrl-l
type_text "$copy_token"
press ctrl-a
press meta_l-c
if ! wait_for_guest_state "universal copy uses the active GUI application" 15 \
ssh_session "[[ \$(wl-paste --no-newline) == '$copy_token' ]]"; then
capture_console "failure-shortcut-universal-copy"
return 1
fi
ssh_session "setsid -f bash -c 'printf \"%s\" \"$paste_token\" | wl-copy' >/dev/null 2>&1"
if ! wait_for_guest_state "universal paste clipboard is seeded" 15 \
ssh_session "[[ \$(wl-paste --no-newline) == '$paste_token' ]]"; then
capture_console "failure-shortcut-universal-paste-seed"
return 1
fi
press ctrl-l
press meta_l-v
sleep 1
press ctrl-a
press ctrl-c
if ! wait_for_guest_state "universal paste uses the active GUI application" 15 \
ssh_session "[[ \$(wl-paste --no-newline) == '$paste_token' ]]"; then
capture_console "failure-shortcut-universal-paste-gui"
return 1
fi
capture_console "success-shortcut-universal-copy-paste-gui"
ssh_session 'while read -r address; do
hyprctl dispatch "hl.dsp.window.close({ window = \"address:$address\" })" >/dev/null 2>&1 ||
hyprctl dispatch closewindow "address:$address" >/dev/null 2>&1 || true
done < <(hyprctl -j clients | jq -r '\''.[] | select(.class | test("(?i)chromium")) | .address'\'')' || true
ssh_session "rm -f '$terminal_result'; \
setsid -f bash -c 'printf \"%s\" \"$terminal_token\" | wl-copy' >/dev/null 2>&1; \
setsid -f foot --app-id=foot -e bash -c 'read -r value; printf \"%s\" \"\$value\" > \"$terminal_result\"; sleep 30' >/dev/null 2>&1"
if ! wait_for_guest_state "terminal paste clipboard is seeded" 15 \
ssh_session "[[ \$(wl-paste --no-newline) == '$terminal_token' ]]"; then
capture_console "failure-shortcut-universal-paste-terminal-seed"
return 1
fi
if ! wait_for_guest_state "universal clipboard test opens a terminal" 30 \
ssh_session "hyprctl -j clients | jq -e '[.[] | select(.class == \"foot\")] | length == 1'"; then
capture_console "failure-shortcut-universal-clipboard-terminal"
return 1
fi
press meta_l-v
sleep 1
press ret
if ! wait_for_guest_state "universal paste uses the terminal-safe shortcut" 15 \
ssh_session "[[ \$(cat '$terminal_result' 2>/dev/null) == '$terminal_token' ]]"; then
capture_console "failure-shortcut-universal-paste-terminal"
return 1
fi
capture_console "success-shortcut-universal-paste-terminal"
cleanup_shortcut_state
}
shortcut_smoke_phase() {
log "Exercising primary desktop shortcuts through the VM keyboard"
local status=0
local name chord namespace plugin
local shortcuts='launcher|meta_l-spc|omarchy-launcher|omarchy.launcher
menu|meta_l-alt-spc|omarchy-menu|omarchy.menu
emojis|meta_l-ctrl-e|omarchy-emojis|omarchy.emojis
clipboard|meta_l-ctrl-v|omarchy-clipboard|omarchy.clipboard
audio-panel|meta_l-ctrl-a|omarchy-keyboard-panel|omarchy.audio'
cleanup_shortcut_state
while IFS='|' read -r name chord namespace plugin; do
if ! verify_surface_shortcut "$name" "$chord" "$namespace" "$plugin"; then
status=1
fi
done <<<"$shortcuts"
if ! verify_desktop_shortcuts; then
status=1
fi
if ! verify_universal_clipboard_shortcuts; then
status=1
fi
cleanup_shortcut_state
return $status
}
# ----------------------------------------------------- driving the installer
drive_configurator() {
log "Driving the installer wizard"
wait_for_screen "keyboard layout" 300
capture_console "success-installer-01-keyboard-layout"
press ret # English (US) is preselected
wait_for_screen "Username" 120
type_text "$GUEST_USER"
capture_console "success-installer-02-username"
press ret
wait_for_screen "Password" 60
type_text "$GUEST_PASSWORD"
capture_console "success-installer-03-password"
press ret
wait_for_screen "Must match" 60
type_text "$GUEST_PASSWORD"
capture_console "success-installer-04-password-confirmation"
press ret
wait_for_screen "Full name" 60
type_text "Omarchy Test"
capture_console "success-installer-05-full-name"
press ret
wait_for_screen "Email address" 60
type_text "test@omarchy.org"
capture_console "success-installer-06-email"
press ret
wait_for_screen "Hostname" 60
type_text "$GUEST_HOSTNAME"
capture_console "success-installer-07-hostname"
press ret
wait_for_screen "Timezone" 180 # tzupdate geo-guess can take a moment
capture_console "success-installer-08-timezone"
press ret
wait_for_screen "look right" 60
capture_console "success-installer-09-review"
press ret
wait_for_screen "install disk" 60
capture_console "success-installer-10-install-disk"
press ret # the single virtio disk
wait_for_screen "recovery possible" 60
capture_console "success-installer-11-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c # toggles the confirm to the unencrypted flow
wait_for_screen "without encryption" 30
capture_console "success-installer-12-disk-warning-unencrypted"
fi
press ret
sleep 2
capture_console "success-installer-13-install-started"
}
wait_for_install() {
log "Waiting for the install to finish (timeout ${INSTALL_TIMEOUT}s)"
local waited=0 text progress_name
while true; do
text=$(ocr_screen)
if grep -qi "Reboot Now" <<<"$text"; then
log "Install finished. Confirming the reboot prompt."
capture_console "success-installer-15-reboot"
press ret
return 0
fi
if grep -qi "installation stopped" <<<"$text"; then
capture_console "failure-installer-installation-stopped"
echo "Install failed — screenshot saved to $RUN_DIR" >&2
return 1
fi
if ! vm_running; then
echo "VM exited during install" >&2
return 1
fi
if ((waited >= INSTALL_TIMEOUT)); then
capture_console "failure-installer-install-timeout"
echo "Timed out after ${INSTALL_TIMEOUT}s waiting for install" >&2
return 1
fi
if ((waited % 60 == 0)); then
printf -v progress_name 'success-installer-14-install-progress-%04ds' "$waited"
capture_console "$progress_name"
echo " ... installing (${waited}s)"
fi
sleep 10
((waited += 10))
done
}
# Encrypted boots stop at the LUKS passphrase prompt. Plymouth's prompt isn't
# reliably OCR-able, so type the passphrase on an interval until a console
# login becomes reachable; early keystrokes are harmlessly discarded.
unlock_luks() {
log "Typing the LUKS passphrase until boot proceeds"
local waited=0
sleep 10
capture_console "success-first-boot-01-luks-prompt"
while true; do
type_text "$GUEST_PASSWORD"
if ((waited == 0)); then
capture_console "success-first-boot-02-luks-passphrase"
fi
press ret
sleep 20
((waited += 30))
press ctrl-alt-f3
sleep 3
if ocr_screen | grep -qi "login:"; then
return 0
fi
if ((waited >= 300)); then
capture_console "failure-first-boot-luks-timeout"
echo "Timed out waiting for LUKS unlock" >&2
return 1
fi
done
}
# Log into a spare console TTY as the user and authorize the harness SSH key —
# the same thing a person would do to give themselves remote access.
bootstrap_ssh() {
log "Authorizing SSH access via console login"
mkdir -p "$BASE_DIR/www"
cat >"$BASE_DIR/www/bootstrap" <<EOF
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo "$(cat "$SSH_KEY.pub")" >>~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
echo "$GUEST_PASSWORD" | sudo -S ufw allow from 10.0.2.2 to any port 22 proto tcp
echo "$GUEST_PASSWORD" | sudo -S systemctl enable --now sshd.service
EOF
(cd "$BASE_DIR/www" && exec python3 -m http.server "$HTTP_PORT" --bind 127.0.0.1 >/dev/null 2>&1) &
HTTP_PID=$!
local waited=0
while true; do
press ctrl-alt-f3
sleep 4
ocr_screen | grep -qi "login:" && break
((waited += 8))
if ((waited >= 300)); then
capture_console "failure-first-boot-console-timeout"
echo "Timed out waiting for a console login prompt" >&2
return 1
fi
sleep 4
done
type_text "$GUEST_USER"
capture_console "success-first-boot-03-console-username"
press ret
wait_for_screen "Password" 60
type_text "$GUEST_PASSWORD"
capture_console "success-first-boot-04-console-password"
press ret
sleep 3
type_text "curl -fsS http://10.0.2.2:$HTTP_PORT/bootstrap | bash"
capture_console "success-first-boot-05-bootstrap-command"
press ret
wait_for_ssh 180 "failure-first-boot-ssh-timeout"
capture_console "success-first-boot-06-bootstrap-complete"
kill "$HTTP_PID" 2>/dev/null || true
HTTP_PID=""
press ctrl-alt-f1
}
# Get a freshly booted system to a running Hyprland session, typing at the
# LUKS prompt and/or SDDM greeter exactly like a user would.
establish_session() {
if $ENCRYPT; then
local waited=0
sleep 10
capture_console "success-session-01-luks-prompt"
until ssh_guest true 2>/dev/null; do
type_text "$GUEST_PASSWORD"
if ((waited == 0)); then
capture_console "success-session-02-luks-passphrase"
fi
press ret
sleep 15
((waited += 16))
if ((waited >= BOOT_TIMEOUT)); then
capture_console "failure-session-luks-timeout"
echo "Timed out unlocking LUKS" >&2
return 1
fi
done
# Encrypted installs autologin once the disk is unlocked
else
wait_for_ssh "$BOOT_TIMEOUT" "failure-session-ssh-timeout"
sleep 5
fi
local waited=0
until session_started; do
if ! $ENCRYPT; then
# SDDM remembers the user and focuses the password field
if ((waited == 0)); then
capture_console "success-session-01-login"
fi
type_text "$GUEST_PASSWORD"
if ((waited == 0)); then
capture_console "success-session-02-password"
fi
press ret
fi
sleep 10
((waited += 11))
if ((waited >= 300)); then
capture_console "failure-session-start-timeout"
echo "Timed out waiting for the Hyprland session" >&2
return 1
fi
done
capture_console "success-session-03-desktop"
}
# -------------------------------------------------------------------- phases
install_phase() {
log "Installing $(basename "$ISO") into a fresh VM (headless, driven via console)"
[[ -f $SSH_KEY ]] || ssh-keygen -t ed25519 -N "" -q -C "omarchy-iso-test" -f "$SSH_KEY"
rm -f "$BASE_DISK"
qemu-img create -f qcow2 "$BASE_DISK" 40G >/dev/null
cp "$OVMF_VARS_TEMPLATE" "$BASE_OVMF"
start_vm "$BASE_DISK" "$RUN_DIR/install-serial.log" \
-drive "file=$ISO,media=cdrom,if=none,format=raw,id=cdrom0" \
-device ide-cd,drive=cdrom0,bootindex=2
drive_configurator
wait_for_install
if $ENCRYPT; then
unlock_luks
fi
bootstrap_ssh
ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install-timing.json 2>/dev/null" \
>"$RUN_DIR/omarchy-install-timing.json" 2>/dev/null || true
ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/pacman.log 2>/dev/null" \
>"$RUN_DIR/pacman.log" 2>/dev/null || true
log "Installed system is up. Saving base image."
stop_vm
}
sync_omarchy() {
[[ -n $SYNC_DIR ]] || return 0
local target=".local/share/omarchy"
if $SYNC_ALL; then
log "Syncing $SYNC_DIR into the guest"
tar -C "$SYNC_DIR" --exclude .git -cf - . | ssh_guest "mkdir -p $target && tar -C $target -xf -"
else
log "Syncing $SYNC_DIR/test/acceptance into the guest"
tar -C "$SYNC_DIR/test" -cf - acceptance acceptance.d | ssh_guest "mkdir -p $target/test && tar -C $target/test -xf -"
fi
}
acceptance_phase() {
log "Booting acceptance VM from base image overlay"
local guest_omarchy_path="/usr/share/omarchy"
$SYNC_ALL && guest_omarchy_path=".local/share/omarchy"
qemu-img create -f qcow2 -b "$BASE_DISK" -F qcow2 "$RUN_DIR/run.qcow2" >/dev/null
start_vm "$RUN_DIR/run.qcow2" "$RUN_DIR/serial.log"
establish_session
sync_omarchy
local status=0
shortcut_smoke_phase || status=1
log "Running acceptance suite"
ssh_guest "OMARCHY_PATH=$guest_omarchy_path OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance bash .local/share/omarchy/test/acceptance" || status=$?
log "Collecting artifacts into $RUN_DIR"
ssh_guest "tar -C /tmp -cf - omarchy-acceptance 2>/dev/null" | tar -C "$RUN_DIR" -xf - || true
ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install.log 2>/dev/null" >"$RUN_DIR/omarchy-install.log" 2>/dev/null || true
if ((status == 0)); then
capture_console "success-acceptance-final"
else
capture_console "failure-acceptance-final"
fi
return $status
}
# ---------------------------------------------------------------------- main
if $REUSE_BASE && [[ -f $BASE_DISK ]]; then
log "Reusing existing base image: $BASE_DISK"
else
install_phase
fi
if $INSTALL_ONLY; then
log "Base image ready: $BASE_DISK"
exit 0
fi
if acceptance_phase; then
log "Acceptance suite passed. Artifacts: $RUN_DIR"
else
log "Acceptance suite FAILED. Artifacts: $RUN_DIR"
exit 1
fi
+295
View File
@@ -0,0 +1,295 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
TEST_DISK="${OMARCHY_ISO_TEST_WINDOWS_DISK:-/tmp/omarchy-iso-test-windows.qcow2}"
TEST_OVMF_VARS="${OMARCHY_ISO_TEST_WINDOWS_OVMF_VARS:-/tmp/OVMF_VARS.omarchy-iso-test-windows.fd}"
DISK_SIZE_GIB=96
MIN_FREE_GIB=64
RECREATE=0
ISO=""
ACTIVE_NBD=""
MOUNT_DIR=""
MOUNTED=0
TEMPORARY_DISK=""
usage() {
cat <<USAGE
Usage: omarchy-iso-test-windows-disk [--recreate] [release/omarchy.iso]
Creates or validates a synthetic Windows-style test disk with:
- a 512MiB EFI partition containing EFI/Microsoft
- an existing ext4 data partition with a preservation marker
- more than 64GiB of contiguous unallocated space for Omarchy
This exercises Windows partition detection and preservation; it is not a
bootable Windows installation.
Options:
--recreate Replace the existing test fixture
-h, --help Show this help
Paths can be overridden with:
OMARCHY_ISO_TEST_WINDOWS_DISK
OMARCHY_ISO_TEST_WINDOWS_OVMF_VARS
USAGE
}
while (( $# > 0 )); do
case "$1" in
--recreate)
RECREATE=1
shift
;;
-h|--help)
usage
exit 0
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
*)
if [[ -n $ISO ]]; then
echo "Only one ISO may be specified." >&2
usage >&2
exit 2
fi
ISO="$1"
shift
;;
esac
done
cleanup_mount() {
if (( MOUNTED )); then
sudo umount "$MOUNT_DIR" >/dev/null 2>&1 || true
MOUNTED=0
fi
if [[ -n $MOUNT_DIR ]]; then
rmdir "$MOUNT_DIR" >/dev/null 2>&1 || true
MOUNT_DIR=""
fi
}
disconnect_disk() {
cleanup_mount
if [[ -n $ACTIVE_NBD ]]; then
sudo qemu-nbd --disconnect "$ACTIVE_NBD" >/dev/null 2>&1 || true
ACTIVE_NBD=""
fi
}
cleanup() {
disconnect_disk
[[ -z $TEMPORARY_DISK ]] || rm -f "$TEMPORARY_DISK"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
find_free_nbd() {
local device
for device in /sys/class/block/nbd*; do
[[ -e $device ]] || continue
[[ ! -e $device/pid ]] || continue
printf '/dev/%s\n' "${device##*/}"
return 0
done
return 1
}
connect_disk() {
local disk="$1" mode="${2:-read-write}"
local -a options=(--format=qcow2)
ACTIVE_NBD="$(find_free_nbd)" || {
echo "No unused NBD device is available." >&2
return 1
}
[[ $mode == read-only ]] && options+=(--read-only)
if ! sudo qemu-nbd "${options[@]}" --connect="$ACTIVE_NBD" "$disk"; then
ACTIVE_NBD=""
return 1
fi
if ! sudo udevadm settle; then
disconnect_disk
return 1
fi
}
mount_partition() {
local partition="$1"
MOUNT_DIR="$(mktemp -d)" || return 1
if ! sudo mount -o "${2:-defaults}" "$partition" "$MOUNT_DIR"; then
rmdir "$MOUNT_DIR" >/dev/null 2>&1 || true
MOUNT_DIR=""
return 1
fi
MOUNTED=1
}
largest_free_region() {
sudo parted -ms "$1" unit B print free </dev/null 2>/dev/null |
awk -F: '
$NF ~ /free/ {
size=$4
gsub(/B/, "", size)
if (size+0 > max+0) max=size
}
END { print max+0 }
'
}
validate_test_disk() {
local part_count pttype free_space_b min_free_b
local esp data valid=1
[[ -f $TEST_DISK ]] || return 1
qemu-img check -q "$TEST_DISK" >/dev/null 2>&1 || return 1
connect_disk "$TEST_DISK" read-only || return 1
esp="${ACTIVE_NBD}p1"
data="${ACTIVE_NBD}p2"
part_count=$(lsblk -nro TYPE "$ACTIVE_NBD" | awk '$1 == "part" { count++ } END { print count+0 }')
pttype=$(lsblk -dnro PTTYPE "$ACTIVE_NBD" 2>/dev/null || true)
free_space_b=$(largest_free_region "$ACTIVE_NBD")
min_free_b=$((MIN_FREE_GIB * 1024 * 1024 * 1024))
[[ $pttype == gpt ]] || valid=0
(( part_count == 2 )) || valid=0
(( free_space_b >= min_free_b )) || valid=0
[[ $(sudo blkid -s TYPE -o value "$esp" 2>/dev/null || true) == vfat ]] || valid=0
[[ $(sudo blkid -s LABEL -o value "$esp" 2>/dev/null || true) == WINDOWS_ESP ]] || valid=0
[[ $(sudo blkid -s TYPE -o value "$data" 2>/dev/null || true) == ext4 ]] || valid=0
[[ $(sudo blkid -s LABEL -o value "$data" 2>/dev/null || true) == WINDOWS_DATA ]] || valid=0
if (( valid )); then
mount_partition "$esp" ro || valid=0
if (( valid )); then
sudo test -d "$MOUNT_DIR/EFI/Microsoft" || valid=0
fi
cleanup_mount
fi
if (( valid )); then
mount_partition "$data" ro || valid=0
if (( valid )); then
sudo test -f "$MOUNT_DIR/OMARCHY-MUST-NOT-TOUCH.txt" || valid=0
fi
cleanup_mount
fi
disconnect_disk
(( valid ))
}
create_test_disk() {
local esp data
mkdir -p "$(dirname "$TEST_DISK")"
TEMPORARY_DISK="${TEST_DISK}.creating.$$"
rm -f "$TEMPORARY_DISK"
gum style --foreground 6 "Creating synthetic Windows test disk at $TEST_DISK"
qemu-img create -f qcow2 "$TEMPORARY_DISK" "${DISK_SIZE_GIB}G"
connect_disk "$TEMPORARY_DISK"
sudo parted --script "$ACTIVE_NBD" mklabel gpt
sudo parted --script "$ACTIVE_NBD" mkpart WINDOWS_ESP fat32 1MiB 513MiB
sudo parted --script "$ACTIVE_NBD" set 1 esp on
sudo parted --script "$ACTIVE_NBD" mkpart WINDOWS_DATA ext4 513MiB 20GiB
sudo partprobe "$ACTIVE_NBD"
sudo udevadm settle
esp="${ACTIVE_NBD}p1"
data="${ACTIVE_NBD}p2"
sudo mkfs.fat -F32 -n WINDOWS_ESP "$esp" >/dev/null
sudo mkfs.ext4 -F -L WINDOWS_DATA "$data" >/dev/null 2>&1
mount_partition "$esp"
sudo mkdir -p "$MOUNT_DIR/EFI/Microsoft/Boot"
printf '%s\n' 'Synthetic Windows ESP for Omarchy ISO testing.' |
sudo tee "$MOUNT_DIR/EFI/Microsoft/README.txt" >/dev/null
cleanup_mount
mount_partition "$data"
printf '%s\n' 'This existing partition must survive the Omarchy installation.' |
sudo tee "$MOUNT_DIR/OMARCHY-MUST-NOT-TOUCH.txt" >/dev/null
cleanup_mount
sync
disconnect_disk
mv -f "$TEMPORARY_DISK" "$TEST_DISK"
TEMPORARY_DISK=""
}
select_iso() {
local -a isos=()
if [[ -n $ISO ]]; then
[[ -f $ISO ]] || {
echo "ISO not found: $ISO" >&2
return 1
}
realpath "$ISO"
return 0
fi
mapfile -t isos < <(
find "$REPO_ROOT/release" -maxdepth 1 -type f -name '*.iso' -printf '%T@ %p\n' 2>/dev/null |
sort -nr |
cut -d' ' -f2-
)
(( ${#isos[@]} > 0 )) || {
echo "No ISO found under $REPO_ROOT/release." >&2
return 1
}
printf '%s\n' "${isos[@]}" | gum choose --header "Select ISO to install"
}
omarchy-pkg-add qemu-full edk2-ovmf parted dosfstools e2fsprogs
sudo modprobe nbd max_part=16
if [[ -f $TEST_DISK ]] && (( ! RECREATE )); then
gum style --foreground 6 "Verifying existing test disk at $TEST_DISK"
if validate_test_disk; then
gum style --foreground 2 "✓ Synthetic Windows test disk is ready"
elif gum confirm --default=false "The existing test disk is not a pristine fixture. Recreate it?"; then
create_test_disk
else
gum style "Left the existing disk untouched."
exit 0
fi
else
create_test_disk
fi
# The synthetic fixture has no real Windows bootloader, so reset OVMF state for
# every run. The ISO still sees EFI/Microsoft and must leave that ESP untouched.
mkdir -p "$(dirname "$TEST_OVMF_VARS")"
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd "$TEST_OVMF_VARS"
gum style --border rounded --padding "0 1" --border-foreground 6 \
"Synthetic Windows disk ready" \
"Disk: $TEST_DISK" \
"Layout: 512MiB Windows ESP + existing data + 64GiB+ free"
if gum confirm "Start installation on this disk?"; then
ISO="$(select_iso)"
trap - EXIT INT TERM
exec "$SCRIPT_DIR/omarchy-iso-boot" \
--reuse \
--disk "$TEST_DISK" \
--ovmf-vars "$TEST_OVMF_VARS" \
"$ISO" \
-- -boot once=d,menu=on
fi
+68 -15
View File
@@ -4,8 +4,8 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VM_DIR="$(dirname "$SCRIPT_DIR")/vm-saves" VM_DIR="$(dirname "$SCRIPT_DIR")/vm-saves"
TMP_DISK="/tmp/omarchy-iso-boot.qcow2" TMP_DISK="${OMARCHY_VM_DISK:-/tmp/omarchy-iso-boot.qcow2}"
TMP_OVMF="/tmp/OVMF_VARS.4m.fd" TMP_OVMF="${OMARCHY_VM_OVMF_VARS:-/tmp/OVMF_VARS.4m.fd}"
mkdir -p "$VM_DIR" mkdir -p "$VM_DIR"
@@ -18,7 +18,7 @@ save_vm() {
fi fi
if [[ ! -f "$TMP_DISK" || ! -f "$TMP_OVMF" ]]; then if [[ ! -f "$TMP_DISK" || ! -f "$TMP_OVMF" ]]; then
echo "Error: No active VM found in /tmp" echo "Error: No active VM found at $TMP_DISK / $TMP_OVMF"
echo "Boot a VM first with: omarchy-iso-boot [iso]" echo "Boot a VM first with: omarchy-iso-boot [iso]"
exit 1 exit 1
fi fi
@@ -53,7 +53,44 @@ list_vms() {
} }
boot_vm() { boot_vm() {
local name="$1" local name=""
local boot_args=()
local active_disk="$TMP_DISK"
local active_ovmf="$TMP_OVMF"
if [[ $# -gt 0 && ${1:-} != --* ]]; then
name="$1"
shift
fi
while (( $# > 0 )); do
case "$1" in
--disk)
active_disk="${2:-}"
boot_args+=("$1" "${2:-}")
shift 2
;;
--disk=*)
active_disk="${1#*=}"
boot_args+=("$1")
shift
;;
--ovmf-vars)
active_ovmf="${2:-}"
boot_args+=("$1" "${2:-}")
shift 2
;;
--ovmf-vars=*)
active_ovmf="${1#*=}"
boot_args+=("$1")
shift
;;
*)
boot_args+=("$1")
shift
;;
esac
done
if [[ -z "$(ls -A "$VM_DIR" 2>/dev/null)" ]]; then if [[ -z "$(ls -A "$VM_DIR" 2>/dev/null)" ]]; then
echo "No VM snapshots available" echo "No VM snapshots available"
@@ -73,25 +110,34 @@ boot_vm() {
exit 1 exit 1
fi fi
if [[ -f "$TMP_DISK" ]] && ! gum confirm "Override existing /tmp VM?"; then if [[ -z $active_disk || -z $active_ovmf ]]; then
echo "Error: active disk and OVMF vars paths must not be empty"
exit 1
fi
if [[ -f $active_disk ]] && ! gum confirm "Override existing active VM disk at $active_disk?"; then
exit 0 exit 0
fi fi
echo "Copying VM '$name' to /tmp..." echo "Copying VM '$name' to $active_disk..."
cp "$vm_path/omarchy-iso-boot.qcow2" "$TMP_DISK" mkdir -p "$(dirname "$active_disk")" "$(dirname "$active_ovmf")"
cp "$vm_path/OVMF_VARS.4m.fd" "$TMP_OVMF" cp "$vm_path/omarchy-iso-boot.qcow2" "$active_disk"
cp "$vm_path/OVMF_VARS.4m.fd" "$active_ovmf"
gum style --foreground 212 "✓ VM ready. Booting..." gum style --foreground 212 "✓ VM ready. Booting..."
exec omarchy-iso-boot /dev/null reuse exec omarchy-iso-boot "${boot_args[@]}" /dev/null reuse
} }
case "${1:-}" in command="${1:-}"
[[ $# -gt 0 ]] && shift
case "$command" in
save) save)
save_vm "$2" save_vm "${1:-}"
;; ;;
boot) boot)
boot_vm "$2" boot_vm "$@"
;; ;;
list) list)
list_vms list_vms
@@ -101,9 +147,16 @@ case "${1:-}" in
"$(gum style --bold 'omarchy-vm') - VM snapshot manager" \ "$(gum style --bold 'omarchy-vm') - VM snapshot manager" \
"" \ "" \
"$(gum style --foreground 245 'Commands:')" \ "$(gum style --foreground 245 'Commands:')" \
" save [name] Save current VM from /tmp" \ " save [name] Save current active VM" \
" boot [name] Boot saved VM (interactive if no name)" \ " boot [name] [boot-options] Boot saved VM (interactive if no name)" \
" list List available VMs" \ " list List available VMs" \
"" \
"$(gum style --foreground 245 'Boot options:')" \
" --ssh-port PORT Forward host PORT to guest SSH (default: 2222)" \
" --ssh-host HOST Host address for SSH forwarding (default: 127.0.0.1)" \
" --disk PATH Active disk path, useful for parallel VMs" \
" --ovmf-vars PATH Active OVMF vars path, useful for parallel VMs" \
" --no-network Boot without a virtual NIC" \
"" \ "" \
"$(gum style --foreground 245 'Current VMs:')" "$(gum style --foreground 245 'Current VMs:')"
list_vms 2>/dev/null || echo " None" list_vms 2>/dev/null || echo " None"
+217 -64
View File
@@ -2,105 +2,258 @@
set -e set -e
# Note that these are packages installed to the Arch container used to build the ISO. OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
# Quattro, edge, dev, and local-source ISOs install the dev packages explicitly.
# Those package recipes track the quattro branch. This avoids relying on
# pacman's provides=omarchy resolution and shows the real package names being
# tested in the offline mirror and target install.
case "$OMARCHY_ISO_REF" in
quattro|edge|dev|local)
: "${OMARCHY_RUNTIME_PACKAGE:=omarchy-dev}"
: "${OMARCHY_SETTINGS_PACKAGE:=omarchy-settings-dev}"
;;
*)
: "${OMARCHY_RUNTIME_PACKAGE:=omarchy}"
: "${OMARCHY_SETTINGS_PACKAGE:=omarchy-settings}"
;;
esac
: "${OMARCHY_NVIM_PACKAGE:=omarchy-nvim}"
export OMARCHY_RUNTIME_PACKAGE OMARCHY_SETTINGS_PACKAGE OMARCHY_NVIM_PACKAGE
# Packages installed into the Arch container used to build the ISO.
pacman-key --init pacman-key --init
pacman --noconfirm -Sy archlinux-keyring pacman --noconfirm -Sy archlinux-keyring
pacman --noconfirm -Sy archiso git sudo base-devel jq grub pacman --noconfirm -Sy archiso git sudo base-devel jq grub imagemagick neovim nodejs npm tree-sitter-cli
# Pre-import the omarchy signing key so pacman can verify packages without a keyserver lookup # Pre-import the omarchy signing key (so pacman trusts our [omarchy] repo
# during the build without keyserver lookups).
pacman-key --add /builder/omarchy.gpg pacman-key --add /builder/omarchy.gpg
pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571
# Install omarchy-keyring for package verification during build # omarchy-keyring is needed inside the offline mirror too.
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Sy omarchy-keyring pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Sy omarchy-keyring
pacman-key --populate omarchy pacman-key --populate omarchy
# Setup build locations # Append the [omarchy] repo to the container's /etc/pacman.conf so subsequent
build_cache_dir="/var/cache" # tools (notably makepkg in build-omarchy-packages.sh) can resolve omarchy-
offline_mirror_dir="$build_cache_dir/airootfs/var/cache/omarchy/mirror/offline" # only build deps like limine-snapper-sync and limine-mkinitcpio-hook.
mkdir -p $build_cache_dir/ if ! grep -q '^\[omarchy\]' /etc/pacman.conf; then
mkdir -p $offline_mirror_dir/ awk '/^\[omarchy\]/,/^$/' /configs/pacman-online-${OMARCHY_MIRROR}.conf >> /etc/pacman.conf
fi
# We base our ISO on the official arch ISO (releng) config # Build locations
cp -r /archiso/configs/releng/* $build_cache_dir/ build_cache_dir=/var/cache
offline_mirror_dir="$build_cache_dir/airootfs/var/cache/omarchy/mirror/offline"
mkdir -p "$build_cache_dir" "$offline_mirror_dir"
# Seed from the official Arch releng profile.
cp -r /archiso/configs/releng/* "$build_cache_dir/"
rm "$build_cache_dir/airootfs/etc/motd" rm "$build_cache_dir/airootfs/etc/motd"
# Avoid using reflector for mirror identification as we are relying on the global CDN # We rely on the global CDN; drop reflector.
rm -rf "$build_cache_dir/airootfs/etc/systemd/system/multi-user.target.wants/reflector.service" rm -rf "$build_cache_dir/airootfs/etc/systemd/system/multi-user.target.wants/reflector.service"
rm -rf "$build_cache_dir/airootfs/etc/systemd/system/reflector.service.d" rm -rf "$build_cache_dir/airootfs/etc/systemd/system/reflector.service.d"
rm -rf "$build_cache_dir/airootfs/etc/xdg/reflector" rm -rf "$build_cache_dir/airootfs/etc/xdg/reflector"
# Bring in our configs # Bring in our archiso profile additions.
cp -r /configs/* $build_cache_dir/ cp -r /configs/* "$build_cache_dir/"
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
# Persist OMARCHY_MIRROR so it's available at install time
echo "$OMARCHY_MIRROR" > "$build_cache_dir/airootfs/root/omarchy_mirror" echo "$OMARCHY_MIRROR" > "$build_cache_dir/airootfs/root/omarchy_mirror"
echo "$OMARCHY_ISO_REF" > "$build_cache_dir/airootfs/root/omarchy_iso_ref"
cat > "$build_cache_dir/airootfs/usr/share/omarchy-iso/package-targets" <<EOF
OMARCHY_RUNTIME_PACKAGE=$OMARCHY_RUNTIME_PACKAGE
OMARCHY_SETTINGS_PACKAGE=$OMARCHY_SETTINGS_PACKAGE
OMARCHY_NVIM_PACKAGE=$OMARCHY_NVIM_PACKAGE
EOF
# Setup Omarchy itself if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then
if [[ -d /omarchy ]]; then touch "$build_cache_dir/airootfs/usr/share/omarchy-iso/install-debug"
cp -rp /omarchy "$build_cache_dir/airootfs/root/omarchy" {
else echo "debug=1"
git clone -b $OMARCHY_INSTALLER_REF https://github.com/$OMARCHY_INSTALLER_REPO.git "$build_cache_dir/airootfs/root/omarchy" echo "built_at=$(date -Is)"
echo "ref=$OMARCHY_ISO_REF"
echo "mirror=$OMARCHY_MIRROR"
echo "runtime_package=$OMARCHY_RUNTIME_PACKAGE"
echo "settings_package=$OMARCHY_SETTINGS_PACKAGE"
echo "nvim_package=$OMARCHY_NVIM_PACKAGE"
if [[ -d /omarchy-source ]]; then
echo "omarchy_source=/omarchy-source"
git -c safe.directory=/omarchy-source -C /omarchy-source rev-parse HEAD 2>/dev/null | sed 's/^/omarchy_commit=/' || true
git -c safe.directory=/omarchy-source -C /omarchy-source status --short 2>/dev/null | sed 's/^/omarchy_status=/' || true
fi
if [[ -d /omarchy-pkgs ]]; then
echo "omarchy_pkgs_source=/omarchy-pkgs"
git -c safe.directory=/omarchy-pkgs -C /omarchy-pkgs rev-parse HEAD 2>/dev/null | sed 's/^/omarchy_pkgs_commit=/' || true
git -c safe.directory=/omarchy-pkgs -C /omarchy-pkgs status --short 2>/dev/null | sed 's/^/omarchy_pkgs_status=/' || true
fi
} > "$build_cache_dir/airootfs/usr/share/omarchy-iso/build-info"
fi fi
# Make log uploader available in the ISO too # When --local-source is in effect, build omarchy* from the mounted source
mkdir -p "$build_cache_dir/airootfs/usr/local/bin/" # trees and drop them in the offline mirror. Otherwise pacman -Syw below
cp "$build_cache_dir/airootfs/root/omarchy/bin/omarchy-upload-log" "$build_cache_dir/airootfs/usr/local/bin/omarchy-upload-log" # downloads the published versions from the omarchy network mirror.
if [[ -d /omarchy-source && -d /omarchy-pkgs ]]; then
bash /builder/build-omarchy-packages.sh "$offline_mirror_dir"
LOCAL_OMARCHY_BUILD=1
fi
# Copy the Omarchy Plymouth theme to the ISO # Node.js binary for offline mise install.
mkdir -p "$build_cache_dir/airootfs/usr/share/plymouth/themes/omarchy"
cp -r "$build_cache_dir/airootfs/root/omarchy/default/plymouth/"* "$build_cache_dir/airootfs/usr/share/plymouth/themes/omarchy/"
# Download and verify Node.js binary for offline installation
NODE_DIST_URL="https://nodejs.org/dist/latest" NODE_DIST_URL="https://nodejs.org/dist/latest"
# Get checksums and parse filename and SHA
NODE_SHASUMS=$(curl -fsSL "$NODE_DIST_URL/SHASUMS256.txt") NODE_SHASUMS=$(curl -fsSL "$NODE_DIST_URL/SHASUMS256.txt")
NODE_FILENAME=$(echo "$NODE_SHASUMS" | grep "linux-x64.tar.gz" | awk '{print $2}') NODE_FILENAME=$(echo "$NODE_SHASUMS" | grep "linux-x64.tar.gz" | awk '{print $2}')
NODE_SHA=$(echo "$NODE_SHASUMS" | grep "linux-x64.tar.gz" | awk '{print $1}') NODE_SHA=$(echo "$NODE_SHASUMS" | grep "linux-x64.tar.gz" | awk '{print $1}')
# Download the tarball
curl -fsSL "$NODE_DIST_URL/$NODE_FILENAME" -o "/tmp/$NODE_FILENAME" curl -fsSL "$NODE_DIST_URL/$NODE_FILENAME" -o "/tmp/$NODE_FILENAME"
echo "$NODE_SHA /tmp/$NODE_FILENAME" | sha256sum -c -
# Verify SHA256 checksum
echo "$NODE_SHA /tmp/$NODE_FILENAME" | sha256sum -c - || {
echo "ERROR: Node.js checksum verification failed!"
exit 1
}
# Copy to ISO
mkdir -p "$build_cache_dir/airootfs/opt/packages/" mkdir -p "$build_cache_dir/airootfs/opt/packages/"
cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/" cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/"
# Add our additional packages to packages.x86_64 # Packages installed into the live ISO environment itself (NOT the target system).
arch_packages=(linux-t2 git gum jq openssl plymouth tzupdate omarchy-keyring lvm2 cryptsetup parted) # The selected omarchy-settings package is needed here so its post_install hook
printf '%s\n' "${arch_packages[@]}" >>"$build_cache_dir/packages.x86_64" # drops Omarchy's plymouthd.conf into /etc/plymouth before mkarchiso builds the
# live initramfs.
arch_packages=(linux-t2 git gum jq openssl plymouth python-terminaltexteffects tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted)
printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.x86_64"
# Build list of all the packages needed for the offline mirror # The live ISO boots linux-t2 (see airootfs/etc/mkinitcpio.d/linux-t2.preset), so
all_packages=($(cat "$build_cache_dir/packages.x86_64")) # stock linux is a second kernel nobody boots: ~147MB of ISO, plus its own archiso
all_packages+=($(grep -v '^#' "$build_cache_dir/airootfs/root/omarchy/install/omarchy-base.packages" | grep -v '^$')) # initramfs, copied into both the ISO tree and the size-constrained FAT EFI image.
all_packages+=($(grep -v '^#' "$build_cache_dir/airootfs/root/omarchy/install/omarchy-other.packages" | grep -v '^$')) #
all_packages+=($(grep -v '^#' /builder/archinstall.packages | grep -v '^$')) # It cannot just be deleted — releng's broadcom-wl hard-depends on it, and it is
# the only releng package that does, so pacman would drag the kernel straight back
# in. broadcom-wl is a prebuilt module for stock linux and cannot load on the
# kernel we boot, so it has done nothing since we started booting T2 anyway. The
# install is entirely offline and the live environment needs no Wi-Fi driver.
#
# Anchored so linux-t2 and linux-firmware are untouched.
sed -i -E '/^(linux|broadcom-wl)$/d' "$build_cache_dir/packages.x86_64"
# Download all the packages to the offline mirror inside the ISO # Build the offline mirror: everything pacstrap might want during the target
# install. With --local-source, the omarchy* packages we just built are
# already in the mirror and we filter them out below. Without it, pacman -Syw
# pulls the published omarchy* from the network mirror like any other package.
if [[ -d /omarchy-source ]]; then
base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages)
else
# Pull the same package lists out of the freshly-downloaded Omarchy runtime
# package so we don't need a local checkout in the non-local-source path.
bootstrap_cache_dir=/tmp/omarchy-pkg-bootstrap
rm -rf "$bootstrap_cache_dir" /tmp/offlinedb-bootstrap /tmp/omarchy-pkglists
mkdir -p "$bootstrap_cache_dir" /tmp/offlinedb-bootstrap
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw "$OMARCHY_RUNTIME_PACKAGE" --cachedir "$bootstrap_cache_dir" --dbpath /tmp/offlinedb-bootstrap >/dev/null
omarchy_pkg=$(find "$bootstrap_cache_dir" -maxdepth 1 -type f -name "$OMARCHY_RUNTIME_PACKAGE-*.pkg.tar.zst" | sort | head -1)
if [[ -z $omarchy_pkg ]]; then
echo "ERROR: downloaded package for $OMARCHY_RUNTIME_PACKAGE not found in $bootstrap_cache_dir" >&2
exit 1
fi
mkdir -p /tmp/omarchy-pkglists
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/omarchy-base.packages usr/share/omarchy/install/omarchy-other.packages
base_pkg_lists=(/tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-base.packages /tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-other.packages)
fi
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
cp "${base_pkg_lists[0]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
cp "${base_pkg_lists[1]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages"
# Collect every package we want available in the offline mirror.
declare -a all_packages
mapfile -t all_packages < <(
{
cat "$build_cache_dir/packages.x86_64"
grep -hv '^#\|^$' "${base_pkg_lists[@]}"
grep -hv '^#\|^$' /builder/archinstall.packages
# Always include the selected Omarchy packages so the target install can
# find the runtime and companion packages in the offline mirror.
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" "$OMARCHY_NVIM_PACKAGE"
} | sort -u
)
# With --local-source we already built these omarchy* packages directly into
# the mirror; strip them from the pacman -Syw list so it doesn't try to fetch
# the published versions on top.
if [[ -n ${LOCAL_OMARCHY_BUILD:-} ]]; then
mapfile -t all_packages < <(
printf '%s\n' "${all_packages[@]}" |
grep -Fxv \
-e "$OMARCHY_RUNTIME_PACKAGE" \
-e "$OMARCHY_SETTINGS_PACKAGE" \
-e "$OMARCHY_NVIM_PACKAGE" || true
)
fi
mkdir -p /tmp/offlinedb mkdir -p /tmp/offlinedb
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw "${all_packages[@]}" --cachedir $offline_mirror_dir/ --dbpath /tmp/offlinedb download_offline_packages() {
repo-add --new "$offline_mirror_dir/offline.db.tar.gz" "$offline_mirror_dir/"*.pkg.tar.zst pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw \
"${all_packages[@]}" --cachedir "$offline_mirror_dir/" --dbpath /tmp/offlinedb --needed
}
# A repository may occasionally republish a package without changing its
# filename. Pacman detects that the persistent cached copy no longer matches
# the refreshed repository checksum and deletes it, but still fails the
# transaction. Retry once so the now-missing package is downloaded.
if ! download_offline_packages; then
echo "Offline package download failed; retrying after pacman cleaned invalid cached files..." >&2
download_offline_packages
fi
prune_stale_package_versions() {
local dir="$1"
local pkgfile name version cmp
local -a stale=()
declare -A newest_file=()
declare -A newest_version=()
while IFS= read -r pkgfile; do
read -r name version < <(pacman -Qp "$pkgfile" 2>/dev/null) || continue
[[ -n $name && -n $version ]] || continue
if [[ -z ${newest_version[$name]+x} ]]; then
newest_version[$name]="$version"
newest_file[$name]="$pkgfile"
continue
fi
cmp=$(vercmp "$version" "${newest_version[$name]}")
if (( cmp > 0 )); then
stale+=("${newest_file[$name]}")
newest_version[$name]="$version"
newest_file[$name]="$pkgfile"
else
stale+=("$pkgfile")
fi
done < <(find "$dir" -maxdepth 1 -type f -name '*.pkg.tar.*' ! -name '*.sig' -print | sort)
if (( ${#stale[@]} > 0 )); then
echo "Pruning stale package versions from offline mirror:"
for pkgfile in "${stale[@]}"; do
echo " $(basename "$pkgfile")"
rm -f "$pkgfile" "$pkgfile.sig"
done
fi
}
# Rebuild the offline repo db from scratch so size/checksum/depends entries
# always reflect the current package files. The persistent cache can contain
# multiple versions of the same package; repo-add keeps the last one it sees,
# which is lexical glob order, not version order (e.g. pkgrel -9 can override
# -15). Prune to one newest version per package before indexing.
prune_stale_package_versions "$offline_mirror_dir"
rm -f "$offline_mirror_dir"/offline.db* "$offline_mirror_dir"/offline.files*
repo-add "$offline_mirror_dir/offline.db.tar.gz" "$offline_mirror_dir/"*.pkg.tar.zst
# Create a symlink to the offline mirror instead of duplicating it. # mkarchiso expects the mirror at /var/cache/omarchy/mirror/offline inside the
# mkarchiso needs packages at /var/cache/omarchy/mirror/offline in the container, # container (the airootfs path); symlink rather than duplicate.
# but they're actually in $build_cache_dir/airootfs/var/cache/omarchy/mirror/offline
mkdir -p /var/cache/omarchy/mirror mkdir -p /var/cache/omarchy/mirror
ln -s "$offline_mirror_dir" "/var/cache/omarchy/mirror/offline" ln -sf "$offline_mirror_dir" /var/cache/omarchy/mirror/offline
# Copy the offline pacman.conf to the ISO's /etc directory so the live environment uses our # Live ISO uses the same offline pacman.conf.
# same config when booted. cp "$build_cache_dir/pacman-offline.conf" "$build_cache_dir/airootfs/etc/pacman.conf"
cp $build_cache_dir/pacman-offline.conf "$build_cache_dir/airootfs/etc/pacman.conf"
# Finally, we assemble the entire ISO # Build the ISO.
mkarchiso -v -w "$build_cache_dir/work/" -o "/out/" "$build_cache_dir/" mkarchiso -v -w "$build_cache_dir/work/" -o /out/ "$build_cache_dir/"
# Fix ownership of output files to match host user # Match host UID/GID on output.
if [ -n "$HOST_UID" ] && [ -n "$HOST_GID" ]; then if [[ -n $HOST_UID && -n $HOST_GID ]]; then
chown -R "$HOST_UID:$HOST_GID" /out/ chown -R "$HOST_UID:$HOST_GID" /out/
fi fi
+85
View File
@@ -0,0 +1,85 @@
#!/bin/bash
# Build Omarchy packages from mounted source (/omarchy-source + /omarchy-pkgs)
# and place the resulting .pkg.tar.zst files in the offline mirror.
set -e
offline_mirror_dir="$1"
if [[ -z $offline_mirror_dir ]]; then
echo "Usage: build-omarchy-packages.sh <offline-mirror-dir>" >&2
exit 1
fi
if [[ ! -d /omarchy-source ]]; then
echo "ERROR: /omarchy-source not mounted (pass --local-source or set OMARCHY_SOURCE_PATH)" >&2
exit 1
fi
if [[ ! -d /omarchy-pkgs ]]; then
echo "ERROR: /omarchy-pkgs not mounted (set OMARCHY_PKGS_PATH or place ../omarchy-pkgs)" >&2
exit 1
fi
work_dir=/tmp/omarchy-pkg-build
rm -rf "$work_dir"
mkdir -p "$work_dir"
if ! id builder &>/dev/null; then
useradd -m -s /bin/bash builder
fi
echo 'builder ALL=(ALL) NOPASSWD: /usr/bin/pacman' > /etc/sudoers.d/99-omarchy-pkg-builder
chmod 440 /etc/sudoers.d/99-omarchy-pkg-builder
chown builder:builder "$work_dir"
pacman -Sy --noconfirm
: "${OMARCHY_RUNTIME_PACKAGE:=omarchy-dev}"
: "${OMARCHY_SETTINGS_PACKAGE:=omarchy-settings-dev}"
: "${OMARCHY_NVIM_PACKAGE:=omarchy-nvim}"
packages=(
"$OMARCHY_SETTINGS_PACKAGE"
"$OMARCHY_RUNTIME_PACKAGE"
"$OMARCHY_NVIM_PACKAGE"
)
# Local-source packages must replace every cached build of the same package,
# even when the checkout's generated pkgver sorts below a published build.
# Otherwise the later generic cache pruning can silently keep edge instead.
for pkg in "${packages[@]}"; do
rm -f "$offline_mirror_dir/$pkg-"*.pkg.tar.*
done
for pkg in "${packages[@]}"; do
echo "----------------------------------------"
echo "Building $pkg"
echo "----------------------------------------"
pkg_work="$work_dir/$pkg"
if [[ ! -d "/omarchy-pkgs/pkgbuilds/$pkg" ]]; then
echo "ERROR: package source not found: /omarchy-pkgs/pkgbuilds/$pkg" >&2
exit 1
fi
cp -a "/omarchy-pkgs/pkgbuilds/$pkg" "$pkg_work"
chown -R builder:builder "$pkg_work"
su builder -c "
cd '$pkg_work' &&
PKGDEST='$work_dir' \
OMARCHY_SRC=/omarchy-source \
makepkg --noconfirm --skippgpcheck --skipchecksums --nodeps -f
"
done
mkdir -p "$offline_mirror_dir"
for package_file in "$work_dir"/*.pkg.tar.zst; do
destination="$offline_mirror_dir/$(basename "$package_file")"
# A cached signature belongs to the previously downloaded or locally built
# package. Keeping it beside a newly built package makes pacman reject the
# otherwise valid local-source build.
rm -f "$destination" "$destination.sig"
mv "$package_file" "$destination"
done
echo
echo "Built Omarchy packages, placed in $offline_mirror_dir:"
ls "$offline_mirror_dir"/omarchy*.pkg.tar.zst | sed 's|^| |'
@@ -0,0 +1,18 @@
# mkinitcpio preset file for the 'linux-t2' package on archiso
#
# The filename is the pkgbase, and that is load-bearing. Kernel packages ship
# nothing in /boot — they only install usr/lib/modules/<kver>/vmlinuz — so
# mkinitcpio's alpm hook sources /etc/mkinitcpio.d/<pkgbase>.preset and copies
# THAT package's kernel to whatever ALL_kver names (is_kernelcopy in
# /usr/share/libalpm/scripts/mkinitcpio). Named linux.preset, this pointed the
# stock kernel at the T2 filenames, so the ISO booted stock Arch while every
# boot entry said linux-t2 and T2 Macs got no keyboard or trackpad.
#
# releng ships its own linux.preset for the stock kernel; leave it alone.
PRESETS=('archiso')
ALL_kver='/boot/vmlinuz-linux-t2'
archiso_config='/etc/mkinitcpio.conf.d/archiso.conf'
archiso_image="/boot/initramfs-linux-t2.img"
@@ -1,9 +0,0 @@
# mkinitcpio preset file for the 'linux' package on archiso
PRESETS=('archiso')
ALL_kver='/boot/vmlinuz-linux-t2'
archiso_config='/etc/mkinitcpio.conf.d/archiso.conf'
archiso_image="/boot/initramfs-linux-t2.img"
@@ -0,0 +1,21 @@
# remove from airootfs!
#
# Mirror omarchy-settings' /etc/skel shell defaults into /root so the live ISO
# console has the same bashrc, starship prompt, and aliases as an installed
# Omarchy system. Fires during airootfs pacstrap (when omarchy-settings is
# installed); the "remove from airootfs" magic string causes
# zzzz99-remove-custom-hooks-from-airootfs.hook to delete this file before
# the squashfs is built, so no runtime hook remains.
[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = omarchy-settings
[Action]
Description = Mirroring /etc/skel shell defaults into /root for the live ISO...
When = PostTransaction
Depends = coreutils
Depends = omarchy-settings
Exec = /bin/sh -c 'install -d -m 0700 /root/.config && for f in .bashrc .bash_profile; do [ -f "/etc/skel/$f" ] && install -m 0644 "/etc/skel/$f" "/root/$f"; done; [ -f /etc/skel/.config/starship.toml ] && install -m 0644 /etc/skel/.config/starship.toml /root/.config/starship.toml; true'
+105 -259
View File
@@ -1,264 +1,110 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#
# Live ISO entry point on tty1: set up the live VT, run the configurator
# wizard, then hand off to the Python install orchestrator. Mirrors the
# stream/env contract from the previously-working installer:
# - stdout teed to /var/log/omarchy-install.log (CSI-stripped) AND to tty
# - stderr direct to /dev/tty so gum (which draws its TUI on stderr)
# renders correctly
# - CLICOLOR_FORCE/FORCE_COLOR so gum emits ANSI even with stdout piped
# - COLUMNS/LINES so gum picks up real terminal size
set -euo pipefail set -euo pipefail
use_omarchy_helpers() { [[ $(tty) == /dev/tty1 ]] || exit 0
export OMARCHY_PATH="/root/omarchy"
export OMARCHY_INSTALL="/root/omarchy/install"
export OMARCHY_INSTALL_LOG_FILE="/var/log/omarchy-install.log"
export OMARCHY_MIRROR="$(cat /root/omarchy_mirror)"
source /root/omarchy/install/helpers/all.sh
}
run_configurator() { export OMARCHY_MIRROR="$(cat /root/omarchy_mirror)"
set_tokyo_night_colors if [[ -f /root/omarchy_iso_ref ]]; then
./configurator export OMARCHY_ISO_REF="$(cat /root/omarchy_iso_ref)"
export OMARCHY_USER="$(jq -r '.users[0].username' user_credentials.json)"
}
install_arch() {
clear_logo
gum style --foreground 3 --padding "1 0 0 $PADDING_LEFT" "Installing..."
echo
touch /var/log/omarchy-install.log
start_log_output
# Set CURRENT_SCRIPT for the trap to display better when nothing is returned for some reason
CURRENT_SCRIPT="install_base_system"
install_base_system > >(sed -u 's/\x1b\[[0-9;]*[a-zA-Z]//g' >>/var/log/omarchy-install.log) 2>&1
unset CURRENT_SCRIPT
stop_log_output
}
install_omarchy() {
chroot_bash -lc "sudo pacman -S --noconfirm --needed gum" >/dev/null
chroot_bash -lc "source /home/$OMARCHY_USER/.local/share/omarchy/install.sh"
configure_login_for_unencrypted_install
# Reboot if requested by installer
if [[ -f /mnt/var/tmp/omarchy-install-completed ]]; then
reboot
fi
}
# Set Tokyo Night color scheme for the terminal
set_tokyo_night_colors() {
if [[ $(tty) == "/dev/tty"* ]]; then
# Tokyo Night color palette
echo -en "\e]P01a1b26" # black (background)
echo -en "\e]P1f7768e" # red
echo -en "\e]P29ece6a" # green
echo -en "\e]P3e0af68" # yellow
echo -en "\e]P47aa2f7" # blue
echo -en "\e]P5bb9af7" # magenta
echo -en "\e]P67dcfff" # cyan
echo -en "\e]P7a9b1d6" # white
echo -en "\e]P8414868" # bright black
echo -en "\e]P9f7768e" # bright red
echo -en "\e]PA9ece6a" # bright green
echo -en "\e]PBe0af68" # bright yellow
echo -en "\e]PC7aa2f7" # bright blue
echo -en "\e]PDbb9af7" # bright magenta
echo -en "\e]PE7dcfff" # bright cyan
echo -en "\e]PFc0caf5" # bright white (foreground)
# Set default foreground and background
echo -en "\033[0m"
clear
fi
}
install_disk() {
jq -er 'first(.disk_config.device_modifications[]? | select(.wipe == true) | .device)' user_configuration.json
}
cleanup_install_disk() {
local disk="$1"
if [[ -z "$disk" || ! -b "$disk" ]]; then
echo "Could not determine install disk for cleanup" >&2
return 1
fi
echo "Cleaning up existing holders on install disk: $disk"
# Ensure that no mounts exist from past install attempts.
findmnt -R /mnt >/dev/null && umount -R /mnt || true
# Turn off swap and unmount anything backed by the selected disk, including
# device-mapper children from a previous install. Active LVM/swap holders can
# prevent the kernel from re-reading the partition table after archinstall
# wipes and recreates it.
while read -r dev; do
[[ -b "$dev" ]] || continue
swapoff "$dev" 2>/dev/null || true
while read -r target; do
[[ -n "$target" ]] || continue
umount "$target" 2>/dev/null || true
done < <(findmnt -rn -S "$dev" -o TARGET 2>/dev/null || true)
done < <(lsblk -rnpo PATH "$disk")
# Deactivate any LVM volume groups whose physical volumes live on the selected
# disk. This is the common case when replacing Fedora/Alma/RHEL installs.
while read -r dev type; do
[[ "$type" == "disk" || "$type" == "part" || "$type" == "crypt" ]] || continue
while read -r vg; do
[[ -n "$vg" ]] || continue
vgchange -an "$vg" 2>/dev/null || true
done < <(pvs --noheadings -o vg_name "$dev" 2>/dev/null | awk '{$1=$1; print}' | sort -u)
done < <(lsblk -rnpo PATH,TYPE "$disk")
# Close any LUKS mappings stacked on the selected disk after filesystems and
# swap have been released.
while read -r dev type; do
[[ "$type" == "crypt" ]] || continue
cryptsetup close "$dev" 2>/dev/null || true
done < <(lsblk -rnpo PATH,TYPE "$disk")
blockdev --flushbufs "$disk" 2>/dev/null || true
partprobe "$disk" 2>/dev/null || true
udevadm settle || true
}
install_base_system() {
# Initialize and populate the keyring
pacman-key --init
pacman-key --populate archlinux
pacman-key --populate omarchy
# Sync the offline database so pacman can find packages
pacman -Sy --noconfirm
cleanup_install_disk "$(install_disk)"
# Workarounds for archinstall 4.2 regressions under Python 3.14:
# 1. sync_log_to_install_medium: `self.target / absolute_logfile` drops
# self.target because the RHS is absolute, so Path.copy() raises EINVAL
# (source == target).
# 2. _add_limine_bootloader: `Path.copy(efi_dir_path)` raises IsADirectoryError
# because 3.14's Path.copy treats target as a literal path, not a directory
# (shutil.copy used to auto-append the source filename).
sed -i \
-e 's|logfile_target = self\.target / absolute_logfile$|logfile_target = self.target / absolute_logfile.relative_to("/")|' \
-e 's|(limine_path / file)\.copy(efi_dir_path)|(limine_path / file).copy(efi_dir_path / file)|' \
-e "s|(limine_path / 'limine-bios.sys')\.copy(boot_limine_path)|(limine_path / 'limine-bios.sys').copy(boot_limine_path / 'limine-bios.sys')|" \
/usr/lib/python3.14/site-packages/archinstall/lib/installer.py
# Install using files generated by the ./configurator
# Skip NTP and WKD sync since we're offline (keyring is pre-populated in ISO)
archinstall \
--config user_configuration.json \
--creds user_credentials.json \
--silent \
--skip-ntp \
--skip-wkd \
--skip-wifi-check
# Archinstall unmounts the ESP when it finishes. Omarchy's boot finalizer
# needs the generated Limine config and EFI artifacts available under /boot.
if ! mountpoint -q /mnt/boot; then
arch-chroot /mnt mount /boot
fi
# The installed fstab keeps the ESP root-only, but Omarchy finalization runs
# as the target user and must discover the Limine config before using sudo to
# replace it. Temporarily allow reads and directory traversal during install.
boot_device=$(findmnt -nro SOURCE --target /mnt/boot)
umount /mnt/boot
mount -t vfat -o rw,fmask=0022,dmask=0022 "$boot_device" /mnt/boot
if ! arch-chroot -u "$OMARCHY_USER" /mnt test -x /boot; then
echo "Target user cannot access the mounted ESP" >&2
return 1
fi
# After archinstall sets up the base system but before our installer runs,
# we need to ensure the offline pacman.conf is in place
cp /etc/pacman.conf /mnt/etc/pacman.conf
# Mount the offline mirror so it's accessible in the chroot
mkdir -p /mnt/var/cache/omarchy/mirror/offline
mount --bind /var/cache/omarchy/mirror/offline /mnt/var/cache/omarchy/mirror/offline
# Mount the packages dir so it's accessible in the chroot
mkdir -p /mnt/opt/packages
mount --bind /opt/packages /mnt/opt/packages
# No need to ask for sudo during the installation (omarchy itself responsible for removing after install)
mkdir -p /mnt/etc/sudoers.d
cat >/mnt/etc/sudoers.d/99-omarchy-installer <<EOF
root ALL=(ALL:ALL) NOPASSWD: ALL
%wheel ALL=(ALL:ALL) NOPASSWD: ALL
$OMARCHY_USER ALL=(ALL:ALL) NOPASSWD: ALL
EOF
chmod 440 /mnt/etc/sudoers.d/99-omarchy-installer
# Copy the local omarchy repo to the user's home directory
mkdir -p /mnt/home/$OMARCHY_USER/.local/share/
cp -r /root/omarchy /mnt/home/$OMARCHY_USER/.local/share/
chown -R 1000:1000 /mnt/home/$OMARCHY_USER/.local/
# Ensure all necessary scripts are executable
find /mnt/home/$OMARCHY_USER/.local/share/omarchy -type f -path "*/bin/*" -exec chmod +x {} \;
chmod +x /mnt/home/$OMARCHY_USER/.local/share/omarchy/boot.sh 2>/dev/null || true
find /mnt/home/$OMARCHY_USER/.local/share/omarchy/default/waybar -type f -name "*.sh" -exec chmod +x {} \; 2>/dev/null || true
}
configure_login_for_unencrypted_install() {
if [[ $(<user_encrypt_installation.txt) != "false" ]]; then
return
fi
# Unencrypted installs must stop at SDDM so the user password is entered
# before reaching the desktop. Omarchy's normal encrypted path may autologin
# because the disk password was already entered at boot.
#
# Keep the Omarchy SDDM theme and seed SDDM's last user/session state so
# first boot looks like the SDDM screen shown after logging out of Omarchy.
mkdir -p /mnt/etc/sddm.conf.d
rm -f /mnt/etc/sddm.conf.d/autologin.conf
cat >/mnt/etc/sddm.conf.d/99-omarchy-login.conf <<EOF
[Theme]
Current=omarchy
[Users]
RememberLastUser=true
RememberLastSession=true
EOF
mkdir -p /mnt/var/lib/sddm
cat >/mnt/var/lib/sddm/state.conf <<EOF
[Last]
Session=omarchy.desktop
User=$OMARCHY_USER
EOF
rm -f /mnt/etc/systemd/system/getty@tty1.service.d/autologin.conf
arch-chroot /mnt chown sddm:sddm /var/lib/sddm /var/lib/sddm/state.conf >/dev/null 2>&1 || true
arch-chroot /mnt systemctl enable sddm.service >/dev/null 2>&1 || true
}
chroot_bash() {
HOME=/home/$OMARCHY_USER \
arch-chroot -u $OMARCHY_USER /mnt/ \
env OMARCHY_CHROOT_INSTALL=1 \
OMARCHY_USER_NAME="$(<user_full_name.txt)" \
OMARCHY_USER_EMAIL="$(<user_email_address.txt)" \
OMARCHY_MIRROR="$OMARCHY_MIRROR" \
USER="$OMARCHY_USER" \
HOME="/home/$OMARCHY_USER" \
/bin/bash "$@"
}
if [[ $(tty) == "/dev/tty1" ]]; then
use_omarchy_helpers
run_configurator
install_arch
install_omarchy
fi fi
if [[ -f /usr/share/omarchy-iso/package-targets ]]; then
# shellcheck disable=SC1091
source /usr/share/omarchy-iso/package-targets
export OMARCHY_RUNTIME_PACKAGE OMARCHY_SETTINGS_PACKAGE OMARCHY_NVIM_PACKAGE
fi
export OMARCHY_PATH=/usr/share/omarchy
export OMARCHY_INSTALL=$OMARCHY_PATH/install
export OMARCHY_INSTALL_LOG_FILE=/var/log/omarchy-install.log
if [[ -f /usr/share/omarchy-iso/install-debug ]]; then
export OMARCHY_INSTALL_DEBUG=1
fi
# Tokyo Night palette so the live VT matches the installed look.
set_tokyo_night_colors() {
echo -en "\e]P01a1b26"; echo -en "\e]P1f7768e"; echo -en "\e]P29ece6a"
echo -en "\e]P3e0af68"; echo -en "\e]P47aa2f7"; echo -en "\e]P5bb9af7"
echo -en "\e]P67dcfff"; echo -en "\e]P7a9b1d6"; echo -en "\e]P8414868"
echo -en "\e]P9f7768e"; echo -en "\e]PA9ece6a"; echo -en "\e]PBe0af68"
echo -en "\e]PC7aa2f7"; echo -en "\e]PDbb9af7"; echo -en "\e]PE7dcfff"
echo -en "\e]PFc0caf5"
echo -en "\033[0m"
clear
}
set_tokyo_night_colors
mkdir -p /var/log
touch "$OMARCHY_INSTALL_LOG_FILE"
export COLUMNS=$(tput cols)
export LINES=$(tput lines)
exec > >(tee >(sed -u 's/\x1b\[[0-9;?]*[A-Za-z]//g' >>"$OMARCHY_INSTALL_LOG_FILE") 2>/dev/null) 2>/dev/tty
export CLICOLOR_FORCE=1
export FORCE_COLOR=1
if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then
echo "=== Omarchy ISO debug build ==="
[[ -f /usr/share/omarchy-iso/build-info ]] && cat /usr/share/omarchy-iso/build-info
pacman -Q omarchy-settings omarchy-keyring 2>/dev/null || true
echo "================================"
fi
# Warm the page cache for the bundled packages while the user works through the
# wizard. The install reads ~3GB out of the offline mirror, and pacman consumes
# it at only ~33MB/s, so on media slower than that the install is read-bound and
# every byte cached here is a byte it never waits for. On faster media this costs
# nothing but otherwise-idle bandwidth: the medium is untouched while the user
# types, and the target disk it writes to later is a different device.
#
# Clean page cache only, so the kernel reclaims it under pressure instead of
# OOMing, and a budget so small machines never evict what was just warmed.
# Set OMARCHY_NO_PREFETCH=1 to A/B the same ISO with this disabled.
warm_offline_mirror() {
local mirror=/var/cache/omarchy/mirror/offline
local budget_kb spent_kb=0 size_kb path
[[ ${OMARCHY_NO_PREFETCH:-} == 1 ]] && return 0
[[ -d $mirror ]] || return 0
budget_kb=$(($(awk '/^MemAvailable:/ { print $2 }' /proc/meminfo) / 2))
((budget_kb > 262144)) || return 0
# Largest first: the install reads most of the mirror, so when the budget
# cannot cover all of it this still front-loads the bytes that dominate.
while read -r size_kb path; do
((spent_kb + size_kb > budget_kb)) && continue
cat -- "$path" >/dev/null 2>&1 || true
spent_kb=$((spent_kb + size_kb))
done < <(du -k "$mirror"/*.pkg.tar.zst 2>/dev/null | sort -rn)
}
warm_offline_mirror &
warm_pid=$!
trap 'kill "$warm_pid" 2>/dev/null' EXIT
cd /root
./configurator
# The foreground dashboard is now the sole visible install UI owner. It starts
# the actual installer as a non-interactive child, logs child output, waits for
# completion, then renders the final installed-time/reboot prompt itself.
export OMARCHY_DASHBOARD_TTY="$(tty)"
rm -f /run/omarchy-install/state.json
/usr/local/bin/omarchy-install-dashboard \
"$OMARCHY_INSTALL_LOG_FILE" \
/run/omarchy-install/state.json \
-- \
/usr/local/bin/omarchy-iso-install \
--config /root/user_configuration.json \
--creds /root/user_credentials.json \
--full-name-file /root/user_full_name.txt \
--email-file /root/user_email_address.txt \
--encrypt-file /root/user_encrypt_installation.txt
+600 -73
View File
@@ -1,10 +1,48 @@
#!/bin/bash #!/bin/bash
# Rely on Omarchy for styling and error handling # On the live ISO, .automated_script.sh exports OMARCHY_PATH and the package
source "$OMARCHY_INSTALL/helpers/all.sh" # targets before running us. The defaults keep bin/omarchy-iso-configurator
# dry runs working outside the ISO.
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
OMARCHY_RUNTIME_PACKAGE="${OMARCHY_RUNTIME_PACKAGE:-omarchy}"
OMARCHY_SETTINGS_PACKAGE="${OMARCHY_SETTINGS_PACKAGE:-omarchy-settings}"
# Ensure tzupdate is available for guessing timezone LOGO_PATH="$OMARCHY_PATH/logo.txt"
command -v tzupdate >/dev/null || sudo pacman -Sy --noconfirm tzupdate
LOGO_WIDTH=$(awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH")
export GUM_CONFIRM_PROMPT_FOREGROUND="6"
export GUM_CONFIRM_SELECTED_FOREGROUND="0"
export GUM_CONFIRM_SELECTED_BACKGROUND="2"
export GUM_CONFIRM_UNSELECTED_FOREGROUND="7"
export GUM_CONFIRM_UNSELECTED_BACKGROUND="0"
# The console can grow after boot (the framebuffer settles late, especially in
# VMs), so remeasure at every redraw rather than trusting startup geometry.
# gum draws on /dev/tty, so measure that rather than inherited COLUMNS.
measure_terminal() {
TERM_WIDTH=$(stty size 2>/dev/null </dev/tty | awk '{print $2}')
(( TERM_WIDTH > 0 )) || TERM_WIDTH=${COLUMNS:-80}
PADDING_LEFT=$(((TERM_WIDTH - LOGO_WIDTH) / 2))
(( PADDING_LEFT < 0 )) && PADDING_LEFT=0
PADDING_LEFT_SPACES=$(printf "%*s" "$PADDING_LEFT" "")
PADDING="0 0 0 $PADDING_LEFT"
export GUM_CHOOSE_PADDING="$PADDING"
export GUM_FILTER_PADDING="$PADDING"
export GUM_INPUT_PADDING="$PADDING"
export GUM_SPIN_PADDING="$PADDING"
export GUM_TABLE_PADDING="$PADDING"
export GUM_CONFIRM_PADDING="$PADDING"
}
measure_terminal
clear_logo() {
measure_terminal
printf "\033[H\033[2J"
gum style --foreground 2 --padding "1 0 0 $PADDING_LEFT" "$(<"$LOGO_PATH")"
}
abort() { abort() {
gum style "${1:-Aborted installation}" gum style "${1:-Aborted installation}"
@@ -16,10 +54,15 @@ abort() {
step() { step() {
clear_logo clear_logo
echo echo
gum style "$1" gum style --padding "0 0 0 $PADDING_LEFT" "$1"
echo echo
} }
# Padded body text to line up with the logo and gum widgets.
say() {
gum style --padding "0 0 0 $PADDING_LEFT" "$@"
}
notice() { notice() {
clear_logo clear_logo
echo echo
@@ -234,6 +277,447 @@ get_root_disk() {
fi fi
} }
# Compute partition device path, handling NVMe/mmcblk's pN naming.
partition_path() {
local _disk="$1" _num="$2"
if [[ "$_disk" == *nvme* || "$_disk" == *mmcblk* ]]; then
echo "${_disk}p${_num}"
else
echo "${_disk}${_num}"
fi
}
# BitLocker writes the "-FVE-FS-" signature at offset 3 of the partition.
detect_bitlocker() {
local p sig
for p in $(lsblk -lnpo NAME,TYPE "$disk" | awk '$2=="part"{print $1}'); do
sig=$(dd if="$p" bs=1 skip=3 count=8 status=none 2>/dev/null)
if [[ "$sig" == "-FVE-FS-" ]]; then
echo "$p"
return 0
fi
done
return 1
}
# Look for an existing Windows ESP (FAT partition containing EFI/Microsoft) on $disk.
detect_windows_esp() {
local p tmp_mp
for p in $(blkid -t TYPE=vfat -o device 2>/dev/null); do
[[ "$p" == "$disk"* ]] || continue
tmp_mp=$(mktemp -d)
if mount -o ro "$p" "$tmp_mp" 2>/dev/null; then
if [[ -d "$tmp_mp/EFI/Microsoft" ]]; then
umount "$tmp_mp"; rmdir "$tmp_mp"
echo "$p"
return 0
fi
umount "$tmp_mp"
fi
rmdir "$tmp_mp" 2>/dev/null || true
done
return 1
}
to_gb() {
awk -v bytes="$1" 'BEGIN {printf "%.1fGB", bytes/1024/1024/1024}'
}
# T2 Macs need their own kernel for keyboard/wifi drivers.
detect_kernel() {
if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then
echo "linux-t2"
else
echo "linux"
fi
}
# Give udev a moment to create a device node after partitioning.
wait_for_device() {
local i
for i in 1 2 3 4 5; do
[[ -b "$1" ]] && return 0
sleep 1
done
}
# Save the configurator answers the installer consumes: git identity and
# encryption choice as plain files, account details as archinstall credentials.
write_user_files() {
echo "$full_name" >user_full_name.txt
echo "$email_address" >user_email_address.txt
printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt
local password_escaped password_hash_escaped username_escaped
password_escaped=$(echo -n "$password" | jq -Rsa)
password_hash_escaped=$(echo -n "$password_hash" | jq -Rsa)
username_escaped=$(echo -n "$username" | jq -Rsa)
local credentials_encryption_line=""
if [[ $encrypt_installation == true ]]; then
credentials_encryption_line=" \"encryption_password\": $password_escaped,"
fi
cat <<-_EOF_ >user_credentials.json
{
$credentials_encryption_line
"root_enc_password": $password_hash_escaped,
"users": [
{
"enc_password": $password_hash_escaped,
"groups": [],
"sudo": true,
"username": $username_escaped
}
]
}
_EOF_
}
# Show what a dry run would have written, then leave no trace behind.
print_dry_run_files() {
echo -e "User Configuration:"
cat user_configuration.json
echo -e "\n\nUser Credentials:"
cat user_credentials.json
echo -e "\n\nUser Full Name:"
cat user_full_name.txt
echo -e "\nUser Email Address:"
cat user_email_address.txt
echo -e "\nDisk Encryption:"
cat user_encrypt_installation.txt
rm -f user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt
}
# Shared error screen for every "can't fit Omarchy here" outcome of the
# partition install. Offers the partition tool as the meaningful second
# action so users have a way to actually fix the situation.
not_enough_space() {
local available="${1:-0}"
step "Not enough free space on $disk"
say --foreground 1 "$disk has $(to_gb $available) of usable free space; Omarchy needs at least 32GB."
say "Open the partition tool to free at least 32GB on $disk, then try again."
echo
if ! gum confirm --affirmative "Back" --negative "Open partition tool" "Return to installation mode?"; then
open_partition_tool
fi
return 1
}
run_partition_install() {
local dry=false
[[ "${1:-}" == "dry" ]] && dry=true
step "Checking for BitLocker on $disk"
local bl_part
bl_part=$(detect_bitlocker || true)
if [[ -n "$bl_part" ]]; then
say --foreground 1 "BitLocker signature detected on $bl_part."
say "Suspend or disable BitLocker in Windows before installing alongside it."
abort "Aborted: BitLocker is active on this disk."
fi
# Omarchy always creates its own dedicated ESP in free space and never
# adopts an existing (Windows) ESP. Sharing the Windows ESP is fragile:
# Windows Update/repair can reclaim or reformat it, the typical 100-260MiB
# Windows ESP is far too small for our Unified Kernel Images, and a shared
# ESP forces the install unencrypted. A separate Linux ESP keeps Windows
# and Omarchy fully isolated and keeps LUKS on the table.
step "Checking existing EFI partitions on $disk"
local detected_win_esp
detected_win_esp=$(detect_windows_esp || true)
if [[ -n "$detected_win_esp" ]]; then
say "Found a Windows ESP at $detected_win_esp — leaving it untouched."
say "Omarchy will create its own dedicated EFI partition in free space."
else
say "A new EFI partition will be created in free space."
fi
step "Analyzing free space on $disk"
partprobe "$disk" 2>/dev/null || true
sleep 1
# An unlabeled disk has no partition table for parted to scan, so we
# synthesize a whole-disk free region and mklabel gpt before mkpart.
local FREE_SPACE_INFO needs_mklabel=false pt_type disk_size_b
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
if [[ -z "$pt_type" ]]; then
disk_size_b=$(lsblk -bdno SIZE "$disk" 2>/dev/null)
if [[ -n "$disk_size_b" && "$disk_size_b" -gt 0 ]]; then
local _mib=$((1024 * 1024))
local _start=$_mib
local _end=$(( (disk_size_b - _mib) / _mib * _mib ))
FREE_SPACE_INFO="$_start $_end $((_end - _start))"
needs_mklabel=true
fi
else
# -s (script mode) keeps parted from prompting on stderr for warnings
# like a stale GPT backup; without it the configurator hangs invisibly.
FREE_SPACE_INFO=$(parted -ms "$disk" unit B print free </dev/null 2>/dev/null | \
awk -F: '
$NF ~ /free/ {
start=$2; end=$3; size=$4;
gsub(/B/, "", start);
gsub(/B/, "", end);
gsub(/B/, "", size);
if (size+0 > max_size+0) {
max_size=size; max_start=start; max_end=end;
}
}
END {
if (max_size+0 > 0) printf "%s %s %s\n", max_start, max_end, max_size;
}')
fi
if [[ -z "$FREE_SPACE_INFO" ]]; then
not_enough_space 0
return $?
fi
local FREE_START_B FREE_END_B FREE_SIZE_B
read -r FREE_START_B FREE_END_B FREE_SIZE_B <<<"$FREE_SPACE_INFO"
local mib=$((1024 * 1024)) gib=$((1024 * 1024 * 1024))
local ALIGN=$mib
local EFI_SIZE_B=$((2 * gib))
local MIN_INSTALL_B=$((32 * gib)) # Minimum total Omarchy allocation
local EFI_START_B EFI_END_B ROOT_START_B ROOT_END_B
EFI_START_B=$(( (FREE_START_B + ALIGN - 1) / ALIGN * ALIGN ))
EFI_END_B=$((EFI_START_B + EFI_SIZE_B))
ROOT_START_B=$(( (EFI_END_B + 1 + ALIGN - 1) / ALIGN * ALIGN ))
ROOT_END_B=$((FREE_END_B / ALIGN * ALIGN))
if (( ROOT_END_B <= ROOT_START_B )); then
not_enough_space "$FREE_SIZE_B"
return $?
fi
local INSTALL_MAX_B=$((ROOT_END_B - EFI_START_B))
if (( INSTALL_MAX_B < MIN_INSTALL_B )); then
not_enough_space "$INSTALL_MAX_B"
return $?
fi
# Default to encrypted and hide the unencrypted path behind Ctrl+C,
# matching the full-disk flow. The dedicated Omarchy ESP always supports
# LUKS, so encryption is always on the table here.
local mode="encrypted" affirmative confirm_status
while true; do
clear_logo
echo
say "Install Omarchy in the $(to_gb $INSTALL_MAX_B) of free space."
case $mode in
encrypted)
say --foreground 8 "Press Ctrl+C for unencrypted install."
affirmative="Yes, install"
;;
unencrypted)
affirmative="Yes, install without encryption"
;;
esac
echo
gum confirm --affirmative "$affirmative" --negative "No, change it" \
"Confirm installing on $disk"
confirm_status=$?
case $confirm_status in
0)
[[ $mode == "encrypted" ]] && encrypt_installation=true || encrypt_installation=false
break
;;
1)
return 1
;;
130)
case $mode in
encrypted) mode="unencrypted" ;;
unencrypted) mode="encrypted" ;;
esac
;;
*)
abort
;;
esac
done
local esp_mount_in_target
if [[ "$encrypt_installation" == "true" ]]; then
esp_mount_in_target=/boot
else
esp_mount_in_target=/efi
fi
kernel_choice=$(detect_kernel)
local last_part efi_part_num root_part_num
last_part=$(lsblk -n -o PARTN "$disk" 2>/dev/null | grep -E '^[0-9]+$' | sort -n | tail -1)
efi_part_num=$(( ${last_part:-0} + 1 ))
root_part_num=$(( efi_part_num + 1 ))
local efi_dev root_partition_device root_mapper
efi_dev=$(partition_path "$disk" "$efi_part_num")
root_partition_device=$(partition_path "$disk" "$root_part_num")
if [[ "$encrypt_installation" == "true" ]]; then
root_mapper="/dev/mapper/omarchy_root"
else
root_mapper="$root_partition_device"
fi
if $dry; then
$needs_mklabel && say "[dry] would initialize GPT on $disk"
say "[dry] would create root partition at $root_partition_device"
say "[dry] would create EFI partition at $efi_dev"
say "[dry] encrypted=$encrypt_installation kernel=$kernel_choice"
else
step "Creating partitions on $disk"
if $needs_mklabel; then
parted --script "$disk" mklabel gpt
partprobe "$disk" 2>/dev/null || true
sleep 1
fi
parted --script "$disk" mkpart primary fat32 "${EFI_START_B}B" "${EFI_END_B}B"
parted --script "$disk" set "$efi_part_num" esp on
parted --script "$disk" name "$efi_part_num" "OMARCHY_EFI"
parted --script "$disk" mkpart primary btrfs "${ROOT_START_B}B" "${ROOT_END_B}B"
parted --script "$disk" name "$root_part_num" "OMARCHY_ROOT"
partprobe "$disk"
sync
sleep 2
wait_for_device "$efi_dev"
wait_for_device "$root_partition_device"
if [[ "$encrypt_installation" == "true" ]]; then
step "Setting up LUKS2 on $root_partition_device"
printf "%s" "$password" | cryptsetup luksFormat --type luks2 --batch-mode "$root_partition_device" -
printf "%s" "$password" | cryptsetup open "$root_partition_device" omarchy_root -
root_mapper="/dev/mapper/omarchy_root"
else
wipefs -af "$root_partition_device"
root_mapper="$root_partition_device"
fi
step "Creating Btrfs filesystem and subvolumes"
mkfs.btrfs -f -L OMARCHY "$root_mapper"
wait_for_device "$root_mapper"
mkdir -p /mnt/btrfs-root
mount "$root_mapper" /mnt/btrfs-root
btrfs subvolume create /mnt/btrfs-root/@
btrfs subvolume create /mnt/btrfs-root/@home
btrfs subvolume create /mnt/btrfs-root/@log
btrfs subvolume create /mnt/btrfs-root/@pkg
umount /mnt/btrfs-root
rmdir /mnt/btrfs-root
mount -o noatime,compress=zstd,subvol=@ "$root_mapper" /mnt
mkdir -p /mnt/home /mnt/var/log /mnt/var/cache/pacman/pkg /mnt"$esp_mount_in_target"
mount -o noatime,compress=zstd,subvol=@home "$root_mapper" /mnt/home
mount -o noatime,compress=zstd,subvol=@log "$root_mapper" /mnt/var/log
mount -o noatime,compress=zstd,subvol=@pkg "$root_mapper" /mnt/var/cache/pacman/pkg
mkfs.fat -F32 -n OMARCHY_EFI "$efi_dev"
mount "$efi_dev" /mnt"$esp_mount_in_target"
fi
clear
write_user_files
local luks_uuid_json="null" luks_uuid
if [[ "$encrypt_installation" == "true" ]]; then
if $dry; then
luks_uuid="00000000-0000-0000-0000-000000000000"
else
luks_uuid=$(blkid -s UUID -o value "$root_partition_device")
fi
luks_uuid_json="\"$luks_uuid\""
fi
cat <<-_EOF_ >user_configuration.json
{
"app_config": null,
"archinstall-language": "English",
"auth_config": {},
"audio_config": { "audio": "pipewire" },
"bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
"custom_commands": [],
"omarchy_install": {
"mode": "protected",
"target_mount": "/mnt",
"boot": {
"esp_mount": "$esp_mount_in_target",
"esp_path": "/EFI/limine",
"efi_binary": "limine_x64.efi",
"enable_fallback": false
},
"storage": {
"esp_device": "$efi_dev",
"root_device": "$root_partition_device",
"root_mapper": "$root_mapper",
"luks_uuid": $luks_uuid_json,
"kernel": "$kernel_choice"
}
},
"disk_config": {
"config_type": "pre_mounted_config",
"mountpoint": "/mnt"
},
"hostname": "$hostname",
"kernels": [ "$kernel_choice" ],
"network_config": { "type": "iso" },
"ntp": true,
"parallel_downloads": 8,
"script": null,
"services": [],
"swap": true,
"timezone": "$timezone",
"locale_config": {
"kb_layout": "$keyboard",
"sys_enc": "UTF-8",
"sys_lang": "en_US.UTF-8"
},
"mirror_config": {
"custom_repositories": [],
"custom_servers": [
{"url": "https://mirror.omarchy.org/\$repo/os/\$arch"},
{"url": "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch"},
{"url": "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"}
],
"mirror_regions": {},
"optional_repositories": []
},
"packages": [
"base-devel",
"git",
"omarchy-keyring",
"$OMARCHY_SETTINGS_PACKAGE",
"$OMARCHY_RUNTIME_PACKAGE"
],
"profile_config": {
"gfx_driver": null,
"greeter": null,
"profile": {}
},
"version": "3.0.9"
}
_EOF_
if $dry; then
print_dry_run_files
fi
}
disk_form() { disk_form() {
step "Let's select where to install Omarchy..." step "Let's select where to install Omarchy..."
@@ -265,7 +749,60 @@ disk_form() {
disk=$(echo "$selected_display" | awk '{print $1}') disk=$(echo "$selected_display" | awk '{print $1}')
} }
disk_form # STEP 4: INSTALL MODE
open_partition_tool() {
step "Partition tool for $disk"
gum style "Create unallocated free space for Omarchy, then write changes and quit."
gum style --foreground 8 "Do not create an Omarchy partition here — leave the target area as Free space."
gum style --foreground 8 "Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists."
echo
gum confirm --affirmative "Open cfdisk" --negative "Back" "Open partition tool for $disk?" || return 0
clear
cfdisk "$disk" || true
partprobe "$disk" 2>/dev/null || true
sync
sleep 1
}
# If the disk has no partition table or no unallocated free space, the only
# viable path is a full-disk install. Skip the install-mode picker and go
# straight to confirmation.
requires_full_disk_install() {
local pt_type free_space_b
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
# a) No partition table: disk is empty
[[ -z "$pt_type" ]] && return 0
# b) No usable free space
free_space_b=$(parted -ms "$disk" unit B print free </dev/null 2>/dev/null | \
awk -F: '
$NF ~ /free/ {
size=$4;
gsub(/B/, "", size);
if (size+0 > max+0) max=size;
}
END {
print max+0;
}')
[[ -z "$free_space_b" || "$free_space_b" -eq 0 ]]
}
install_mode_form() {
local choices=("Full disk install")
# The free-space/protected path registers an EFI boot entry and is UEFI-only.
# Full-disk installs still support BIOS through the orchestrator's BIOS branch.
if [[ -d /sys/firmware/efi ]]; then
choices+=("Free space install (alongside existing data)")
fi
choices+=("Choose a different disk")
step "Let's select how to install Omarchy..."
install_mode=$(gum choose --header "Select installation mode on $disk" "${choices[@]}") || abort
}
confirm_disk_overwrite() { confirm_disk_overwrite() {
local mode="encrypted" local mode="encrypted"
@@ -274,10 +811,10 @@ confirm_disk_overwrite() {
while true; do while true; do
clear_logo clear_logo
echo echo
gum style "Everything will be overwritten. There is no recovery possible." say "Everything will be overwritten. There is no recovery possible."
if [[ $mode == "encrypted" ]]; then if [[ $mode == "encrypted" ]]; then
gum style --foreground 8 "Press Ctrl+C for unencrypted install." say --foreground 8 "Press Ctrl+C for unencrypted install."
affirmative="Yes, install" affirmative="Yes, install"
else else
affirmative="Yes, install without encryption" affirmative="Yes, install without encryption"
@@ -305,44 +842,45 @@ confirm_disk_overwrite() {
done done
} }
while ! confirm_disk_overwrite; do select_installation() {
disk_form local full_disk_only
done
while true; do
if requires_full_disk_install; then
full_disk_only=true
install_mode="Full disk install"
else
full_disk_only=false
install_mode_form
fi
case "$install_mode" in
"Full disk install")
if confirm_disk_overwrite; then
return 0
fi
if $full_disk_only; then
disk_form
fi
;;
"Free space install (alongside existing data)")
if run_partition_install "$@"; then
exit 0
fi
;;
"Choose a different disk")
disk_form
;;
esac
done
}
disk_form
select_installation "$@"
clear clear
# Save user full name and email address, so the autostart can write_user_files
# set them as OMARCHY_USER_NAME and OMARCHY_USER_EMAIL
echo "$full_name" >user_full_name.txt
echo "$email_address" >user_email_address.txt
# Escape username and password for safe inclusion
password_escaped=$(echo -n "$password" | jq -Rsa)
password_hash_escaped=$(echo -n "$password_hash" | jq -Rsa)
username_escaped=$(echo -n "$username" | jq -Rsa)
if [[ $encrypt_installation == true ]]; then
credentials_encryption_line=" \"encryption_password\": $password_escaped,"
else
credentials_encryption_line=""
fi
cat <<-_EOF_ >user_credentials.json
{
$credentials_encryption_line
"root_enc_password": $password_hash_escaped,
"users": [
{
"enc_password": $password_hash_escaped,
"groups": [],
"sudo": true,
"username": $username_escaped
}
]
}
_EOF_
printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt
# Setup partition layout # Setup partition layout
disk_size=$(lsblk -bdno SIZE "$disk") disk_size=$(lsblk -bdno SIZE "$disk")
@@ -357,12 +895,7 @@ boot_partition_size=$((2 * gib))
main_partition_start=$((boot_partition_size + boot_partition_start)) main_partition_start=$((boot_partition_size + boot_partition_start))
main_partition_size=$((disk_size_in_mib - main_partition_start - gpt_backup_reserve)) main_partition_size=$((disk_size_in_mib - main_partition_start - gpt_backup_reserve))
# Detect T2 Mac and set appropriate kernel kernel_choice=$(detect_kernel)
if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then
kernel_choice="linux-t2"
else
kernel_choice="linux"
fi
if [[ $encrypt_installation == true ]]; then if [[ $encrypt_installation == true ]]; then
disk_encryption_config=$(cat <<-_EOF_ disk_encryption_config=$(cat <<-_EOF_
@@ -372,7 +905,7 @@ if [[ $encrypt_installation == true ]]; then
"lvm_volumes": [], "lvm_volumes": [],
"iter_time": 2000, "iter_time": 2000,
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ], "partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ],
"encryption_password": $password_escaped "encryption_password": $(echo -n "$password" | jq -Rsa)
} }
_EOF_ _EOF_
) )
@@ -386,14 +919,22 @@ cat <<-_EOF_ >user_configuration.json
"archinstall-language": "English", "archinstall-language": "English",
"auth_config": {}, "auth_config": {},
"audio_config": { "audio": "pipewire" }, "audio_config": { "audio": "pipewire" },
"bootloader": "Limine", "bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
"custom_commands": [], "custom_commands": [],
"disk_config": { "omarchy_install": {
"btrfs_options": { "mode": "full_disk",
"snapshot_config": { "target_mount": "/mnt",
"type": "Snapper" "boot": {
} "esp_mount": "/boot",
"esp_path": "/EFI/limine",
"efi_binary": "limine_x64.efi",
"enable_fallback": true
}, },
"storage": {
"kernel": "$kernel_choice"
}
},
"disk_config": {
"config_type": "default_layout", "config_type": "default_layout",
"device_modifications": [ "device_modifications": [
{ {
@@ -479,7 +1020,8 @@ cat <<-_EOF_ >user_configuration.json
"base-devel", "base-devel",
"git", "git",
"omarchy-keyring", "omarchy-keyring",
"snapper" "$OMARCHY_SETTINGS_PACKAGE",
"$OMARCHY_RUNTIME_PACKAGE"
], ],
"profile_config": { "profile_config": {
"gfx_driver": null, "gfx_driver": null,
@@ -491,20 +1033,5 @@ cat <<-_EOF_ >user_configuration.json
_EOF_ _EOF_
if [[ $1 == "dry" ]]; then if [[ $1 == "dry" ]]; then
echo -e "User Configuration:" print_dry_run_files
cat user_configuration.json
echo -e "\n\nUser Credentials:"
cat user_credentials.json
echo -e "\n\nUser Full Name:"
cat user_full_name.txt
echo -e "\nUser Email Address:"
cat user_email_address.txt
echo -e "\nDisk Encryption:"
cat user_encrypt_installation.txt
rm user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt
fi fi
+602
View File
@@ -0,0 +1,602 @@
#!/usr/bin/env bash
# Foreground Omarchy ISO install UI supervisor.
#
# This process is the only owner of the visible install UI. It launches the
# actual installer as a non-interactive child, sends child output to the support
# log, renders progress while the child runs, then renders the final installed
# time + reboot prompt itself. There is deliberately no background dashboard.
set -euo pipefail
usage() {
echo "Usage: $0 <log-file> <state-file> -- <installer command...>" >&2
exit 2
}
(( $# >= 4 )) || usage
LOG_FILE="$1"
STATE_FILE="$2"
shift 2
[[ ${1:-} == "--" ]] || usage
shift
(( $# > 0 )) || usage
INSTALL_CMD=("$@")
TTY_PATH="${OMARCHY_DASHBOARD_TTY:-/dev/tty}"
LOGO_PATH="${OMARCHY_PATH:-/usr/share/omarchy}/logo.txt"
CSI=$'\033['
RESET="${CSI}0m"
DIM="${CSI}2m"
HIDE_CURSOR="${CSI}?25l"
SHOW_CURSOR="${CSI}?25h"
CLEAR="${CSI}2J${CSI}H"
CLEAR_LINE="${CSI}2K"
CLEAR_TO_END="${CSI}J"
GREEN="${CSI}32m"
WHITE="${CSI}37m"
RED="${CSI}31m"
DARK="${CSI}90m"
# The dashboard owns the final reboot prompt now, so keep the gum confirm
# styling that used to come from install/helpers/presentation.sh.
export GUM_CONFIRM_PROMPT_FOREGROUND="${GUM_CONFIRM_PROMPT_FOREGROUND:-6}"
export GUM_CONFIRM_SELECTED_FOREGROUND="${GUM_CONFIRM_SELECTED_FOREGROUND:-0}"
export GUM_CONFIRM_SELECTED_BACKGROUND="${GUM_CONFIRM_SELECTED_BACKGROUND:-2}"
export GUM_CONFIRM_UNSELECTED_FOREGROUND="${GUM_CONFIRM_UNSELECTED_FOREGROUND:-7}"
export GUM_CONFIRM_UNSELECTED_BACKGROUND="${GUM_CONFIRM_UNSELECTED_BACKGROUND:-0}"
child_pid=""
child_pgid=""
tips=(
"Super + Space opens the Omarchy menu for apps, settings, and more"
"Super + K shows all the key bindings"
"Super is the Windows or command key on your keyboard"
"Use Xournal++ to sign PDFs"
"Share files with phones and laptops using LocalSend"
"Turn any website into an app with Install > Web App in the menu"
"Super + Return opens a terminal, Super + Shift + Return the browser"
"Edit images with Pinta, videos with Kdenlive, docs with LibreOffice"
"Super + Ctrl + Print grabs text off the screen with OCR"
"Print takes a screenshot, Alt + Print records the screen"
"Set a reminder with Super + Ctrl + R"
"Switch themes from Style > Theme in the Omarchy menu"
"Double-click the menu bar to make it transparent"
"Run a full Windows VM via Install > Windows in the menu"
"Super + Ctrl + V opens the clipboard manager"
"Super + 1 through 0 switches workspaces, add Shift to bring the window"
"Super + Print picks a color from anywhere on screen"
"Keep the system fresh with Update in the Omarchy menu"
)
cleanup() {
printf '%s%s' "$RESET" "$SHOW_CURSOR" >"$TTY_PATH" 2>/dev/null || true
}
dashboard_log() {
printf '[dashboard] %s\n' "$*" >>"$LOG_FILE" 2>/dev/null || true
}
stop_child() {
[[ -n ${child_pid:-} ]] || return 0
if kill -0 "$child_pid" 2>/dev/null; then
if [[ -n ${child_pgid:-} ]]; then
kill -- -"$child_pgid" 2>/dev/null || true
fi
kill "$child_pid" 2>/dev/null || true
for _ in {1..20}; do
kill -0 "$child_pid" 2>/dev/null || break
sleep 0.05
done
if kill -0 "$child_pid" 2>/dev/null; then
[[ -n ${child_pgid:-} ]] && kill -9 -- -"$child_pgid" 2>/dev/null || true
kill -9 "$child_pid" 2>/dev/null || true
fi
fi
wait "$child_pid" 2>/dev/null || true
}
interrupt() {
local code="$1"
stop_child
cleanup
exit "$code"
}
trap cleanup EXIT
trap 'interrupt 130' INT
trap 'interrupt 143' TERM
term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; }
term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; }
repeat() {
local char="$1" count="$2" out="" i
for ((i=0; i<count; i++)); do out+="$char"; done
printf '%s' "$out"
}
logo_width() {
if [[ -f $LOGO_PATH ]]; then
awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH"
else
printf '81\n'
fi
}
logo_height() {
if [[ -f $LOGO_PATH ]]; then
wc -l <"$LOGO_PATH"
else
printf '1\n'
fi
}
LOGO_WIDTH="$(logo_width)"
LOGO_HEIGHT="$(logo_height)"
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
CONTENT_WIDTH="$LOGO_WIDTH"
DYNAMIC_ROW=$((LOGO_HEIGHT + 3))
LAST_PCT=1
left_padding() {
local width="${1:-$CONTENT_WIDTH}" cols pad
cols="$(term_cols)"
pad=$(( (cols - width) / 2 ))
(( pad < 0 )) && pad=0
printf '%*s' "$pad" ''
}
visible_len() {
local text="$1"
text="$(printf '%b' "$text" | sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g')"
printf '%s' "${#text}"
}
blank_line() { printf '\r%s\n' "$CLEAR_LINE"; }
line_at() {
local width="${1:-$CONTENT_WIDTH}" indent="${2:-0}"
shift 2
printf '\r%s%s%*s' "$CLEAR_LINE" "$(left_padding "$width")" "$indent" ''
printf '%b' "$*"
}
center() {
local text="$1" width="${2:-$CONTENT_WIDTH}" len inner_pad
len="$(visible_len "$text")"
inner_pad=$(( (width - len) / 2 ))
(( inner_pad < 0 )) && inner_pad=0
printf '\r%s%s%*s%b%s\n' "$CLEAR_LINE" "$(left_padding "$width")" "$inner_pad" '' "$text" "$RESET"
}
render_logo() {
local pad
pad="$(left_padding "$LOGO_WIDTH")"
if [[ -f $LOGO_PATH ]]; then
while IFS= read -r line; do
printf '\r%s%s%b%s%b\n' "$CLEAR_LINE" "$pad" "$GREEN" "$line" "$RESET"
done <"$LOGO_PATH"
else
center "${GREEN}Omarchy${RESET}" "$CONTENT_WIDTH"
fi
}
progress_bar() {
local pct="$1" width="${2:-34}" filled empty
(( pct < 0 )) && pct=0
(( pct > 100 )) && pct=100
filled=$(( pct * width / 100 ))
empty=$(( width - filled ))
printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET"
}
install_progress() {
local phase finished phase_started elapsed span active pct
local start=1 end=99 expected=90
local -a state
[[ -f $STATE_FILE ]] || { printf '1'; return; }
mapfile -t state < <(jq -r '
(.current_phase // "Starting installation"),
((.finished_at // 0) | tonumber? // 0 | floor),
((.phase_started_at // .started_at // 0) | tonumber? // 0 | floor)
' "$STATE_FILE" 2>/dev/null)
(( ${#state[@]} == 3 )) || { printf '1'; return; }
phase="${state[0]}" finished="${state[1]}" phase_started="${state[2]}"
if (( finished > 0 )) || [[ $phase == "Installation complete" ]]; then
printf '100'
return
fi
# Phase weights: start%, end%, expected seconds in phase.
case "$phase" in
"Starting installation") start=1 end=2 expected=2 ;;
"Preparing live environment") start=2 end=6 expected=5 ;;
"Verifying protected mounts") start=6 end=8 expected=3 ;;
"Installing Arch + Omarchy") start=8 end=86 expected=95 ;;
"Installing base system") start=8 end=84 expected=85 ;;
"Configuring protected boot") start=84 end=86 expected=8 ;;
"Configuring hibernation") start=86 end=88 expected=5 ;;
"Configuring system") start=88 end=93 expected=10 ;;
"Finalizing Limine boot") start=93 end=96 expected=8 ;;
"Finalizing user") start=96 end=98 expected=10 ;;
"Finalizing in chroot") start=88 end=98 expected=10 ;;
"Configuring login") start=98 end=99 expected=3 ;;
"Configuring DNS resolver") start=99 end=100 expected=2 ;;
"Validating protected boot setup") start=99 end=100 expected=5 ;;
"Validating boot setup") start=99 end=100 expected=5 ;;
esac
(( phase_started > 0 )) || phase_started=$EPOCHSECONDS
elapsed=$(( EPOCHSECONDS - phase_started ))
(( elapsed < 0 )) && elapsed=0
span=$(( end - start ))
(( span < 0 )) && span=0
(( expected < 1 )) && expected=1
active=$(( elapsed * span / expected ))
(( active > span - 1 )) && active=$(( span - 1 ))
(( active < 0 )) && active=0
pct=$(( start + active ))
(( pct > 100 )) && pct=100
printf '%s' "$pct"
}
# One tip per ~8 seconds (frames tick every 0.5s), in order, so each stays
# readable and none are skipped when progress jumps.
current_tip() {
local frame="$1"
printf '%s' "${tips[$(( frame / 16 % ${#tips[@]} ))]}"
}
render_static() {
printf '%s' "$CLEAR"
blank_line
render_logo
blank_line
}
render_dynamic() {
local frame="$1" pct
pct="$(install_progress)"
[[ $pct =~ ^[0-9]+$ ]] || pct=$LAST_PCT
(( pct > 100 )) && pct=100
(( pct < LAST_PCT )) && pct=$LAST_PCT
LAST_PCT=$pct
printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW"
center "Installing Omarchy" "$CONTENT_WIDTH"
blank_line
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) ""
progress_bar "$pct" 34
printf '\n'
blank_line
center "${DIM}Tip:${RESET} ${GREEN}$(current_tip "$frame")${RESET}" "$CONTENT_WIDTH"
printf '%s' "$CLEAR_TO_END"
}
install_duration() {
local path line duration
if [[ -f $STATE_FILE ]]; then
duration="$(jq -r '
((.started_at // 0) | tonumber? // 0) as $started
| ((.finished_at // 0) | tonumber? // 0) as $finished
| if $started <= 0 or $finished <= 0 or $finished < $started then empty
else (($finished - $started) | round) as $secs
| if $secs >= 3600 then "\($secs / 3600 | floor)h \($secs % 3600 / 60 | floor)m \($secs % 60)s"
else "\($secs / 60 | floor)m \($secs % 60)s"
end
end
' "$STATE_FILE" 2>/dev/null || true)"
[[ -n $duration ]] && { printf '%s' "$duration"; return 0; }
fi
for path in /mnt/var/log/omarchy-install.log "$LOG_FILE"; do
[[ -f $path ]] || continue
for prefix in Total Omarchy; do
line="$(grep "^$prefix:" "$path" 2>/dev/null | tail -n 1 || true)"
[[ -n $line ]] || continue
duration="${line#*:}"
duration="${duration#${duration%%[![:space:]]*}}"
[[ -n $duration ]] && { printf '%s' "$duration"; return 0; }
done
done
return 0
}
render_finish() {
local duration effect_canvas_width
duration="$(install_duration || true)"
duration="${duration:-Complete}"
effect_canvas_width="$(term_cols)"
(( effect_canvas_width > 1 )) && effect_canvas_width=$((effect_canvas_width - 1))
{
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
blank_line
render_logo
blank_line
center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH"
blank_line
} >"$TTY_PATH"
if command -v tte >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 && [[ -f $LOGO_PATH ]]; then
# TTE reuse mode restores a saved cursor before moving up over its input.
# Save this anchor so the effect redraws the same logo rows shown above.
printf '%s%d;1H\0337' "$CSI" "$((LOGO_HEIGHT + 2))" >"$TTY_PATH"
timeout 8s tte -i "$LOGO_PATH" \
--canvas-width "$effect_canvas_width" \
--anchor-text c \
--frame-rate 260 \
--reuse-canvas \
laseretch \
>"$TTY_PATH" 2>/dev/null || true
printf '%s%d;1H' "$CSI" "$((LOGO_HEIGHT + 8))" >"$TTY_PATH"
fi
}
reboot_prompt() {
local prompt_pad
# Position the cursor one blank line below the title so the button sits right there.
printf '%s%d;1H%s' "$CSI" "$((LOGO_HEIGHT + 5))" "$CLEAR_LINE" >"$TTY_PATH"
prompt_pad=$(( ( $(term_cols) - 18 ) / 2 ))
(( prompt_pad < 0 )) && prompt_pad=0
gum confirm \
--padding "0 0 0 $prompt_pad" \
--show-help=false \
--default \
--affirmative "Reboot Now" \
--negative "" \
"" <"$TTY_PATH" >"$TTY_PATH" 2>&1
}
truncate_for_width() {
local text="$1" width="$2"
if (( ${#text} > width )); then
printf '%s…' "${text:0:$((width - 1))}"
else
printf '%s' "$text"
fi
}
render_failure_log_tail() {
local rows lines width line tail_log label
rows=$(term_size)
rows=${rows%% *}
lines=$((rows - LOGO_HEIGHT - 18))
(( lines < 6 )) && lines=6
(( lines > 14 )) && lines=14
width=$((CONTENT_WIDTH - 4))
(( width < 40 )) && width=40
tail_log="${OMARCHY_FAILURE_TAIL_LOG:-}"
if [[ -z $tail_log ]]; then
if [[ -f /mnt/var/log/omarchy-install.log ]]; then
tail_log=/mnt/var/log/omarchy-install.log
else
tail_log="$LOG_FILE"
fi
fi
[[ -f $tail_log ]] || return 0
label="Last log lines:"
[[ $tail_log == /mnt/* ]] && label="Last target log lines:"
line_at "$CONTENT_WIDTH" 2 "${DIM}${label}${RESET}"
printf '\n'
tail -n "$lines" "$tail_log" 2>/dev/null | while IFS= read -r line; do
line_at "$CONTENT_WIDTH" 2 "$(truncate_for_width "$line" "$width")"
printf '\n'
done
}
find_log_uploader() {
local candidate
for candidate in omarchy-upload-log /usr/local/bin/omarchy-upload-log /usr/bin/omarchy-upload-log /mnt/usr/bin/omarchy-upload-log; do
if [[ $candidate == */* ]]; then
[[ -x $candidate ]] && { printf '%s' "$candidate"; return 0; }
elif command -v "$candidate" >/dev/null 2>&1; then
command -v "$candidate"
return 0
fi
done
return 1
}
prompt_enter() {
printf '\nPress Enter to continue…' >"$TTY_PATH"
IFS= read -r _ <"$TTY_PATH" || true
}
upload_failure_log() {
local uploader
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
if uploader=$(find_log_uploader); then
"$uploader" install >"$TTY_PATH" 2>&1 || true
else
{
echo "No log uploader is available in this environment."
echo "Install log: $LOG_FILE"
[[ -f /mnt/var/log/omarchy-install.log ]] && echo "Target log: /mnt/var/log/omarchy-install.log"
} >"$TTY_PATH"
fi
prompt_enter
}
view_failure_log() {
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
if command -v less >/dev/null 2>&1 && [[ -f $LOG_FILE ]]; then
less "$LOG_FILE" <"$TTY_PATH" >"$TTY_PATH" 2>&1 || true
else
tail -n 200 "$LOG_FILE" >"$TTY_PATH" 2>&1 || true
prompt_enter
fi
}
render_failure() {
local status="$1" state_summary="${2:-}" line
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
blank_line
render_logo
blank_line
center "${RED}Omarchy installation stopped${RESET}" "$CONTENT_WIDTH"
center "Installer exited with status $status" "$CONTENT_WIDTH"
if [[ -n $state_summary ]]; then
blank_line
while IFS= read -r line; do
[[ -n $line ]] || continue
center "${DIM}$(truncate_for_width "$line" "$CONTENT_WIDTH")${RESET}" "$CONTENT_WIDTH"
done <<<"$state_summary"
fi
blank_line
render_failure_log_tail
blank_line
center "${DIM}Get help at https://omarchy.org/discord${RESET}" "$CONTENT_WIDTH"
blank_line
}
failure_menu() {
local choice uploader
[[ ${OMARCHY_UI_FAILURE_ACTION:-} == "exit" ]] && return 0
while true; do
uploader=""
uploader=$(find_log_uploader || true)
if [[ -n $uploader ]]; then
choice=$(gum choose \
--height 6 \
--header "What would you like to do?" \
"Upload log for support" \
"View full log" \
"Drop to shell" \
"Reboot" \
"Power off" \
<"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell"
else
choice=$(gum choose \
--height 5 \
--header "What would you like to do?" \
"View full log" \
"Drop to shell" \
"Reboot" \
"Power off" \
<"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell"
fi
case "$choice" in
"Upload log for support")
upload_failure_log
render_failure "${failure_status:-1}" "${failure_summary:-}" >"$TTY_PATH" 2>/dev/null || true
;;
"View full log")
view_failure_log
render_failure "${failure_status:-1}" "${failure_summary:-}" >"$TTY_PATH" 2>/dev/null || true
;;
"Reboot")
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
;;
"Power off")
poweroff 2>/dev/null || systemctl poweroff 2>/dev/null || true
;;
"Drop to shell"|"")
return 0
;;
esac
done
}
sanitize_child_output() {
# Archinstall is only a library subsystem in Omarchy's installer. Its own
# post-install success/reboot banner can be buffered until process exit and
# appear after an Omarchy failure, which makes the unified log contradictory.
# Keep that third-party banner out of Omarchy's support log; the raw
# archinstall log is still uploaded separately by omarchy-upload-log.
sed -u \
-e $'s/\x1b\\[[0-9;?]*[A-Za-z]//g' \
-e '/^Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers$/d' \
-e '/^Enabling service systemd-timesyncd$/d' \
-e '/^Setting password for root$/d' \
-e '/^Updating \/mnt\/etc\/fstab$/d' \
-e '/^Syncing the system\.\.\.$/d' \
-e '/^Installation completed without any errors\.$/d' \
-e '/^Log files temporarily available at \/var\/log\/archinstall\.$/d' \
-e '/^You may reboot when ready\.$/d'
}
launch_child() {
mkdir -p "$(dirname "$LOG_FILE")"
# Child is non-interactive. All visible UI belongs to this supervisor.
PYTHONUNBUFFERED=1 setsid "${INSTALL_CMD[@]}" </dev/null > >(sanitize_child_output >>"$LOG_FILE") 2>&1 &
child_pid=$!
child_pgid=$child_pid
}
[[ -e $TTY_PATH ]] || exit 2
printf '%s' "$HIDE_CURSOR" >"$TTY_PATH"
render_static >"$TTY_PATH"
launch_child
frame=0
while jobs -pr | grep -qx "$child_pid"; do
render_dynamic "$frame" >"$TTY_PATH" 2>/dev/null || true
frame=$((frame + 1))
sleep 0.5
done
set +e
wait "$child_pid"
status=$?
set -e
child_pid=""
child_pgid=""
dashboard_log "installer child exited with status $status"
if (( status != 0 )); then
failure_status="$status"
failure_summary=""
if [[ -f $STATE_FILE ]]; then
failure_summary="$(jq -r '
(.current_phase // empty | "last installer phase: \(.)"),
((.phases // []) | map(select(.status == "failed")) | last // empty
| "failed phase: \(.name): \(.error)")
' "$STATE_FILE" 2>/dev/null || true)"
if [[ -n $failure_summary ]]; then
while IFS= read -r line; do
[[ -n $line ]] && dashboard_log "$line"
done <<<"$failure_summary"
fi
fi
render_failure "$failure_status" "$failure_summary" >"$TTY_PATH" 2>/dev/null || true
failure_menu
exit "$failure_status"
fi
set +e
render_finish
finish_render_status=$?
set -e
if (( finish_render_status != 0 )); then
dashboard_log "finish screen render failed with status $finish_render_status; using fallback"
{
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
printf '\nFinished installing\n\n'
} >"$TTY_PATH" 2>/dev/null || true
fi
if reboot_prompt; then
if [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]]; then
exit 0
fi
printf '%s' "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
fi
exit 0
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
#
# Clean up any holders / mounts / LVM / crypt devices on the install disk
# before archinstall starts wiping. Idempotent — called once per install attempt
# from the orchestrator's cleanup_disk phase.
#
# Ported verbatim from .automated_script.sh's cleanup_install_disk(). Kept in
# bash because lsblk / findmnt / lvm / cryptsetup are all easier to drive
# here than from Python.
set -uo pipefail
if [[ ${1:-} == --protected ]]; then
target="${2:-}"
[[ -n $target ]] || { echo "Usage: $0 --protected <target>" >&2; exit 1; }
findmnt -R "$target" >/dev/null 2>&1 && umount -R "$target" || true
exit 0
fi
disk="${1:-}"
[[ -n $disk && -b $disk ]] || { echo "Need a block device argument; got '$disk'" >&2; exit 1; }
echo "Cleaning up existing holders on install disk: $disk"
findmnt -R /mnt >/dev/null && umount -R /mnt || true
while read -r dev; do
[[ -b $dev ]] || continue
swapoff "$dev" 2>/dev/null || true
while read -r target; do
[[ -n $target ]] && umount "$target" 2>/dev/null || true
done < <(findmnt -rn -S "$dev" -o TARGET 2>/dev/null || true)
done < <(lsblk -rnpo PATH "$disk")
while read -r dev type; do
[[ $type == disk || $type == part || $type == crypt ]] || continue
while read -r vg; do
[[ -n $vg ]] && vgchange -an "$vg" 2>/dev/null || true
done < <(pvs --noheadings -o vg_name "$dev" 2>/dev/null | awk '{$1=$1; print}' | sort -u)
done < <(lsblk -rnpo PATH,TYPE "$disk")
while read -r dev type; do
[[ $type == crypt ]] && cryptsetup close "$dev" 2>/dev/null || true
done < <(lsblk -rnpo PATH,TYPE "$disk")
blockdev --flushbufs "$disk" 2>/dev/null || true
partprobe "$disk" 2>/dev/null || true
udevadm settle || true
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
#
# Live-ISO entry point for the Omarchy install orchestrator. Invoked from
# /root/.automated_script.sh after the configurator wizard has produced its
# user_*.json / user_*.txt files. The outer shim owns install logging.
set -eEo pipefail
# Parse our args; convert to env vars before exec'ing Python so archinstall's
# own argparse (which runs at module import time inside the orchestrator)
# doesn't choke on flags it doesn't recognize.
while [[ $# -gt 0 ]]; do
case $1 in
--config) export OMARCHY_INSTALL_CONFIG="$2"; shift 2 ;;
--creds) export OMARCHY_INSTALL_CREDS="$2"; shift 2 ;;
--full-name-file) export OMARCHY_INSTALL_FULL_NAME_FILE="$2"; shift 2 ;;
--email-file) export OMARCHY_INSTALL_EMAIL_FILE="$2"; shift 2 ;;
--encrypt-file) export OMARCHY_INSTALL_ENCRYPT_FILE="$2"; shift 2 ;;
*) echo "omarchy-iso-install: unknown arg $1" >&2; exit 2 ;;
esac
done
cd /usr/share/omarchy-iso
exec python -m orchestrator.main
Whitespace-only changes.
@@ -0,0 +1,246 @@
"""Thin compatibility wall around the archinstall Python library.
ONLY this module imports from archinstall. Everything else uses these helpers.
If archinstall's API churns, the blast radius is contained here.
Tested against archinstall 4.4 (Python 3.14).
The canonical call sequence (mirrored from archinstall.scripts.guided.py) is:
FilesystemHandler(disk_config).perform_filesystem_operations()
with Installer(mountpoint, disk_config, kernels=, silent=) as inst:
# guided.py:84-85 SKIPS this for DiskLayoutType.Pre_mount — pre-mounted
# configs do their own mounting before the Installer context opens.
if disk_config.config_type != DiskLayoutType.Pre_mount:
inst.mount_ordered_layout()
inst.sanity_check(offline=, skip_ntp=, skip_wkd=)
inst.generate_key_files() # encrypted only
inst.set_mirrors(handler, mirror_config, on_target=False)
inst.minimal_installation(...) # base + linux pacstrap
inst.set_mirrors(handler, mirror_config, on_target=True)
inst.setup_swap(algo=...)
inst.create_users(users)
inst.add_additional_packages(packages)
inst.set_timezone(tz)
inst.activate_time_synchronization()
inst.set_user_password(root_user)
inst.enable_service(services)
inst.genfstab()
Our orchestrator installs Omarchy's Limine files directly instead of invoking
archinstall's bootloader helper, so EFI paths and efibootmgr labels are ours
from the start.
"""
from __future__ import annotations
import importlib
from contextlib import contextmanager
from pathlib import Path
from typing import Iterator
# Imports are top-level so a missing/incompatible archinstall surfaces at
# orchestrator startup, not deep inside a phase.
from archinstall.lib.args import ArchConfig, ArchConfigHandler
from archinstall.lib.authentication.authentication_handler import AuthenticationHandler
from archinstall.lib.disk.filesystem import FilesystemHandler
from archinstall.lib.disk.utils import get_parent_device_path, get_unique_path_for_device, udev_sync
from archinstall.lib.hardware import SysInfo
from archinstall.lib.installer import Installer
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.models import Bootloader
from archinstall.lib.models.device import DiskLayoutType, EncryptionType
from archinstall.lib.models.users import User
from .ui import info
def load_arch_config(config_path: Path, creds_path: Path) -> ArchConfigHandler:
"""Build an ArchConfigHandler from on-disk JSON.
archinstall's ArchConfigHandler reads --config / --creds from sys.argv
via argparse at construction time (lib/args.py:_parse_args). It does
NOT consult env vars for these paths. We can't pass them on our real
argv because the wrapper strips them before exec'ing Python (so other
archinstall arg-parsing code doesn't choke on our flags), so we hand
archinstall a synthetic argv just for this call.
"""
import sys
saved_argv = sys.argv
sys.argv = [
saved_argv[0] if saved_argv else "omarchy-iso-install",
"--config", str(config_path),
"--creds", str(creds_path),
]
try:
return ArchConfigHandler()
finally:
sys.argv = saved_argv
def make_mirror_handler(offline: bool = True) -> MirrorListHandler:
return MirrorListHandler(offline=offline, verbose=False)
def perform_filesystem_operations(arch_config: ArchConfig) -> None:
"""Partition, format, encrypt. archinstall's FilesystemHandler is its own
object (separate from Installer) so we run it before opening the
Installer context manager.
parted's partition-table commit races udev: the disk wipe and the first
BLKPG partition registration trigger probes that briefly hold the disk
open, and the kernel then refuses to register the remaining partitions
(_ped.IOException: "Partition(s) ... have been written, but we have been
unable to inform the kernel"). The table is written correctly when that
happens — only the kernel's view is stale — so settle udev and redo the
operations. The wipe/partition/format sequence is idempotent."""
if not arch_config.disk_config:
raise RuntimeError("disk_config missing from arch config")
handler = FilesystemHandler(arch_config.disk_config)
# archinstall's perform_filesystem_operations defaults to a ~4.5s
# "5...4...3...2...1" countdown (FilesystemHandler._final_warning) before it
# wipes the disk. That warning is meant for its interactive TUI; in our
# orchestrated install it renders nowhere and just sleeps. Suppress it when
# the running archinstall still takes the flag — newer releases dropped both
# the countdown and the parameter, so only pass it when it's accepted.
fs_kwargs = (
{"show_countdown": False}
if _method_accepts(handler.perform_filesystem_operations, "show_countdown")
else {}
)
attempts = 3
for attempt in range(1, attempts + 1):
udev_sync()
try:
handler.perform_filesystem_operations(**fs_kwargs)
return
except Exception as exc:
if attempt == attempts or "unable to inform the kernel" not in str(exc):
raise
info(f"› partition commit lost a udev race (attempt {attempt}/{attempts}); retrying")
@contextmanager
def open_installer(
arch_config: ArchConfig,
mountpoint: Path,
silent: bool = True,
) -> Iterator[Installer]:
"""Yield an open Installer; ensures __exit__ runs even on exception so
/mnt is left clean for a retry."""
if not arch_config.disk_config:
raise RuntimeError("disk_config missing from arch config")
with Installer(
mountpoint,
arch_config.disk_config,
kernels=arch_config.kernels,
silent=silent,
) as installer:
yield installer
def is_encrypted(arch_config: ArchConfig) -> bool:
disk = arch_config.disk_config
if not disk or not disk.disk_encryption:
return False
return disk.disk_encryption.encryption_type != EncryptionType.NO_ENCRYPTION
def is_pre_mount(arch_config: ArchConfig) -> bool:
return bool(
arch_config.disk_config
and arch_config.disk_config.config_type == DiskLayoutType.Pre_mount
)
def bootloader_enabled(arch_config: ArchConfig) -> bool:
bl = arch_config.bootloader_config
return bool(bl and bl.bootloader != Bootloader.NO_BOOTLOADER)
def is_limine(arch_config: ArchConfig) -> bool:
bl = arch_config.bootloader_config
return bool(bl and bl.bootloader == Bootloader.Limine)
def has_uefi() -> bool:
return SysInfo.has_uefi()
def parent_device_path(dev_path: Path) -> Path:
return get_parent_device_path(dev_path)
def unique_device_path(dev_path: Path) -> Path | None:
return get_unique_path_for_device(dev_path)
def _application_handler():
"""Return archinstall's application handler across archinstall versions."""
module = importlib.import_module("archinstall.lib.applications.application_handler")
handler = getattr(module, "application_handler", None)
if handler is not None and callable(getattr(handler, "install_applications", None)):
return handler
handler_class = getattr(module, "ApplicationHandler", None)
if handler_class is None:
raise RuntimeError("archinstall application handler is unavailable")
try:
handler = handler_class()
except TypeError as exc:
raise RuntimeError("archinstall ApplicationHandler cannot be constructed") from exc
if not callable(getattr(handler, "install_applications", None)):
raise RuntimeError("archinstall ApplicationHandler has no install_applications method")
return handler
def _method_accepts(method, name: str) -> bool:
"""Return whether a bound archinstall method accepts a named argument.
Do not use inspect.signature here: Python 3.14 may evaluate archinstall's
lazy annotations, and some archinstall releases annotate with names that are
only imported under TYPE_CHECKING.
"""
fn = getattr(method, "__func__", method)
code = getattr(fn, "__code__", None)
if code is None:
return True
positional = code.co_varnames[:code.co_argcount]
kwonly = code.co_varnames[code.co_argcount:code.co_argcount + code.co_kwonlyargcount]
return name in (*positional, *kwonly)
def _method_accepts_users(method) -> bool:
return _method_accepts(method, "users")
def install_applications(installer: Installer, arch_config: ArchConfig) -> None:
"""Install archinstall application selections such as PipeWire audio.
The configurator still writes archinstall's audio_config. We own phase
ordering now, but should not silently drop archinstall's hardware-aware
application installers (SOF/ALSA firmware detection, PipeWire packages,
Bluetooth selections, etc.).
"""
app_config = arch_config.app_config
if not app_config:
return
users = arch_config.auth_config.users if arch_config.auth_config else None
handler = _application_handler()
install_applications_method = handler.install_applications
if _method_accepts_users(install_applications_method):
install_applications_method(installer, app_config, users)
else:
install_applications_method(installer, app_config)
def root_user(arch_config: ArchConfig) -> User | None:
auth = arch_config.auth_config
if not auth or not auth.root_enc_password:
return None
return User("root", auth.root_enc_password, False)
@@ -0,0 +1,112 @@
"""Install context: parsed configurator output, invocation paths, and a
mutable `state` dict for objects that live across phases (e.g., the
archinstall config handler and mirror list handler)."""
from __future__ import annotations
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
@dataclass
class InstallContext:
config_path: Path
creds_path: Path
full_name: str
email: str
encrypt: bool
user_configuration: dict
user_credentials: dict
arch_config_path: Path
omarchy_install: dict[str, Any]
target: Path = Path("/mnt")
omarchy_path: Path = Path("/usr/share/omarchy")
state_dir: Path = Path("/run/omarchy-install")
log_path: Path = Path("/var/log/omarchy-install.log")
target_log_path: Path = Path("/mnt/var/log/omarchy-install.log")
# Mutable per-run state shared across phases (e.g., 'arch_config_handler',
# 'mirror_handler'). Phases populate as needed; later phases read.
state: dict[str, Any] = field(default_factory=dict)
@classmethod
def from_env(cls) -> "InstallContext":
config_str = os.environ.get("OMARCHY_INSTALL_CONFIG")
creds_str = os.environ.get("OMARCHY_INSTALL_CREDS")
if not config_str or not creds_str:
raise RuntimeError("OMARCHY_INSTALL_CONFIG and OMARCHY_INSTALL_CREDS must be set")
config_path = Path(config_str)
creds_path = Path(creds_str)
user_configuration = json.loads(config_path.read_text())
omarchy_install = user_configuration.get("omarchy_install") or _default_omarchy_install(user_configuration)
arch_configuration = dict(user_configuration)
arch_configuration.pop("omarchy_install", None)
state_dir = Path(os.environ.get("OMARCHY_INSTALL_STATE_DIR", "/run/omarchy-install"))
state_dir.mkdir(parents=True, exist_ok=True)
arch_config_path = state_dir / "archinstall-user_configuration.json"
arch_config_path.write_text(json.dumps(arch_configuration, indent=2) + "\n")
ctx = cls(
config_path=config_path,
creds_path=creds_path,
full_name=_read_text(os.environ.get("OMARCHY_INSTALL_FULL_NAME_FILE")),
email=_read_text(os.environ.get("OMARCHY_INSTALL_EMAIL_FILE")),
encrypt=_read_text(os.environ.get("OMARCHY_INSTALL_ENCRYPT_FILE")).lower() in ("true", "yes", "1"),
user_configuration=user_configuration,
user_credentials=json.loads(creds_path.read_text()),
arch_config_path=arch_config_path,
omarchy_install=omarchy_install,
state_dir=state_dir,
)
disk_config = user_configuration.get("disk_config", {})
target_mount = omarchy_install.get("target_mount") or disk_config.get("mountpoint")
if target_mount:
ctx.target = Path(target_mount)
return ctx
@property
def username(self) -> str:
return self.user_credentials["users"][0]["username"]
@property
def mode(self) -> str:
if mode := self.omarchy_install.get("mode"):
return mode
cfg_type = self.user_configuration.get("disk_config", {}).get("config_type")
return "protected" if cfg_type == "pre_mounted_config" else "full_disk"
@property
def is_protected(self) -> bool:
return self.mode == "protected"
def _default_omarchy_install(user_configuration: dict) -> dict[str, Any]:
disk_config = user_configuration.get("disk_config", {})
mode = "protected" if disk_config.get("config_type") == "pre_mounted_config" else "full_disk"
return {
"mode": mode,
"target_mount": disk_config.get("mountpoint") or "/mnt",
"boot": {
"esp_mount": "/boot",
"esp_path": "/EFI/limine",
"efi_binary": "limine_x64.efi",
"enable_fallback": mode == "full_disk",
},
"storage": {},
}
def _read_text(path: str | None) -> str:
if not path:
return ""
p = Path(path)
if not p.exists():
return ""
return p.read_text().strip()
@@ -0,0 +1,95 @@
"""Omarchy install orchestrator.
Single tool that owns the full install phase ordering, with archinstall used as
a library subsystem (not as the top-level installer).
The live-ISO wrapper consumes CLI args and passes configuration paths via
OMARCHY_INSTALL_* environment variables before Python starts. This keeps
archinstall's import-time CLI parsing from seeing Omarchy-specific flags.
"""
from __future__ import annotations
import sys
from .context import InstallContext
from .phases import PhaseError, run
from .ui import error, info
def build_phases(ctx: InstallContext):
"""Phase order. Each entry is (display name, callable taking InstallContext).
The ordering is the whole point of this orchestrator: package-install
hooks (limine-mkinitcpio-hook, in particular) and useradd happen at
points where their prerequisites are guaranteed to be in place.
Full-disk and protected installs use the same phase sequence. The
configurator only changes the JSON input: full-disk asks archinstall to
create/mount the layout, while protected provides an already-mounted target
and the partition details Omarchy needs for boot/fstab generation.
"""
from .phases_impl import (
prepare_live,
prepare_install_target,
arch_install_system,
configure_hibernation,
run_system_finalizer,
finalize_limine_boot,
run_chroot_finalizer,
configure_dns_resolver,
configure_login,
validate_boot,
)
return [
("Preparing live environment", prepare_live),
("Preparing install target", prepare_install_target),
("Installing Arch + Omarchy", arch_install_system),
("Configuring hibernation", configure_hibernation),
("Configuring system", run_system_finalizer),
("Finalizing Limine boot", finalize_limine_boot),
("Finalizing user", run_chroot_finalizer),
("Configuring login", configure_login),
("Configuring DNS resolver", configure_dns_resolver),
("Validating boot setup", validate_boot),
]
def main() -> int:
try:
ctx = InstallContext.from_env()
except RuntimeError as e:
error(f"Configuration error: {e}")
return 2
info(f"Installing Omarchy for {ctx.username} → {ctx.target}")
from .phases_impl import (
cleanup_bind_mounts,
cleanup_protected_state,
cleanup_target_hook_masks,
)
success = False
try:
try:
run(ctx, build_phases(ctx))
success = True
except PhaseError:
error("Installation halted.")
return 1
except KeyboardInterrupt:
error("Installation interrupted.")
return 130
info("Installation complete.")
return 0
finally:
cleanup_bind_mounts(ctx)
cleanup_target_hook_masks(ctx)
if not success:
cleanup_protected_state(ctx)
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,78 @@
"""Phase state machine. Each phase is a (name, callable) pair; callables take
the InstallContext and either return cleanly or raise to abort the install."""
from __future__ import annotations
import json
import time
import traceback
from collections.abc import Callable
from pathlib import Path
from .context import InstallContext
from .ui import error, info
PhaseFn = Callable[[InstallContext], None]
class PhaseError(Exception):
"""Raised when a phase fails. Wrapped with the phase name."""
def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None:
ctx.state_dir.mkdir(parents=True, exist_ok=True)
state_path = ctx.state_dir / "state.json"
state = {
"started_at": time.time(),
"total_phases": len(phases),
"current_index": 0,
"current_phase": "Starting installation",
"phases": [],
}
_write_state(state_path, state)
for index, (name, fn) in enumerate(phases):
state["current_index"] = index
state["current_phase"] = name
state["phase_started_at"] = time.time()
_write_state(state_path, state)
info(f"› {name}")
started = time.time()
try:
fn(ctx)
except Exception as exc: # noqa: BLE001
elapsed = time.time() - started
state["phases"].append({
"name": name,
"status": "failed",
"elapsed": elapsed,
"error": str(exc),
})
_write_state(state_path, state)
error(f"Phase '{name}' failed after {elapsed:.1f}s: {exc}")
traceback.print_exc()
raise PhaseError(f"phase {name} failed: {exc}") from exc
elapsed = time.time() - started
state["phases"].append({"name": name, "status": "ok", "elapsed": elapsed})
_write_state(state_path, state)
state["current_index"] = max(len(phases) - 1, 0)
state["current_phase"] = "Installation complete"
state["finished_at"] = time.time()
_write_state(state_path, state)
timing_path = ctx.target / "var" / "log" / "omarchy-install-timing.json"
timing_path.parent.mkdir(parents=True, exist_ok=True)
_write_state(timing_path, state)
def _write_state(path: Path, state: dict) -> None:
# Dashboard polls this file while phases update it. Write atomically so the
# reader never observes a truncated/partial JSON document and resets UI.
tmp = path.with_name(f".{path.name}.tmp")
tmp.write_text(json.dumps(state, indent=2, default=str))
tmp.replace(path)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,28 @@
"""Orchestrator progress lines.
These land in the install log, not on a screen: omarchy-install-dashboard owns
the visible UI, captures the orchestrator's stdout into the support log, and
strips CSI sequences on the way in. The colouring was therefore never seen by
anyone, while gum charged a Go process start per line for it. Plain writes keep
the same indented shape at no cost.
Flush every line: pacstrap and arch-chroot inherit this fd, so buffering here
would reorder our lines against theirs in the log.
"""
from __future__ import annotations
import sys
def _emit(text: str) -> None:
sys.stdout.write(f"\n {text}\n")
sys.stdout.flush()
def info(text: str) -> None:
_emit(text)
def error(text: str) -> None:
_emit(text)
@@ -2,4 +2,4 @@ title Omarchy (x86_64, UEFI)
sort-key 01 sort-key 01
linux /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2 linux /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
initrd /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img initrd /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 initramfs_async=0
+2 -2
View File
@@ -54,13 +54,13 @@ timeout_style=hidden
menuentry "Omarchy (%ARCH%, ${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' { menuentry "Omarchy (%ARCH%, ${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' {
set gfxpayload=keep set gfxpayload=keep
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 initramfs_async=0
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
} }
menuentry "Omarchy with speakup screen reader (%ARCH%, ${archiso_platform})" --hotkey s --class arch --class gnu-linux --class gnu --class os --id 'archlinux-accessibility' { menuentry "Omarchy with speakup screen reader (%ARCH%, ${archiso_platform})" --hotkey s --class arch --class gnu-linux --class gnu --class os --id 'archlinux-accessibility' {
set gfxpayload=keep set gfxpayload=keep
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on xe.enable_panel_replay=0 linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on xe.enable_panel_replay=0 initramfs_async=0
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
} }
+2 -2
View File
@@ -30,13 +30,13 @@ timeout_style=hidden
menuentry "Omarchy (%ARCH%, ${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' { menuentry "Omarchy (%ARCH%, ${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' {
set gfxpayload=keep set gfxpayload=keep
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" quiet splash xe.enable_panel_replay=0 linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" quiet splash xe.enable_panel_replay=0 initramfs_async=0
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
} }
menuentry "Omarchy with speakup screen reader (%ARCH%, ${archiso_platform})" --hotkey s --class arch --class gnu-linux --class gnu --class os --id 'archlinux-accessibility' { menuentry "Omarchy with speakup screen reader (%ARCH%, ${archiso_platform})" --hotkey s --class arch --class gnu-linux --class gnu --class os --id 'archlinux-accessibility' {
set gfxpayload=keep set gfxpayload=keep
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" accessibility=on xe.enable_panel_replay=0 linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" accessibility=on xe.enable_panel_replay=0 initramfs_async=0
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
} }
+8 -1
View File
@@ -13,6 +13,13 @@ ParallelDownloads = 5
SigLevel = Required DatabaseOptional SigLevel = Required DatabaseOptional
LocalFileSigLevel = Optional LocalFileSigLevel = Optional
# No signature checks for the offline mirror: it ships inside the ISO, whose
# integrity is already covered by the airootfs sha512 and the ISO signature.
# "Optional TrustAll" is NOT a bypass — Optional verifies whenever a .sig file
# exists (they all do), and pacstrap verifies against the LIVE keyring (pacman
# -r relocates the root but not GpgDir). That made installs depend on archiso's
# slow boot-time pacman-init.service having finished, which stalled or failed
# pacstrap on real hardware with "required key missing from keyring".
[offline] [offline]
SigLevel = Optional TrustAll SigLevel = Never
Server = file:///var/cache/omarchy/mirror/offline/ Server = file:///var/cache/omarchy/mirror/offline/
+1
View File
@@ -30,5 +30,6 @@ SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/edge/$arch Server = https://pkgs.omarchy.org/edge/$arch
[arch-mact2] [arch-mact2]
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
SigLevel = Never SigLevel = Never
+1
View File
@@ -30,5 +30,6 @@ SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/edge/$arch Server = https://pkgs.omarchy.org/edge/$arch
[arch-mact2] [arch-mact2]
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
SigLevel = Never SigLevel = Never
+1
View File
@@ -30,5 +30,6 @@ SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/stable/$arch Server = https://pkgs.omarchy.org/stable/$arch
[arch-mact2] [arch-mact2]
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
SigLevel = Never SigLevel = Never
+21 -3
View File
@@ -12,15 +12,33 @@ bootmodes=('bios.syslinux' 'uefi.grub')
arch="x86_64" arch="x86_64"
pacman_conf="pacman-offline.conf" pacman_conf="pacman-offline.conf"
airootfs_image_type="squashfs" airootfs_image_type="squashfs"
airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M') # Package archives in the offline mirror are already zstd-compressed. Storing
# them in an outer stream saves little space but makes pacman decompress the
# outer layer while hashing and extracting every package during installation.
#
# Everything else in the live root is zstd rather than xz. Squashfs decompresses
# on the page-fault path through a single stream (CONFIG_SQUASHFS_DECOMP_SINGLE),
# where xz manages ~100MB/s against zstd's ~900MB/s, and the live root is read
# cold on every boot: kernel, plymouth, systemd, python, archinstall, gum. The
# whole ISO grows well under a percent for it, and dropping the x86 BCJ filter
# also removes one of the blockers listed in plans/aarch64-support.md.
airootfs_image_tool_options=(
'-comp' 'zstd'
'-Xcompression-level' '19'
'-b' '1M'
'-action' 'uncompressed@subpathname(var/cache/omarchy/mirror/offline)'
)
bootstrap_tarball_compression=('zstd' '-c' '-T0' '--auto-threads=logical' '--long' '-19') bootstrap_tarball_compression=('zstd' '-c' '-T0' '--auto-threads=logical' '--long' '-19')
file_permissions=( file_permissions=(
["/etc/shadow"]="0:0:400" ["/etc/shadow"]="0:0:400"
["/root"]="0:0:750" ["/root"]="0:0:750"
["/root/.automated_script.sh"]="0:0:755" ["/root/.automated_script.sh"]="0:0:755"
["/root/.gnupg"]="0:0:700" ["/root/.gnupg"]="0:0:700"
["/usr/local/bin/choose-mirror"]="0:0:755"
["/root/configurator"]="0:0:755" ["/root/configurator"]="0:0:755"
["/var/cache/omarchy/mirror/offline/"]="0:0:775" ["/usr/local/bin/choose-mirror"]="0:0:755"
["/usr/local/bin/omarchy-iso-cleanup-disk"]="0:0:755"
["/usr/local/bin/omarchy-install-dashboard"]="0:0:755"
["/usr/local/bin/omarchy-iso-install"]="0:0:755"
["/usr/local/bin/omarchy-upload-log"]="0:0:755" ["/usr/local/bin/omarchy-upload-log"]="0:0:755"
["/var/cache/omarchy/mirror/offline/"]="0:0:775"
) )
+3 -3
View File
@@ -6,7 +6,7 @@ ENDTEXT
MENU LABEL Omarchy install medium (x86_64, NBD) MENU LABEL Omarchy install medium (x86_64, NBD)
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2 LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
INITRD ::/%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img INITRD ::/%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% archiso_nbd_srv=${pxeserver} cms_verify=y APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% archiso_nbd_srv=${pxeserver} cms_verify=y initramfs_async=0
SYSAPPEND 3 SYSAPPEND 3
LABEL arch64_nfs LABEL arch64_nfs
@@ -17,7 +17,7 @@ ENDTEXT
MENU LABEL Omarchy install medium (x86_64, NFS) MENU LABEL Omarchy install medium (x86_64, NFS)
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2 LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
INITRD ::/%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img INITRD ::/%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
APPEND archisobasedir=%INSTALL_DIR% archiso_nfs_srv=${pxeserver}:/run/archiso/bootmnt cms_verify=y APPEND archisobasedir=%INSTALL_DIR% archiso_nfs_srv=${pxeserver}:/run/archiso/bootmnt cms_verify=y initramfs_async=0
SYSAPPEND 3 SYSAPPEND 3
LABEL arch64_http LABEL arch64_http
@@ -28,5 +28,5 @@ ENDTEXT
MENU LABEL Omarchy install medium (x86_64, HTTP) MENU LABEL Omarchy install medium (x86_64, HTTP)
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2 LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
INITRD ::/%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img INITRD ::/%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
APPEND archisobasedir=%INSTALL_DIR% archiso_http_srv=http://${pxeserver}/ cms_verify=y APPEND archisobasedir=%INSTALL_DIR% archiso_http_srv=http://${pxeserver}/ cms_verify=y initramfs_async=0
SYSAPPEND 3 SYSAPPEND 3
+2 -2
View File
@@ -6,7 +6,7 @@ ENDTEXT
MENU LABEL Omarchy install medium (x86_64, BIOS) MENU LABEL Omarchy install medium (x86_64, BIOS)
LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2 LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
INITRD /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img INITRD /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 initramfs_async=0
# Accessibility boot option # Accessibility boot option
LABEL arch64speech LABEL arch64speech
@@ -17,4 +17,4 @@ ENDTEXT
MENU LABEL Omarchy install medium (x86_64, BIOS) with ^speech MENU LABEL Omarchy install medium (x86_64, BIOS) with ^speech
LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2 LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
INITRD /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img INITRD /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on xe.enable_panel_replay=0 APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on xe.enable_panel_replay=0 initramfs_async=0
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+6 -6
View File
@@ -16,7 +16,7 @@ These exist outside omarchy-iso. Flagged for visibility — without them, nothin
1. **aarch64 base packages must exist somewhere we can pacman from.** Vanilla Arch (`geo.mirror.pkgbuild.com`) is x86_64-only — there is no `core/os/aarch64`. The realistic source is **Arch Linux ARM** (`mirror.archlinuxarm.org`) for `core`/`extra`/`alarm`/`aur`. Decision required: target Arch Linux ARM as the aarch64 base distribution. 1. **aarch64 base packages must exist somewhere we can pacman from.** Vanilla Arch (`geo.mirror.pkgbuild.com`) is x86_64-only — there is no `core/os/aarch64`. The realistic source is **Arch Linux ARM** (`mirror.archlinuxarm.org`) for `core`/`extra`/`alarm`/`aur`. Decision required: target Arch Linux ARM as the aarch64 base distribution.
2. **`pkgs.omarchy.org/{stable,edge}/aarch64/`** must serve a real repo. Probed today, both return 404. omarchy-pkgs already has multi-arch build support per its README, so this is a publish step, not a port. 2. **`pkgs.omarchy.org/{stable,edge}/aarch64/`** must serve a real repo. Probed today, both return 404. omarchy-pkgs already has multi-arch build support per its README, so this is a publish step, not a port.
3. **Omarchy installer guard** at `~/.local/share/omarchy/install/preflight/guard.sh:25` hard-aborts on `uname -m != x86_64`. Must be relaxed to allow aarch64 (or scoped behind a flag). 3. **Omarchy ISO architecture guard** must allow supported architectures or scope any x86_64-only checks behind an explicit guard.
4. **archinstall + Limine** must work end-to-end on aarch64. archinstall supports it; Limine supports aarch64 UEFI. Worth a manual verification before committing to this bootloader path on ARM. 4. **archinstall + Limine** must work end-to-end on aarch64. archinstall supports it; Limine supports aarch64 UEFI. Worth a manual verification before committing to this bootloader path on ARM.
--- ---
@@ -76,9 +76,9 @@ aarch64 has no BIOS — only UEFI. Drop the syslinux path entirely on ARM.
The cleanest cut: keep one `grub.cfg` shared, strip the x86 shell/memtest fragments at build time when `OMARCHY_ARCH=aarch64`. Don't fork the file. The cleanest cut: keep one `grub.cfg` shared, strip the x86 shell/memtest fragments at build time when `OMARCHY_ARCH=aarch64`. Don't fork the file.
### 6. mkinitcpio preset — `configs/airootfs/etc/mkinitcpio.d/linux.preset` ### 6. mkinitcpio preset — `configs/airootfs/etc/mkinitcpio.d/linux-t2.preset`
References `vmlinuz-linux-t2` and `initramfs-linux-t2.img` (lines 5, 8). For aarch64, swap to `vmlinuz-linux` / `initramfs-linux.img`. Generate the file at build time from a template, keyed on arch. Names `vmlinuz-linux-t2` and `initramfs-linux-t2.img`. The filename is the pkgbase and the alpm hook keys off it, so aarch64 does not rename this file — it simply does not ship it, and releng's own `linux.preset` covers the stock kernel. Drop `linux-t2` from `arch_packages` on aarch64 and the boot entries point at `vmlinuz-linux` / `initramfs-linux.img`.
### 7. Configurator — `configs/airootfs/root/configurator` ### 7. Configurator — `configs/airootfs/root/configurator`
@@ -109,9 +109,9 @@ Both scripts hardcode the OVMF path; small, mechanical fix.
### 10. Release script — `bin/omarchy-iso-release:52` ### 10. Release script — `bin/omarchy-iso-release:52`
```bash ```bash
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-"$INSTALLER_REF".iso | head -n1) latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*${OMARCHY_ARCH}-"$ISO_REF".iso | head -n1)
``` ```
Hardcoded `x86_64` glob. Take an arch arg, or do `*${OMARCHY_ARCH}-${INSTALLER_REF}.iso`. The ISO filename already contains arch (set by archiso from `profiledef.sh`), so this just needs the glob parameterized. Hardcoded `x86_64` glob. Take an arch arg, or do `*${OMARCHY_ARCH}-${ISO_REF}.iso`. The ISO filename already contains arch (set by archiso from `profiledef.sh`), so this just needs the glob parameterized.
### 11. CI — `.github/workflows/nightly-build.yml` ### 11. CI — `.github/workflows/nightly-build.yml`
@@ -145,7 +145,7 @@ Code:
- `configs/efiboot/loader/loader.conf` — per-arch default entry - `configs/efiboot/loader/loader.conf` — per-arch default entry
- `configs/efiboot/loader/entries/01-archiso-x86_64-linux.conf` — generate aarch64 sibling at build time - `configs/efiboot/loader/entries/01-archiso-x86_64-linux.conf` — generate aarch64 sibling at build time
- `configs/grub/grub.cfg`, `loopback.cfg` — strip x86-only fragments on aarch64 - `configs/grub/grub.cfg`, `loopback.cfg` — strip x86-only fragments on aarch64
- `configs/airootfs/etc/mkinitcpio.d/linux.preset` — template by arch (drop linux-t2 on aarch64) - `configs/airootfs/etc/mkinitcpio.d/linux-t2.preset` — omit on aarch64 (drop linux-t2 there; releng's linux.preset covers the stock kernel)
- `configs/airootfs/root/configurator` — branch archinstall JSON mirror list on `uname -m`; optionally guard lspci T2 probe - `configs/airootfs/root/configurator` — branch archinstall JSON mirror list on `uname -m`; optionally guard lspci T2 probe
- `.github/workflows/nightly-build.yml` — matrix x86_64/aarch64 - `.github/workflows/nightly-build.yml` — matrix x86_64/aarch64
+1 -1
View File
@@ -60,7 +60,7 @@ Create a second ISO flavor for OEM preloading that is installed to internal stor
- `grub/loopback.cfg`: remove `archlinux-accessibility` menuentry. - `grub/loopback.cfg`: remove `archlinux-accessibility` menuentry.
- `syslinux/archiso_sys-linux.cfg`: remove `arch64speech` label block. - `syslinux/archiso_sys-linux.cfg`: remove `arch64speech` label block.
- When `OMARCHY_MODE=oem`, override `iso_application` in `profiledef.sh` to `"Omarchy OEM Installer"`. - When `OMARCHY_MODE=oem`, override `iso_application` in `profiledef.sh` to `"Omarchy OEM Installer"`.
- Rename output ISO with `-oem` suffix (in addition to existing `-$OMARCHY_INSTALLER_REF` suffix). - Rename output ISO with `-oem` suffix (in addition to existing `-$OMARCHY_ISO_REF` suffix).
### Hardening ### Hardening
- Add post-injection build assertion that fails build if any expected boot config does not contain `copytoram=y`. - Add post-injection build assertion that fails build if any expected boot config does not contain `copytoram=y`.