Compare commits
204
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0631c0530a | ||
|
|
ed710a3b41 | ||
|
|
89245f44ec | ||
|
|
e47ce379ad | ||
|
|
a8c3a671ea | ||
|
|
c2b93afe01 | ||
|
|
436f37f926 | ||
|
|
529001d566 | ||
|
|
78824fe084 | ||
|
|
8653d07e06 | ||
|
|
5e80c917ea | ||
|
|
f47785a196 | ||
|
|
6ac95226af | ||
|
|
f97a664ea3 | ||
|
|
338e687f32 | ||
|
|
4e8ee7da9b | ||
|
|
ca79d0f4dc | ||
|
|
463f862b6c | ||
|
|
3544261e8c | ||
|
|
a274a2c56e | ||
|
|
fa54a8b2dc | ||
|
|
a27385483f | ||
|
|
01a5203009 | ||
|
|
a72a109e7a | ||
|
|
3c98b0e10c | ||
|
|
9d4b155e22 | ||
|
|
04aafcb781 | ||
|
|
c3f9743847 | ||
|
|
434db24b81 | ||
|
|
969d461eb9 | ||
|
|
4583a0e5e6 | ||
|
|
f79578f2db | ||
|
|
4e3e82922d | ||
|
|
59ffc3321e | ||
|
|
1463936de3 | ||
|
|
f6775344f6 | ||
|
|
49dcd808aa | ||
|
|
c2a58526f4 | ||
|
|
a5e605af4c | ||
|
|
8c1e143686 | ||
|
|
4ad8a22abe | ||
|
|
764a65f7ee | ||
|
|
c167bf9c1d | ||
|
|
f87b08bba7 | ||
|
|
588973b0cd | ||
|
|
c77a7b555b | ||
|
|
08b7460900 | ||
|
|
341bb84093 | ||
|
|
c303a9f746 | ||
|
|
abbd9eda34 | ||
|
|
c8a0072403 | ||
|
|
ff6c9582ca | ||
|
|
94c779d028 | ||
|
|
d44bbc5e5f | ||
|
|
d51a280636 | ||
|
|
eadb117560 | ||
|
|
2ed796535d | ||
|
|
7e830d6a28 | ||
|
|
8c77631f68 | ||
|
|
58221e5ed6 | ||
|
|
b1c09fa1a8 | ||
|
|
ef3b0e4b61 | ||
|
|
b280cbcc42 | ||
|
|
ce9e648ade | ||
|
|
239c8e7f64 | ||
|
|
afa8293b2b | ||
|
|
c50d0bc299 | ||
|
|
660edcb3af | ||
|
|
3a51e875b6 | ||
|
|
c7b8d4b584 | ||
|
|
38f56103e1 | ||
|
|
b16303f294 | ||
|
|
6232620806 | ||
|
|
8d1d3c906d | ||
|
|
fd12ace8bc | ||
|
|
1d656af51d | ||
|
|
af4679b153 | ||
|
|
c43bad26b5 | ||
|
|
7d7f6451a3 | ||
|
|
0890472c9b | ||
|
|
1162d111bd | ||
|
|
ca55321621 | ||
|
|
0734e982ca | ||
|
|
61f84a95cf | ||
|
|
63ba457034 | ||
|
|
337ba2e590 | ||
|
|
de0f013b5e | ||
|
|
948a53cc0a | ||
|
|
cab315bb8a | ||
|
|
2178c26a92 | ||
|
|
f3973abb4c | ||
|
|
3e0cd31c2b | ||
|
|
7e79355c71 | ||
|
|
d7e4233042 | ||
|
|
5ad90afbae | ||
|
|
c2396fb447 | ||
|
|
9ad189ef82 | ||
|
|
1e26d18031 | ||
|
|
6decb2f683 | ||
|
|
3420bc9aa8 | ||
|
|
a21ab8bcb1 | ||
|
|
c5baf2c791 | ||
|
|
ec218bade6 | ||
|
|
c179d78814 | ||
|
|
3a91cf780c | ||
|
|
39113a2d2b | ||
|
|
16cc1058be | ||
|
|
8734136908 | ||
|
|
a18d4651a3 | ||
|
|
a5a7110bea | ||
|
|
34c0a14766 | ||
|
|
be721e17f4 | ||
|
|
21dbbcb917 | ||
|
|
260edc13c5 | ||
|
|
e049f28bfa | ||
|
|
ed7c62febe | ||
|
|
b568b0b796 | ||
|
|
c1828b13ad | ||
|
|
22f94e6da6 | ||
|
|
8dad5a7436 | ||
|
|
0477275d08 | ||
|
|
13e0201e9b | ||
|
|
9c6d14a80b | ||
|
|
05a1c6f271 | ||
|
|
26e6a78d3a | ||
|
|
7a56a9c1e6 | ||
|
|
b36280f1a6 | ||
|
|
064b5e2393 | ||
|
|
6d6b516221 | ||
|
|
26f87f6789 | ||
|
|
1742e6d997 | ||
|
|
1daa755b70 | ||
|
|
89cc55d559 | ||
|
|
0c63044066 | ||
|
|
f8010a355a | ||
|
|
ca2b58fa4d | ||
|
|
53bc2550f9 | ||
|
|
874e6cd24d | ||
|
|
cf9f2745f3 | ||
|
|
01b932023a | ||
|
|
a4f5c1bf14 | ||
|
|
8982984f40 | ||
|
|
a74d20f8fb | ||
|
|
1224531ce1 | ||
|
|
8b0fd93011 | ||
|
|
b34f53629d | ||
|
|
e427304f29 | ||
|
|
b8df9d3f50 | ||
|
|
9f4e5feef0 | ||
|
|
023cd14f2a | ||
|
|
80042969ed | ||
|
|
6c00d03272 | ||
|
|
5925634c74 | ||
|
|
eb9112ae45 | ||
|
|
5297020040 | ||
|
|
01f393f10d | ||
|
|
7b42d8011e | ||
|
|
2d8ed3d1c3 | ||
|
|
7a8d9708e3 | ||
|
|
eadb1c05d4 | ||
|
|
94494c8837 | ||
|
|
ed4536cab4 | ||
|
|
dc8242c8c7 | ||
|
|
c43841c827 | ||
|
|
b341fa6eea | ||
|
|
f73062e156 | ||
|
|
0ace44e96e | ||
|
|
78af4402e4 | ||
|
|
6aff2b3e20 | ||
|
|
772d4e2813 | ||
|
|
0cbe625343 | ||
|
|
5756f35844 | ||
|
|
83a54e7fd5 | ||
|
|
716e118c81 | ||
|
|
9cf9cd1612 | ||
|
|
fba9fb3758 | ||
|
|
72b7a1f245 | ||
|
|
1f465cfeac | ||
|
|
6cd3702c27 | ||
|
|
bba89468f1 | ||
|
|
5ba771dc1b | ||
|
|
54991588e9 | ||
|
|
9d9a69e0c4 | ||
|
|
a202c81a0c | ||
|
|
e5152cd7b2 | ||
|
|
e3cb89d712 | ||
|
|
4188411b9b | ||
|
|
26805d9dd2 | ||
|
|
1a886cb5de | ||
|
|
a1ce189cbb | ||
|
|
4ded01b207 | ||
|
|
872c8fb03f | ||
|
|
5c2d06c5fc | ||
|
|
cab0c5b50a | ||
|
|
35036bdd62 | ||
|
|
3afa6b63ff | ||
|
|
9e2d0ac204 | ||
|
|
c9f7b80d3d | ||
|
|
09913880e6 | ||
|
|
aa75d85d14 | ||
|
|
ec1405625f | ||
|
|
82f854054e | ||
|
|
27b332c64f | ||
|
|
880d8c57af |
No files matched your search
@@ -1,2 +1,9 @@
|
||||
release/**
|
||||
vm-saves/**
|
||||
|
||||
# Python bytecode (orchestrator)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Acceptance test harness artifacts
|
||||
test-runs/**
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -8,32 +8,52 @@ See the ISO link on [omarchy.org](https://omarchy.org).
|
||||
|
||||
## Creating the ISO
|
||||
|
||||
Run `./bin/omarchy-iso-make` and the output goes into `./release`.
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Run `./bin/omarchy-iso-sign [gpg-user] [release/omarchy.iso]`.
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Run `./bin/omarchy-iso-release` to create, test, sign, and upload the ISO in one flow.
|
||||
Run `./bin/omarchy-iso-release VERSION` to create, test, sign, and upload the ISO in one flow. Add `--rc` to release an RC build instead.
|
||||
+1
-1
Submodule archiso updated: 69047bdc03...424e78130d.
+148
-16
@@ -2,36 +2,168 @@
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: omarchy-iso-boot [release/omarchy.iso] [reuse]"
|
||||
echo " SSH available at: ssh -p 2222 root@localhost (after installing openssh)"
|
||||
ISO=""
|
||||
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=()
|
||||
|
||||
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")
|
||||
if gum confirm --default=false "Reuse existing disk?"; then
|
||||
REUSE="reuse"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z $ISO ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! pacman -Q qemu-full >/dev/null || ! pacman -Q edk2-ovmf >/dev/null; then
|
||||
sudo pacman -S --noconfirm --needed --quiet qemu-full edk2-ovmf
|
||||
if [[ -z $DISK || -z $OVMF_VARS ]]; then
|
||||
echo "Disk and OVMF vars paths must not be empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DISK="/tmp/omarchy-iso-boot.qcow2"
|
||||
if [[ ! -f $DISK || $2 != "reuse" ]]; then
|
||||
qemu-img create -f qcow2 "$DISK" 20G
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd /tmp/OVMF_VARS.4m.fd
|
||||
omarchy-pkg-add qemu-full edk2-ovmf
|
||||
|
||||
if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
|
||||
while IFS='=' read -r name value; do
|
||||
case "$name" in
|
||||
WAYLAND_DISPLAY|DISPLAY|XAUTHORITY|XDG_SESSION_TYPE)
|
||||
export "$name=$value"
|
||||
;;
|
||||
esac
|
||||
done < <(systemctl --user show-environment 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
|
||||
echo "Unable to find a graphical session for QEMU." >&2
|
||||
echo "Run omarchy-iso-boot from the Omarchy desktop session." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $DISK || $REUSE != "reuse" ]]; then
|
||||
qemu-img create -f qcow2 "$DISK" 40G
|
||||
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
|
||||
|
||||
# Boot from ISO before we've installed, then boot from drive after
|
||||
qemu-system-x86_64 \
|
||||
-cpu host -enable-kvm -machine q35,accel=kvm \
|
||||
-smp "$(nproc)" \
|
||||
-smp "$(($(nproc) < 8 ? $(nproc) : 8))" \
|
||||
-m 8192 \
|
||||
-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 \
|
||||
-device virtio-blk-pci,drive=drive0,bootindex=1 \
|
||||
-drive file="$1",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 virtio-vga-gl \
|
||||
-display gtk,gl=on \
|
||||
-display sdl,gl=on \
|
||||
-usb -device usb-tablet \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
|
||||
-device virtio-net-pci,netdev=net0 \
|
||||
-boot menu=on
|
||||
-serial none \
|
||||
"${NET_ARGS[@]}" \
|
||||
-boot menu=on \
|
||||
"${EXTRA_QEMU_ARGS[@]}"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
if [[ -z "${OMARCHY_PATH:-}" ]]; then
|
||||
for candidate in "$repo_root/../omarchy-installer" "$repo_root/../omarchy" /usr/share/omarchy; do
|
||||
if [[ -f "$candidate/logo.txt" ]]; then
|
||||
export OMARCHY_PATH="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
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 "$@"
|
||||
Executable
+96
@@ -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"
|
||||
+99
-21
@@ -1,10 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Abort if anything fails
|
||||
set -e
|
||||
|
||||
# Parse command line arguments
|
||||
NO_CACHE=""
|
||||
LOCAL_OMARCHY_PATH=""
|
||||
LOCAL_PKGS_PATH=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--no-cache)
|
||||
@@ -15,31 +15,108 @@ while [[ $# -gt 0 ]]; do
|
||||
NO_BOOT_OFFER=1
|
||||
shift
|
||||
;;
|
||||
--debug)
|
||||
OMARCHY_INSTALL_DEBUG=1
|
||||
shift
|
||||
;;
|
||||
--edge)
|
||||
OMARCHY_ISO_REF=edge
|
||||
OMARCHY_MIRROR=edge
|
||||
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)
|
||||
OMARCHY_ISO_REF=dev
|
||||
OMARCHY_MIRROR=edge
|
||||
shift
|
||||
;;
|
||||
--quattro)
|
||||
OMARCHY_INSTALLER_REF=quattro
|
||||
OMARCHY_MIRROR=edge
|
||||
shift
|
||||
;;
|
||||
--rc)
|
||||
OMARCHY_ISO_REF=rc
|
||||
OMARCHY_MIRROR=rc
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: $0 [--no-cache] [--no-boot-offer]"
|
||||
echo "Usage: $0 [--no-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--local-source <omarchy-checkout> <pkgs-checkout>]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Ensure we have the submodule checked out
|
||||
if [[ -n $LOCAL_OMARCHY_PATH ]]; then
|
||||
[[ -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
|
||||
|
||||
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
|
||||
|
||||
# Prepare build repo
|
||||
BUILD_SCRIPT="builder/build-iso.sh"
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
BUILD_RELEASE_PATH="$BUILD_ROOT/release"
|
||||
mkdir -p "$BUILD_RELEASE_PATH"
|
||||
|
||||
OMARCHY_INSTALLER_REPO="${OMARCHY_INSTALLER_REPO:-basecamp/omarchy}"
|
||||
OMARCHY_INSTALLER_REF="${OMARCHY_INSTALLER_REF:-master}"
|
||||
OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
|
||||
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
|
||||
|
||||
DOCKER_ARGS=(
|
||||
--rm
|
||||
--privileged
|
||||
-e "OMARCHY_INSTALLER_REPO=$OMARCHY_INSTALLER_REPO"
|
||||
-e "OMARCHY_INSTALLER_REF=$OMARCHY_INSTALLER_REF"
|
||||
-e "OMARCHY_ISO_REF=$OMARCHY_ISO_REF"
|
||||
-e "OMARCHY_MIRROR=$OMARCHY_MIRROR"
|
||||
-e "OMARCHY_INSTALL_DEBUG=${OMARCHY_INSTALL_DEBUG:-}"
|
||||
-e "HOST_UID=$(id -u)"
|
||||
-e "HOST_GID=$(id -g)"
|
||||
-v "$BUILD_RELEASE_PATH/:/out/"
|
||||
@@ -49,31 +126,32 @@ DOCKER_ARGS=(
|
||||
-v "$BUILD_ROOT/configs:/configs:ro"
|
||||
)
|
||||
|
||||
# Use local pacman cache if you already have one on host to speed up repeat runs
|
||||
if [ -d "/var/cache/pacman/pkg" ]; then
|
||||
if [[ -n $LOCAL_OMARCHY_PATH ]]; then
|
||||
DOCKER_ARGS+=(
|
||||
-v "$LOCAL_OMARCHY_PATH:/omarchy-source:ro"
|
||||
-v "$LOCAL_PKGS_PATH:/omarchy-pkgs:ro"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -d /var/cache/pacman/pkg ]]; then
|
||||
DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg")
|
||||
fi
|
||||
|
||||
# Mount the build cache directory where packages are downloaded
|
||||
# Mount the build cache directory where packages are downloaded. Channels use
|
||||
# different package snapshots and must never share an offline mirror cache.
|
||||
if [[ -z "$NO_CACHE" ]]; then
|
||||
# using a dated dir on the host to refresh daily if building multiple times
|
||||
BUILD_DATE=$(date +%Y-%m-%d)
|
||||
OFFLINE_REPO_BUILD_CACHE_DIR="$HOME/.cache/omarchy/iso_${BUILD_DATE}/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"
|
||||
# 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")
|
||||
fi
|
||||
|
||||
# Run the builder with assembled args
|
||||
docker run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
|
||||
|
||||
# Move as install ref
|
||||
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"
|
||||
|
||||
# Offer to boot the new build
|
||||
if [[ -z "$NO_BOOT_OFFER" ]]; then
|
||||
if [[ -z "${NO_BOOT_OFFER:-}" ]]; then
|
||||
echo
|
||||
gum confirm "Boot $iso_ref?" && omarchy-iso-boot "$iso_ref"
|
||||
fi
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
OMARCHY_INSTALLER_REF=dev omarchy-iso-make
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
omarchy-pkg-add rclone
|
||||
mkdir -p ~/.config/rclone
|
||||
|
||||
access_key=$(op read "op://Shared/Cloudflare Buckets/iso.omarchy.org/access_key_id" --account Omarchy)
|
||||
secret_access_key=$(op read "op://Shared/Cloudflare Buckets/iso.omarchy.org/secret_access_key" --account Omarchy)
|
||||
endpoint=$(op read "op://Shared/Cloudflare Buckets/iso.omarchy.org/endpoint" --account Omarchy)
|
||||
|
||||
cat > ~/.config/rclone/rclone.conf << EOF
|
||||
[Omarchy]
|
||||
type = s3
|
||||
provider = Cloudflare
|
||||
access_key_id = $access_key
|
||||
secret_access_key = $secret_access_key
|
||||
endpoint = $endpoint
|
||||
EOF
|
||||
+48
-24
@@ -1,38 +1,62 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: omarchy-iso-release [version]"
|
||||
MAKE_ARGS=()
|
||||
ISO_REF=quattro
|
||||
RELEASE_SUFFIX=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--no-make)
|
||||
MAKE_ARGS+=("$1")
|
||||
shift
|
||||
;;
|
||||
--rc)
|
||||
MAKE_ARGS+=("$1")
|
||||
ISO_REF=rc
|
||||
RELEASE_SUFFIX="-rc"
|
||||
shift
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: omarchy-iso-release [--no-make] [--rc] [version]"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [[ -n "${BUILD_VERSION-}" ]]; then
|
||||
echo "Usage: omarchy-iso-release [--no-make] [--rc] [version]"
|
||||
exit 1
|
||||
fi
|
||||
BUILD_VERSION="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "${BUILD_VERSION-}" ]]; then
|
||||
echo "Usage: omarchy-iso-release [--no-make] [--rc] [version]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
|
||||
BUILD_RELEASE_PATH="$BUILD_ROOT/release"
|
||||
BULD_VERSION="$1"
|
||||
|
||||
# Make ISOs
|
||||
if [[ $1 != "--no-make" ]]; then
|
||||
echo -e "\e[32mMaking master ISO\e[0m"
|
||||
OMARCHY_INSTALLER_REF=master omarchy-iso-make --no-cache --no-boot-offer
|
||||
|
||||
echo -e "\e[32m\nMaking dev ISO\e[0m"
|
||||
|
||||
OMARCHY_INSTALLER_REF=dev omarchy-iso-make --no-cache --no-boot-offer
|
||||
if [[ ! " ${MAKE_ARGS[*]} " =~ " --no-make " ]]; then
|
||||
echo -e "\e[32mMaking $ISO_REF ISO\e[0m"
|
||||
omarchy-iso-make --no-cache --no-boot-offer "${MAKE_ARGS[@]}"
|
||||
else
|
||||
echo -e "\e[31mSkipping remaking ISOs\e[0m"
|
||||
fi
|
||||
|
||||
# Sign and upload master ISO
|
||||
latest_master_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-master.iso | head -n1)
|
||||
release_master_iso="$BUILD_RELEASE_PATH/omarchy-$BULD_VERSION.iso"
|
||||
cp "$latest_master_iso" "$release_master_iso"
|
||||
echo -e "\e[32mSigning master ISO\e[0m"
|
||||
omarchy-iso-sign "$release_master_iso"
|
||||
omarchy-iso-upload "$release_master_iso"
|
||||
# Sign and upload ISO
|
||||
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"
|
||||
cp "$latest_iso" "$release_iso"
|
||||
|
||||
# Sign and upload dev ISO
|
||||
latest_dev_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-dev.iso | head -n1)
|
||||
release_dev_iso="$BUILD_RELEASE_PATH/omarchy-$BULD_VERSION-dev.iso"
|
||||
cp "$latest_dev_iso" "$release_dev_iso"
|
||||
echo -e "\e[32mSigning dev ISO\e[0m"
|
||||
omarchy-iso-sign "$release_dev_iso"
|
||||
omarchy-iso-upload "$release_dev_iso"
|
||||
echo -e "\e[32mSigning $ISO_REF ISO\e[0m"
|
||||
omarchy-iso-sign "$release_iso"
|
||||
|
||||
iso_sha=$(sha256sum "$release_iso")
|
||||
echo -e "\e[32mSHA256 for $ISO_REF ISO: $iso_sha\e[0m"
|
||||
|
||||
omarchy-iso-upload "$release_iso"
|
||||
+40
-6
@@ -1,14 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "usage: omarchy-iso-sign [release/omarchy.iso]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! pacman -Q gnupg >/dev/null; then
|
||||
sudo pacman -S --noconfirm --needed --quiet gnupg
|
||||
echo "you'll need to generate a new key with: gpg --full-generate-key"
|
||||
iso="$1"
|
||||
key_ref="op://Shared/Omarchy GPG Private Key/omarchy-private.asc"
|
||||
|
||||
if [[ ! -f $iso ]]; then
|
||||
echo "ISO not found: $iso" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gnupg_user_key=$(gpg --list-secret-keys --keyid-format=long | awk '/^[[:space:]]/{print $1; exit}')
|
||||
gpg --local-user "$gnupg_user_key" --output $1.sig --detach-sig $1
|
||||
if ! command -v op >/dev/null; then
|
||||
echo "1Password CLI is required to retrieve the signing key." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! pacman -Q gnupg >/dev/null 2>&1; then
|
||||
sudo pacman -S --noconfirm --needed --quiet gnupg
|
||||
fi
|
||||
|
||||
gnupg_home="$(mktemp -d)"
|
||||
chmod 700 "$gnupg_home"
|
||||
trap 'rm -rf "$gnupg_home"' EXIT
|
||||
|
||||
op read "$key_ref" --account Omarchy |
|
||||
gpg --batch --homedir "$gnupg_home" --import >/dev/null
|
||||
|
||||
signing_key="$(
|
||||
gpg --batch --homedir "$gnupg_home" --with-colons --list-secret-keys |
|
||||
awk -F: '$1 == "fpr" { print $10; exit }'
|
||||
)"
|
||||
|
||||
if [[ -z $signing_key ]]; then
|
||||
echo "No private signing key found in $key_ref" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gpg --batch --yes \
|
||||
--homedir "$gnupg_home" \
|
||||
--local-user "$signing_key" \
|
||||
--output "$iso.sig" \
|
||||
--detach-sig "$iso"
|
||||
Executable
+914
@@ -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
|
||||
Executable
+295
@@ -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
|
||||
@@ -6,7 +6,7 @@ if [ $# -eq 0 ]; then
|
||||
fi
|
||||
|
||||
if [[ ! -f ~/.config/rclone/rclone.conf ]]; then
|
||||
echo "You must first configure rclone with the R2 credentials: rclone config"
|
||||
echo "You must first configure rclone with the R2 credentials: omarchy-iso-rclone-config"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
+68
-15
@@ -4,8 +4,8 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VM_DIR="$(dirname "$SCRIPT_DIR")/vm-saves"
|
||||
TMP_DISK="/tmp/omarchy-iso-boot.qcow2"
|
||||
TMP_OVMF="/tmp/OVMF_VARS.4m.fd"
|
||||
TMP_DISK="${OMARCHY_VM_DISK:-/tmp/omarchy-iso-boot.qcow2}"
|
||||
TMP_OVMF="${OMARCHY_VM_OVMF_VARS:-/tmp/OVMF_VARS.4m.fd}"
|
||||
|
||||
mkdir -p "$VM_DIR"
|
||||
|
||||
@@ -18,7 +18,7 @@ save_vm() {
|
||||
fi
|
||||
|
||||
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]"
|
||||
exit 1
|
||||
fi
|
||||
@@ -53,7 +53,44 @@ list_vms() {
|
||||
}
|
||||
|
||||
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
|
||||
echo "No VM snapshots available"
|
||||
@@ -73,25 +110,34 @@ boot_vm() {
|
||||
exit 1
|
||||
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
|
||||
fi
|
||||
|
||||
echo "Copying VM '$name' to /tmp..."
|
||||
cp "$vm_path/omarchy-iso-boot.qcow2" "$TMP_DISK"
|
||||
cp "$vm_path/OVMF_VARS.4m.fd" "$TMP_OVMF"
|
||||
echo "Copying VM '$name' to $active_disk..."
|
||||
mkdir -p "$(dirname "$active_disk")" "$(dirname "$active_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..."
|
||||
|
||||
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_vm "$2"
|
||||
save_vm "${1:-}"
|
||||
;;
|
||||
boot)
|
||||
boot_vm "$2"
|
||||
boot_vm "$@"
|
||||
;;
|
||||
list)
|
||||
list_vms
|
||||
@@ -101,9 +147,16 @@ case "${1:-}" in
|
||||
"$(gum style --bold 'omarchy-vm') - VM snapshot manager" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Commands:')" \
|
||||
" save [name] Save current VM from /tmp" \
|
||||
" boot [name] Boot saved VM (interactive if no name)" \
|
||||
" list List available VMs" \
|
||||
" save [name] Save current active VM" \
|
||||
" boot [name] [boot-options] Boot saved VM (interactive if no name)" \
|
||||
" 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:')"
|
||||
list_vms 2>/dev/null || echo " None"
|
||||
|
||||
@@ -7,6 +7,7 @@ intel-ucode
|
||||
linux-firmware
|
||||
linux
|
||||
limine
|
||||
omarchy-keyring
|
||||
openssh
|
||||
pipewire
|
||||
snapper
|
||||
|
||||
+225
-57
@@ -2,90 +2,258 @@
|
||||
|
||||
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 --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
|
||||
|
||||
# Setup build locations
|
||||
build_cache_dir="/var/cache"
|
||||
# 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 --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571
|
||||
|
||||
# omarchy-keyring is needed inside the offline mirror too.
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Sy omarchy-keyring
|
||||
pacman-key --populate omarchy
|
||||
|
||||
# Append the [omarchy] repo to the container's /etc/pacman.conf so subsequent
|
||||
# tools (notably makepkg in build-omarchy-packages.sh) can resolve omarchy-
|
||||
# only build deps like limine-snapper-sync and limine-mkinitcpio-hook.
|
||||
if ! grep -q '^\[omarchy\]' /etc/pacman.conf; then
|
||||
awk '/^\[omarchy\]/,/^$/' /configs/pacman-online-${OMARCHY_MIRROR}.conf >> /etc/pacman.conf
|
||||
fi
|
||||
|
||||
# Build locations
|
||||
build_cache_dir=/var/cache
|
||||
offline_mirror_dir="$build_cache_dir/airootfs/var/cache/omarchy/mirror/offline"
|
||||
mkdir -p $build_cache_dir/
|
||||
mkdir -p $offline_mirror_dir/
|
||||
mkdir -p "$build_cache_dir" "$offline_mirror_dir"
|
||||
|
||||
# We base our ISO on the official arch ISO (releng) config
|
||||
cp -r /archiso/configs/releng/* $build_cache_dir/
|
||||
# Seed from the official Arch releng profile.
|
||||
cp -r /archiso/configs/releng/* "$build_cache_dir/"
|
||||
rm "$build_cache_dir/airootfs/etc/motd"
|
||||
|
||||
# Avoid using reflector for mirror identification as we are relying on the global CDN
|
||||
rm "$build_cache_dir/airootfs/etc/systemd/system/multi-user.target.wants/reflector.service"
|
||||
# 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/reflector.service.d"
|
||||
rm -rf "$build_cache_dir/airootfs/etc/xdg/reflector"
|
||||
|
||||
# Bring in our configs
|
||||
cp -r /configs/* $build_cache_dir/
|
||||
# Bring in our archiso profile additions.
|
||||
cp -r /configs/* "$build_cache_dir/"
|
||||
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
|
||||
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
|
||||
|
||||
# Clone Omarchy itself
|
||||
git clone -b $OMARCHY_INSTALLER_REF https://github.com/$OMARCHY_INSTALLER_REPO.git "$build_cache_dir/airootfs/root/omarchy"
|
||||
if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then
|
||||
touch "$build_cache_dir/airootfs/usr/share/omarchy-iso/install-debug"
|
||||
{
|
||||
echo "debug=1"
|
||||
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
|
||||
|
||||
# Make log uploader available in the ISO too
|
||||
mkdir -p "$build_cache_dir/airootfs/usr/local/bin/"
|
||||
cp "$build_cache_dir/airootfs/root/omarchy/bin/omarchy-upload-log" "$build_cache_dir/airootfs/usr/local/bin/omarchy-upload-log"
|
||||
# When --local-source is in effect, build omarchy* from the mounted source
|
||||
# trees and drop them in the offline mirror. Otherwise pacman -Syw below
|
||||
# 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
|
||||
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.js binary for offline mise install.
|
||||
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_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}')
|
||||
|
||||
# Download the tarball
|
||||
curl -fsSL "$NODE_DIST_URL/$NODE_FILENAME" -o "/tmp/$NODE_FILENAME"
|
||||
|
||||
# Verify SHA256 checksum
|
||||
echo "$NODE_SHA /tmp/$NODE_FILENAME" | sha256sum -c - || {
|
||||
echo "ERROR: Node.js checksum verification failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy to ISO
|
||||
echo "$NODE_SHA /tmp/$NODE_FILENAME" | sha256sum -c -
|
||||
mkdir -p "$build_cache_dir/airootfs/opt/packages/"
|
||||
cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/"
|
||||
|
||||
# Add our additional packages to packages.x86_64
|
||||
arch_packages=(linux-t2 git gum jq openssl plymouth tzupdate)
|
||||
printf '%s\n' "${arch_packages[@]}" >>"$build_cache_dir/packages.x86_64"
|
||||
# Packages installed into the live ISO environment itself (NOT the target system).
|
||||
# The selected omarchy-settings package is needed here so its post_install hook
|
||||
# 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
|
||||
all_packages=($(cat "$build_cache_dir/packages.x86_64"))
|
||||
all_packages+=($(grep -v '^#' "$build_cache_dir/airootfs/root/omarchy/install/omarchy-base.packages" | grep -v '^$'))
|
||||
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 '^$'))
|
||||
# The live ISO boots linux-t2 (see airootfs/etc/mkinitcpio.d/linux-t2.preset), so
|
||||
# stock linux is a second kernel nobody boots: ~147MB of ISO, plus its own archiso
|
||||
# initramfs, copied into both the ISO tree and the size-constrained FAT EFI image.
|
||||
#
|
||||
# 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
|
||||
pacman --config /configs/pacman-online.conf --noconfirm -Syw "${all_packages[@]}" --cachedir $offline_mirror_dir/ --dbpath /tmp/offlinedb
|
||||
repo-add --new "$offline_mirror_dir/offline.db.tar.gz" "$offline_mirror_dir/"*.pkg.tar.zst
|
||||
download_offline_packages() {
|
||||
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 needs packages at /var/cache/omarchy/mirror/offline in the container,
|
||||
# but they're actually in $build_cache_dir/airootfs/var/cache/omarchy/mirror/offline
|
||||
# mkarchiso expects the mirror at /var/cache/omarchy/mirror/offline inside the
|
||||
# container (the airootfs path); symlink rather than duplicate.
|
||||
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 pacman.conf to the ISO's /etc directory so the live environment uses our
|
||||
# same config when booted
|
||||
cp $build_cache_dir/pacman.conf "$build_cache_dir/airootfs/etc/pacman.conf"
|
||||
# Live ISO uses the same offline pacman.conf.
|
||||
cp "$build_cache_dir/pacman-offline.conf" "$build_cache_dir/airootfs/etc/pacman.conf"
|
||||
|
||||
# Finally, we assemble the entire ISO
|
||||
mkarchiso -v -w "$build_cache_dir/work/" -o "/out/" "$build_cache_dir/"
|
||||
# Build the ISO.
|
||||
mkarchiso -v -w "$build_cache_dir/work/" -o /out/ "$build_cache_dir/"
|
||||
|
||||
# Fix ownership of output files to match host user
|
||||
if [ -n "$HOST_UID" ] && [ -n "$HOST_GID" ]; then
|
||||
chown -R "$HOST_UID:$HOST_GID" /out/
|
||||
# Match host UID/GID on output.
|
||||
if [[ -n $HOST_UID && -n $HOST_GID ]]; then
|
||||
chown -R "$HOST_UID:$HOST_GID" /out/
|
||||
fi
|
||||
Executable
+85
@@ -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,13 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEaLAsIhYJKwYBBAHaRw8BAQdAYmKmrWMlfpdIPh3QzvEMzzxdNd/kqDl/Bj8H
|
||||
mCavKji0Gk9tYXJjaHkgPHBrZ3NAb21hcmNoeS5vcmc+iJAEExYKADgWIQRA37Yw
|
||||
/0K8/7BHBGzwE07mgMrFcQUCaLAsIgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIX
|
||||
gAAKCRDwE07mgMrFcRF2AQCjjvjgju/kowkN69nenqqRnE+v2MrHmWh2wU0ugIt9
|
||||
3gD/bXGugWc5J1vqoT+5aCnJAtfRRmHEfsrAiFgjduB+VA24OARosCwiEgorBgEE
|
||||
AZdVAQUBAQdAJoU73WdgxE/pEYK5ofyRvQDs1IFRdfJHV9j4Jm+tuDUDAQgHiHgE
|
||||
GBYKACAWIQRA37Yw/0K8/7BHBGzwE07mgMrFcQUCaLAsIgIbDAAKCRDwE07mgMrF
|
||||
cYXsAQC5eBEGK0xKsGwtnDDVeNAapx32dpvLgJ94W4DtIpdLlwEA85cSsTiBMuGy
|
||||
6kwYmHzqw7oFx3VYGXdMNl0SMKzvJA4=
|
||||
=G7wU
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -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,14 @@
|
||||
# Fix Panther Lake audio: blacklist SOF modules that conflict with HDA drivers
|
||||
# (prevents boot issues on Dell XPS 2026 and similar Panther Lake systems)
|
||||
blacklist snd_sof_pci_intel_ptl
|
||||
blacklist snd_sof_pci_intel_lnl
|
||||
blacklist snd_sof_pci_intel_mtl
|
||||
blacklist snd_sof_intel_hda_generic
|
||||
blacklist snd_sof_intel_hda_common
|
||||
blacklist snd_sof_intel_hda
|
||||
blacklist snd_sof_pci
|
||||
blacklist snd_sof
|
||||
blacklist soundwire_intel
|
||||
blacklist snd_soc_cs35l56_sdw
|
||||
blacklist snd_soc_cs35l56
|
||||
blacklist snd_soc_skl_hda_dsp
|
||||
@@ -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'
|
||||
@@ -1,135 +1,110 @@
|
||||
#!/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
|
||||
|
||||
use_omarchy_helpers() {
|
||||
export OMARCHY_PATH="/root/omarchy"
|
||||
export OMARCHY_INSTALL="/root/omarchy/install"
|
||||
export OMARCHY_INSTALL_LOG_FILE="/var/log/omarchy-install.log"
|
||||
source /root/omarchy/install/helpers/all.sh
|
||||
}
|
||||
[[ $(tty) == /dev/tty1 ]] || exit 0
|
||||
|
||||
run_configurator() {
|
||||
set_tokyo_night_colors
|
||||
./configurator
|
||||
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
|
||||
install_base_system 2>&1 | sed -u 's/\x1b\[[0-9;]*[a-zA-Z]//g' >>/var/log/omarchy-install.log
|
||||
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 || bash"
|
||||
|
||||
# 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_base_system() {
|
||||
# Initialize and populate the keyring
|
||||
pacman-key --init
|
||||
pacman-key --populate archlinux
|
||||
|
||||
# Sync the offline database so pacman can find packages
|
||||
pacman -Sy --noconfirm
|
||||
|
||||
# Ensure that no mounts exist from past install attempts
|
||||
findmnt -R /mnt >/dev/null && umount -R /mnt
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
chmod +x /mnt/home/$OMARCHY_USER/.local/share/omarchy/default/waybar/indicators/screen-recording.sh 2>/dev/null || 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)" \
|
||||
USER="$OMARCHY_USER" \
|
||||
HOME="/home/$OMARCHY_USER" \
|
||||
/bin/bash "$@"
|
||||
}
|
||||
|
||||
if [[ $(tty) == "/dev/tty1" ]]; then
|
||||
use_omarchy_helpers
|
||||
run_configurator
|
||||
install_arch
|
||||
install_omarchy
|
||||
export OMARCHY_MIRROR="$(cat /root/omarchy_mirror)"
|
||||
if [[ -f /root/omarchy_iso_ref ]]; then
|
||||
export OMARCHY_ISO_REF="$(cat /root/omarchy_iso_ref)"
|
||||
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
|
||||
@@ -1,10 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Rely on Omarchy for styling and error handling
|
||||
source "$OMARCHY_INSTALL/helpers/all.sh"
|
||||
# On the live ISO, .automated_script.sh exports OMARCHY_PATH and the package
|
||||
# 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
|
||||
command -v tzupdate >/dev/null || sudo pacman -Sy --noconfirm tzupdate
|
||||
LOGO_PATH="$OMARCHY_PATH/logo.txt"
|
||||
|
||||
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() {
|
||||
gum style "${1:-Aborted installation}"
|
||||
@@ -16,10 +54,15 @@ abort() {
|
||||
step() {
|
||||
clear_logo
|
||||
echo
|
||||
gum style "$1"
|
||||
gum style --padding "0 0 0 $PADDING_LEFT" "$1"
|
||||
echo
|
||||
}
|
||||
|
||||
# Padded body text to line up with the logo and gum widgets.
|
||||
say() {
|
||||
gum style --padding "0 0 0 $PADDING_LEFT" "$@"
|
||||
}
|
||||
|
||||
notice() {
|
||||
clear_logo
|
||||
echo
|
||||
@@ -43,6 +86,7 @@ Dutch|nl
|
||||
English (UK)|uk
|
||||
English (US)|us
|
||||
English (US, Dvorak)|dvorak
|
||||
English (US, Colemak)|colemak
|
||||
Estonian|et
|
||||
Finnish|fi
|
||||
French|fr
|
||||
@@ -108,7 +152,7 @@ user_form() {
|
||||
done
|
||||
|
||||
while true; do
|
||||
password=$(gum input --placeholder "Used for user + root + encryption" --prompt.foreground="#845DF9" --password --prompt "Password> ") || abort
|
||||
password=$(gum input --placeholder "Used for user + root, and disk encryption when enabled" --prompt.foreground="#845DF9" --password --prompt "Password> ") || abort
|
||||
password_confirmation=$(gum input --placeholder "Must match the password you just typed" --prompt.foreground="#845DF9" --password --prompt "Confirm> ") || abort
|
||||
|
||||
if [[ -n "$password" && "$password" == "$password_confirmation" ]]; then
|
||||
@@ -121,21 +165,21 @@ user_form() {
|
||||
done
|
||||
|
||||
# Hash the password using yescrypt
|
||||
password_hash=$(openssl passwd -6 "$password")
|
||||
password_hash=$(printf '%s' "$password" | openssl passwd -6 -stdin)
|
||||
|
||||
full_name=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Full name> ")
|
||||
email_address=$(gum input --placeholder "Used for git authentication (hit return to skip)" --prompt.foreground="#845DF9" --prompt "Email address> ")
|
||||
|
||||
while true; do
|
||||
hostname=$(gum input --placeholder "Alphanumeric without spaces (or return for 'omarchy')" --prompt.foreground="#845DF9" --prompt "Hostname> ")
|
||||
hostname=$(gum input --placeholder "Letters, digits, and dashes (or return for 'omarchy')" --prompt.foreground="#845DF9" --prompt "Hostname> ")
|
||||
|
||||
if [[ "$hostname" =~ ^[A-Za-z_][A-Za-z0-9_-]*\$?$ ]]; then
|
||||
if [[ "$hostname" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$ ]]; then
|
||||
break
|
||||
elif [[ -z $hostname ]]; then
|
||||
hostname="omarchy"
|
||||
break
|
||||
else
|
||||
notice "Hostname must be alphanumeric using dashes or underscores but no spaces" 1
|
||||
notice "Hostname must be 1-63 letters, digits, or dashes, and cannot start or end with a dash" 1
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -176,26 +220,512 @@ done
|
||||
|
||||
get_disk_info() {
|
||||
local device="$1"
|
||||
local size model
|
||||
local size vendor model label
|
||||
|
||||
# Get basic disk information
|
||||
size=$(lsblk -dno SIZE "$device" 2>/dev/null)
|
||||
model=$(lsblk -dno MODEL "$device" 2>/dev/null | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//')
|
||||
vendor=$(lsblk -dno VENDOR "$device" 2>/dev/null | sed 's/ *$//')
|
||||
model=$(lsblk -dno MODEL "$device" 2>/dev/null | sed 's/ *$//')
|
||||
|
||||
# Combine vendor and model, avoiding duplication
|
||||
label=""
|
||||
if [[ -n $vendor && -n $model ]]; then
|
||||
if [[ $model == *$vendor* ]]; then
|
||||
label="$model"
|
||||
else
|
||||
label="$vendor $model"
|
||||
fi
|
||||
elif [[ -n $model ]]; then
|
||||
label="$model"
|
||||
elif [[ -n $vendor ]]; then
|
||||
label="$vendor"
|
||||
fi
|
||||
|
||||
# Format display string
|
||||
local display="$device"
|
||||
[[ -n "$size" ]] && display="$display ($size)"
|
||||
[[ -n "$model" ]] && display="$display - $model"
|
||||
[[ -n $size ]] && display="$display ($size)"
|
||||
[[ -n $label ]] && display="$display - $label"
|
||||
|
||||
# Append compact partition summary
|
||||
local part_summary
|
||||
part_summary=$(lsblk -nro TYPE,NAME,FSTYPE,MOUNTPOINT "$device" 2>/dev/null | \
|
||||
awk '$1=="part" { printf "%s%s%s", s, ($3==""?"unknown":$3), ($4==""?"":"("$4")"); s=", " }')
|
||||
[[ -n $part_summary ]] && display+=" [$part_summary]"
|
||||
|
||||
echo "$display"
|
||||
}
|
||||
|
||||
get_root_disk() {
|
||||
local device="$1"
|
||||
local parent
|
||||
|
||||
[[ -n $device ]] || return 1
|
||||
|
||||
# /run/archiso/bootmnt usually resolves to the boot partition, but the
|
||||
# installer picker works with whole disks. Walk back to the parent disk so
|
||||
# the install media itself never shows up as a wipe target.
|
||||
device=$(readlink -f "$device" 2>/dev/null || printf "%s\n" "$device")
|
||||
|
||||
while true; do
|
||||
parent=$(lsblk -dno PKNAME "$device" 2>/dev/null | tail -n1)
|
||||
[[ -n $parent ]] || break
|
||||
device="/dev/$parent"
|
||||
done
|
||||
|
||||
if [[ $(lsblk -dno TYPE "$device" 2>/dev/null) == "disk" ]]; then
|
||||
printf "%s\n" "$device"
|
||||
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() {
|
||||
step "Let's select where to install Omarchy..."
|
||||
|
||||
# Don't offer the install media as an option (Arch ISO mounts it here)
|
||||
local boot_source
|
||||
local exclude_disk
|
||||
exclude_disk=$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null || true)
|
||||
boot_source=$(findmnt -no SOURCE /run/archiso/bootmnt 2>/dev/null || true)
|
||||
exclude_disk=$(get_root_disk "$boot_source")
|
||||
|
||||
# List all installable disks, excluding the boot device if present
|
||||
local available_disks
|
||||
@@ -219,45 +749,138 @@ disk_form() {
|
||||
disk=$(echo "$selected_display" | awk '{print $1}')
|
||||
}
|
||||
|
||||
disk_form
|
||||
|
||||
while true; do
|
||||
gum style "Everything will be overwritten. There is no recovery possible."
|
||||
# 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
|
||||
if gum confirm --affirmative "Yes, format disk" --negative "No, change it" "Confirm overwriting ${disk}"; then
|
||||
break
|
||||
else
|
||||
disk_form
|
||||
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
|
||||
done
|
||||
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() {
|
||||
local mode="encrypted"
|
||||
local affirmative confirm_status
|
||||
|
||||
while true; do
|
||||
clear_logo
|
||||
echo
|
||||
say "Everything will be overwritten. There is no recovery possible."
|
||||
|
||||
if [[ $mode == "encrypted" ]]; then
|
||||
say --foreground 8 "Press Ctrl+C for unencrypted install."
|
||||
affirmative="Yes, install"
|
||||
else
|
||||
affirmative="Yes, install without encryption"
|
||||
fi
|
||||
|
||||
echo
|
||||
gum confirm --affirmative "$affirmative" --negative "No, change it" "Confirm overwriting ${disk}"
|
||||
confirm_status=$?
|
||||
|
||||
case $confirm_status in
|
||||
0)
|
||||
[[ $mode == "encrypted" ]] && encrypt_installation=true || encrypt_installation=false
|
||||
return 0
|
||||
;;
|
||||
1)
|
||||
return 1
|
||||
;;
|
||||
130)
|
||||
[[ $mode == "encrypted" ]] && mode="unencrypted" || mode="encrypted"
|
||||
;;
|
||||
*)
|
||||
abort
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
select_installation() {
|
||||
local full_disk_only
|
||||
|
||||
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
|
||||
|
||||
# Save user full name and email address, so the autostart can
|
||||
# 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)
|
||||
|
||||
cat <<-_EOF_ >user_credentials.json
|
||||
{
|
||||
"encryption_password": $password_escaped,
|
||||
"root_enc_password": $password_hash_escaped,
|
||||
"users": [
|
||||
{
|
||||
"enc_password": $password_hash_escaped,
|
||||
"groups": [],
|
||||
"sudo": true,
|
||||
"username": $username_escaped
|
||||
}
|
||||
]
|
||||
}
|
||||
_EOF_
|
||||
write_user_files
|
||||
|
||||
# Setup partition layout
|
||||
disk_size=$(lsblk -bdno SIZE "$disk")
|
||||
@@ -272,11 +895,22 @@ boot_partition_size=$((2 * gib))
|
||||
main_partition_start=$((boot_partition_size + boot_partition_start))
|
||||
main_partition_size=$((disk_size_in_mib - main_partition_start - gpt_backup_reserve))
|
||||
|
||||
# Detect T2 Mac and set appropriate kernel
|
||||
if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then
|
||||
kernel_choice="linux-t2"
|
||||
kernel_choice=$(detect_kernel)
|
||||
|
||||
if [[ $encrypt_installation == true ]]; then
|
||||
disk_encryption_config=$(cat <<-_EOF_
|
||||
,
|
||||
"disk_encryption": {
|
||||
"encryption_type": "luks",
|
||||
"lvm_volumes": [],
|
||||
"iter_time": 2000,
|
||||
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ],
|
||||
"encryption_password": $(echo -n "$password" | jq -Rsa)
|
||||
}
|
||||
_EOF_
|
||||
)
|
||||
else
|
||||
kernel_choice="linux"
|
||||
disk_encryption_config=""
|
||||
fi
|
||||
|
||||
cat <<-_EOF_ >user_configuration.json
|
||||
@@ -285,14 +919,22 @@ cat <<-_EOF_ >user_configuration.json
|
||||
"archinstall-language": "English",
|
||||
"auth_config": {},
|
||||
"audio_config": { "audio": "pipewire" },
|
||||
"bootloader": "Limine",
|
||||
"bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
|
||||
"custom_commands": [],
|
||||
"disk_config": {
|
||||
"btrfs_options": {
|
||||
"snapshot_config": {
|
||||
"type": "Snapper"
|
||||
}
|
||||
"omarchy_install": {
|
||||
"mode": "full_disk",
|
||||
"target_mount": "/mnt",
|
||||
"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",
|
||||
"device_modifications": [
|
||||
{
|
||||
@@ -348,14 +990,7 @@ cat <<-_EOF_ >user_configuration.json
|
||||
],
|
||||
"wipe": true
|
||||
}
|
||||
],
|
||||
"disk_encryption": {
|
||||
"encryption_type": "luks",
|
||||
"lvm_volumes": [],
|
||||
"iter_time": 2000,
|
||||
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ],
|
||||
"encryption_password": $password_escaped
|
||||
}
|
||||
]$disk_encryption_config
|
||||
},
|
||||
"hostname": "$hostname",
|
||||
"kernels": [ "$kernel_choice" ],
|
||||
@@ -383,7 +1018,10 @@ cat <<-_EOF_ >user_configuration.json
|
||||
},
|
||||
"packages": [
|
||||
"base-devel",
|
||||
"git"
|
||||
"git",
|
||||
"omarchy-keyring",
|
||||
"$OMARCHY_SETTINGS_PACKAGE",
|
||||
"$OMARCHY_RUNTIME_PACKAGE"
|
||||
],
|
||||
"profile_config": {
|
||||
"gfx_driver": null,
|
||||
@@ -395,17 +1033,5 @@ cat <<-_EOF_ >user_configuration.json
|
||||
_EOF_
|
||||
|
||||
if [[ $1 == "dry" ]]; then
|
||||
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
|
||||
|
||||
rm user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt
|
||||
print_dry_run_files
|
||||
fi
|
||||
+602
@@ -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
@@ -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
@@ -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
|
||||
linux /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
|
||||
initrd /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
|
||||
options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash
|
||||
options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 initramfs_async=0
|
||||
@@ -54,13 +54,13 @@ timeout_style=hidden
|
||||
|
||||
menuentry "Omarchy (%ARCH%, ${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' {
|
||||
set gfxpayload=keep
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on
|
||||
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
|
||||
}
|
||||
|
||||
@@ -104,6 +104,3 @@ menuentry 'System restart' --class reboot --class restart {
|
||||
}
|
||||
|
||||
|
||||
# GRUB init tune for accessibility
|
||||
play 600 988 1 1319 4
|
||||
|
||||
@@ -30,13 +30,13 @@ timeout_style=hidden
|
||||
|
||||
menuentry "Omarchy (%ARCH%, ${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' {
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" accessibility=on
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#
|
||||
# /etc/pacman.conf
|
||||
#
|
||||
# See the pacman.conf(5) manpage for option and repository directives
|
||||
[options]
|
||||
HoldPkg = pacman glibc
|
||||
Architecture = auto
|
||||
NoProgressBar
|
||||
ParallelDownloads = 5
|
||||
|
||||
# By default, pacman accepts packages signed by keys that its local keyring
|
||||
# trusts (see pacman-key and its man page), as well as unsigned packages.
|
||||
SigLevel = Required DatabaseOptional
|
||||
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]
|
||||
SigLevel = Never
|
||||
Server = file:///var/cache/omarchy/mirror/offline/
|
||||
@@ -15,26 +15,21 @@ LocalFileSigLevel = Optional
|
||||
|
||||
[core]
|
||||
Server = https://mirror.omarchy.org/$repo/os/$arch
|
||||
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
|
||||
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[extra]
|
||||
Server = https://mirror.omarchy.org/$repo/os/$arch
|
||||
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
|
||||
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[multilib]
|
||||
Server = https://mirror.omarchy.org/$repo/os/$arch
|
||||
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
|
||||
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/$arch
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
SigLevel = Never
|
||||
@@ -0,0 +1,35 @@
|
||||
#
|
||||
# /etc/pacman.conf
|
||||
#
|
||||
# See the pacman.conf(5) manpage for option and repository directives
|
||||
|
||||
[options]
|
||||
HoldPkg = pacman glibc
|
||||
Architecture = auto
|
||||
ParallelDownloads = 5
|
||||
|
||||
# By default, pacman accepts packages signed by keys that its local keyring
|
||||
# trusts (see pacman-key and its man page), as well as unsigned packages.
|
||||
SigLevel = Required DatabaseOptional
|
||||
LocalFileSigLevel = Optional
|
||||
|
||||
[core]
|
||||
Server = https://rc-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[extra]
|
||||
Server = https://rc-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[multilib]
|
||||
Server = https://rc-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
SigLevel = Never
|
||||
@@ -0,0 +1,35 @@
|
||||
#
|
||||
# /etc/pacman.conf
|
||||
#
|
||||
# See the pacman.conf(5) manpage for option and repository directives
|
||||
|
||||
[options]
|
||||
HoldPkg = pacman glibc
|
||||
Architecture = auto
|
||||
ParallelDownloads = 5
|
||||
|
||||
# By default, pacman accepts packages signed by keys that its local keyring
|
||||
# trusts (see pacman-key and its man page), as well as unsigned packages.
|
||||
SigLevel = Required DatabaseOptional
|
||||
LocalFileSigLevel = Optional
|
||||
|
||||
[core]
|
||||
Server = https://stable-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[extra]
|
||||
Server = https://stable-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[multilib]
|
||||
Server = https://stable-mirror.omarchy.org/$repo/os/$arch
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/stable/$arch
|
||||
|
||||
[arch-mact2]
|
||||
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
|
||||
Server = https://github.com/NoaHimesaka1873/arch-mact2-mirror/releases/download/release
|
||||
SigLevel = Never
|
||||
@@ -1,18 +0,0 @@
|
||||
#
|
||||
# /etc/pacman.conf
|
||||
#
|
||||
# See the pacman.conf(5) manpage for option and repository directives
|
||||
[options]
|
||||
HoldPkg = pacman glibc
|
||||
Architecture = auto
|
||||
NoProgressBar
|
||||
ParallelDownloads = 5
|
||||
|
||||
# By default, pacman accepts packages signed by keys that its local keyring
|
||||
# trusts (see pacman-key and its man page), as well as unsigned packages.
|
||||
SigLevel = Required DatabaseOptional
|
||||
LocalFileSigLevel = Optional
|
||||
|
||||
[offline]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = file:///var/cache/omarchy/mirror/offline/
|
||||
+22
-4
@@ -10,17 +10,35 @@ install_dir="arch"
|
||||
buildmodes=('iso')
|
||||
bootmodes=('bios.syslinux' 'uefi.grub')
|
||||
arch="x86_64"
|
||||
pacman_conf="pacman.conf"
|
||||
pacman_conf="pacman-offline.conf"
|
||||
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')
|
||||
file_permissions=(
|
||||
["/etc/shadow"]="0:0:400"
|
||||
["/root"]="0:0:750"
|
||||
["/root/.automated_script.sh"]="0:0:755"
|
||||
["/root/.gnupg"]="0:0:700"
|
||||
["/usr/local/bin/choose-mirror"]="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"
|
||||
["/var/cache/omarchy/mirror/offline/"]="0:0:775"
|
||||
)
|
||||
@@ -6,7 +6,7 @@ ENDTEXT
|
||||
MENU LABEL Omarchy install medium (x86_64, NBD)
|
||||
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
|
||||
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
|
||||
|
||||
LABEL arch64_nfs
|
||||
@@ -17,7 +17,7 @@ ENDTEXT
|
||||
MENU LABEL Omarchy install medium (x86_64, NFS)
|
||||
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
|
||||
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
|
||||
|
||||
LABEL arch64_http
|
||||
@@ -28,5 +28,5 @@ ENDTEXT
|
||||
MENU LABEL Omarchy install medium (x86_64, HTTP)
|
||||
LINUX ::/%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
|
||||
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
|
||||
@@ -6,7 +6,7 @@ ENDTEXT
|
||||
MENU LABEL Omarchy install medium (x86_64, BIOS)
|
||||
LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
|
||||
INITRD /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
|
||||
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash
|
||||
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 initramfs_async=0
|
||||
|
||||
# Accessibility boot option
|
||||
LABEL arch64speech
|
||||
@@ -17,4 +17,4 @@ ENDTEXT
|
||||
MENU LABEL Omarchy install medium (x86_64, BIOS) with ^speech
|
||||
LINUX /%INSTALL_DIR%/boot/x86_64/vmlinuz-linux-t2
|
||||
INITRD /%INSTALL_DIR%/boot/x86_64/initramfs-linux-t2.img
|
||||
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on
|
||||
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
@@ -0,0 +1,178 @@
|
||||
# Plan: aarch64 (Generic UEFI ARM64) build for omarchy-iso
|
||||
|
||||
## Context
|
||||
|
||||
omarchy-iso currently produces a single x86_64 Arch Linux live ISO. The build is x86-baked from top to bottom — profile arch, package list filename, microcode, kernel choice (`linux-t2`), bootloader configs (BIOS syslinux + UEFI GRUB), squashfs BCJ filter, Node.js download URL, QEMU smoke-test, release filename, and CI runner.
|
||||
|
||||
Target: a parallel **generic UEFI aarch64** ISO — boots on Ampere servers, AWS Graviton VMs, Snapdragon X laptops, ARM dev kits. Anything that exposes vanilla UEFI + ACPI. Apple Silicon (Asahi) and SBCs (U-Boot/rpi-firmware) are out of scope.
|
||||
|
||||
**Assumption:** cross-repo dependencies are someone else's problem to land first. This plan only covers omarchy-iso. The hard prerequisites are listed up front so it's clear what blocks the first green build.
|
||||
|
||||
---
|
||||
|
||||
## Hard prerequisites (NOT in this plan, but block any green build)
|
||||
|
||||
These exist outside omarchy-iso. Flagged for visibility — without them, nothing here boots:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Approach
|
||||
|
||||
Add an `--arch aarch64` flag to `bin/omarchy-iso-make`. Branch on it everywhere x86_64 is currently assumed. Don't fork the profile directory — overlay arch-specific files at build time so the two architectures share one source of truth.
|
||||
|
||||
Output artifact naming already uses no arch suffix until release-time renaming, so two parallel builds coexist cleanly.
|
||||
|
||||
---
|
||||
|
||||
## Concrete changes
|
||||
|
||||
### 1. Build entrypoint — `bin/omarchy-iso-make`
|
||||
|
||||
- Add `--arch x86_64|aarch64` flag (default `x86_64` for backward compat).
|
||||
- Pass `OMARCHY_ARCH` env var into the container.
|
||||
- Switch the docker image / platform per arch:
|
||||
- x86_64: `archlinux/archlinux:latest` (unchanged)
|
||||
- aarch64: `archlinuxarm/archlinuxarm:latest` (or `menci/archlinuxarm` — pick whichever publishes a recent multi-arch image), with `--platform linux/arm64`. Native arm64 host preferred; QEMU emulation works but is slow.
|
||||
- Don't change pacman cache mount on the host: pacman keeps per-arch subdirs, so the cache is safe to share.
|
||||
|
||||
### 2. Profile — `configs/profiledef.sh`
|
||||
|
||||
Change `arch` and the squashfs BCJ filter at runtime. Either:
|
||||
- Keep one `profiledef.sh` and have `builder/build-iso.sh` `sed` the arch line + BCJ filter (`x86` → `arm`) before `mkarchiso` runs, **or**
|
||||
- Source `OMARCHY_ARCH` directly inside `profiledef.sh` (mkarchiso sources this file with bash, so env access is fine).
|
||||
|
||||
`bootmodes` also needs to drop `bios.syslinux` for aarch64 (BIOS boot is x86-only) — `bootmodes=('uefi.grub')` on ARM.
|
||||
|
||||
### 3. Build script — `builder/build-iso.sh`
|
||||
|
||||
- **Line 56–57**: Node.js URL grep. Branch on `OMARCHY_ARCH`:
|
||||
- `x86_64` → `linux-x64.tar.gz`
|
||||
- `aarch64` → `linux-arm64.tar.gz`
|
||||
- **Line 73**: package additions. Drop `linux-t2` for aarch64 (T2-only x86 kernel) — use plain `linux`. Write to `packages.${OMARCHY_ARCH}` instead of hardcoded `packages.x86_64`.
|
||||
- **Line 77**: read same `packages.${OMARCHY_ARCH}` for offline mirror enumeration.
|
||||
- **archiso releng overlay** (`cp -r /archiso/configs/releng/*`): the upstream `releng` profile ships only `packages.x86_64`. For aarch64, copy `packages.x86_64` to `packages.aarch64` and prune obviously x86-only entries (`memtest86+`, `intel-ucode`, `amd-ucode`, `edk2-shell` x64 binary, `syslinux`). Land this as a small fixup step in `build-iso.sh` rather than a full fork of `releng`.
|
||||
|
||||
### 4. archinstall package list — `builder/archinstall.packages`
|
||||
|
||||
Drop microcode for aarch64 — `intel-ucode` and `amd-ucode` don't exist for ARM. Either:
|
||||
- Two lists (`archinstall.packages.x86_64`, `archinstall.packages.aarch64`), or
|
||||
- One list with sentinel comments and a filter step in `build-iso.sh:80` that drops microcode lines when `OMARCHY_ARCH=aarch64`.
|
||||
|
||||
The second is less duplication for one-line drift.
|
||||
|
||||
### 5. Bootloader configs
|
||||
|
||||
aarch64 has no BIOS — only UEFI. Drop the syslinux path entirely on ARM.
|
||||
|
||||
- **Skip on aarch64**: `configs/syslinux/*` (BIOS syslinux), `configs/grub/*` references to `shellx64.efi`/`shellia32.efi` (lines 81–85 of `grub.cfg`).
|
||||
- **`configs/efiboot/loader/loader.conf`**: change `default 01-archiso-x86_64-linux.conf` to a per-arch entry. Either two loader.conf files (selected by build-iso.sh) or rename the entry generically (`01-archiso-linux.conf`) and keep one file.
|
||||
- **`configs/efiboot/loader/entries/01-archiso-x86_64-linux.conf`**: produce an aarch64 sibling (`01-archiso-aarch64-linux.conf`) at build time — same template, drop microcode initrd lines, swap title. The kernel/initrd paths use `%ARCH%` already in `grub.cfg` but are hardcoded `x86_64` in this file (line 3–4).
|
||||
- **`configs/grub/grub.cfg` and `loopback.cfg`**: remove or guard the x86_64 conditionals. The `%ARCH%` placeholder gets substituted by mkarchiso from `profiledef.sh:arch`, so the kernel/initrd paths flip automatically. Manually-written `grub_cpu == 'x86_64'` blocks (lines 34–39, 68, 81–85) should drop on aarch64 builds — easiest done with a build-time sed for the aarch64 path, or by extracting them into a separate `grub-x86_64.cfg.fragment` only included on x86 builds.
|
||||
|
||||
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-t2.preset`
|
||||
|
||||
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`
|
||||
|
||||
Lines 320–325:
|
||||
```bash
|
||||
if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then
|
||||
kernel_choice="linux-t2"
|
||||
else
|
||||
kernel_choice="linux"
|
||||
fi
|
||||
```
|
||||
T2 detection is harmless on aarch64 (`lspci` returns no match), so `kernel_choice` falls through to `linux`. **No change strictly required**, but clearer to wrap the lspci probe in `[[ $(uname -m) == "x86_64" ]]` so the intent reads correctly. Cheap.
|
||||
|
||||
The archinstall JSON's `mirror_config.custom_servers` (lines 422–424) points at `mirror.omarchy.org`, `mirror.rackspace.com/archlinux`, `geo.mirror.pkgbuild.com` — none of these serve aarch64. For aarch64 the list must be `mirror.archlinuxarm.org` and friends. Branch the JSON template on `OMARCHY_ARCH` (or `uname -m` at runtime, since this file runs on the live ISO).
|
||||
|
||||
### 8. pacman configs — `configs/pacman-online-{stable,rc,edge,offline}.conf`
|
||||
|
||||
The `$arch` placeholder is pacman-resolved (matches `Architecture = auto`), so `https://stable-mirror.omarchy.org/$repo/os/$arch` adapts automatically — **as long as the mirror serves aarch64**. The omarchy mirror is the bottleneck (prerequisite #2). The `arch-mact2` repo is x86-only by definition; gate it behind arch on aarch64 builds (drop the section).
|
||||
|
||||
### 9. Smoke-test scripts — `bin/omarchy-iso-boot`, `bin/omarchy-vm`
|
||||
|
||||
Detect arch from the ISO filename (or accept a flag) and switch:
|
||||
- x86_64: `qemu-system-x86_64`, `/usr/share/edk2/x64/OVMF_CODE.4m.fd` + `OVMF_VARS.4m.fd` (unchanged).
|
||||
- aarch64: `qemu-system-aarch64`, `-machine virt -cpu max`, `/usr/share/edk2/aarch64/QEMU_CODE.fd` + `QEMU_VARS.fd` (Arch package: `edk2-armvirt`).
|
||||
|
||||
Both scripts hardcode the OVMF path; small, mechanical fix.
|
||||
|
||||
### 10. Release script — `bin/omarchy-iso-release:52`
|
||||
|
||||
```bash
|
||||
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}-${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`
|
||||
|
||||
Add a matrix:
|
||||
```yaml
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x86_64, aarch64]
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-latest
|
||||
- arch: aarch64
|
||||
runner: ubuntu-24.04-arm # GitHub-hosted ARM runner, GA
|
||||
```
|
||||
Pass `--arch ${{ matrix.arch }}` to the build. Upload artifacts named per-arch.
|
||||
|
||||
If GitHub-hosted ARM runners aren't available on this org's plan, fall back to QEMU emulation on `ubuntu-latest` (slow, ~3-4× build time) or self-hosted.
|
||||
|
||||
---
|
||||
|
||||
## Files to modify (summary)
|
||||
|
||||
Code:
|
||||
- `bin/omarchy-iso-make` — add `--arch` flag, branch docker image/platform
|
||||
- `bin/omarchy-iso-boot` — branch QEMU binary + OVMF path
|
||||
- `bin/omarchy-vm` — same as iso-boot
|
||||
- `bin/omarchy-iso-release` — parameterize ISO glob
|
||||
- `builder/build-iso.sh` — main branching: Node.js URL, package list filename, releng overlay fixups, kernel package
|
||||
- `builder/archinstall.packages` — filter microcode on aarch64 (sentinel comments + filter step)
|
||||
- `configs/profiledef.sh` — env-driven `arch` + BCJ filter + bootmodes
|
||||
- `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/grub/grub.cfg`, `loopback.cfg` — strip x86-only fragments 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
|
||||
- `.github/workflows/nightly-build.yml` — matrix x86_64/aarch64
|
||||
|
||||
No new files unless we go the dual-list route on archinstall.packages or efiboot loader entries; preferred path is build-time templating to keep one source of truth.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
**Local (host is x86_64):**
|
||||
1. `bin/omarchy-iso-make --arch x86_64` — must produce a byte-similar ISO to today's nightly (smoke test for regressions in the refactor).
|
||||
2. `bin/omarchy-iso-make --arch aarch64` — succeeds (slow under QEMU binfmt; expect 30-60 min).
|
||||
3. `bin/omarchy-iso-boot release/omarchy-*-aarch64-*.iso` — boots to the configurator under `qemu-system-aarch64 -machine virt`. Walk through the picker, confirm archinstall lays down a working system, reboot into the installed system.
|
||||
4. Live-iso shell: `pacman -Sy && pacman -Si linux` returns an aarch64 package from `mirror.archlinuxarm.org`.
|
||||
|
||||
**On real hardware (post green QEMU run):**
|
||||
- AWS Graviton VM (`c7g.medium`, dd ISO to a volume, attach as boot) — fastest cloud check, no hardware needed.
|
||||
- One physical ARM64 UEFI box if available (Ampere dev kit, Snapdragon X laptop, etc.) — the long pole.
|
||||
|
||||
**CI:**
|
||||
- Both matrix legs go green on a manual `workflow_dispatch` before merging.
|
||||
- Nightly produces both ISOs as artifacts.
|
||||
|
||||
---
|
||||
|
||||
## Open risks (call out before implementation)
|
||||
|
||||
1. **mkarchiso on aarch64 is less-trodden ground.** It accepts `arch="aarch64"`, but the upstream Arch project doesn't dogfood it. Expect to file/patch around small bugs in archiso's helper scripts. Keep the archiso submodule pin tight so a regression doesn't surprise nightly.
|
||||
2. **Limine + LUKS + Btrfs + Snapper on aarch64** — every one of these works individually on ARM, but the combination is what omarchy ships. Worth one manual end-to-end pass before declaring done.
|
||||
3. **Apple T2 / linux-t2 / `arch-mact2` repo** are silently dropped on aarch64; users mistakenly trying to install the aarch64 ISO on a T2 Mac will get an obviously-wrong result. The configurator could refuse to install when arch mismatch is detected, but that's outside this plan.
|
||||
@@ -0,0 +1,395 @@
|
||||
# Consumer Secure Boot Plan: Microsoft Shim + MOK + UKI
|
||||
|
||||
## Goal
|
||||
|
||||
Support Omarchy installs on consumer laptops that ship with UEFI Secure Boot enabled and trust the Microsoft UEFI CA, without requiring users to disable Secure Boot.
|
||||
|
||||
This plan targets normal consumer hardware, especially Windows dual-boot machines. OEM-controlled hardware with Omarchy-owned firmware keys is a separate path.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
Use a Microsoft-signed first-stage shim, then boot only signed Omarchy-controlled artifacts.
|
||||
|
||||
```text
|
||||
Firmware Secure Boot
|
||||
-> Microsoft-signed Omarchy shim
|
||||
-> MokManager when key enrollment is pending
|
||||
-> signed second-stage boot manager
|
||||
-> signed Unified Kernel Image (UKI)
|
||||
-> encrypted Omarchy root
|
||||
```
|
||||
|
||||
For the installed OS, use machine-local Machine Owner Key (MOK) signing for UKIs generated during install and later kernel updates.
|
||||
|
||||
## Why MOK For Installed Systems
|
||||
|
||||
Omarchy's installed UKI is machine-specific because it includes or depends on install-time data such as:
|
||||
|
||||
- Kernel command line for encrypted root.
|
||||
- Initramfs generated for the installed system.
|
||||
- Kernel package version selected at install or update time.
|
||||
|
||||
We must not ship Omarchy's private release signing key on the ISO or installed system. Therefore:
|
||||
|
||||
1. The official ISO is signed by Omarchy release infrastructure.
|
||||
2. During install, generate a machine-local Secure Boot keypair.
|
||||
3. Enroll the public certificate through MokManager.
|
||||
4. Sign installed UKIs and update-generated UKIs with the local private key.
|
||||
5. Store the local private key only on the encrypted installed root, never on the ESP.
|
||||
|
||||
This preserves offline tamper resistance for `/boot` while avoiding any central private key on user machines.
|
||||
|
||||
## Scope Constraints
|
||||
|
||||
- UEFI only.
|
||||
- Microsoft UEFI CA trust path only for consumer machines.
|
||||
- No custom firmware key enrollment for v1 consumer installs.
|
||||
- No TPM auto-unlock for Omarchy root in v1.
|
||||
- No attempt to modify Windows BitLocker protectors or TPM ownership.
|
||||
- Secure Boot mode uses signed UKIs only; no unsigned external kernel, initramfs, or command line files.
|
||||
- Secure Boot mode may use a different boot manager than the current Limine path until Limine has a proven verified-boot story.
|
||||
|
||||
## Bootloader Decision
|
||||
|
||||
Signing Limine as an EFI binary is not sufficient if Limine can then boot unsigned kernels, initrds, or configs. For v1 Secure Boot support, use a boot path with a small, enforceable verification surface:
|
||||
|
||||
```text
|
||||
shim -> systemd-boot -> signed UKI
|
||||
```
|
||||
|
||||
Policy:
|
||||
|
||||
- Keep Limine as the normal non-Secure-Boot bootloader unless we later prove Limine enforces signed payloads correctly.
|
||||
- Use systemd-boot only for Secure Boot installs if that is the fastest reliable path.
|
||||
- Boot only UKIs with embedded command line and initramfs.
|
||||
- Disable or avoid unsigned boot entry editing in Secure Boot mode.
|
||||
|
||||
If later validation proves Limine can enforce signed payloads behind shim, this plan can swap the second stage while keeping the shim, MOK, UKI, and signing architecture.
|
||||
|
||||
## Required Artifacts
|
||||
|
||||
### Omarchy Shim Artifacts
|
||||
|
||||
These are release artifacts, not generated on normal developer machines:
|
||||
|
||||
- `shimx64.efi`, signed by Microsoft UEFI CA.
|
||||
- `mmx64.efi` / MokManager artifact compatible with the shim.
|
||||
- Optional `fbx64.efi` fallback artifact if needed by the selected shim packaging.
|
||||
- Shim SBAT metadata owned by Omarchy.
|
||||
- Omarchy public release certificate embedded in shim.
|
||||
|
||||
Do not depend on another distribution's shim as a permanent product strategy. Omarchy needs its own shim submission, SBAT identity, revocation path, and release process.
|
||||
|
||||
### Omarchy Signing Material
|
||||
|
||||
Release infrastructure owns:
|
||||
|
||||
- Omarchy Secure Boot release private key, stored offline or in an HSM-backed CI secret store.
|
||||
- Omarchy Secure Boot release public certificate, embedded in shim and shipped for verification.
|
||||
|
||||
Installed systems own:
|
||||
|
||||
- Machine-local MOK private key, stored on encrypted root with `0600` permissions.
|
||||
- Machine-local MOK public certificate, enrolled through MokManager.
|
||||
|
||||
Private keys must never be committed to this repo.
|
||||
|
||||
## Build-Time Flow
|
||||
|
||||
Add a Secure Boot-capable ISO build mode.
|
||||
|
||||
```text
|
||||
bin/omarchy-iso-make --secure-boot
|
||||
```
|
||||
|
||||
Build behavior:
|
||||
|
||||
1. Add Microsoft-signed Omarchy shim artifacts to the ISO UEFI boot path.
|
||||
2. Build the live environment boot payload as a UKI.
|
||||
3. Sign the live UKI with the Omarchy release key in official builds.
|
||||
4. Sign any second-stage EFI binary with the Omarchy release key.
|
||||
5. Assert that every Secure Boot UEFI entry uses the signed shim path.
|
||||
6. Assert that no unsigned kernel/initramfs path is offered in Secure Boot UEFI mode.
|
||||
|
||||
Local developer builds should support a dev-signing mode for QEMU validation:
|
||||
|
||||
```text
|
||||
OMARCHY_SECURE_BOOT_SIGNING_MODE=dev
|
||||
```
|
||||
|
||||
Dev mode creates local test keys and is only expected to boot in QEMU firmware enrolled with those test keys. It must not be presented as Microsoft Secure Boot compatible.
|
||||
|
||||
## Installed-System Flow
|
||||
|
||||
When the live installer detects Secure Boot enabled:
|
||||
|
||||
1. Show a Secure Boot explanation before disk mutation.
|
||||
2. If Windows is detected, warn the user to suspend BitLocker and have the recovery key available.
|
||||
3. Install Omarchy's shim to its own ESP directory, for example `EFI/Omarchy`.
|
||||
4. Do not modify `EFI/Microsoft`.
|
||||
5. Generate a machine-local MOK keypair under the installed encrypted root.
|
||||
6. Build an installed UKI with embedded kernel, initramfs, command line, and OS release data.
|
||||
7. Sign the installed UKI with the machine-local MOK private key.
|
||||
8. Sign the selected second-stage boot manager with the machine-local MOK private key, unless it is release-signed and expected to remain static.
|
||||
9. Schedule public certificate enrollment with `mokutil --import`.
|
||||
10. Tell the user they must complete MokManager enrollment on the next reboot.
|
||||
11. Preserve current firmware boot order by default on dual-boot machines.
|
||||
|
||||
Expected first reboot sequence after install:
|
||||
|
||||
```text
|
||||
Firmware -> Omarchy shim -> MokManager -> user enrolls Omarchy machine key -> reboot -> Omarchy shim -> signed boot manager -> signed UKI -> LUKS unlock
|
||||
```
|
||||
|
||||
## Kernel Update Flow
|
||||
|
||||
Secure Boot installs need deterministic UKI regeneration and signing after kernel or initramfs changes.
|
||||
|
||||
Add a target-system hook that:
|
||||
|
||||
1. Regenerates the initramfs.
|
||||
2. Rebuilds the UKI.
|
||||
3. Signs the UKI with the machine-local MOK private key.
|
||||
4. Writes it atomically to the ESP.
|
||||
5. Keeps at least one previously booted signed UKI as fallback.
|
||||
|
||||
Failure policy:
|
||||
|
||||
- If signing fails, do not replace the currently bootable UKI.
|
||||
- If ESP space is low, keep the current UKI and fail loudly.
|
||||
- If the local signing key is missing, block the update from producing an unsigned boot artifact.
|
||||
|
||||
## File Changes
|
||||
|
||||
### `bin/omarchy-iso-make`
|
||||
|
||||
Add flags:
|
||||
|
||||
```text
|
||||
--secure-boot build Secure Boot-capable ISO flavor
|
||||
--secure-boot-dev build with local test keys for QEMU only
|
||||
```
|
||||
|
||||
Pass mode and signing configuration into the Docker build environment.
|
||||
|
||||
### `builder/build-iso.sh`
|
||||
|
||||
Add Secure Boot build support:
|
||||
|
||||
- Copy shim artifacts into the UEFI boot path.
|
||||
- Build live UKI.
|
||||
- Sign live UKI and second-stage EFI artifact.
|
||||
- Fail official Secure Boot builds if release signing material is unavailable.
|
||||
- Write `/root/omarchy_secure_boot_mode` into the live environment.
|
||||
- Assert that Secure Boot boot entries do not point at unsigned payloads.
|
||||
|
||||
### `builder/secure-boot/`
|
||||
|
||||
Add non-secret Secure Boot build inputs:
|
||||
|
||||
- Public release certificate.
|
||||
- SBAT template.
|
||||
- Artifact manifest with expected shim/MokManager checksums.
|
||||
- Developer test-key generation script for QEMU only.
|
||||
|
||||
Do not store private keys in this directory.
|
||||
|
||||
### `configs/airootfs/root/configurator`
|
||||
|
||||
Add Secure Boot detection and UX:
|
||||
|
||||
- Detect UEFI Secure Boot state through efivars and/or `mokutil --sb-state`.
|
||||
- Detect Windows through ESP vendor paths and partition metadata.
|
||||
- Warn about BitLocker recovery risk when Windows is present.
|
||||
- Explain one-time MokManager enrollment.
|
||||
- Write `secure_boot.sh` with simple shell state for `.automated_script.sh`.
|
||||
|
||||
Example state:
|
||||
|
||||
```bash
|
||||
SECURE_BOOT_ENABLED=true
|
||||
SECURE_BOOT_INSTALL=true
|
||||
SECURE_BOOT_BOOTLOADER=systemd-boot
|
||||
SECURE_BOOT_UKI=true
|
||||
SECURE_BOOT_MOK_COMMON_NAME="Omarchy Machine Owner Key"
|
||||
```
|
||||
|
||||
### `configs/airootfs/root/.automated_script.sh`
|
||||
|
||||
Add Secure Boot install support:
|
||||
|
||||
- Source `secure_boot.sh` after configurator runs.
|
||||
- Install the Secure Boot boot path when `SECURE_BOOT_INSTALL=true`.
|
||||
- Generate machine-local MOK keypair in the installed encrypted root.
|
||||
- Build and sign UKI.
|
||||
- Schedule MOK enrollment with `mokutil --import`.
|
||||
- Install kernel-update hooks for UKI rebuild/signing.
|
||||
- Preserve dual-boot firmware boot order unless explicitly promoted.
|
||||
|
||||
### Target System Files
|
||||
|
||||
Add installed files similar to:
|
||||
|
||||
```text
|
||||
/etc/omarchy/secure-boot.conf
|
||||
/etc/kernel/cmdline
|
||||
/etc/pacman.d/hooks/90-omarchy-uki.hook
|
||||
/usr/local/sbin/omarchy-build-uki
|
||||
/usr/local/sbin/omarchy-sign-uki
|
||||
/boot/EFI/Omarchy/shimx64.efi
|
||||
/boot/EFI/Omarchy/mmx64.efi
|
||||
/boot/EFI/Omarchy/systemd-bootx64.efi
|
||||
/boot/EFI/Linux/omarchy-linux.efi
|
||||
/boot/EFI/Linux/omarchy-linux-fallback.efi
|
||||
```
|
||||
|
||||
Exact paths can change during implementation, but signed boot artifacts must stay under Omarchy-owned ESP directories.
|
||||
|
||||
## Package Requirements
|
||||
|
||||
Live ISO and installed Secure Boot path need tools for:
|
||||
|
||||
- Building UKIs.
|
||||
- Signing PE/COFF EFI binaries.
|
||||
- Importing MOK certificates.
|
||||
- Inspecting Secure Boot state.
|
||||
|
||||
Candidate packages:
|
||||
|
||||
```text
|
||||
sbsigntools
|
||||
efitools
|
||||
mokutil
|
||||
openssl
|
||||
systemd
|
||||
```
|
||||
|
||||
Validate Arch package availability before implementation. Do not assume an official Arch `shim-signed` package exists.
|
||||
|
||||
## Dual-Boot Policy
|
||||
|
||||
On Windows dual-boot machines:
|
||||
|
||||
- Never modify Windows boot files.
|
||||
- Never clear or change TPM state.
|
||||
- Never change BitLocker protectors.
|
||||
- Preserve existing firmware default boot order by default.
|
||||
- Warn that adding a boot entry or changing ESP contents can trigger BitLocker recovery.
|
||||
- Recommend suspending BitLocker from Windows before install and resuming it after both OSes boot.
|
||||
|
||||
If the user chooses to make Omarchy first in boot order, capture the old `BootOrder` and provide rollback guidance.
|
||||
|
||||
## Security Model
|
||||
|
||||
Protected against:
|
||||
|
||||
- Offline replacement of unsigned kernels on the ESP.
|
||||
- Offline modification of initramfs or kernel command line when UKI signature verification is enforced.
|
||||
- Accidental boot of unsigned Omarchy boot payloads in Secure Boot mode.
|
||||
|
||||
Not protected against in v1:
|
||||
|
||||
- A compromised running root user signing malicious future UKIs with the local MOK key.
|
||||
- Evil-maid attacks that trick the user into enrolling a malicious MOK.
|
||||
- Attacks requiring TPM-measured boot or remote attestation.
|
||||
|
||||
Mitigations:
|
||||
|
||||
- Store local private key only on encrypted root.
|
||||
- Make MokManager enrollment instructions explicit and brand-specific.
|
||||
- Display expected certificate fingerprint before reboot.
|
||||
- Keep release signing key outside shipped media.
|
||||
- Keep SBAT metadata current for revocation response.
|
||||
|
||||
## QEMU Validation
|
||||
|
||||
Use QEMU with OVMF Secure Boot enabled and test keys enrolled.
|
||||
|
||||
Test matrix:
|
||||
|
||||
1. Secure Boot ISO boots with test shim/test db in OVMF.
|
||||
2. Unsigned live UKI fails to boot.
|
||||
3. Tampered live UKI fails to boot.
|
||||
4. Secure Boot install completes to empty disk.
|
||||
5. First installed boot enters MokManager enrollment flow.
|
||||
6. After MOK enrollment, installed Omarchy boots and prompts for LUKS unlock.
|
||||
7. Tampered installed UKI fails to boot.
|
||||
8. Kernel update regenerates and signs a new UKI.
|
||||
9. Failed signing leaves previous UKI bootable.
|
||||
10. Dual-boot disk with Windows-style ESP preserves `EFI/Microsoft` and boot order.
|
||||
|
||||
QEMU cannot fully prove Microsoft production trust. Hardware validation is required with firmware that trusts Microsoft UEFI CA and has Secure Boot enabled.
|
||||
|
||||
## Hardware Validation
|
||||
|
||||
Minimum hardware tests:
|
||||
|
||||
1. Microsoft Secure Boot enabled laptop boots official Secure Boot ISO.
|
||||
2. Windows dual-boot install preserves Windows boot.
|
||||
3. BitLocker-suspended install avoids recovery prompt after resuming BitLocker.
|
||||
4. BitLocker-active install warning is visible before any disk mutation.
|
||||
5. Installed Omarchy boots only after MOK enrollment.
|
||||
6. Secure Boot remains enabled after install.
|
||||
7. Kernel update boots with newly signed UKI.
|
||||
8. Manually tampered UKI is rejected.
|
||||
9. Firmware boot order is preserved unless user opted to promote Omarchy.
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
### Phase 1: Research And Decisions
|
||||
|
||||
- Confirm shim submission requirements and timeline.
|
||||
- Confirm selected second-stage boot manager.
|
||||
- Confirm UKI generation mechanism on Arch.
|
||||
- Confirm package availability for signing and MOK workflows.
|
||||
- Decide local MOK key storage and update-hook policy.
|
||||
|
||||
### Phase 2: Dev Secure Boot In QEMU
|
||||
|
||||
- Add dev keys and QEMU OVMF Secure Boot harness.
|
||||
- Build a dev-signed live UKI.
|
||||
- Boot ISO in QEMU with test keys.
|
||||
- Prove unsigned and tampered payload rejection.
|
||||
|
||||
### Phase 3: Installed UKI + MOK Flow
|
||||
|
||||
- Generate local MOK key during install.
|
||||
- Build and sign installed UKI.
|
||||
- Schedule MokManager enrollment.
|
||||
- Boot installed system in QEMU after enrollment.
|
||||
- Add kernel update hook and fallback UKI handling.
|
||||
|
||||
### Phase 4: Official Shim And Release Signing
|
||||
|
||||
- Build Omarchy shim with SBAT metadata and embedded release cert.
|
||||
- Submit shim artifacts for Microsoft signing.
|
||||
- Integrate signed shim artifacts into official build pipeline.
|
||||
- Add release signing with HSM/offline key handling.
|
||||
|
||||
### Phase 5: Consumer Dual-Boot UX
|
||||
|
||||
- Add Secure Boot and BitLocker warnings to configurator.
|
||||
- Preserve Windows boot order by default.
|
||||
- Add MOK enrollment instructions and certificate fingerprint display.
|
||||
- Validate on real Windows hardware.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Official Secure Boot ISO boots on Microsoft Secure Boot hardware without disabling Secure Boot.
|
||||
- Secure Boot install does not ship or expose Omarchy release private keys.
|
||||
- Installed Omarchy boots through shim after MOK enrollment.
|
||||
- Installed Omarchy root remains LUKS-encrypted and passphrase-unlocked.
|
||||
- Kernel updates produce signed UKIs automatically.
|
||||
- Tampered UKIs fail to boot.
|
||||
- Windows ESP contents are preserved.
|
||||
- Windows boot order is preserved by default.
|
||||
- Secure Boot mode never offers an unsigned kernel/initramfs boot entry.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Whether v1 should use systemd-boot, GRUB, or a verified Limine configuration as the second stage.
|
||||
- Whether the local MOK private key should be encrypted with a user-supplied passphrase or stored root-only on encrypted root.
|
||||
- How to support signed out-of-tree kernel modules, especially proprietary GPU drivers or DKMS packages.
|
||||
- How snapshot booting should work when each bootable kernel must be a signed UKI.
|
||||
- Whether official builds should always be Secure Boot-capable or ship a separate Secure Boot ISO flavor.
|
||||
- What user-facing recovery path should exist if MOK enrollment is skipped or fails.
|
||||
@@ -0,0 +1,158 @@
|
||||
# Dual-Boot Installation Support for Omarchy ISO
|
||||
|
||||
## Context
|
||||
|
||||
Currently, the Omarchy installer wipes the entire selected disk (`"wipe": true`) and creates two partitions: a 2GB FAT32 ESP and a LUKS-encrypted Btrfs root. This means users must dedicate an entire disk to Omarchy. Adding dual-boot support lets users install alongside an existing OS (typically Windows) while keeping the Omarchy partition encrypted.
|
||||
|
||||
## Core Approach: `pre_mounted_config`
|
||||
|
||||
**Do our own partitioning, then hand off to archinstall for package installation only.**
|
||||
|
||||
archinstall's `manual_partitioning` mode has a long history of bugs with existing partitions and LUKS (archinstall issues #998, #2072, #2444). Instead, we:
|
||||
|
||||
1. Detect existing partitions and free space ourselves (with `sgdisk`/`parted`)
|
||||
2. Create the LUKS partition and Btrfs subvolumes ourselves (with `cryptsetup`/`mkfs.btrfs`)
|
||||
3. Mount everything at `/mnt/archinstall`
|
||||
4. Tell archinstall to use `"config_type": "pre_mounted_config"` — it installs packages into our pre-mounted tree and does zero disk operations
|
||||
5. Handle encryption config (crypttab, mkinitcpio hooks, bootloader cmdline) in post-install
|
||||
|
||||
The existing full-disk path stays completely unchanged. Dual-boot is a parallel code path.
|
||||
|
||||
## Implementation Guardrails
|
||||
|
||||
- **Run target changes in chroot** — all target-system commands (`mkinitcpio -P`, `limine-scan`, bootloader config updates) must run via `arch-chroot "$INSTALL_ROOT" ...`
|
||||
- **Edit target files under `$INSTALL_ROOT` only** — write `mkinitcpio` hook config, `limine.conf`, `crypttab`, and related files in `$INSTALL_ROOT/etc` and `$INSTALL_ROOT/boot` (never in the live environment root)
|
||||
- **Settle kernel partition state** — after creating the new partition, run `partprobe "$DISK"` and `udevadm settle` before `cryptsetup` to avoid device-node races
|
||||
- **Deterministic fstab** — always generate `/etc/fstab` for dual-boot with overwrite semantics (e.g., `genfstab -U "$INSTALL_ROOT" > "$INSTALL_ROOT/etc/fstab"`) to avoid duplicate entries on retries
|
||||
- **Hook compatibility** — when updating mkinitcpio hooks, add `encrypt` for busybox initramfs or `sd-encrypt` for systemd initramfs; never assume one universally
|
||||
- **Safe rollback on failure** — on any dual-boot setup failure: unmount mounts, close LUKS mapping, and surface recovery guidance; optionally delete the just-created partition only if it was created in this run
|
||||
|
||||
## Scope Constraints
|
||||
|
||||
- **UEFI/GPT only** — dual-boot on MBR/BIOS is too fragile; those users get full-disk only
|
||||
- **No auto-shrinking** — users must free up space from their existing OS first (e.g., Windows Disk Management). Too risky to shrink partitions automatically
|
||||
- **Minimum 20GB free space** required for Omarchy
|
||||
|
||||
## User Flow
|
||||
|
||||
```
|
||||
Select disk → Detect layout →
|
||||
|
||||
If disk has existing OS + ESP + ≥20GB free space:
|
||||
"How would you like to install?"
|
||||
→ "Install alongside Windows (use 85GB free space)"
|
||||
→ "Erase entire disk"
|
||||
|
||||
If disk is empty or no free space:
|
||||
Existing "Confirm overwriting" flow (unchanged)
|
||||
```
|
||||
|
||||
## File Changes
|
||||
|
||||
### 1. `configs/airootfs/root/configurator`
|
||||
|
||||
**Add after disk selection (line 233), before config generation:**
|
||||
|
||||
- `detect_disk_layout()` — examines selected disk for:
|
||||
- GPT partition table (required for dual-boot)
|
||||
- Existing ESP partition (via `lsblk` PARTTYPE matching EFI GUID)
|
||||
- Free unallocated space (via `sgdisk -F`/`-E` or `parted print free`)
|
||||
- OS detection for installer UX warnings/prompts (not boot correctness): mount ESP read-only, check common vendor paths like `EFI/Microsoft`, `EFI/systemd`, `EFI/ubuntu`, `EFI/fedora`, `EFI/GRUB`
|
||||
|
||||
- `install_mode_form()` — shows gum choose with dual-boot vs erase options when conditions are met. Sets `DUAL_BOOT=true/false`.
|
||||
|
||||
**Generate different JSON when `DUAL_BOOT=true`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"disk_config": {
|
||||
"config_type": "pre_mounted_config",
|
||||
"mountpoint": "/mnt/archinstall"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
No `device_modifications`, no `disk_encryption`, no `btrfs_options` — we handle all of that ourselves. Everything else (hostname, kernels, packages, etc.) stays the same.
|
||||
|
||||
**Write `install_mode.sh`** alongside the other config files:
|
||||
|
||||
```bash
|
||||
DUAL_BOOT=true
|
||||
DISK=/dev/nvme0n1
|
||||
ESP_PARTITION=/dev/nvme0n1p1
|
||||
```
|
||||
|
||||
### 2. `configs/airootfs/root/.automated_script.sh`
|
||||
|
||||
**Source `install_mode.sh`** in `run_configurator()` if it exists.
|
||||
|
||||
**Add `setup_dual_boot_partitions()`** — runs before archinstall when `DUAL_BOOT=true`:
|
||||
|
||||
1. Select the largest contiguous free region and run `sgdisk` to create new partition there (type 8309 = Linux LUKS)
|
||||
2. `partprobe "$DISK"` + `udevadm settle` so new partition node is present
|
||||
3. `cryptsetup luksFormat` + `luksOpen` with user's password
|
||||
4. `mkfs.btrfs` on `/dev/mapper/omarchy_root`
|
||||
5. Create Btrfs subvolumes: `@`, `@home`, `@log`, `@pkg`
|
||||
6. Mount subvolumes at `/mnt/archinstall` with `compress=zstd`
|
||||
7. Mount existing ESP (untouched) at `/mnt/archinstall/boot`
|
||||
8. Generate target `/etc/fstab` from mounted filesystems before post-install steps
|
||||
|
||||
**Add `configure_dual_boot_post_install()`** — runs after archinstall:
|
||||
|
||||
1. Write `/etc/crypttab` entry using LUKS UUID/PARTUUID (stable identifier), e.g. `omarchy_root UUID=<luks-uuid> none luks,discard`
|
||||
2. Update mkinitcpio hooks before `filesystems`: `encrypt` (busybox) or `sd-encrypt` (systemd)
|
||||
3. Regenerate initramfs with `arch-chroot "$INSTALL_ROOT" mkinitcpio -P`
|
||||
4. Configure Limine kernel cmdline to match initramfs mode:
|
||||
- busybox `encrypt`: `cryptdevice=UUID=<uuid>:omarchy_root root=/dev/mapper/omarchy_root`
|
||||
- systemd `sd-encrypt`: `rd.luks.name=<uuid>=omarchy_root root=/dev/mapper/omarchy_root` (or equivalent `rd.luks.uuid=` form)
|
||||
5. Run `arch-chroot "$INSTALL_ROOT" limine-scan` to detect other OS boot entries (Windows, etc.)
|
||||
6. Preserve current firmware default boot order unless user explicitly chooses to make Limine first (capture and restore `BootOrder` if `efibootmgr` side effects reorder entries)
|
||||
|
||||
**Use `$INSTALL_ROOT` variable** — defaults to `/mnt` for full-disk, `/mnt/archinstall` for dual-boot. Post-install steps (offline mirror bind-mount, sudoers, omarchy copy) reference this instead of hardcoded `/mnt`.
|
||||
|
||||
### 3. `builder/build-iso.sh` + `builder/archinstall.packages`
|
||||
|
||||
Add `parted` to packages available in the live ISO (needed for free space detection).
|
||||
|
||||
## Bootloader Coexistence
|
||||
|
||||
Limine installs to `ESP/EFI/arch-limine/` and registers via `efibootmgr`. This sits alongside:
|
||||
- Windows Boot Manager at `ESP/EFI/Microsoft/Boot/bootmgfw.efi`
|
||||
- Any other bootloader in its own ESP subdirectory
|
||||
|
||||
`limine-scan` auto-detects other UEFI boot entries and adds them to `limine.conf`. Users can choose OS at boot.
|
||||
|
||||
**Boot order policy**: creating a new EFI entry must not silently change the current default boot target. Keep existing firmware boot order by default and only promote Limine when user opts in.
|
||||
|
||||
Implementation note: when registering Limine with `efibootmgr`, do not pass reorder flags by default; if firmware still changes order, explicitly restore prior `BootOrder` unless the user opted to promote Limine.
|
||||
|
||||
**ESP size note**: Windows typically creates a 100-260MB ESP. This is enough for Limine + kernel + initramfs, but Snapper boot snapshots (which copy kernels to ESP) will be limited or disabled.
|
||||
|
||||
**ESP free-space threshold**:
|
||||
- If ESP has `<100MB` free at install time: show a strong warning (do not block install)
|
||||
- If ESP has `<64MB` free: hide dual-boot option and require full-disk install (or user frees space first)
|
||||
|
||||
**BitLocker warning**: If Windows is detected, show a warning about having the BitLocker recovery key ready, since modifying ESP/UEFI entries can trigger recovery.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| Disk is empty | Full-disk install only (current behavior) |
|
||||
| Existing OS but no free space | Full-disk install only, with note to free space first |
|
||||
| Free space < 20GB | Dual-boot option not shown |
|
||||
| MBR/BIOS disk | Dual-boot option not shown |
|
||||
| ESP < 100MB free after mount | Warn user; continue allowed |
|
||||
| ESP < 64MB free after mount | Dual-boot option not shown |
|
||||
| Multiple free regions | Use largest contiguous region |
|
||||
| Partitioning/setup failure mid-run | Cleanup mounts/LUKS and abort safely |
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Regression**: Full-disk install in VM with empty disk — must work identically to today
|
||||
2. **Dual-boot with Windows**: VM with Windows installed + 40GB free space — detect Windows, install alongside, both OSes boot
|
||||
3. **Dual-boot with Linux**: VM with another Linux + free space — similar test
|
||||
4. **No free space**: VM with fully partitioned disk — dual-boot option should not appear
|
||||
5. **Encryption**: After dual-boot install, verify LUKS unlock prompt at boot and encrypted root
|
||||
6. **Initramfs mode**: Validate both busybox and systemd hook paths boot correctly (`encrypt` vs `sd-encrypt`)
|
||||
7. **Boot order**: Verify existing default UEFI entry remains unchanged unless user opted to promote Limine
|
||||
@@ -0,0 +1,224 @@
|
||||
# OEM Install + First-Boot Setup Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Create a second ISO flavor for OEM preloading that is installed to internal storage at factory time, then boots into customer setup on first power-on (no external media required), and ends in a fully encrypted Omarchy install.
|
||||
|
||||
## Product Requirements
|
||||
|
||||
- Factory can preload one image to internal disk.
|
||||
- Customer first boot launches setup flow automatically (already works: `.automated_script.sh` runs configurator on tty1).
|
||||
- Customer chooses credentials/settings.
|
||||
- Final installed OS uses full-disk encryption (LUKS) with customer password (already works: configurator collects password and passes it to archinstall as `encryption_password`).
|
||||
- Existing consumer ISO behavior remains unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Chosen Architecture
|
||||
|
||||
### Bootstrapping model
|
||||
- OEM devices ship with an OEM bootstrap ISO image written to internal disk.
|
||||
- On first boot, `copytoram=y` kernel arg loads the live environment into RAM, freeing the internal disk.
|
||||
- Bootstrap environment launches existing configurator + installer.
|
||||
- Installer performs a fresh install to internal disk with LUKS encryption enabled.
|
||||
- Bootstrap environment is overwritten during install.
|
||||
|
||||
### Recovery model: zero-recovery
|
||||
- No permanent recovery partition.
|
||||
- Simplest and strongest security story.
|
||||
- Recovery requires external reinstall media.
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No on-device recovery workflow.
|
||||
- No changes to normal installer UX outside explicit OEM mode checks and warnings.
|
||||
- No factory credential provisioning.
|
||||
|
||||
---
|
||||
|
||||
## Repo Implementation Plan
|
||||
|
||||
## 1) Add OEM build mode
|
||||
|
||||
### Files
|
||||
- `bin/omarchy-iso-make`
|
||||
- `builder/build-iso.sh`
|
||||
|
||||
### Changes
|
||||
- Add CLI flag `--oem` to `bin/omarchy-iso-make`.
|
||||
- Pass `OMARCHY_MODE=oem` into Docker build env (default `normal`).
|
||||
- In `builder/build-iso.sh`, write mode marker `/root/omarchy_mode` containing `oem` or `normal` (same pattern as `omarchy_mirror`).
|
||||
- Keep OEM on current boot stack (`uefi.grub` + `bios.syslinux`); do not add or depend on systemd-boot paths.
|
||||
- When `OMARCHY_MODE=oem`, inject `copytoram=y` into all shipped OEM boot entry points before `mkarchiso` runs:
|
||||
- `grub/grub.cfg`: append to `linux` cmdline entries.
|
||||
- `grub/loopback.cfg`: append to `linux` cmdline entries.
|
||||
- `syslinux/archiso_sys-linux.cfg`: append to `APPEND` lines.
|
||||
- When `OMARCHY_MODE=oem`, remove accessibility/speakup boot entries:
|
||||
- `grub/grub.cfg`: remove `archlinux-accessibility` menuentry.
|
||||
- `grub/loopback.cfg`: remove `archlinux-accessibility` menuentry.
|
||||
- `syslinux/archiso_sys-linux.cfg`: remove `arch64speech` label block.
|
||||
- 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_ISO_REF` suffix).
|
||||
|
||||
### 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 OEM boot config still contains accessibility/speakup entries.
|
||||
- Make injection idempotent (do not duplicate `copytoram=y` if already present).
|
||||
|
||||
### Acceptance criteria
|
||||
- `./bin/omarchy-iso-make --oem` builds successfully.
|
||||
- OEM ISO boot entries contain `copytoram=y` in all supported boot paths.
|
||||
- OEM ISO does not expose accessibility/speakup boot entries.
|
||||
- Normal build output and behavior remain unchanged.
|
||||
|
||||
### Suggested verification commands
|
||||
- `bsdtar -xOf out/<iso>.iso boot/grub/grub.cfg | rg copytoram=y`
|
||||
- `bsdtar -xOf out/<iso>.iso boot/grub/loopback.cfg | rg copytoram=y`
|
||||
- `bsdtar -xOf out/<iso>.iso boot/syslinux/archiso_sys-linux.cfg | rg copytoram=y`
|
||||
- `bsdtar -xOf out/<iso>.iso boot/grub/grub.cfg | rg 'accessibility|speakup'` (should return no matches)
|
||||
- `bsdtar -xOf out/<iso>.iso boot/syslinux/archiso_sys-linux.cfg | rg 'speech|accessibility'` (should return no matches)
|
||||
|
||||
---
|
||||
|
||||
## 2) Add OEM safety guardrails
|
||||
|
||||
### Why
|
||||
OEM image boots from internal disk and later wipes that same disk. Live environment must be fully in RAM before install begins.
|
||||
|
||||
### File
|
||||
- `configs/airootfs/root/.automated_script.sh`
|
||||
|
||||
### Changes
|
||||
- Before `run_configurator`, if `/root/omarchy_mode` is `oem`:
|
||||
- Verify `/proc/cmdline` contains `copytoram=y`; abort with clear actionable error if missing.
|
||||
- Verify factory hardware minimum using `MemTotal`:
|
||||
- Require `MemTotal >= 8 GiB`.
|
||||
- Abort with clear error if below minimum.
|
||||
- Verify runtime memory safety using `MemAvailable`:
|
||||
- Minimum threshold: `1.5 x ISO size + 1 GiB`.
|
||||
- Abort with clear error if below threshold.
|
||||
- Verify runtime state indicates live root is in RAM (for example, expected archiso runtime mounts present and boot media not actively mounted as install target path).
|
||||
|
||||
### Acceptance criteria
|
||||
- OEM mode aborts safely if `copytoram=y` missing from cmdline.
|
||||
- OEM mode aborts safely if `MemTotal < 8 GiB`.
|
||||
- OEM mode aborts safely if `MemAvailable` below threshold.
|
||||
- OEM mode aborts safely if runtime state does not confirm RAM-backed live environment.
|
||||
|
||||
### Error message requirements
|
||||
- State exactly what failed.
|
||||
- State why install is blocked.
|
||||
- Provide next step (reboot using proper OEM media or use external recovery media).
|
||||
|
||||
---
|
||||
|
||||
## 3) OEM disk warning
|
||||
|
||||
### Why
|
||||
With `copytoram=y`, archiso copies squashfs to RAM and releases boot device. Existing configurator call `findmnt -no SOURCE /run/archiso/bootmnt` may return empty, so exclusion filter is skipped and all physical disks are shown, including former boot disk. No disk selection logic change is required.
|
||||
|
||||
OEM mode still needs an unmistakable warning before destructive install proceeds.
|
||||
|
||||
### File
|
||||
- `configs/airootfs/root/configurator`
|
||||
|
||||
### Changes
|
||||
- Read mode from `/root/omarchy_mode`.
|
||||
- If mode is `oem`, add OEM-specific warning text before existing destructive confirmation.
|
||||
- Upgrade confirmation in OEM mode to explicit typed confirmation (for example: `YES, WIPE DISK`) while preserving existing normal-mode flow.
|
||||
|
||||
### Acceptance criteria
|
||||
- Normal ISO behavior is unchanged.
|
||||
- OEM ISO shows OEM-specific warning before disk wipe confirmation.
|
||||
- OEM typed confirmation is required before destructive action.
|
||||
- In VM with `copytoram=y`, former boot disk appears in disk list without exclusion-logic changes.
|
||||
|
||||
---
|
||||
|
||||
## 4) Documentation
|
||||
|
||||
### File
|
||||
- `README.md`
|
||||
|
||||
### Changes
|
||||
- Document `--oem` build flag and resulting artifact naming.
|
||||
- Document factory workflow: writing OEM ISO to internal disk and expected first-boot UX.
|
||||
- Document failure and recovery behavior:
|
||||
- Missing `copytoram=y`, `MemTotal < 8 GiB`, or low `MemAvailable` causes safe abort.
|
||||
- Interrupted/failed install requires external reinstall media.
|
||||
- Add short operator checklist for manufacturing line validation.
|
||||
|
||||
---
|
||||
|
||||
## 5) Validation
|
||||
|
||||
### VM test matrix
|
||||
1. Build OEM ISO.
|
||||
2. Write OEM ISO to virtual disk (not external media emulation).
|
||||
3. Boot from virtual disk and verify:
|
||||
- `/proc/cmdline` contains `copytoram=y`.
|
||||
- Guardrail checks pass in healthy case.
|
||||
- OEM boot menu does not include accessibility/speech entries.
|
||||
4. Complete setup flow on first boot.
|
||||
5. Install to same disk.
|
||||
6. Reboot into installed OS.
|
||||
7. Verify encryption:
|
||||
- `lsblk` shows crypt mapping.
|
||||
- `cryptsetup status` reports active LUKS device.
|
||||
- Boot requires unlock passphrase.
|
||||
8. Validate OEM warning + typed confirmation appears and gates destructive action.
|
||||
|
||||
### Boot path coverage
|
||||
- UEFI + GRUB path.
|
||||
- BIOS/syslinux path (if still shipped).
|
||||
|
||||
### Hardware test matrix
|
||||
- UEFI test on at least two hardware families (Intel + AMD).
|
||||
- Confirm no external media required.
|
||||
- Confirm first boot starts setup automatically.
|
||||
|
||||
### Negative tests
|
||||
- OEM boot without `copytoram=y` -> safe abort with clear error.
|
||||
- `MemTotal < 8 GiB` -> safe abort with clear error.
|
||||
- Low `MemAvailable` -> safe abort with clear error.
|
||||
- Runtime state not RAM-backed -> safe abort with clear error.
|
||||
- Power interruption before installer starts -> deterministic reboot behavior.
|
||||
- Power interruption during install -> recovery requires external media (documented).
|
||||
|
||||
---
|
||||
|
||||
## 6) Security hardening checklist
|
||||
|
||||
- No factory credentials exist; customer sets all passwords at first boot.
|
||||
- Clear shell history and transient setup artifacts at end of install.
|
||||
- Verify no secrets in logs shipped to customer.
|
||||
- Regenerate host identity on first installed boot as needed:
|
||||
- `machine-id`
|
||||
- SSH host keys (if installed/enabled)
|
||||
- Keep package signature verification and keyring handling intact.
|
||||
|
||||
---
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
1. Implement OEM build flag, mode marker, boot config injection, and build assertions.
|
||||
2. Implement copytoram, memory, and runtime-state guardrails in `.automated_script.sh`.
|
||||
3. Add OEM warning text and typed destructive confirmation in configurator.
|
||||
4. Add docs and operator checklist.
|
||||
5. Run VM matrix, including negative tests.
|
||||
6. Run hardware matrix.
|
||||
7. Cut first OEM preproduction release for manufacturing pilot.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- `--oem` build exists and is documented.
|
||||
- Device preloaded with OEM image boots directly to setup.
|
||||
- Setup installs to internal disk without external media.
|
||||
- Final installed system is LUKS-encrypted with customer-owned passphrase.
|
||||
- OEM safety guardrails fail closed with actionable errors.
|
||||
- Existing non-OEM ISO behavior remains unchanged.
|
||||
- VM and hardware QA matrix pass.
|
||||
@@ -0,0 +1,643 @@
|
||||
# Protected Partition Install Plan: Dual-Boot + Restore
|
||||
|
||||
## Goal
|
||||
|
||||
Implement one shared non-destructive install path that supports both:
|
||||
|
||||
- Dual-boot installs alongside an existing OS.
|
||||
- OEM/restore installs from a permanent internal restore partition set.
|
||||
|
||||
Beyond install: every standalone Omarchy machine — whether OEM-provisioned or installed from the consumer ISO onto an empty disk — exposes a "factory reset" command that durably stages the next boot into the on-disk restore partition set. The installed-OS command leaves the machine as though it had just been prepared to boot from restore; the restore environment owns the destructive wipe/reinstall flow. Dual-boot installs do not get factory reset (no room for a restore partition set).
|
||||
|
||||
The current full-disk install path remains unchanged until the protected-partition path is proven. The protected path does its own partitioning, encryption, filesystems, mounts, and boot configuration, then hands a prepared mount tree to archinstall with `pre_mounted_config`.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
Use a single installer engine with multiple partition policies.
|
||||
|
||||
```text
|
||||
full_disk existing archinstall default_layout + wipe:true path
|
||||
dual_boot protect existing OS partitions, reuse existing ESP, add Omarchy LUKS root
|
||||
restore_install protect restore partition set, create/recreate installed ESP + Omarchy LUKS root
|
||||
```
|
||||
|
||||
The key rule: protected modes must never pass the whole disk to archinstall with `wipe: true`.
|
||||
|
||||
## Why This Combines Both Features
|
||||
|
||||
Dual-boot and restore installs are the same technical problem:
|
||||
|
||||
1. Detect disk layout.
|
||||
2. Identify protected partitions.
|
||||
3. Create or format only explicitly writable Omarchy partitions.
|
||||
4. Set up encrypted root manually.
|
||||
5. Mount a complete install tree manually.
|
||||
6. Run archinstall only as a package/base-system installer.
|
||||
7. Configure encryption, fstab, initramfs, and bootloader after archinstall.
|
||||
|
||||
Only the partition policy differs.
|
||||
|
||||
## Scope Constraints
|
||||
|
||||
- UEFI/GPT only for `dual_boot` and `restore_install` v1.
|
||||
- Existing BIOS/syslinux ISO behavior stays available for normal full-disk installs.
|
||||
- No automatic shrinking of existing partitions.
|
||||
- Dual-boot requires pre-existing free space.
|
||||
- Restore install requires a detected Omarchy restore partition set.
|
||||
- Installed Omarchy root is encrypted with LUKS.
|
||||
- Restore partitions are not encrypted unless a later product requirement demands it.
|
||||
- Restore payload partitions are normally left unmounted in the installed OS and mounted read-only by the restore environment. A separate restore state partition is the only restore-owned partition mounted read-write for reset triggers and recovery logs.
|
||||
- Swap matches the current full-disk install: zram for memory pressure, plus a Btrfs swapfile in a dedicated `NODATACOW` `/swap` subvolume sized to RAM for hibernation. The subvolume, swapfile, `resume` mkinitcpio hook, and `resume=<dev> resume_offset=<offset>` Limine cmdline drop-in are created post-install by Omarchy's existing `omarchy-hibernation-setup`. Protected modes must not break this: the `encrypt`/`sd-encrypt` hook must precede `resume` in mkinitcpio so the swapfile is reachable after LUKS unlock, and Limine must honor `/etc/limine-entry-tool.d/*.conf` drop-ins so the post-install hook can append resume parameters. No swap partition.
|
||||
|
||||
## Partition Policies
|
||||
|
||||
### Full Disk
|
||||
|
||||
Current behavior:
|
||||
|
||||
```text
|
||||
p1 installed ESP FAT32 2G
|
||||
p2 installed root LUKS rest of disk
|
||||
```
|
||||
|
||||
Implementation stays on archinstall `default_layout` with `wipe: true` for now.
|
||||
|
||||
Phase 7 replaces this with a layout that mirrors `restore_install`:
|
||||
|
||||
```text
|
||||
p1 restore ESP FAT32 protected after install
|
||||
p2 restore payload ext4 protected after install, read-only payload
|
||||
p3 restore state ext4 protected after install, writable trigger/log state
|
||||
p4 installed ESP FAT32 writable
|
||||
p5 installed root LUKS writable
|
||||
```
|
||||
|
||||
Rationale: factory reset requires an on-disk restore partition set. Making this the default for standalone installs gives every consumer machine the same self-recovery capability OEM-provisioned machines get. The legacy two-partition layout becomes opt-in (e.g. `--no-restore`) for users who explicitly want the disk space back.
|
||||
|
||||
### Dual Boot
|
||||
|
||||
Example target disk before install:
|
||||
|
||||
```text
|
||||
p1 existing ESP FAT32 protected, reused as /efi
|
||||
p2 Windows MSR MSR protected
|
||||
p3 Windows NTFS protected
|
||||
free space writable region
|
||||
```
|
||||
|
||||
After install:
|
||||
|
||||
```text
|
||||
p1 existing ESP FAT32 protected, mounted as /efi during install
|
||||
p2 Windows MSR MSR protected
|
||||
p3 Windows NTFS protected
|
||||
p4 Omarchy root LUKS new encrypted Btrfs root
|
||||
```
|
||||
|
||||
Policy:
|
||||
|
||||
- Protect all existing partitions.
|
||||
- Reuse the existing ESP only for Omarchy boot files under `EFI/Omarchy`. Do not write top-level `vmlinuz-*`, `initramfs-*`, or vendor-looking paths on a shared ESP.
|
||||
- Mount the shared ESP at `/efi`, keep `/boot` on the encrypted root, and install an Omarchy-owned kernel/initramfs sync hook that copies boot artifacts into `/efi/EFI/Omarchy` after kernel or initramfs updates.
|
||||
- Refuse to install if the existing ESP is below 200 MB total or has less than 150 MB free. Warn below 300 MB total or below 200 MB free. These thresholds size for one Omarchy kernel + initramfs + Limine alongside a typical vendor loader; revisit if the Omarchy initramfs grows materially. Free space must be measured from the mounted ESP, not inferred from partition size.
|
||||
- Create one new LUKS partition in the largest single contiguous free region of at least 40 GB. Refuse otherwise. Do not stitch across multiple non-contiguous regions.
|
||||
- Preserve current firmware boot order unless the user explicitly opts to make Omarchy first.
|
||||
- Abort with a clear error if any Windows partition is detected as BitLocker/FVE, including suspended BitLocker. Do not proceed past detection. Adding a new partition to a disk with a BitLocker-protected volume changes the firmware/boot environment enough that Windows can demand the recovery key on next boot — and many users do not have it. The error message must instruct the user to turn off BitLocker from inside Windows and wait for decryption to complete, then reboot into the Omarchy installer and retry. Do not offer an "I have my recovery key, continue anyway" override in v1; users who really want it can fully disable BitLocker themselves.
|
||||
- Warn if non-BitLocker Windows is detected, since boot order changes can still surprise users.
|
||||
|
||||
### Restore Install
|
||||
|
||||
Recommended factory disk image layout:
|
||||
|
||||
```text
|
||||
p1 restore ESP FAT32 protected, boots restore environment
|
||||
p2 restore payload ext4 protected, contains live installer/offline payload
|
||||
p3 restore state ext4 protected from formatting, writable reset trigger/log state
|
||||
p4 installed ESP FAT32 writable, created/recreated by installer
|
||||
p5 installed root LUKS writable, created/recreated by installer
|
||||
```
|
||||
|
||||
Policy:
|
||||
|
||||
- Protect restore ESP, restore payload, and restore state partitions from delete/format operations. Only the restore state partition is mounted read-write, and only for trigger files and logs.
|
||||
- On first boot, create installed ESP and installed root in remaining disk space.
|
||||
- On reinstall, format the installed Omarchy partitions in place by default. Only delete and recreate them when their geometry no longer matches policy (e.g. the disk was resized). Format-in-place preserves PARTUUIDs, keeps NVRAM entries valid, and avoids touching the partition table.
|
||||
- Set installed Omarchy first in boot order after successful install.
|
||||
- Keep restore boot available as firmware fallback and as a boot menu entry.
|
||||
- Require `copytoram=y` before mutating the internal disk from restore mode.
|
||||
|
||||
Use separate restore and installed ESPs for v1. A shared ESP saves space but makes restore easier to break during normal bootloader maintenance or reinstall.
|
||||
|
||||
## Restore Image Provisioning
|
||||
|
||||
Create a factory artifact separate from the normal consumer ISO:
|
||||
|
||||
```text
|
||||
omarchy-restore.img.zst
|
||||
```
|
||||
|
||||
Factory provisioning writes this image to the internal disk once:
|
||||
|
||||
```bash
|
||||
zstd -dc omarchy-restore.img.zst | dd of=/dev/nvme0n1 bs=16M status=progress conv=fsync
|
||||
```
|
||||
|
||||
The restore image should contain:
|
||||
|
||||
- A restore ESP with `EFI/BOOT/BOOTX64.EFI` fallback boot path.
|
||||
- A restore payload partition labeled `OMARCHY_RESTORE`.
|
||||
- A small restore state partition labeled `OMARCHY_STATE` for reset triggers and recovery logs.
|
||||
- The archiso live environment payload, offline package mirror, configurator, and installer scripts.
|
||||
- A boot entry that passes `omarchy.mode=restore copytoram=y`.
|
||||
- Boot configuration that locates the restore payload by stable label/UUID, not by transient disk name.
|
||||
- A signed manifest covering the archiso payload and offline mirror. The verification public key must be shipped outside the mutable restore payload, such as in the restore boot environment and installed reset tool package.
|
||||
|
||||
The factory flasher must verify the integrity of `omarchy-restore.img.zst` before writing. The on-disk restore environment must verify its signed payload manifest at boot before mutating the internal disk. A plain checksum stored on the same writable disk is not sufficient. Without a signed manifest, a tampered or corrupted restore image silently destroys the user's data the next time they invoke "factory reset."
|
||||
|
||||
On first restore boot:
|
||||
|
||||
1. Bind the candidate restore disk to the actual boot source (`/run/archiso/bootmnt`, kernel command line, or equivalent). Do not select an arbitrary disk just because it has matching labels; cloned restore images can have duplicate labels and UUIDs.
|
||||
2. Move the backup GPT header to the real end of disk with `sgdisk -e` if the factory image was written to a larger disk. This relocates the header only — existing partitions are not grown. The newly-available space at the end of the disk is what installed ESP and installed root consume.
|
||||
3. On first boot only, randomize the GPT disk GUID, partition GUIDs/PARTUUIDs, and filesystem UUIDs for restore-owned partitions before generating any NVRAM entries, fstab entries, or installer state. Labels may remain stable; UUIDs must not stay cloned across machines.
|
||||
4. Detect and protect restore partitions by the newly-randomized PARTUUIDs.
|
||||
5. Create installed ESP and installed root in remaining space.
|
||||
6. Continue through the shared protected install path.
|
||||
|
||||
## Factory Reset
|
||||
|
||||
Goal: an installed Omarchy machine that has a restore partition set (any standalone install — `restore_install` today, `full_disk` after Phase 7) exposes a user-facing factory reset command that prepares the device to boot from internal restore without external media. After the next restore boot, the restore environment returns the device to a fresh-install state.
|
||||
|
||||
### User Flow
|
||||
|
||||
1. User runs `omarchy-factory-reset` from a terminal with sudo. (A GUI entry under Settings is deferred to a follow-up.)
|
||||
2. The command prints which partitions will be destroyed (installed ESP, installed root) and which will be preserved (restore ESP, restore payload, restore state), by stable identifier, and asks the user to type a confirmation phrase. The typed-phrase gate is intentional friction; no `--yes` shortcut in v1.
|
||||
3. On confirmation, the command writes a trigger file to the restore state partition, verifies or recreates the restore boot entry, and sets `BootNext` via `efibootmgr` to that restore entry.
|
||||
4. The command exits only after the trigger file is fsynced, the restore state partition is unmounted, and `BootNext` is confirmed. At that point the installed-OS side is complete: the machine has been left as though it had just been prepared to boot from the restore partition.
|
||||
5. The user reboots when ready. A later GUI may offer a "reboot now" action, but rebooting is a convenience after the staged state is durable, not part of the safety-critical mutation.
|
||||
6. The restore environment detects the trigger on boot, verifies the signed restore payload manifest, writes a reset-in-progress marker to restore state, and temporarily sets persistent `BootOrder` to restore first before mutating installed partitions.
|
||||
7. The restore environment runs the existing `restore_install` reinstall path — format installed ESP, LUKS-format installed root, run the standard install flow — non-interactively, then deletes the trigger and reset-in-progress marker.
|
||||
8. On install completion, the restore environment restores persistent `BootOrder` to installed Omarchy first, restore second, sets `BootNext` to the installed Omarchy entry, and reboots.
|
||||
9. The user lands in a fresh installed Omarchy. Normal persistent `BootOrder` (Omarchy first, restore second) has been restored.
|
||||
|
||||
User data on installed ESP and installed root is destroyed. Restore ESP, restore payload, and restore state are protected from destructive operations throughout. The restore payload is the source of truth for the rebuild; restore state contains only trigger and recovery state.
|
||||
|
||||
### Trigger Mechanism
|
||||
|
||||
Use a flag file on the restore state partition rather than NVRAM variables or the restore payload. NVRAM is fragile across firmware updates, and the restore payload should stay immutable/read-only so signed manifest verification remains meaningful.
|
||||
|
||||
- Path: `<restore-state-mount>/.omarchy-factory-reset`
|
||||
- Contents: timestamp, hostname at trigger time, optional reason. Plain text, one `key=value` per line.
|
||||
- The installed-OS command mounts the restore state partition by `OMARCHY_STATE` label, writes the flag, fsync, and unmounts. It may mount the restore payload read-only only long enough to verify the signed manifest before triggering.
|
||||
- The restore environment checks for the flag at boot. If present, it runs the reset flow non-interactively and deletes the flag on success. If absent, restore boot behaves normally (manual install/repair menu).
|
||||
- The flag must be deleted before reboot. Otherwise the machine resets itself in a loop.
|
||||
|
||||
### Boot Order
|
||||
|
||||
`efibootmgr --bootnext <restore-entry>` is one-shot — it takes effect on the next boot only, then NVRAM reverts to the existing `BootOrder`. This is what we want before the destructive phase: if the reset is interrupted before restore boot, the next normal boot still tries the installed Omarchy first.
|
||||
|
||||
After the restore environment accepts the trigger and before it wipes installed partitions, it must temporarily set persistent `BootOrder` to restore first, installed Omarchy second. This prevents a power loss during reinstall from leaving firmware stuck on a half-written installed ESP. On successful reinstall, the restore environment deletes the reset-in-progress marker, restores persistent `BootOrder` to installed Omarchy first and restore second, then sets `BootNext` to the installed Omarchy entry to land the user in their fresh OS.
|
||||
|
||||
If the installed OS cannot write and confirm `BootNext`, the staging command must remove any trigger it wrote and abort; a manual reinstall can still proceed through the firmware boot menu, but `omarchy-factory-reset` has not successfully prepared the machine. If the restore environment cannot set temporary restore-first `BootOrder` for an unattended reset, abort before destructive operations; manual reinstall can still proceed with an explicit warning.
|
||||
|
||||
### Safety
|
||||
|
||||
The factory reset command is a staging tool, not the destructive installer. It must:
|
||||
|
||||
- Require root and refuse to run otherwise.
|
||||
- Refuse to run if no complete restore partition set is detected (restore ESP, restore payload, restore state), e.g. on a dual-boot machine or a `--no-restore` full-disk install.
|
||||
- Refuse to run if the signed restore payload manifest check fails — a corrupt restore is not recoverable from inside the running OS, and triggering a reset would brick the machine.
|
||||
- Verify the restore boot entry exists in NVRAM before triggering; if not, repopulate it from `EFI/BOOT/BOOTX64.EFI` fallback or abort with a message.
|
||||
- Confirm `BootNext` points to the restore boot entry before exiting, so the system is actually prepared to boot restore.
|
||||
- Print a final pre-confirmation list of partitions that will be destroyed, by stable identifier, so the user sees exactly what they are committing to.
|
||||
|
||||
The restore environment, when running due to the trigger flag, must still apply every guard from the Safety Model. The trigger is permission to run the reinstall flow, not permission to skip safety checks.
|
||||
|
||||
### File Changes for Factory Reset
|
||||
|
||||
- Ship `omarchy-factory-reset` on the installed system. Source location depends on Omarchy's package boundary — either as part of the Omarchy package on the offline mirror, or copied from the live ISO into the installed system during install. Pick one consistent approach during implementation.
|
||||
- Restore state must be mountable read-write from the installed OS by an admin via `OMARCHY_STATE`. Restore payload should only be mounted read-only for manifest verification via `OMARCHY_RESTORE`.
|
||||
|
||||
## Safety Model
|
||||
|
||||
Protected installs must use stable identifiers, not only `/dev/nvme0n1pX` names.
|
||||
|
||||
Track:
|
||||
|
||||
```text
|
||||
protected partition PARTUUIDs
|
||||
restore state PARTUUID
|
||||
writable partition PARTUUIDs
|
||||
selected disk ID
|
||||
install mode
|
||||
install root
|
||||
```
|
||||
|
||||
Every destructive operation must call a guard first:
|
||||
|
||||
```text
|
||||
assert_writable_partition <partition>
|
||||
assert_not_protected <partition>
|
||||
```
|
||||
|
||||
Guarded operations include:
|
||||
|
||||
- `sgdisk --delete`
|
||||
- `sgdisk --new`
|
||||
- `wipefs`
|
||||
- `mkfs.fat`
|
||||
- `mkfs.btrfs`
|
||||
- `cryptsetup luksFormat`
|
||||
- partition table rewrites
|
||||
|
||||
In restore mode, also assert:
|
||||
|
||||
- `/proc/cmdline` contains `copytoram=y`.
|
||||
- The restore payload is not mounted read-write from the target disk at mutation time.
|
||||
- Restore ESP, restore payload, and restore state partitions are detected and marked protected from delete/format operations.
|
||||
- The installed target partitions do not overlap protected partition ranges.
|
||||
|
||||
### Power Loss Recovery
|
||||
|
||||
A torn install must be recoverable on next boot rather than leaving the disk in a state that needs manual intervention.
|
||||
|
||||
- After partition table changes but before LUKS format: the new partition exists but holds no recognizable filesystem or LUKS header. On the next install boot, treat any writable partition without a valid header as discardable and re-run the format step.
|
||||
- After LUKS format but before archinstall completes: the LUKS header exists but no working OS is installed. On the next boot from restore or ISO, offer to wipe and retry rather than attempting to resume.
|
||||
- During archinstall: same as above. Resuming partway through a torn pacstrap is more brittle than restarting it.
|
||||
|
||||
In dual-boot mode, "wipe and retry" applies only to the writable Omarchy partition. Protected partitions must be re-verified by PARTUUID before any retry runs — power loss does not unlock protected ranges.
|
||||
|
||||
## Installer State Contract
|
||||
|
||||
The configurator writes `install_mode.sh` for `.automated_script.sh` to source.
|
||||
|
||||
Example dual-boot state:
|
||||
|
||||
```bash
|
||||
INSTALL_MODE=dual_boot
|
||||
INSTALL_ROOT=/mnt/archinstall
|
||||
DISK=/dev/nvme0n1
|
||||
BOOT_PARTITION=/dev/nvme0n1p1
|
||||
ROOT_PARTITION=/dev/nvme0n1p4
|
||||
PROTECTED_PARTUUIDS="1111-2222 3333-4444 5555-6666"
|
||||
WRITABLE_PARTUUIDS="7777-8888"
|
||||
PROMOTE_OMARCHY_BOOT=false
|
||||
```
|
||||
|
||||
Example restore state:
|
||||
|
||||
```bash
|
||||
INSTALL_MODE=restore_install
|
||||
INSTALL_ROOT=/mnt/archinstall
|
||||
DISK=/dev/nvme0n1
|
||||
RESTORE_ESP_PARTITION=/dev/nvme0n1p1
|
||||
RESTORE_PAYLOAD_PARTITION=/dev/nvme0n1p2
|
||||
RESTORE_STATE_PARTITION=/dev/nvme0n1p3
|
||||
INSTALLED_ESP_PARTITION=/dev/nvme0n1p4
|
||||
ROOT_PARTITION=/dev/nvme0n1p5
|
||||
PROTECTED_PARTUUIDS="1111-2222 3333-4444 5555-6666"
|
||||
WRITABLE_PARTUUIDS="7777-8888 9999-aaaa"
|
||||
PROMOTE_OMARCHY_BOOT=true
|
||||
```
|
||||
|
||||
Keep this file simple shell, generated only by our configurator, and quote values safely.
|
||||
|
||||
Restore mode uses `INSTALLED_ESP_PARTITION` (distinct from `RESTORE_ESP_PARTITION`) because two ESPs are present. It also tracks `RESTORE_STATE_PARTITION` separately because that partition is writable state but protected from delete/format operations. Dual-boot mode keeps `BOOT_PARTITION` since only one ESP exists and is unambiguous.
|
||||
|
||||
## Archinstall Config
|
||||
|
||||
Protected modes generate this disk config and explicitly disable archinstall bootloader writes:
|
||||
|
||||
```json
|
||||
{
|
||||
"bootloader": null,
|
||||
"disk_config": {
|
||||
"config_type": "pre_mounted_config",
|
||||
"mountpoint": "/mnt/archinstall"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do not include `device_modifications`, `disk_encryption`, or `btrfs_options` in protected modes. The installer handles those before and after archinstall. If archinstall does not accept `"bootloader": null`, Phase 2a must identify the supported equivalent; protected mode must not proceed while archinstall can write boot files or NVRAM entries independently.
|
||||
|
||||
Full-disk mode keeps the existing generated `default_layout` config.
|
||||
|
||||
**Verify before Phase 2b.** Under `pre_mounted_config`, archinstall still tends to run `genfstab`, write a bootloader, and may write `/etc/crypttab`. The plan currently assumes archinstall acts only as a base-system installer in protected mode; this needs to be confirmed on a throwaway VM. Once confirmed, decide for each step (fstab, crypttab, bootloader, initramfs, kernel cmdline) whether the manual steps below run *instead of* archinstall's behavior, *after* archinstall has had its turn, or with archinstall's step disabled in config. The bootloader outcome is a hard gate: protected mode must have a confirmed way to prevent archinstall from writing Limine files or NVRAM entries. Do not start writing the manual pre/post steps until this is resolved — the unknown sits underneath every downstream step.
|
||||
|
||||
## File Changes
|
||||
|
||||
### `configs/airootfs/root/configurator`
|
||||
|
||||
Add shared detection and policy selection:
|
||||
|
||||
- `detect_disk_layout` finds GPT state, partitions, partition ranges, filesystem labels, ESPs, ESP free space, and free regions.
|
||||
- `detect_existing_os` mounts ESP read-only and checks known vendor paths like `EFI/Microsoft`, `EFI/ubuntu`, `EFI/fedora`, `EFI/GRUB`, and `EFI/systemd`.
|
||||
- `detect_bitlocker` checks every Windows/basic-data candidate partition on the selected disk for BitLocker. Use `blkid -o value -s TYPE`; BitLocker volumes report `BitLocker` rather than `ntfs`. As a backup, read 8 bytes at offset 3 of the partition — BitLocker volumes contain `-FVE-FS-` instead of `NTFS `. If any partition reports BitLocker, the dual-boot path aborts before any disk mutation with the message described in the dual-boot policy.
|
||||
- `detect_restore_layout` finds `OMARCHY_RESTORE` and `OMARCHY_STATE` partitions and validates the restore ESP/payload/state set.
|
||||
- `find_free_regions` finds unallocated GPT space using `sgdisk` or `parted`. Selection rule: pick the largest single contiguous region of at least 40 GB. Do not concatenate non-contiguous regions.
|
||||
- `install_mode_form` chooses between full disk, dual boot, and restore install based on context.
|
||||
|
||||
User flow:
|
||||
|
||||
```text
|
||||
Normal ISO boot:
|
||||
select disk
|
||||
detect layout
|
||||
if existing OS/install is detected:
|
||||
show detected installs and disk layout summary
|
||||
always offer "Exclusive install (erase disk)" using the existing full-disk `wipe: true` path
|
||||
if existing ESP + enough free space + guardrails pass:
|
||||
also offer "Install alongside"
|
||||
else:
|
||||
show why "Install alongside" is unavailable (for example: no ESP, no suitable free space, BitLocker/FVE, ESP too small)
|
||||
else:
|
||||
keep current erase-disk flow
|
||||
|
||||
Restore boot:
|
||||
detect restore disk automatically
|
||||
show first-install or reinstall warning
|
||||
protect restore ESP/payload/state partitions
|
||||
create/recreate installed partitions only
|
||||
```
|
||||
|
||||
Generated files:
|
||||
|
||||
- `user_credentials.json`
|
||||
- `user_configuration.json`
|
||||
- `user_full_name.txt`
|
||||
- `user_email_address.txt`
|
||||
- `install_mode.sh`
|
||||
|
||||
### `configs/airootfs/root/.automated_script.sh`
|
||||
|
||||
Add shared protected install functions:
|
||||
|
||||
- Source `install_mode.sh` after configurator runs.
|
||||
- Set `INSTALL_ROOT=/mnt/archinstall` for all modes, including full-disk. A single mount root avoids subtle bugs in code touched by both paths and removes one branch from every post-install step.
|
||||
- Before changing the mount root, grep the live ISO and post-install scripts for hardcoded `/mnt` references and produce an explicit list of call sites. Update each one consciously rather than via a global rename.
|
||||
- Add `setup_protected_partitions` before archinstall.
|
||||
- Add `configure_protected_post_install` after archinstall.
|
||||
- Add cleanup trap for mounts, LUKS mappings, and temporary directories.
|
||||
|
||||
Protected pre-install steps:
|
||||
|
||||
1. Verify mode-specific guardrails.
|
||||
2. Create writable partitions if needed.
|
||||
3. Run `partprobe "$DISK"` and `udevadm settle`.
|
||||
4. Format installed ESP when policy requires it.
|
||||
5. `cryptsetup luksFormat` the root partition.
|
||||
6. `cryptsetup open` root as `omarchy_root`.
|
||||
7. `mkfs.btrfs /dev/mapper/omarchy_root`.
|
||||
8. Create Btrfs subvolumes `@`, `@home`, `@log`, and `@pkg`.
|
||||
9. Mount subvolumes at `$INSTALL_ROOT` with `compress=zstd`.
|
||||
10. Mount ESP at `$INSTALL_ROOT/efi`; keep `$INSTALL_ROOT/boot` as a normal directory on encrypted root.
|
||||
11. Generate deterministic fstab with overwrite semantics.
|
||||
|
||||
Protected post-install steps:
|
||||
|
||||
1. Configure root unlock for the initramfs hook actually in use. Root unlock is driven by initramfs + kernel cmdline, not by `/etc/crypttab`:
|
||||
- `encrypt` hook: kernel cmdline `cryptdevice=UUID=<luks-uuid>:omarchy_root root=/dev/mapper/omarchy_root`. No crypttab entry needed for root.
|
||||
- `sd-encrypt` hook: write `/etc/crypttab.initramfs` (not `/etc/crypttab`) with the LUKS UUID, and pass `rd.luks.name=<luks-uuid>=omarchy_root root=/dev/mapper/omarchy_root` on the kernel cmdline.
|
||||
2. Update mkinitcpio hooks for the initramfs mode actually present.
|
||||
3. Regenerate initramfs in chroot with `mkinitcpio -P`.
|
||||
4. Install an Omarchy-owned kernel/initramfs sync hook that copies boot artifacts from `/boot` to `/efi/EFI/Omarchy` after kernel or initramfs updates.
|
||||
5. Run the initial boot artifact sync before writing Limine config.
|
||||
6. Configure Limine kernel cmdline for encrypted root per the hook chosen above, pointing at namespaced artifacts under `EFI/Omarchy`.
|
||||
7. Install or update Limine on the selected installed ESP.
|
||||
8. Run `limine-scan` in chroot when appropriate.
|
||||
9. Apply mode-specific boot order policy.
|
||||
10. Leave swapfile creation, `resume` hook, and `resume=` cmdline injection to Omarchy's existing `omarchy-hibernation-setup` post-install hook. Verify `/etc/limine-entry-tool.d/` drop-ins are honored by the Limine config layout written here so that hook works unchanged.
|
||||
|
||||
### `builder/archinstall.packages`
|
||||
|
||||
Ensure the live environment has the tools needed for protected installs:
|
||||
|
||||
- `parted`
|
||||
- `gptfdisk`
|
||||
- `btrfs-progs`
|
||||
- `cryptsetup`
|
||||
- `dosfstools`
|
||||
- `efibootmgr`
|
||||
- `mtools`
|
||||
|
||||
`efibootmgr` already exists in this file, but keep it explicit in the protected-install dependency checklist.
|
||||
|
||||
### `builder/build-iso.sh`
|
||||
|
||||
Add build-time mode support:
|
||||
|
||||
- Persist `/root/omarchy_mode` as `normal`, `dual_boot_capable`, or `restore`.
|
||||
- For restore builds, inject `copytoram=y omarchy.mode=restore` into restore boot entries.
|
||||
- For restore builds, assert boot entries contain `copytoram=y`.
|
||||
- For normal ISO builds, keep existing behavior except adding dual-boot capability.
|
||||
|
||||
### `bin/omarchy-iso-make`
|
||||
|
||||
Add optional build flags:
|
||||
|
||||
```text
|
||||
--restore-image build omarchy-restore.img.zst factory artifact
|
||||
```
|
||||
|
||||
Keep the existing ISO output path unchanged unless a restore image is explicitly requested.
|
||||
|
||||
## Bootloader Policy
|
||||
|
||||
### Default Boot Target
|
||||
|
||||
For any standalone install (full-disk or restore_install, with or without a restore partition set), installed Omarchy is the default boot target. The persistent `BootOrder` after every successful install or factory reset is:
|
||||
|
||||
1. Installed Omarchy
|
||||
2. Restore (when a restore partition set exists)
|
||||
3. Firmware fallback
|
||||
|
||||
The user should never have to pick Omarchy from a boot menu under normal operation. Restore is reachable but never default. Boot-menu entries inside Limine for restore/reinstall are convenience; they do not replace this NVRAM ordering.
|
||||
|
||||
`BootNext` is used only for one-shot transitions (factory reset trigger, post-reset return to installed Omarchy) and must not be used to make Omarchy "stickily" default — that's the job of `BootOrder`. The only temporary exception is an in-progress factory reset: after the restore environment accepts the trigger and before it mutates installed partitions, restore becomes the persistent first boot target until reinstall succeeds.
|
||||
|
||||
For dual-boot, the previous OS's firmware order is preserved unless the user explicitly opts to make Omarchy first. Dual-boot is the only mode where Omarchy may not be the firmware default.
|
||||
|
||||
### Dual Boot
|
||||
|
||||
- Install Omarchy boot files under `EFI/Omarchy` on the shared ESP.
|
||||
- Do not overwrite existing vendor directories.
|
||||
- Preserve firmware default boot order unless user opts in.
|
||||
- Run `limine-scan` so Windows/Linux entries are discoverable from Omarchy's menu. Verify on a real Windows + Linux dual-boot before depending on this — Limine's auto-detection is less battle-tested than GRUB's. If it does not pick them up cleanly, fall back to the firmware boot menu and document the limitation rather than hand-writing entries.
|
||||
- BitLocker is handled at detection time by aborting the install (see Dual Boot policy). The bootloader path therefore only ever sees Windows volumes with BitLocker fully off/decrypted.
|
||||
|
||||
### Restore Install
|
||||
|
||||
- Installed Omarchy gets its own ESP and boot entry.
|
||||
- Restore ESP keeps `EFI/BOOT/BOOTX64.EFI` fallback path.
|
||||
- After successful install, write `BootOrder` as installed Omarchy first, restore second. Do not reorder on every boot — only after install or factory reset.
|
||||
- Add a restore/reinstall entry to the installed Limine menu if it can be done cleanly.
|
||||
- If NVRAM writes fail, rely on fallback boot paths and show a warning. The user can still recover via firmware boot menu.
|
||||
|
||||
## Rollout Plan
|
||||
|
||||
### Phase 1: Refactor Without Behavior Change
|
||||
|
||||
- Introduce `INSTALL_MODE=full_disk`.
|
||||
- Introduce `$INSTALL_ROOT` and update post-install paths.
|
||||
- Keep existing full-disk JSON and archinstall behavior.
|
||||
- Add tests or dry-run checks for config generation.
|
||||
|
||||
### Phase 2a: Archinstall pre_mounted_config Spike
|
||||
|
||||
Single-purpose spike on a throwaway VM. No production code changes.
|
||||
|
||||
- Drive archinstall with a hand-crafted mount tree and `pre_mounted_config`.
|
||||
- Record exactly what archinstall does: fstab generation, crypttab writes, bootloader install, initramfs regeneration, kernel cmdline writes.
|
||||
- Prove the protected-mode config disables archinstall bootloader and NVRAM writes, or document the supported replacement setting. This is a release blocker for Phase 2b.
|
||||
- Decide whether the manual fstab/Limine/initramfs steps in this plan run before, after, or instead of archinstall's own steps.
|
||||
- Update this plan with the answers before Phase 2b begins. The downstream design depends on this; do not skip.
|
||||
|
||||
### Phase 2b: Shared Protected Install Engine
|
||||
|
||||
- Add `pre_mounted_config` generation.
|
||||
- Add manual LUKS, Btrfs, subvolume, mount, fstab, and cleanup logic.
|
||||
- Add encrypted boot post-install configuration.
|
||||
- Add namespaced ESP boot artifact sync under `EFI/Omarchy`.
|
||||
- Keep it behind internal mode switches until validated.
|
||||
|
||||
### Phase 3: Dual-Boot UI and Policy
|
||||
|
||||
- Add disk layout detection.
|
||||
- When another install is detected, show an install-mode choice instead of assuming dual boot: "Install alongside" when eligible, and "Exclusive install (erase disk)" using the existing full-disk `wipe: true` path.
|
||||
- If another install is detected but dual boot is not eligible, explain why and still offer the exclusive install path.
|
||||
- Add Windows/BitLocker warnings.
|
||||
- Validate alongside Windows and Linux.
|
||||
|
||||
### Phase 4: Restore Install Policy
|
||||
|
||||
- Add restore layout detection.
|
||||
- Add restore partition set protection.
|
||||
- Add first-install and reinstall flows.
|
||||
- Validate that restore ESP, payload, and state survive installation and later reinstall.
|
||||
|
||||
### Phase 5: Restore Factory Image
|
||||
|
||||
- Build `omarchy-restore.img.zst`.
|
||||
- Add factory flashing instructions or a dedicated factory flasher ISO/PXE workflow.
|
||||
- Validate first boot from internal disk with no external media.
|
||||
- Sign `omarchy-restore.img.zst` and the restore payload manifest. Verify in the factory flasher and at restore boot before any disk mutation.
|
||||
- Add first-boot UUID randomization for cloned restore images before generating installed boot entries or state.
|
||||
- Decide on Secure Boot before this phase ships. Shipping with SB disabled is a non-starter for some buyers, and BitLocker (when present on a dual-boot machine restored later) reseals against firmware state. Either implement a signed shim/Limine path, or commit to "SB off" with a documented rationale and a way for the user to flip it on later if they have keys.
|
||||
|
||||
### Phase 6: Factory Reset
|
||||
|
||||
- Implement `omarchy-factory-reset` as the installed-OS staging command: write and fsync the trigger flag, verify/recreate the restore boot entry, set and confirm `BootNext`, then leave the machine ready to boot restore.
|
||||
- Wire the restore environment to detect the trigger flag and run the reinstall flow non-interactively.
|
||||
- Set `BootNext` for one-shot transitions and temporary restore-first persistent `BootOrder` during the destructive reset window.
|
||||
- Validate on `restore_install` machines first (the flow already has a restore partition set).
|
||||
|
||||
### Phase 7: Restore Partition for Standalone Installs
|
||||
|
||||
- Extend the standalone full-disk install path to create restore ESP + restore payload + restore state + installed ESP + installed root, mirroring the `restore_install` layout.
|
||||
- Populate restore payload at install time from the live ISO offline mirror.
|
||||
- Add `--no-restore` opt-out for users who explicitly want the legacy two-partition layout.
|
||||
- After this phase, every consumer install gets factory reset by default.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Full-disk install behavior remains unchanged through Phase 6. Phase 7 changes the default layout to include a restore partition set; legacy layout remains available via `--no-restore`.
|
||||
- Dual-boot install preserves existing OS partitions.
|
||||
- Dual-boot install creates encrypted Omarchy root in free space.
|
||||
- When another install is detected, the installer offers the current exclusive full-disk wipe path and only offers dual boot when guardrails pass.
|
||||
- Dual-boot install writes Omarchy boot artifacts only under `EFI/Omarchy` on a shared ESP and leaves existing vendor paths untouched.
|
||||
- Dual-boot install aborts cleanly when BitLocker/FVE is detected, including suspended BitLocker, before any disk mutation, with an actionable message.
|
||||
- Restore install preserves restore ESP, restore payload, and restore state partitions.
|
||||
- Restore install creates or recreates only installed Omarchy partitions.
|
||||
- Protected modes never generate archinstall config with `wipe: true`.
|
||||
- Protected modes prevent archinstall from independently writing boot files or NVRAM entries.
|
||||
- Protected modes abort before destructive operations if a target partition is protected.
|
||||
- Restore payload integrity is verified with a signed manifest before any restore-triggered disk mutation.
|
||||
- Installed Omarchy boots and requires LUKS unlock.
|
||||
- Installed Omarchy is the default firmware boot target for any standalone install (full-disk or restore_install).
|
||||
- Restore boot remains available after first install but is never the default.
|
||||
- Reinstall from restore works without external media.
|
||||
- `omarchy-factory-reset` from the installed OS leaves the machine staged exactly like a freshly prepared restore-boot device: trigger flag durable on restore state, restore boot entry present, and `BootNext` pointing at restore. No installed partitions are destroyed before the next restore boot.
|
||||
- A subsequent restore boot from that staged state wipes installed partitions, reinstalls a fresh Omarchy, and lands the user back in the installed OS without external media. Restore ESP, payload, and state partitions survive intact.
|
||||
- Factory reset refuses to run when no complete restore partition set is present (e.g. dual-boot, `--no-restore` install).
|
||||
|
||||
## Validation Matrix
|
||||
|
||||
### Regression
|
||||
|
||||
1. Full-disk UEFI install on empty disk.
|
||||
2. Current normal ISO boot menu and default install behavior.
|
||||
3. Existing offline package installation flow.
|
||||
|
||||
### Dual Boot
|
||||
|
||||
1. Windows + free space + existing ESP (BitLocker off).
|
||||
2. Windows + BitLocker enabled — installer must abort before any disk mutation with a clear "turn off BitLocker and wait for decryption" message.
|
||||
3. Windows + BitLocker suspended — installer must still abort, because v1 treats any BitLocker/FVE signature as unsafe.
|
||||
4. Linux distro + free space + existing ESP.
|
||||
5. GPT disk with no free space.
|
||||
6. ESP below total-size warning threshold.
|
||||
7. ESP below total-size minimum threshold.
|
||||
8. ESP large enough by total size but below free-space minimum.
|
||||
9. Multiple free regions; choose largest suitable region.
|
||||
10. Firmware boot order preservation.
|
||||
|
||||
### Restore
|
||||
|
||||
1. Factory image written to larger virtual disk.
|
||||
2. First boot from internal restore ESP.
|
||||
3. First install randomizes cloned disk/partition/filesystem UUIDs before creating installed boot entries.
|
||||
4. First install creates installed ESP/root and preserves restore ESP, payload, and state.
|
||||
5. Installed OS boots with encrypted root.
|
||||
6. Installed Omarchy is the firmware default; restore is reachable but not default.
|
||||
7. Restore boot still works after installed OS exists.
|
||||
8. Reinstall formats only the installed Omarchy partitions in place; PARTUUIDs unchanged.
|
||||
9. Restore mode without `copytoram=y` aborts before disk mutation.
|
||||
10. Missing restore payload or restore state aborts before disk mutation.
|
||||
11. NVRAM boot entry write failure still leaves fallback boot path usable.
|
||||
|
||||
### Factory Reset
|
||||
|
||||
1. `omarchy-factory-reset` from installed OS leaves the system staged for restore boot: trigger flag durable on restore state, restore boot entry present, and `BootNext` confirmed.
|
||||
2. No installed partitions are destroyed before the staged system actually boots into restore.
|
||||
3. Booting into restore from the staged state reinstalls and returns to a fresh installed Omarchy as the default-booted OS.
|
||||
4. Trigger flag is deleted before the post-reset reboot. Machine does not loop.
|
||||
5. Reset interrupted between trigger and restore boot: next normal boot still goes to installed Omarchy (because `BootNext` is one-shot).
|
||||
6. Reset interrupted during reinstall (power loss): persistent temporary restore-first `BootOrder` boots restore again and re-runs the flow from the trigger flag/reset-in-progress state.
|
||||
7. Reset refused when no complete restore partition set is detected.
|
||||
8. Reset refused when signed restore payload verification fails.
|
||||
9. Reset refused when running without root.
|
||||
10. Reset refused when restore boot entry is missing from NVRAM and cannot be repopulated from fallback.
|
||||
|
||||
### Negative Safety Tests
|
||||
|
||||
1. Attempt to format protected restore ESP.
|
||||
2. Attempt to format protected restore payload.
|
||||
3. Attempt to format protected restore state.
|
||||
4. Attempt to format existing Windows partition in dual-boot mode.
|
||||
5. Partition overlap with protected range.
|
||||
6. Power loss before partition creation.
|
||||
7. Power loss after partition creation but before archinstall.
|
||||
8. Power loss during archinstall.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Restore payload size: full offline mirror versus smaller network-capable restore image. Affects whether the consumer ISO's restore-partition default in Phase 7 is feasible at typical disk sizes.
|
||||
- Restore update strategy after devices ship.
|
||||
- Whether restore should also expose diagnostics / log upload alongside factory reset.
|
||||
- Whether factory reset should preserve `/home` user data optionally, or always wipe. v1 always wipes (simpler, matches user expectation of "factory reset"); a "keep my files" variant could land later.
|
||||
|
||||
Resolved:
|
||||
|
||||
- User-facing reset staging command: yes, `omarchy-factory-reset` (Phase 6).
|
||||
- Secure Boot: promoted to Phase 5 decision gate.
|
||||
- Btrfs subvolume layout: protected modes use the current full-disk layout, `@`, `@home`, `@log`, and `@pkg`. The `/swap` subvolume for hibernation is created post-install by `omarchy-hibernation-setup` and is not predeclared at install time.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- One shared protected-partition implementation supports both dual-boot and restore install policies.
|
||||
- Full-disk installs remain stable through Phase 6 and gain a default restore partition set in Phase 7.
|
||||
- Dual-boot users can install alongside existing OSes without data loss.
|
||||
- OEM devices can boot internal restore on first power-on, install Omarchy, and retain restore for future reinstall.
|
||||
- All destructive operations are guarded by protected partition checks.
|
||||
- Installed Omarchy is the firmware default boot target on every standalone install.
|
||||
- `omarchy-factory-reset` stages any standalone install to boot internal restore without external media; the restore-triggered reinstall returns the user to a fresh installed Omarchy.
|
||||
- VM and hardware validation matrices pass.
|
||||
Reference in new issue
Block a user