Compare commits
144
Commits
main
..
cpu-governor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c69fa5768 | ||
|
|
4f60a18c87 | ||
|
|
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 | ||
|
|
c8a0072403 | ||
|
|
ff6c9582ca | ||
|
|
d44bbc5e5f | ||
|
|
d51a280636 | ||
|
|
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 |
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,31 +8,51 @@ See the ISO link on [omarchy.org](https://omarchy.org).
|
||||
|
||||
## Creating the ISO
|
||||
|
||||
Run `./bin/omarchy-iso-make` and the output goes into `./release`. You can build from your local `$OMARCHY_PATH` for testing with `--local-source`, from the dev branch with `--dev`, or from the quattro branch with `--quattro`. The dev and quattro builds use the edge package mirror.
|
||||
Run `./bin/omarchy-iso-make`; output goes into `./release`. By default the ISO uses the Omarchy packages and tracks the `quattro` branch, from the stable mirror. Pass `--edge` to use `omarchy-dev` and `omarchy-settings-dev` from the edge mirror.
|
||||
|
||||
### Environment Variables
|
||||
For local development, build the ISO from sibling checkouts:
|
||||
|
||||
You can customize the repositories used during the build process by passing in variables:
|
||||
|
||||
- `OMARCHY_INSTALLER_REPO` - GitHub repository for the installer (default: `basecamp/omarchy`)
|
||||
- `OMARCHY_INSTALLER_REF` - Git ref (branch/tag) for the installer (default: `master`)
|
||||
|
||||
Example usage:
|
||||
```bash
|
||||
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 [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
|
||||
|
||||
|
||||
+121
-13
@@ -2,19 +2,113 @@
|
||||
|
||||
set -e
|
||||
|
||||
ISO="$1"
|
||||
REUSE="$2"
|
||||
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=()
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: omarchy-iso-boot [options] [release/omarchy.iso] [reuse]
|
||||
|
||||
Options:
|
||||
--reuse Reuse the existing VM disk
|
||||
--ssh-port PORT Forward host PORT to guest port 22 (default: $SSH_PORT)
|
||||
--ssh-host HOST Host address for SSH forwarding (default: $SSH_HOST)
|
||||
--no-network Boot without a virtual NIC
|
||||
--disk PATH VM disk path (default: $DISK)
|
||||
--ovmf-vars PATH OVMF vars path (default: $OVMF_VARS)
|
||||
-h, --help Show this help
|
||||
-- Pass the remaining arguments directly to QEMU
|
||||
|
||||
SSH access after starting sshd in the guest:
|
||||
ssh -p $SSH_PORT root@localhost
|
||||
scp -P $SSH_PORT /path/to/system-manifest root@localhost:/tmp/
|
||||
USAGE
|
||||
}
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--reuse|reuse)
|
||||
REUSE="reuse"
|
||||
shift
|
||||
;;
|
||||
--ssh-port)
|
||||
SSH_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-port=*)
|
||||
SSH_PORT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ssh-host)
|
||||
SSH_HOST="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-host=*)
|
||||
SSH_HOST="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--no-network|--no-net)
|
||||
NETWORK=0
|
||||
shift
|
||||
;;
|
||||
--disk)
|
||||
DISK="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--disk=*)
|
||||
DISK="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ovmf-vars)
|
||||
OVMF_VARS="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ovmf-vars=*)
|
||||
OVMF_VARS="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
EXTRA_QEMU_ARGS+=("$@")
|
||||
break
|
||||
;;
|
||||
*)
|
||||
if [[ -z $ISO ]]; then
|
||||
ISO="$1"
|
||||
else
|
||||
echo "Unexpected argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z $ISO && -t 0 ]]; then
|
||||
ISO=$(ls -t release/*.iso 2>/dev/null | gum choose --header "Select ISO to boot")
|
||||
if gum confirm --default=false "Reuse existing disk?"; then
|
||||
REUSE="reuse"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ISO" ]; then
|
||||
echo "Usage: omarchy-iso-boot [release/omarchy.iso] [reuse]"
|
||||
echo " SSH available at: ssh -p 2222 root@localhost (after installing openssh)"
|
||||
if [[ -z $ISO ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $DISK || -z $OVMF_VARS ]]; then
|
||||
echo "Disk and OVMF vars paths must not be empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -36,19 +130,32 @@ if [[ -z ${WAYLAND_DISPLAY:-} && -z ${DISPLAY:-} ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DISK="/tmp/omarchy-iso-boot.qcow2"
|
||||
if [[ ! -f $DISK || $REUSE != "reuse" ]]; then
|
||||
qemu-img create -f qcow2 "$DISK" 40G
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd /tmp/OVMF_VARS.4m.fd
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd "$OVMF_VARS"
|
||||
fi
|
||||
|
||||
NET_ARGS=()
|
||||
if [[ $NETWORK != "0" && $NETWORK != "false" && $NETWORK != "no" ]]; then
|
||||
if [[ -n $SSH_PORT && $SSH_PORT != "0" ]]; then
|
||||
NET_ARGS=(-netdev "user,id=net0,hostfwd=tcp:${SSH_HOST}:${SSH_PORT}-:22" -device virtio-net-pci,netdev=net0)
|
||||
echo "SSH forwarding enabled: ssh -p $SSH_PORT root@localhost"
|
||||
echo "SCP example: scp -P $SSH_PORT /path/to/system-manifest root@localhost:/tmp/"
|
||||
else
|
||||
NET_ARGS=(-netdev user,id=net0 -device virtio-net-pci,netdev=net0)
|
||||
echo "Networking enabled without SSH port forwarding."
|
||||
fi
|
||||
else
|
||||
echo "Networking disabled."
|
||||
fi
|
||||
|
||||
# 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="$ISO",media=cdrom,if=none,format=raw,id=cdrom0 \
|
||||
@@ -56,6 +163,7 @@ qemu-system-x86_64 \
|
||||
-device virtio-vga-gl \
|
||||
-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[@]}"
|
||||
@@ -3,25 +3,14 @@ set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
cleanup() {
|
||||
[[ -n "${mock_omarchy_install:-}" ]] && rm -rf "$(dirname "$mock_omarchy_install")"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -z "${OMARCHY_INSTALL:-}" ]]; then
|
||||
if [[ -r /root/omarchy/install/helpers/all.sh ]]; then
|
||||
export OMARCHY_INSTALL=/root/omarchy/install
|
||||
else
|
||||
mock_root="$(mktemp -d)"
|
||||
mock_omarchy_install="$mock_root/install"
|
||||
mkdir -p "$mock_omarchy_install/helpers"
|
||||
cat >"$mock_omarchy_install/helpers/all.sh" <<'EOF'
|
||||
PADDING_LEFT=0
|
||||
PADDING_LEFT_SPACES=""
|
||||
clear_logo() { clear; }
|
||||
EOF
|
||||
export OMARCHY_INSTALL="$mock_omarchy_install"
|
||||
fi
|
||||
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
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# Preview the shipped Omarchy installer dashboard without building an ISO.
|
||||
set -euo pipefail
|
||||
|
||||
# The dashboard drives the package phase from the target's pacman database, so
|
||||
# fake one here; without it the preview falls back to its time curve and the bar
|
||||
# barely moves, which reads as a bug.
|
||||
PREVIEW_PACKAGES="${PREVIEW_PACKAGES:-925}"
|
||||
|
||||
write_simulation_state() {
|
||||
local state_file="$1" started_at="$2" phase_started_at="$3" current_phase="$4"
|
||||
local target="$5" index="$6" total="$7"
|
||||
local finished_at="${8:-null}" temporary="${state_file}.tmp"
|
||||
|
||||
jq -n \
|
||||
--argjson started_at "$started_at" \
|
||||
--argjson phase_started_at "$phase_started_at" \
|
||||
--arg current_phase "$current_phase" \
|
||||
--arg target "$target" \
|
||||
--argjson current_index "$index" \
|
||||
--argjson total_phases "$total" \
|
||||
--argjson finished_at "$finished_at" \
|
||||
'{
|
||||
started_at: $started_at,
|
||||
phase_started_at: $phase_started_at,
|
||||
current_phase: $current_phase,
|
||||
target: $target,
|
||||
current_index: $current_index,
|
||||
total_phases: $total_phases
|
||||
} + if $finished_at == null then {} else {finished_at: $finished_at} end' \
|
||||
>"$temporary"
|
||||
mv -f -- "$temporary" "$state_file"
|
||||
}
|
||||
|
||||
# Fill the fake database over the phase's duration, so the bar is driven by the
|
||||
# same count it uses on a real install.
|
||||
simulate_packages() {
|
||||
local db="$1" duration="$2" total="$3" steps=25 i n
|
||||
for ((i = 1; i <= steps; i++)); do
|
||||
n=$(( total * i / steps ))
|
||||
while (( $(ls -1 "$db" 2>/dev/null | wc -l) < n )); do
|
||||
mkdir -p "$db/preview-$(( $(ls -1 "$db" 2>/dev/null | wc -l) ))-1.0-1"
|
||||
done
|
||||
sleep "$(awk -v d="$duration" -v s="$steps" 'BEGIN { printf "%.3f", d / s }')"
|
||||
done
|
||||
}
|
||||
|
||||
simulate_install() {
|
||||
local state_file="$1"
|
||||
local started_at="$EPOCHREALTIME"
|
||||
local phase phase_started_at finished_at duration i index total
|
||||
local target db
|
||||
target="$(dirname -- "$state_file")/target"
|
||||
db="$target/var/lib/pacman/local"
|
||||
mkdir -p "$db"
|
||||
local -a phases=(
|
||||
"Starting installation" 0.5
|
||||
"Preparing live environment" 0.8
|
||||
"Preparing install target" 0.3
|
||||
"Installing Arch + Omarchy" 5.0
|
||||
"Configuring hibernation" 0.4
|
||||
"Configuring system" 1.2
|
||||
"Finalizing Limine boot" 1.0
|
||||
"Finalizing user" 1.0
|
||||
"Configuring login" 0.6
|
||||
"Configuring DNS resolver" 0.3
|
||||
"Validating boot setup" 0.8
|
||||
)
|
||||
total=$(( ${#phases[@]} / 2 ))
|
||||
|
||||
for ((i = 0; i < ${#phases[@]}; i += 2)); do
|
||||
phase="${phases[i]}"
|
||||
duration="${phases[i + 1]}"
|
||||
index=$(( i / 2 ))
|
||||
phase_started_at="$EPOCHREALTIME"
|
||||
write_simulation_state "$state_file" "$started_at" "$phase_started_at" \
|
||||
"$phase" "$target" "$index" "$total"
|
||||
if [[ $phase == "Installing Arch + Omarchy" ]]; then
|
||||
simulate_packages "$db" "$duration" "$PREVIEW_PACKAGES"
|
||||
else
|
||||
sleep "$duration"
|
||||
fi
|
||||
done
|
||||
|
||||
finished_at="$EPOCHREALTIME"
|
||||
write_simulation_state "$state_file" "$started_at" "$finished_at" \
|
||||
"Installation complete" "$target" "$(( total - 1 ))" "$total" "$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"
|
||||
|
||||
# Match what the simulated database will reach, so the preview shows the
|
||||
# package-driven bar rather than the missing-signal fallback.
|
||||
printf '%s\n' "$PREVIEW_PACKAGES" >"$preview_dir/expected-packages"
|
||||
|
||||
OMARCHY_UI_AUTO_REBOOT=no \
|
||||
OMARCHY_EXPECTED_PACKAGES_FILE="$preview_dir/expected-packages" \
|
||||
PREVIEW_PACKAGES="$PREVIEW_PACKAGES" \
|
||||
"$dashboard" "$log_file" "$state_file" -- \
|
||||
"$0" --simulate-install "$state_file"
|
||||
+75
-26
@@ -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,13 +15,28 @@ while [[ $# -gt 0 ]]; do
|
||||
NO_BOOT_OFFER=1
|
||||
shift
|
||||
;;
|
||||
--local-source)
|
||||
LOCAL_SOURCE=1
|
||||
--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_INSTALLER_REF=dev
|
||||
OMARCHY_ISO_REF=dev
|
||||
OMARCHY_MIRROR=edge
|
||||
shift
|
||||
;;
|
||||
@@ -31,40 +46,77 @@ while [[ $# -gt 0 ]]; do
|
||||
shift
|
||||
;;
|
||||
--rc)
|
||||
OMARCHY_ISO_REF=rc
|
||||
OMARCHY_MIRROR=rc
|
||||
OMARCHY_INSTALLER_REF=rc
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: $0 [--no-cache] [--no-boot-offer] [--local-source] [--dev] [--quattro] [--rc]"
|
||||
echo "Usage: $0 [--no-cache] [--no-boot-offer] [--debug] [--edge] [--dev|--rc] [--local-source <omarchy-checkout> <pkgs-checkout>]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Clear out old packages
|
||||
sudo rm -rf /var/cache/pacman/pkg/*
|
||||
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
|
||||
|
||||
# Ensure we have the submodule checked out
|
||||
lint_file_permissions() {
|
||||
local profiledef="configs/profiledef.sh"
|
||||
local -A declared=()
|
||||
local missing=()
|
||||
|
||||
while IFS= read -r path; do
|
||||
[[ -n $path ]] && declared["$path"]=1
|
||||
done < <(grep -oP '^\s*\["\K[^"]+(?="\])' "$profiledef" || true)
|
||||
|
||||
while IFS= read -r f; do
|
||||
[[ -f $f ]] || continue
|
||||
|
||||
local rel="${f#configs/airootfs}"
|
||||
local mode
|
||||
mode=$(git ls-files --stage -- "$f" | awk '{print $1}')
|
||||
|
||||
if [[ $mode != 100755 && $(head -c 2 "$f") != "#!" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -z ${declared[$rel]+x} ]]; then
|
||||
missing+=("$rel")
|
||||
fi
|
||||
done < <(find configs/airootfs/usr/local/bin configs/airootfs/root -type f 2>/dev/null)
|
||||
|
||||
if (( ${#missing[@]} > 0 )); then
|
||||
echo "ERROR: the following executable files are missing from $profiledef file_permissions:" >&2
|
||||
printf ' %s\n' "${missing[@]}" >&2
|
||||
echo "Add entries like [\"<path>\"]=\"0:0:755\" to profiledef.sh and retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
lint_file_permissions
|
||||
|
||||
sudo rm -rf /var/cache/pacman/pkg/*
|
||||
git submodule update --init --recursive --jobs=8
|
||||
|
||||
# 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/"
|
||||
@@ -74,13 +126,14 @@ DOCKER_ARGS=(
|
||||
-v "$BUILD_ROOT/configs:/configs:ro"
|
||||
)
|
||||
|
||||
# Use local source of Omarchy from $OMARCHY_PATH instead of git checkout
|
||||
if [[ -n "${LOCAL_SOURCE-}" ]]; then
|
||||
DOCKER_ARGS+=(-v "$OMARCHY_PATH:/omarchy:ro")
|
||||
if [[ -n $LOCAL_OMARCHY_PATH ]]; then
|
||||
DOCKER_ARGS+=(
|
||||
-v "$LOCAL_OMARCHY_PATH:/omarchy-source:ro"
|
||||
-v "$LOCAL_PKGS_PATH:/omarchy-pkgs:ro"
|
||||
)
|
||||
fi
|
||||
|
||||
# Use local pacman cache if you already have one on host to speed up repeat runs
|
||||
if [ -d "/var/cache/pacman/pkg" ]; then
|
||||
if [[ -d /var/cache/pacman/pkg ]]; then
|
||||
DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg")
|
||||
fi
|
||||
|
||||
@@ -89,20 +142,16 @@ fi
|
||||
if [[ -z "$NO_CACHE" ]]; then
|
||||
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,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
MAKE_ARGS=()
|
||||
INSTALLER_REF=master
|
||||
ISO_REF=quattro
|
||||
RELEASE_SUFFIX=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -12,7 +12,7 @@ while [[ $# -gt 0 ]]; do
|
||||
;;
|
||||
--rc)
|
||||
MAKE_ARGS+=("$1")
|
||||
INSTALLER_REF=rc
|
||||
ISO_REF=rc
|
||||
RELEASE_SUFFIX="-rc"
|
||||
shift
|
||||
;;
|
||||
@@ -42,21 +42,21 @@ BUILD_RELEASE_PATH="$BUILD_ROOT/release"
|
||||
|
||||
# Make ISOs
|
||||
if [[ ! " ${MAKE_ARGS[*]} " =~ " --no-make " ]]; then
|
||||
echo -e "\e[32mMaking $INSTALLER_REF ISO\e[0m"
|
||||
echo -e "\e[32mMaking $ISO_REF ISO\e[0m"
|
||||
omarchy-iso-make --no-cache --no-boot-offer "${MAKE_ARGS[@]}"
|
||||
else
|
||||
echo -e "\e[31mSkipping remaking ISOs\e[0m"
|
||||
fi
|
||||
|
||||
# Sign and upload ISO
|
||||
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-"$INSTALLER_REF".iso | head -n1)
|
||||
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-"$ISO_REF".iso | head -n1)
|
||||
release_iso="$BUILD_RELEASE_PATH/omarchy-$BUILD_VERSION$RELEASE_SUFFIX.iso"
|
||||
cp "$latest_iso" "$release_iso"
|
||||
|
||||
echo -e "\e[32mSigning $INSTALLER_REF ISO\e[0m"
|
||||
echo -e "\e[32mSigning $ISO_REF ISO\e[0m"
|
||||
omarchy-iso-sign "$release_iso"
|
||||
|
||||
iso_sha=$(sha256sum "$release_iso")
|
||||
echo -e "\e[32mSHA256 for $INSTALLER_REF ISO: $iso_sha\e[0m"
|
||||
echo -e "\e[32mSHA256 for $ISO_REF ISO: $iso_sha\e[0m"
|
||||
|
||||
omarchy-iso-upload "$release_iso"
|
||||
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
|
||||
+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"
|
||||
|
||||
+268
-64
@@ -2,105 +2,309 @@
|
||||
|
||||
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
|
||||
|
||||
# Pre-import the omarchy signing key so pacman can verify packages without a keyserver lookup
|
||||
# Pre-import the omarchy signing key (so pacman trusts our [omarchy] repo
|
||||
# during the build without keyserver lookups).
|
||||
pacman-key --add /builder/omarchy.gpg
|
||||
pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571
|
||||
|
||||
# Install omarchy-keyring for package verification during build
|
||||
# omarchy-keyring is needed inside the offline mirror too.
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Sy omarchy-keyring
|
||||
pacman-key --populate omarchy
|
||||
|
||||
# Setup 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/
|
||||
# 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
|
||||
|
||||
# We base our ISO on the official arch ISO (releng) config
|
||||
cp -r /archiso/configs/releng/* $build_cache_dir/
|
||||
# Build locations
|
||||
build_cache_dir=/var/cache
|
||||
offline_mirror_dir="$build_cache_dir/airootfs/var/cache/omarchy/mirror/offline"
|
||||
mkdir -p "$build_cache_dir" "$offline_mirror_dir"
|
||||
|
||||
# Seed from the official Arch releng profile.
|
||||
cp -r /archiso/configs/releng/* "$build_cache_dir/"
|
||||
rm "$build_cache_dir/airootfs/etc/motd"
|
||||
|
||||
# Avoid using reflector for mirror identification as we are relying on the global CDN
|
||||
# We rely on the global CDN; drop reflector.
|
||||
rm -rf "$build_cache_dir/airootfs/etc/systemd/system/multi-user.target.wants/reflector.service"
|
||||
rm -rf "$build_cache_dir/airootfs/etc/systemd/system/reflector.service.d"
|
||||
rm -rf "$build_cache_dir/airootfs/etc/xdg/reflector"
|
||||
|
||||
# Bring in our configs
|
||||
cp -r /configs/* $build_cache_dir/
|
||||
|
||||
# Persist OMARCHY_MIRROR so it's available at install time
|
||||
# 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
|
||||
|
||||
# Setup Omarchy itself
|
||||
if [[ -d /omarchy ]]; then
|
||||
cp -rp /omarchy "$build_cache_dir/airootfs/root/omarchy"
|
||||
else
|
||||
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 omarchy-keyring lvm2 cryptsetup parted)
|
||||
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 '^$'))
|
||||
# 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
|
||||
|
||||
# Download all the packages to the offline mirror inside the ISO
|
||||
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-${OMARCHY_MIRROR}.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
|
||||
}
|
||||
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Denominator for the install dashboard's progress bar. Resolving the mirror's
|
||||
# own package lists against the mirror we just indexed, with an empty local db,
|
||||
# is the question pacstrap asks at install time — same resolver, same repo, same
|
||||
# lists — so no hand-kept constant can drift.
|
||||
#
|
||||
# It over-counts by ~1 in 925: archinstall.packages lists both amd-ucode and
|
||||
# intel-ucode because the mirror must contain either. phases.py records expected
|
||||
# and actual in the timing JSON, so growing drift shows up in acceptance runs.
|
||||
# The early-bootstrap set is already inside this closure, so restating it would
|
||||
# only add a second list to drift.
|
||||
resolve_expected_packages() {
|
||||
local resolve_root=/tmp/omarchy-expected-packages
|
||||
local resolved
|
||||
local -a targets
|
||||
|
||||
rm -rf "$resolve_root"
|
||||
mkdir -p "$resolve_root/var/lib/pacman"
|
||||
|
||||
mapfile -t targets < <(
|
||||
{
|
||||
grep -hv '^#\|^$' /builder/archinstall.packages
|
||||
# Read the shipped copy, which is what _runtime_package_list reads at
|
||||
# install time, not the build-time source it came from.
|
||||
grep -hv '^#\|^$' \
|
||||
"$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
|
||||
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" \
|
||||
"$OMARCHY_NVIM_PACKAGE"
|
||||
} | sort -u
|
||||
)
|
||||
|
||||
pacman --config "$build_cache_dir/pacman-offline.conf" \
|
||||
--root "$resolve_root" --dbpath "$resolve_root/var/lib/pacman" \
|
||||
--noconfirm -Sy >/dev/null || return 1
|
||||
|
||||
# Capture before counting: no pipefail here, so a pacman failure inside a
|
||||
# pipeline would become a plausible partial count, which never trips the
|
||||
# dashboard's fallback.
|
||||
resolved="$(pacman --config "$build_cache_dir/pacman-offline.conf" \
|
||||
--root "$resolve_root" --dbpath "$resolve_root/var/lib/pacman" \
|
||||
--noconfirm -S --print --print-format '%n' "${targets[@]}")" || return 1
|
||||
|
||||
printf '%s\n' "$resolved" | sort -u | grep -c .
|
||||
}
|
||||
|
||||
# Worth failing the build over: -S --print only aborts when a target is missing
|
||||
# from the offline repo, which would fail pacstrap the same way. A count that
|
||||
# merely looks wrong is not — the dashboard falls back without the file.
|
||||
if ! expected_packages="$(resolve_expected_packages)"; then
|
||||
echo "ERROR: could not resolve the target package count from the offline mirror." >&2
|
||||
echo " pacman -S --print aborts the whole transaction if any single target" >&2
|
||||
echo " is missing, so this almost certainly means pacstrap would fail the" >&2
|
||||
echo " same way at install time." >&2
|
||||
exit 1
|
||||
fi
|
||||
if (( expected_packages < 600 || expected_packages > 2000 )); then
|
||||
echo "WARNING: resolved target package count $expected_packages is outside the" >&2
|
||||
echo " expected 600-2000 range; shipping no denominator so the install" >&2
|
||||
echo " dashboard falls back to its time-based curve." >&2
|
||||
else
|
||||
printf '%s\n' "$expected_packages" \
|
||||
>"$build_cache_dir/airootfs/usr/share/omarchy-iso/expected-packages"
|
||||
echo "Target install resolves to $expected_packages packages."
|
||||
fi
|
||||
|
||||
# Copy the offline pacman.conf to the ISO's /etc directory so the live environment uses our
|
||||
# same config when booted.
|
||||
cp $build_cache_dir/pacman-offline.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,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,264 +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"
|
||||
export OMARCHY_MIRROR="$(cat /root/omarchy_mirror)"
|
||||
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
|
||||
|
||||
# Set CURRENT_SCRIPT for the trap to display better when nothing is returned for some reason
|
||||
CURRENT_SCRIPT="install_base_system"
|
||||
install_base_system > >(sed -u 's/\x1b\[[0-9;]*[a-zA-Z]//g' >>/var/log/omarchy-install.log) 2>&1
|
||||
unset CURRENT_SCRIPT
|
||||
stop_log_output
|
||||
}
|
||||
|
||||
install_omarchy() {
|
||||
chroot_bash -lc "sudo pacman -S --noconfirm --needed gum" >/dev/null
|
||||
chroot_bash -lc "source /home/$OMARCHY_USER/.local/share/omarchy/install.sh"
|
||||
|
||||
configure_login_for_unencrypted_install
|
||||
|
||||
# Reboot if requested by installer
|
||||
if [[ -f /mnt/var/tmp/omarchy-install-completed ]]; then
|
||||
reboot
|
||||
fi
|
||||
}
|
||||
|
||||
# Set Tokyo Night color scheme for the terminal
|
||||
set_tokyo_night_colors() {
|
||||
if [[ $(tty) == "/dev/tty"* ]]; then
|
||||
# Tokyo Night color palette
|
||||
echo -en "\e]P01a1b26" # black (background)
|
||||
echo -en "\e]P1f7768e" # red
|
||||
echo -en "\e]P29ece6a" # green
|
||||
echo -en "\e]P3e0af68" # yellow
|
||||
echo -en "\e]P47aa2f7" # blue
|
||||
echo -en "\e]P5bb9af7" # magenta
|
||||
echo -en "\e]P67dcfff" # cyan
|
||||
echo -en "\e]P7a9b1d6" # white
|
||||
echo -en "\e]P8414868" # bright black
|
||||
echo -en "\e]P9f7768e" # bright red
|
||||
echo -en "\e]PA9ece6a" # bright green
|
||||
echo -en "\e]PBe0af68" # bright yellow
|
||||
echo -en "\e]PC7aa2f7" # bright blue
|
||||
echo -en "\e]PDbb9af7" # bright magenta
|
||||
echo -en "\e]PE7dcfff" # bright cyan
|
||||
echo -en "\e]PFc0caf5" # bright white (foreground)
|
||||
|
||||
# Set default foreground and background
|
||||
echo -en "\033[0m"
|
||||
clear
|
||||
fi
|
||||
}
|
||||
|
||||
install_disk() {
|
||||
jq -er 'first(.disk_config.device_modifications[]? | select(.wipe == true) | .device)' user_configuration.json
|
||||
}
|
||||
|
||||
cleanup_install_disk() {
|
||||
local disk="$1"
|
||||
|
||||
if [[ -z "$disk" || ! -b "$disk" ]]; then
|
||||
echo "Could not determine install disk for cleanup" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Cleaning up existing holders on install disk: $disk"
|
||||
|
||||
# Ensure that no mounts exist from past install attempts.
|
||||
findmnt -R /mnt >/dev/null && umount -R /mnt || true
|
||||
|
||||
# Turn off swap and unmount anything backed by the selected disk, including
|
||||
# device-mapper children from a previous install. Active LVM/swap holders can
|
||||
# prevent the kernel from re-reading the partition table after archinstall
|
||||
# wipes and recreates it.
|
||||
while read -r dev; do
|
||||
[[ -b "$dev" ]] || continue
|
||||
|
||||
swapoff "$dev" 2>/dev/null || true
|
||||
|
||||
while read -r target; do
|
||||
[[ -n "$target" ]] || continue
|
||||
umount "$target" 2>/dev/null || true
|
||||
done < <(findmnt -rn -S "$dev" -o TARGET 2>/dev/null || true)
|
||||
done < <(lsblk -rnpo PATH "$disk")
|
||||
|
||||
# Deactivate any LVM volume groups whose physical volumes live on the selected
|
||||
# disk. This is the common case when replacing Fedora/Alma/RHEL installs.
|
||||
while read -r dev type; do
|
||||
[[ "$type" == "disk" || "$type" == "part" || "$type" == "crypt" ]] || continue
|
||||
|
||||
while read -r vg; do
|
||||
[[ -n "$vg" ]] || continue
|
||||
vgchange -an "$vg" 2>/dev/null || true
|
||||
done < <(pvs --noheadings -o vg_name "$dev" 2>/dev/null | awk '{$1=$1; print}' | sort -u)
|
||||
done < <(lsblk -rnpo PATH,TYPE "$disk")
|
||||
|
||||
# Close any LUKS mappings stacked on the selected disk after filesystems and
|
||||
# swap have been released.
|
||||
while read -r dev type; do
|
||||
[[ "$type" == "crypt" ]] || continue
|
||||
cryptsetup close "$dev" 2>/dev/null || true
|
||||
done < <(lsblk -rnpo PATH,TYPE "$disk")
|
||||
|
||||
blockdev --flushbufs "$disk" 2>/dev/null || true
|
||||
partprobe "$disk" 2>/dev/null || true
|
||||
udevadm settle || true
|
||||
}
|
||||
|
||||
install_base_system() {
|
||||
# Initialize and populate the keyring
|
||||
pacman-key --init
|
||||
pacman-key --populate archlinux
|
||||
pacman-key --populate omarchy
|
||||
|
||||
# Sync the offline database so pacman can find packages
|
||||
pacman -Sy --noconfirm
|
||||
|
||||
cleanup_install_disk "$(install_disk)"
|
||||
|
||||
# Workarounds for archinstall 4.2 regressions under Python 3.14:
|
||||
# 1. sync_log_to_install_medium: `self.target / absolute_logfile` drops
|
||||
# self.target because the RHS is absolute, so Path.copy() raises EINVAL
|
||||
# (source == target).
|
||||
# 2. _add_limine_bootloader: `Path.copy(efi_dir_path)` raises IsADirectoryError
|
||||
# because 3.14's Path.copy treats target as a literal path, not a directory
|
||||
# (shutil.copy used to auto-append the source filename).
|
||||
sed -i \
|
||||
-e 's|logfile_target = self\.target / absolute_logfile$|logfile_target = self.target / absolute_logfile.relative_to("/")|' \
|
||||
-e 's|(limine_path / file)\.copy(efi_dir_path)|(limine_path / file).copy(efi_dir_path / file)|' \
|
||||
-e "s|(limine_path / 'limine-bios.sys')\.copy(boot_limine_path)|(limine_path / 'limine-bios.sys').copy(boot_limine_path / 'limine-bios.sys')|" \
|
||||
/usr/lib/python3.14/site-packages/archinstall/lib/installer.py
|
||||
|
||||
# Install using files generated by the ./configurator
|
||||
# Skip NTP and WKD sync since we're offline (keyring is pre-populated in ISO)
|
||||
archinstall \
|
||||
--config user_configuration.json \
|
||||
--creds user_credentials.json \
|
||||
--silent \
|
||||
--skip-ntp \
|
||||
--skip-wkd \
|
||||
--skip-wifi-check
|
||||
|
||||
# Archinstall unmounts the ESP when it finishes. Omarchy's boot finalizer
|
||||
# needs the generated Limine config and EFI artifacts available under /boot.
|
||||
if ! mountpoint -q /mnt/boot; then
|
||||
arch-chroot /mnt mount /boot
|
||||
fi
|
||||
|
||||
# The installed fstab keeps the ESP root-only, but Omarchy finalization runs
|
||||
# as the target user and must discover the Limine config before using sudo to
|
||||
# replace it. Temporarily allow reads and directory traversal during install.
|
||||
boot_device=$(findmnt -nro SOURCE --target /mnt/boot)
|
||||
umount /mnt/boot
|
||||
mount -t vfat -o rw,fmask=0022,dmask=0022 "$boot_device" /mnt/boot
|
||||
|
||||
if ! arch-chroot -u "$OMARCHY_USER" /mnt test -x /boot; then
|
||||
echo "Target user cannot access the mounted ESP" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# After archinstall sets up the base system but before our installer runs,
|
||||
# we need to ensure the offline pacman.conf is in place
|
||||
cp /etc/pacman.conf /mnt/etc/pacman.conf
|
||||
|
||||
# Mount the offline mirror so it's accessible in the chroot
|
||||
mkdir -p /mnt/var/cache/omarchy/mirror/offline
|
||||
mount --bind /var/cache/omarchy/mirror/offline /mnt/var/cache/omarchy/mirror/offline
|
||||
|
||||
# Mount the packages dir so it's accessible in the chroot
|
||||
mkdir -p /mnt/opt/packages
|
||||
mount --bind /opt/packages /mnt/opt/packages
|
||||
|
||||
# No need to ask for sudo during the installation (omarchy itself responsible for removing after install)
|
||||
mkdir -p /mnt/etc/sudoers.d
|
||||
cat >/mnt/etc/sudoers.d/99-omarchy-installer <<EOF
|
||||
root ALL=(ALL:ALL) NOPASSWD: ALL
|
||||
%wheel ALL=(ALL:ALL) NOPASSWD: ALL
|
||||
$OMARCHY_USER ALL=(ALL:ALL) NOPASSWD: ALL
|
||||
EOF
|
||||
chmod 440 /mnt/etc/sudoers.d/99-omarchy-installer
|
||||
|
||||
# Copy the local omarchy repo to the user's home directory
|
||||
mkdir -p /mnt/home/$OMARCHY_USER/.local/share/
|
||||
cp -r /root/omarchy /mnt/home/$OMARCHY_USER/.local/share/
|
||||
|
||||
chown -R 1000:1000 /mnt/home/$OMARCHY_USER/.local/
|
||||
|
||||
# Ensure all necessary scripts are executable
|
||||
find /mnt/home/$OMARCHY_USER/.local/share/omarchy -type f -path "*/bin/*" -exec chmod +x {} \;
|
||||
chmod +x /mnt/home/$OMARCHY_USER/.local/share/omarchy/boot.sh 2>/dev/null || true
|
||||
find /mnt/home/$OMARCHY_USER/.local/share/omarchy/default/waybar -type f -name "*.sh" -exec chmod +x {} \; 2>/dev/null || true
|
||||
}
|
||||
|
||||
configure_login_for_unencrypted_install() {
|
||||
if [[ $(<user_encrypt_installation.txt) != "false" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Unencrypted installs must stop at SDDM so the user password is entered
|
||||
# before reaching the desktop. Omarchy's normal encrypted path may autologin
|
||||
# because the disk password was already entered at boot.
|
||||
#
|
||||
# Keep the Omarchy SDDM theme and seed SDDM's last user/session state so
|
||||
# first boot looks like the SDDM screen shown after logging out of Omarchy.
|
||||
mkdir -p /mnt/etc/sddm.conf.d
|
||||
rm -f /mnt/etc/sddm.conf.d/autologin.conf
|
||||
cat >/mnt/etc/sddm.conf.d/99-omarchy-login.conf <<EOF
|
||||
[Theme]
|
||||
Current=omarchy
|
||||
|
||||
[Users]
|
||||
RememberLastUser=true
|
||||
RememberLastSession=true
|
||||
EOF
|
||||
|
||||
mkdir -p /mnt/var/lib/sddm
|
||||
cat >/mnt/var/lib/sddm/state.conf <<EOF
|
||||
[Last]
|
||||
Session=omarchy.desktop
|
||||
User=$OMARCHY_USER
|
||||
EOF
|
||||
|
||||
rm -f /mnt/etc/systemd/system/getty@tty1.service.d/autologin.conf
|
||||
arch-chroot /mnt chown sddm:sddm /var/lib/sddm /var/lib/sddm/state.conf >/dev/null 2>&1 || true
|
||||
arch-chroot /mnt systemctl enable sddm.service >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
chroot_bash() {
|
||||
HOME=/home/$OMARCHY_USER \
|
||||
arch-chroot -u $OMARCHY_USER /mnt/ \
|
||||
env OMARCHY_CHROOT_INSTALL=1 \
|
||||
OMARCHY_USER_NAME="$(<user_full_name.txt)" \
|
||||
OMARCHY_USER_EMAIL="$(<user_email_address.txt)" \
|
||||
OMARCHY_MIRROR="$OMARCHY_MIRROR" \
|
||||
USER="$OMARCHY_USER" \
|
||||
HOME="/home/$OMARCHY_USER" \
|
||||
/bin/bash "$@"
|
||||
}
|
||||
|
||||
if [[ $(tty) == "/dev/tty1" ]]; then
|
||||
use_omarchy_helpers
|
||||
run_configurator
|
||||
install_arch
|
||||
install_omarchy
|
||||
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
|
||||
@@ -234,6 +277,447 @@ get_root_disk() {
|
||||
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..."
|
||||
|
||||
@@ -265,7 +749,60 @@ disk_form() {
|
||||
disk=$(echo "$selected_display" | awk '{print $1}')
|
||||
}
|
||||
|
||||
disk_form
|
||||
# STEP 4: INSTALL MODE
|
||||
|
||||
open_partition_tool() {
|
||||
step "Partition tool for $disk"
|
||||
gum style "Create unallocated free space for Omarchy, then write changes and quit."
|
||||
gum style --foreground 8 "Do not create an Omarchy partition here — leave the target area as Free space."
|
||||
gum style --foreground 8 "Tip: free-space install needs at least 32GB unallocated, plus 2GB more if no ESP already exists."
|
||||
echo
|
||||
gum confirm --affirmative "Open cfdisk" --negative "Back" "Open partition tool for $disk?" || return 0
|
||||
|
||||
clear
|
||||
cfdisk "$disk" || true
|
||||
partprobe "$disk" 2>/dev/null || true
|
||||
sync
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# If the disk has no partition table or no unallocated free space, the only
|
||||
# viable path is a full-disk install. Skip the install-mode picker and go
|
||||
# straight to confirmation.
|
||||
requires_full_disk_install() {
|
||||
local pt_type free_space_b
|
||||
pt_type=$(lsblk -dno PTTYPE "$disk" 2>/dev/null)
|
||||
|
||||
# a) No partition table: disk is empty
|
||||
[[ -z "$pt_type" ]] && return 0
|
||||
|
||||
# b) No usable free space
|
||||
free_space_b=$(parted -ms "$disk" unit B print free </dev/null 2>/dev/null | \
|
||||
awk -F: '
|
||||
$NF ~ /free/ {
|
||||
size=$4;
|
||||
gsub(/B/, "", size);
|
||||
if (size+0 > max+0) max=size;
|
||||
}
|
||||
END {
|
||||
print max+0;
|
||||
}')
|
||||
[[ -z "$free_space_b" || "$free_space_b" -eq 0 ]]
|
||||
}
|
||||
|
||||
install_mode_form() {
|
||||
local choices=("Full disk install")
|
||||
|
||||
# The free-space/protected path registers an EFI boot entry and is UEFI-only.
|
||||
# Full-disk installs still support BIOS through the orchestrator's BIOS branch.
|
||||
if [[ -d /sys/firmware/efi ]]; then
|
||||
choices+=("Free space install (alongside existing data)")
|
||||
fi
|
||||
choices+=("Choose a different disk")
|
||||
|
||||
step "Let's select how to install Omarchy..."
|
||||
install_mode=$(gum choose --header "Select installation mode on $disk" "${choices[@]}") || abort
|
||||
}
|
||||
|
||||
confirm_disk_overwrite() {
|
||||
local mode="encrypted"
|
||||
@@ -274,10 +811,10 @@ confirm_disk_overwrite() {
|
||||
while true; do
|
||||
clear_logo
|
||||
echo
|
||||
gum style "Everything will be overwritten. There is no recovery possible."
|
||||
say "Everything will be overwritten. There is no recovery possible."
|
||||
|
||||
if [[ $mode == "encrypted" ]]; then
|
||||
gum style --foreground 8 "Press Ctrl+C for unencrypted install."
|
||||
say --foreground 8 "Press Ctrl+C for unencrypted install."
|
||||
affirmative="Yes, install"
|
||||
else
|
||||
affirmative="Yes, install without encryption"
|
||||
@@ -305,44 +842,45 @@ confirm_disk_overwrite() {
|
||||
done
|
||||
}
|
||||
|
||||
while ! confirm_disk_overwrite; do
|
||||
disk_form
|
||||
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)
|
||||
|
||||
if [[ $encrypt_installation == true ]]; then
|
||||
credentials_encryption_line=" \"encryption_password\": $password_escaped,"
|
||||
else
|
||||
credentials_encryption_line=""
|
||||
fi
|
||||
|
||||
cat <<-_EOF_ >user_credentials.json
|
||||
{
|
||||
$credentials_encryption_line
|
||||
"root_enc_password": $password_hash_escaped,
|
||||
"users": [
|
||||
{
|
||||
"enc_password": $password_hash_escaped,
|
||||
"groups": [],
|
||||
"sudo": true,
|
||||
"username": $username_escaped
|
||||
}
|
||||
]
|
||||
}
|
||||
_EOF_
|
||||
|
||||
printf '%s\n' "$encrypt_installation" >user_encrypt_installation.txt
|
||||
write_user_files
|
||||
|
||||
# Setup partition layout
|
||||
disk_size=$(lsblk -bdno SIZE "$disk")
|
||||
@@ -357,12 +895,7 @@ boot_partition_size=$((2 * gib))
|
||||
main_partition_start=$((boot_partition_size + boot_partition_start))
|
||||
main_partition_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"
|
||||
else
|
||||
kernel_choice="linux"
|
||||
fi
|
||||
kernel_choice=$(detect_kernel)
|
||||
|
||||
if [[ $encrypt_installation == true ]]; then
|
||||
disk_encryption_config=$(cat <<-_EOF_
|
||||
@@ -372,7 +905,7 @@ if [[ $encrypt_installation == true ]]; then
|
||||
"lvm_volumes": [],
|
||||
"iter_time": 2000,
|
||||
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ],
|
||||
"encryption_password": $password_escaped
|
||||
"encryption_password": $(echo -n "$password" | jq -Rsa)
|
||||
}
|
||||
_EOF_
|
||||
)
|
||||
@@ -386,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": [
|
||||
{
|
||||
@@ -479,7 +1020,8 @@ cat <<-_EOF_ >user_configuration.json
|
||||
"base-devel",
|
||||
"git",
|
||||
"omarchy-keyring",
|
||||
"snapper"
|
||||
"$OMARCHY_SETTINGS_PACKAGE",
|
||||
"$OMARCHY_RUNTIME_PACKAGE"
|
||||
],
|
||||
"profile_config": {
|
||||
"gfx_driver": null,
|
||||
@@ -491,20 +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
|
||||
|
||||
echo -e "\nDisk Encryption:"
|
||||
cat user_encrypt_installation.txt
|
||||
|
||||
rm user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt
|
||||
print_dry_run_files
|
||||
fi
|
||||
+744
@@ -0,0 +1,744 @@
|
||||
#!/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_PM=10
|
||||
|
||||
# ── Install progress ─────────────────────────────────────────────────────────
|
||||
#
|
||||
# Position = max(floor, work), both non-decreasing, in per-mille of the bar.
|
||||
#
|
||||
# floor lo + (span-1) * t / (t+tau), on time since this process saw the
|
||||
# phase. Asymptotic, so it approaches the band end without reaching it.
|
||||
# tau is a shape constant, not a duration prediction.
|
||||
# work packages installed / PKG_TOTAL, for the package phase only (~85% of
|
||||
# the install). libalpm makes one directory per package under
|
||||
# <target>/var/lib/pacman/local; the denominator comes from
|
||||
# build-iso.sh, so there is no second package list to drift.
|
||||
#
|
||||
# Per-mille because a whole-percent asymptote over a 2-point band has two
|
||||
# reachable values and stops moving. No give-up predicate: two discontinuous
|
||||
# curves behind a monotonic clamp freeze. With no signal the floor alone still
|
||||
# finishes the band.
|
||||
EXPECTED_PACKAGES_FILE="${OMARCHY_EXPECTED_PACKAGES_FILE:-/usr/share/omarchy-iso/expected-packages}"
|
||||
PKG_TOTAL=0
|
||||
if [[ -r $EXPECTED_PACKAGES_FILE ]]; then
|
||||
read -r PKG_TOTAL _ <"$EXPECTED_PACKAGES_FILE" 2>/dev/null || PKG_TOTAL=0
|
||||
fi
|
||||
[[ $PKG_TOTAL =~ ^[0-9]+$ ]] || PKG_TOTAL=0
|
||||
# A wrong denominator is worse than none: it never triggers the fallback.
|
||||
(( PKG_TOTAL < 300 || PKG_TOTAL > 5000 )) && PKG_TOTAL=0
|
||||
|
||||
PKG_BONUS_MAX_PM=100 # cap on how far the bar may run ahead of the count
|
||||
PKG_TAIL_PM=40 # band held back so a stalled bar has somewhere to go
|
||||
PKG_FLOOR_TAU=900 # with a work signal, time is only a backstop until the
|
||||
# first package, so slow it until it cannot outrun data
|
||||
|
||||
POS=10 # position, per-mille, monotonic
|
||||
PROGRESS_PM=10
|
||||
NOW=0 # monotonic seconds, refreshed once per frame
|
||||
DASH_T0=0
|
||||
PHASE_NAME="" # last phase this process observed
|
||||
PHASE_T0=0 # NOW when it was first observed
|
||||
PHASE_LO=10 # POS on entry, for an unknown phase with no index
|
||||
PKG_DB_DIR=""
|
||||
PKG_COUNT=0
|
||||
PKG_BASE=-1 # package count when the phase started
|
||||
PKG_SEEN=0 # running max of packages since PKG_BASE
|
||||
PKG_LAST_CHANGE=0
|
||||
PKG_BONUS=0
|
||||
|
||||
# Monotonic clock: tzupdate and systemd-timesyncd can step the wall clock
|
||||
# mid-install, which would jump every time-driven band to its ceiling.
|
||||
set_now() {
|
||||
local up rest
|
||||
if read -r up rest </proc/uptime 2>/dev/null && [[ $up == [0-9]* ]]; then
|
||||
NOW=${up%%.*}
|
||||
else
|
||||
NOW=$EPOCHSECONDS
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
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 pm="$1" width="${2:-34}" filled empty
|
||||
(( pm < 0 )) && pm=0
|
||||
(( pm > 1000 )) && pm=1000
|
||||
filled=$(( pm * width / 1000 ))
|
||||
empty=$(( width - filled ))
|
||||
printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET"
|
||||
}
|
||||
|
||||
# Packages libalpm has finished installing. local/ holds one directory per
|
||||
# package and nothing else in it is a directory, so */ is exact. One glob, no
|
||||
# forks: this runs twice a second while the disk is saturated.
|
||||
count_packages() {
|
||||
local -a entries
|
||||
PKG_COUNT=0
|
||||
[[ -n $PKG_DB_DIR ]] || return 0
|
||||
entries=("$PKG_DB_DIR"/*/)
|
||||
# Unmatched globs expand to the pattern; nullglob stays off script-wide.
|
||||
if (( ${#entries[@]} == 1 )) && [[ ! -d ${entries[0]} ]]; then
|
||||
return 0
|
||||
fi
|
||||
PKG_COUNT=${#entries[@]}
|
||||
return 0
|
||||
}
|
||||
|
||||
# Sets PROGRESS_PM. Never call in a command substitution: POS, the phase timer
|
||||
# and the package counters must survive between frames. Print nothing — output
|
||||
# here would land on the TTY between the cursor escape and the bar.
|
||||
install_progress() {
|
||||
local phase finished index total target
|
||||
local lo hi tau span t pos work gap n pkg_signal=0
|
||||
local -a state
|
||||
|
||||
PROGRESS_PM=$POS
|
||||
[[ -f $STATE_FILE ]] || return 0
|
||||
|
||||
mapfile -t state < <(jq -r '
|
||||
(.current_phase // "Starting installation"),
|
||||
((.finished_at // 0) | tonumber? // 0 | floor),
|
||||
((.current_index // -1) | tonumber? // -1 | floor),
|
||||
((.total_phases // 0) | tonumber? // 0 | floor),
|
||||
(.target // "/mnt")
|
||||
' "$STATE_FILE" 2>/dev/null)
|
||||
(( ${#state[@]} == 5 )) || return 0
|
||||
phase="${state[0]}" finished="${state[1]}"
|
||||
index="${state[2]}" total="${state[3]}" target="${state[4]}"
|
||||
[[ -n $target ]] || target="/mnt"
|
||||
|
||||
if (( finished > 0 )) || [[ $phase == "Installation complete" ]]; then
|
||||
POS=1000
|
||||
PROGRESS_PM=1000
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Transitions are observed here rather than read from the state file, which
|
||||
# keeps wall clock out of the dashboard entirely.
|
||||
if [[ $phase != "$PHASE_NAME" ]]; then
|
||||
PHASE_NAME="$phase"
|
||||
PHASE_T0=$NOW
|
||||
PHASE_LO=$POS
|
||||
PKG_BASE=-1
|
||||
PKG_SEEN=0
|
||||
PKG_BONUS=0
|
||||
PKG_LAST_CHANGE=$NOW
|
||||
fi
|
||||
|
||||
# Bands in per-mille, plus tau. The tail bands are wide relative to what they
|
||||
# cost on NVMe: run_system_finalizer can DKMS-build a driver and
|
||||
# finalize_limine_boot rebuilds every UKI, so they vary most on real hardware.
|
||||
# Over-allotting costs a step at a boundary; under-allotting costs a stall.
|
||||
case "$phase" in
|
||||
"Starting installation") lo=10 hi=25 tau=5 ;;
|
||||
"Preparing live environment") lo=25 hi=50 tau=25 ;;
|
||||
"Preparing install target") lo=50 hi=65 tau=20 ;;
|
||||
"Installing Arch + Omarchy") lo=65 hi=700 tau=150 pkg_signal=1 ;;
|
||||
"Configuring hibernation") lo=700 hi=720 tau=10 ;;
|
||||
"Configuring system") lo=720 hi=850 tau=120 ;;
|
||||
"Finalizing Limine boot") lo=850 hi=950 tau=90 ;;
|
||||
"Finalizing user") lo=950 hi=978 tau=30 ;;
|
||||
"Configuring login") lo=978 hi=988 tau=5 ;;
|
||||
"Configuring DNS resolver") lo=988 hi=994 tau=5 ;;
|
||||
"Validating boot setup") lo=994 hi=1000 tau=5 ;;
|
||||
*)
|
||||
# Unknown names used to inherit a 1->99 sweep. Derive an even band from
|
||||
# the published index instead; with no index, a bounded slice, so a
|
||||
# renamed phase degrades to one cell rather than a runaway.
|
||||
if (( total >= 2 && index >= 0 && index < total )); then
|
||||
lo=$(( 10 + 990 * index / total ))
|
||||
hi=$(( 10 + 990 * (index + 1) / total ))
|
||||
else
|
||||
lo=$PHASE_LO
|
||||
hi=$(( PHASE_LO + 30 ))
|
||||
fi
|
||||
tau=60
|
||||
;;
|
||||
esac
|
||||
|
||||
(( pkg_signal && PKG_TOTAL > 0 )) || pkg_signal=0
|
||||
(( pkg_signal )) && tau=$PKG_FLOOR_TAU
|
||||
|
||||
t=$(( NOW - PHASE_T0 ))
|
||||
(( t < 0 )) && t=0
|
||||
span=$(( hi - lo ))
|
||||
(( span < 2 )) && span=2
|
||||
(( tau < 1 )) && tau=1
|
||||
|
||||
pos=$(( lo + (span - 1) * t / (t + tau) ))
|
||||
|
||||
if (( pkg_signal )); then
|
||||
PKG_DB_DIR="$target/var/lib/pacman/local"
|
||||
count_packages
|
||||
# Baseline on first sight: a target that already holds packages would
|
||||
# otherwise pin the bar at the band ceiling for the whole phase.
|
||||
(( PKG_BASE < 0 )) && PKG_BASE=$PKG_COUNT
|
||||
n=$(( PKG_COUNT - PKG_BASE ))
|
||||
(( n < 0 )) && n=0
|
||||
# Running max: a reinstalled package briefly drops its version-stamped dir.
|
||||
if (( n > PKG_SEEN )); then
|
||||
PKG_SEEN=$n
|
||||
PKG_LAST_CHANGE=$NOW
|
||||
fi
|
||||
gap=$(( NOW - PKG_LAST_CHANGE ))
|
||||
(( gap < 0 )) && gap=0
|
||||
(( gap > PKG_BONUS_MAX_PM )) && gap=$PKG_BONUS_MAX_PM
|
||||
(( gap > PKG_BONUS )) && PKG_BONUS=$gap
|
||||
work=$(( lo + (span - 1 - PKG_TAIL_PM) * PKG_SEEN / PKG_TOTAL + PKG_BONUS ))
|
||||
(( work > pos )) && pos=$work
|
||||
fi
|
||||
|
||||
(( pos > hi - 1 )) && pos=$(( hi - 1 ))
|
||||
(( pos > POS )) && POS=$pos
|
||||
(( POS > 1000 )) && POS=1000
|
||||
PROGRESS_PM=$POS
|
||||
return 0
|
||||
}
|
||||
|
||||
# One tip per 8 seconds, in order. On elapsed time, not a frame counter: a frame
|
||||
# is 0.5s of sleep plus a render, which stretches under pacstrap I/O — on the
|
||||
# machine where the tips, not the bar, are carrying liveness.
|
||||
current_tip() {
|
||||
printf '%s' "${tips[$(( (NOW - DASH_T0) / 8 % ${#tips[@]} ))]}"
|
||||
}
|
||||
|
||||
render_static() {
|
||||
printf '%s' "$CLEAR"
|
||||
blank_line
|
||||
render_logo
|
||||
blank_line
|
||||
}
|
||||
|
||||
render_dynamic() {
|
||||
local pm
|
||||
set_now
|
||||
install_progress
|
||||
pm=$PROGRESS_PM
|
||||
[[ $pm =~ ^[0-9]+$ ]] || pm=$LAST_PM
|
||||
(( pm > 1000 )) && pm=1000
|
||||
# POS is monotone by construction; this is a guard, not the mechanism.
|
||||
(( pm < LAST_PM )) && pm=$LAST_PM
|
||||
LAST_PM=$pm
|
||||
|
||||
printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW"
|
||||
center "Installing Omarchy" "$CONTENT_WIDTH"
|
||||
blank_line
|
||||
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) ""
|
||||
progress_bar "$pm" 34
|
||||
printf '\n'
|
||||
blank_line
|
||||
center "${DIM}Tip:${RESET} ${GREEN}$(current_tip)${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"
|
||||
set_now
|
||||
DASH_T0=$NOW
|
||||
PHASE_T0=$NOW
|
||||
PKG_LAST_CHANGE=$NOW
|
||||
launch_child
|
||||
|
||||
while jobs -pr | grep -qx "$child_pid"; do
|
||||
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
||||
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
|
||||
# finished_at lands microseconds before the child exits, so the 0.5s poll
|
||||
# rarely renders a full bar before the finish screen replaces it.
|
||||
render_dynamic >"$TTY_PATH" 2>/dev/null || true
|
||||
sleep 0.4
|
||||
fi
|
||||
|
||||
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,100 @@
|
||||
"""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 (
|
||||
boost_cpu_governor,
|
||||
cleanup_bind_mounts,
|
||||
cleanup_protected_state,
|
||||
cleanup_target_hook_masks,
|
||||
restore_cpu_governors,
|
||||
)
|
||||
|
||||
governors = boost_cpu_governor()
|
||||
|
||||
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:
|
||||
restore_cpu_governors(governors)
|
||||
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,105 @@
|
||||
"""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 os
|
||||
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(),
|
||||
# The dashboard counts packages under <target>/var/lib/pacman/local;
|
||||
# publish the path rather than have the UI assume /mnt.
|
||||
"target": str(ctx.target),
|
||||
"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()
|
||||
# Expected vs actual for the bar's denominator, so drift is visible in
|
||||
# acceptance runs rather than only by watching a bar creep.
|
||||
state["installed_packages"] = _installed_package_count(ctx.target)
|
||||
state["expected_packages"] = _expected_package_count()
|
||||
_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 _installed_package_count(target: Path) -> int:
|
||||
"""Packages libalpm installed into the target — one directory each under
|
||||
local/, which is what the dashboard counts live."""
|
||||
local_db = target / "var" / "lib" / "pacman" / "local"
|
||||
try:
|
||||
with os.scandir(local_db) as entries:
|
||||
return sum(1 for entry in entries if entry.is_dir())
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
|
||||
def _expected_package_count() -> int:
|
||||
path = Path("/usr/share/omarchy-iso/expected-packages")
|
||||
try:
|
||||
return int(path.read_text().split()[0])
|
||||
except (OSError, ValueError, IndexError):
|
||||
return 0
|
||||
|
||||
|
||||
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 xe.enable_panel_replay=0
|
||||
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 xe.enable_panel_replay=0
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 initramfs_async=0
|
||||
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
|
||||
}
|
||||
|
||||
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 xe.enable_panel_replay=0
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on xe.enable_panel_replay=0 initramfs_async=0
|
||||
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
|
||||
}
|
||||
|
||||
|
||||
@@ -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 xe.enable_panel_replay=0
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" quiet splash xe.enable_panel_replay=0 initramfs_async=0
|
||||
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
|
||||
}
|
||||
|
||||
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 xe.enable_panel_replay=0
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" accessibility=on xe.enable_panel_replay=0 initramfs_async=0
|
||||
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,13 @@ ParallelDownloads = 5
|
||||
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 = Optional TrustAll
|
||||
SigLevel = Never
|
||||
Server = file:///var/cache/omarchy/mirror/offline/
|
||||
@@ -30,5 +30,6 @@ 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
|
||||
@@ -27,8 +27,9 @@ Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/rc/$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
|
||||
@@ -30,5 +30,6 @@ 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
|
||||
+21
-3
@@ -12,15 +12,33 @@ bootmodes=('bios.syslinux' 'uefi.grub')
|
||||
arch="x86_64"
|
||||
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 xe.enable_panel_replay=0
|
||||
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=0 initramfs_async=0
|
||||
|
||||
# Accessibility boot option
|
||||
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 xe.enable_panel_replay=0
|
||||
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on xe.enable_panel_replay=0 initramfs_async=0
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -16,7 +16,7 @@ These exist outside omarchy-iso. Flagged for visibility — without them, nothin
|
||||
|
||||
1. **aarch64 base packages must exist somewhere we can pacman from.** Vanilla Arch (`geo.mirror.pkgbuild.com`) is x86_64-only — there is no `core/os/aarch64`. The realistic source is **Arch Linux ARM** (`mirror.archlinuxarm.org`) for `core`/`extra`/`alarm`/`aur`. Decision required: target Arch Linux ARM as the aarch64 base distribution.
|
||||
2. **`pkgs.omarchy.org/{stable,edge}/aarch64/`** must serve a real repo. Probed today, both return 404. omarchy-pkgs already has multi-arch build support per its README, so this is a publish step, not a port.
|
||||
3. **Omarchy installer guard** at `~/.local/share/omarchy/install/preflight/guard.sh:25` hard-aborts on `uname -m != x86_64`. Must be relaxed to allow aarch64 (or scoped behind a flag).
|
||||
3. **Omarchy ISO architecture guard** must allow supported architectures or scope any x86_64-only checks behind an explicit guard.
|
||||
4. **archinstall + Limine** must work end-to-end on aarch64. archinstall supports it; Limine supports aarch64 UEFI. Worth a manual verification before committing to this bootloader path on ARM.
|
||||
|
||||
---
|
||||
@@ -109,9 +109,9 @@ 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"/*x86_64-"$INSTALLER_REF".iso | head -n1)
|
||||
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*${OMARCHY_ARCH}-"$ISO_REF".iso | head -n1)
|
||||
```
|
||||
Hardcoded `x86_64` glob. Take an arch arg, or do `*${OMARCHY_ARCH}-${INSTALLER_REF}.iso`. The ISO filename already contains arch (set by archiso from `profiledef.sh`), so this just needs the glob parameterized.
|
||||
Hardcoded `x86_64` glob. Take an arch arg, or do `*${OMARCHY_ARCH}-${ISO_REF}.iso`. The ISO filename already contains arch (set by archiso from `profiledef.sh`), so this just needs the glob parameterized.
|
||||
|
||||
### 11. CI — `.github/workflows/nightly-build.yml`
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ Create a second ISO flavor for OEM preloading that is installed to internal stor
|
||||
- `grub/loopback.cfg`: remove `archlinux-accessibility` menuentry.
|
||||
- `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_INSTALLER_REF` suffix).
|
||||
- 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`.
|
||||
|
||||
Reference in new issue
Block a user