Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c36557fd5 | ||
|
|
168c6edb0b |
No files matched your search
@@ -1,9 +1,2 @@
|
||||
release/**
|
||||
vm-saves/**
|
||||
|
||||
# Python bytecode (orchestrator)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Acceptance test harness artifacts
|
||||
test-runs/**
|
||||
@@ -1,6 +1,6 @@
|
||||
# Omarchy ISO
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Downloading the latest ISO
|
||||
|
||||
@@ -8,51 +8,31 @@ See the ISO link on [omarchy.org](https://omarchy.org).
|
||||
|
||||
## Creating the ISO
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
For local development, build the ISO from sibling checkouts:
|
||||
### Environment Variables
|
||||
|
||||
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
|
||||
./bin/omarchy-iso-make --local-source ../omarchy-installer ../omarchy-pkgs
|
||||
OMARCHY_INSTALLER_REPO="myuser/omarchy-fork" OMARCHY_INSTALLER_REF="some-feature" ./bin/omarchy-iso-make
|
||||
```
|
||||
|
||||
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 rclone configuration (`rclone config`).
|
||||
Run `./bin/omarchy-iso-upload [release/omarchy.iso]`. This requires you've configured rclone (use `rclone config`).
|
||||
|
||||
## Full release of the ISO
|
||||
|
||||
|
||||
+13
-121
@@ -2,113 +2,19 @@
|
||||
|
||||
set -e
|
||||
|
||||
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=()
|
||||
ISO="$1"
|
||||
REUSE="$2"
|
||||
|
||||
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
|
||||
if [ $# -eq 0 ]; then
|
||||
ISO=$(ls -t release/*.iso 2>/dev/null | gum choose --header "Select ISO to boot")
|
||||
if gum confirm --default=false "Reuse existing disk?"; then
|
||||
REUSE="reuse"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z $ISO ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $DISK || -z $OVMF_VARS ]]; then
|
||||
echo "Disk and OVMF vars paths must not be empty" >&2
|
||||
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)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -130,32 +36,19 @@ 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 "$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."
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd /tmp/OVMF_VARS.4m.fd
|
||||
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) < 8 ? $(nproc) : 8))" \
|
||||
-smp "$(nproc)" \
|
||||
-m 8192 \
|
||||
-drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
|
||||
-drive if=pflash,format=raw,file="$OVMF_VARS" \
|
||||
-drive if=pflash,format=raw,file=/tmp/OVMF_VARS.4m.fd \
|
||||
-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 \
|
||||
@@ -163,7 +56,6 @@ qemu-system-x86_64 \
|
||||
-device virtio-vga-gl \
|
||||
-display sdl,gl=on \
|
||||
-usb -device usb-tablet \
|
||||
-serial none \
|
||||
"${NET_ARGS[@]}" \
|
||||
-boot menu=on \
|
||||
"${EXTRA_QEMU_ARGS[@]}"
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
|
||||
-device virtio-net-pci,netdev=net0 \
|
||||
-boot menu=on
|
||||
@@ -3,14 +3,25 @@ set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
if [[ -z "${OMARCHY_PATH:-}" ]]; then
|
||||
for candidate in "$repo_root/../omarchy-installer" "$repo_root/../omarchy" /usr/share/omarchy; do
|
||||
if [[ -f "$candidate/logo.txt" ]]; then
|
||||
export OMARCHY_PATH="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
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
|
||||
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 "$@"
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Preview the shipped Omarchy installer dashboard without building an ISO.
|
||||
set -euo pipefail
|
||||
|
||||
write_simulation_state() {
|
||||
local state_file="$1" started_at="$2" phase_started_at="$3" current_phase="$4"
|
||||
local finished_at="${5:-null}" temporary="${state_file}.tmp"
|
||||
|
||||
jq -n \
|
||||
--argjson started_at "$started_at" \
|
||||
--argjson phase_started_at "$phase_started_at" \
|
||||
--arg current_phase "$current_phase" \
|
||||
--argjson finished_at "$finished_at" \
|
||||
'{
|
||||
started_at: $started_at,
|
||||
phase_started_at: $phase_started_at,
|
||||
current_phase: $current_phase
|
||||
} + if $finished_at == null then {} else {finished_at: $finished_at} end' \
|
||||
>"$temporary"
|
||||
mv -f -- "$temporary" "$state_file"
|
||||
}
|
||||
|
||||
simulate_install() {
|
||||
local state_file="$1"
|
||||
local started_at="$EPOCHREALTIME"
|
||||
local phase phase_started_at finished_at duration i
|
||||
local -a phases=(
|
||||
"Starting installation" 0.5
|
||||
"Preparing live environment" 0.8
|
||||
"Installing Arch + Omarchy" 5.0
|
||||
"Configuring system" 1.2
|
||||
"Finalizing Limine boot" 1.0
|
||||
"Finalizing user" 1.0
|
||||
"Configuring login" 0.6
|
||||
"Validating boot setup" 0.8
|
||||
)
|
||||
|
||||
for ((i = 0; i < ${#phases[@]}; i += 2)); do
|
||||
phase="${phases[i]}"
|
||||
duration="${phases[i + 1]}"
|
||||
phase_started_at="$EPOCHREALTIME"
|
||||
write_simulation_state "$state_file" "$started_at" "$phase_started_at" "$phase"
|
||||
sleep "$duration"
|
||||
done
|
||||
|
||||
finished_at="$EPOCHREALTIME"
|
||||
write_simulation_state "$state_file" "$started_at" "$finished_at" "Installation complete" "$finished_at"
|
||||
sleep 0.3
|
||||
}
|
||||
|
||||
if [[ ${1:-} == "--simulate-install" ]]; then
|
||||
(( $# == 2 )) || exit 2
|
||||
simulate_install "$2"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if (( $# > 0 )); then
|
||||
echo "Usage: $0" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
dashboard="$repo_root/configs/airootfs/usr/local/bin/omarchy-install-dashboard"
|
||||
|
||||
if [[ ! -x $dashboard ]]; then
|
||||
echo "Installer dashboard is not executable: $dashboard" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z ${OMARCHY_PATH:-} ]]; then
|
||||
for candidate in "$repo_root/../omarchy" /usr/share/omarchy; do
|
||||
if [[ -f $candidate/logo.txt ]]; then
|
||||
export OMARCHY_PATH="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -z ${OMARCHY_PATH:-} || ! -f $OMARCHY_PATH/logo.txt ]]; then
|
||||
echo "Unable to find Omarchy logo.txt; set OMARCHY_PATH to an Omarchy checkout." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
preview_dir="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$preview_dir"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
log_file="$preview_dir/install.log"
|
||||
state_file="$preview_dir/state.json"
|
||||
touch "$log_file"
|
||||
|
||||
OMARCHY_UI_AUTO_REBOOT=no \
|
||||
"$dashboard" "$log_file" "$state_file" -- \
|
||||
"$0" --simulate-install "$state_file"
|
||||
+26
-75
@@ -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,28 +15,13 @@ while [[ $# -gt 0 ]]; do
|
||||
NO_BOOT_OFFER=1
|
||||
shift
|
||||
;;
|
||||
--debug)
|
||||
OMARCHY_INSTALL_DEBUG=1
|
||||
shift
|
||||
;;
|
||||
--edge)
|
||||
OMARCHY_ISO_REF=edge
|
||||
OMARCHY_MIRROR=edge
|
||||
shift
|
||||
;;
|
||||
--local-source)
|
||||
LOCAL_OMARCHY_PATH="${2:-}"
|
||||
LOCAL_PKGS_PATH="${3:-}"
|
||||
if [[ -z $LOCAL_OMARCHY_PATH || -z $LOCAL_PKGS_PATH ]]; then
|
||||
echo "Error: --local-source requires two args: <omarchy-checkout> <pkgs-checkout>" >&2
|
||||
exit 1
|
||||
fi
|
||||
LOCAL_SOURCE=1
|
||||
OMARCHY_MIRROR=edge
|
||||
OMARCHY_ISO_REF=local
|
||||
shift 3
|
||||
shift
|
||||
;;
|
||||
--dev)
|
||||
OMARCHY_ISO_REF=dev
|
||||
OMARCHY_INSTALLER_REF=dev
|
||||
OMARCHY_MIRROR=edge
|
||||
shift
|
||||
;;
|
||||
@@ -46,77 +31,40 @@ 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] [--debug] [--edge] [--dev|--rc] [--local-source <omarchy-checkout> <pkgs-checkout>]"
|
||||
echo "Usage: $0 [--no-cache] [--no-boot-offer] [--local-source] [--dev] [--quattro] [--rc]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n $LOCAL_OMARCHY_PATH ]]; then
|
||||
[[ -d $LOCAL_OMARCHY_PATH ]] || { echo "Error: Omarchy checkout not found at $LOCAL_OMARCHY_PATH" >&2; exit 1; }
|
||||
[[ -d $LOCAL_PKGS_PATH ]] || { echo "Error: pkgs checkout not found at $LOCAL_PKGS_PATH" >&2; exit 1; }
|
||||
LOCAL_OMARCHY_PATH=$(realpath "$LOCAL_OMARCHY_PATH")
|
||||
LOCAL_PKGS_PATH=$(realpath "$LOCAL_PKGS_PATH")
|
||||
fi
|
||||
|
||||
lint_file_permissions() {
|
||||
local profiledef="configs/profiledef.sh"
|
||||
local -A declared=()
|
||||
local missing=()
|
||||
|
||||
while IFS= read -r path; do
|
||||
[[ -n $path ]] && declared["$path"]=1
|
||||
done < <(grep -oP '^\s*\["\K[^"]+(?="\])' "$profiledef" || true)
|
||||
|
||||
while IFS= read -r f; do
|
||||
[[ -f $f ]] || continue
|
||||
|
||||
local rel="${f#configs/airootfs}"
|
||||
local mode
|
||||
mode=$(git ls-files --stage -- "$f" | awk '{print $1}')
|
||||
|
||||
if [[ $mode != 100755 && $(head -c 2 "$f") != "#!" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -z ${declared[$rel]+x} ]]; then
|
||||
missing+=("$rel")
|
||||
fi
|
||||
done < <(find configs/airootfs/usr/local/bin configs/airootfs/root -type f 2>/dev/null)
|
||||
|
||||
if (( ${#missing[@]} > 0 )); then
|
||||
echo "ERROR: the following executable files are missing from $profiledef file_permissions:" >&2
|
||||
printf ' %s\n' "${missing[@]}" >&2
|
||||
echo "Add entries like [\"<path>\"]=\"0:0:755\" to profiledef.sh and retry." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
lint_file_permissions
|
||||
|
||||
# Clear out old packages
|
||||
sudo rm -rf /var/cache/pacman/pkg/*
|
||||
|
||||
# Ensure we have the submodule checked out
|
||||
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_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
|
||||
OMARCHY_INSTALLER_REPO="${OMARCHY_INSTALLER_REPO:-basecamp/omarchy}"
|
||||
OMARCHY_INSTALLER_REF="${OMARCHY_INSTALLER_REF:-master}"
|
||||
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
|
||||
|
||||
DOCKER_ARGS=(
|
||||
--rm
|
||||
--privileged
|
||||
-e "OMARCHY_ISO_REF=$OMARCHY_ISO_REF"
|
||||
-e "OMARCHY_INSTALLER_REPO=$OMARCHY_INSTALLER_REPO"
|
||||
-e "OMARCHY_INSTALLER_REF=$OMARCHY_INSTALLER_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/"
|
||||
@@ -126,14 +74,13 @@ DOCKER_ARGS=(
|
||||
-v "$BUILD_ROOT/configs:/configs:ro"
|
||||
)
|
||||
|
||||
if [[ -n $LOCAL_OMARCHY_PATH ]]; then
|
||||
DOCKER_ARGS+=(
|
||||
-v "$LOCAL_OMARCHY_PATH:/omarchy-source:ro"
|
||||
-v "$LOCAL_PKGS_PATH:/omarchy-pkgs: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")
|
||||
fi
|
||||
|
||||
if [[ -d /var/cache/pacman/pkg ]]; then
|
||||
# Use local pacman cache if you already have one on host to speed up repeat runs
|
||||
if [ -d "/var/cache/pacman/pkg" ]; then
|
||||
DOCKER_ARGS+=(-v "/var/cache/pacman/pkg:/var/cache/pacman/pkg")
|
||||
fi
|
||||
|
||||
@@ -142,16 +89,20 @@ 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_ISO_REF.iso"
|
||||
iso_ref="${latest_iso%.*}-$OMARCHY_INSTALLER_REF.iso"
|
||||
mv -f "$latest_iso" "$iso_ref"
|
||||
|
||||
if [[ -z "${NO_BOOT_OFFER:-}" ]]; then
|
||||
# Offer to boot the new build
|
||||
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=()
|
||||
ISO_REF=quattro
|
||||
INSTALLER_REF=master
|
||||
RELEASE_SUFFIX=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -12,7 +12,7 @@ while [[ $# -gt 0 ]]; do
|
||||
;;
|
||||
--rc)
|
||||
MAKE_ARGS+=("$1")
|
||||
ISO_REF=rc
|
||||
INSTALLER_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 $ISO_REF ISO\e[0m"
|
||||
echo -e "\e[32mMaking $INSTALLER_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-"$ISO_REF".iso | head -n1)
|
||||
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-"$INSTALLER_REF".iso | head -n1)
|
||||
release_iso="$BUILD_RELEASE_PATH/omarchy-$BUILD_VERSION$RELEASE_SUFFIX.iso"
|
||||
cp "$latest_iso" "$release_iso"
|
||||
|
||||
echo -e "\e[32mSigning $ISO_REF ISO\e[0m"
|
||||
echo -e "\e[32mSigning $INSTALLER_REF ISO\e[0m"
|
||||
omarchy-iso-sign "$release_iso"
|
||||
|
||||
iso_sha=$(sha256sum "$release_iso")
|
||||
echo -e "\e[32mSHA256 for $ISO_REF ISO: $iso_sha\e[0m"
|
||||
echo -e "\e[32mSHA256 for $INSTALLER_REF ISO: $iso_sha\e[0m"
|
||||
|
||||
omarchy-iso-upload "$release_iso"
|
||||
@@ -1,914 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,295 +0,0 @@
|
||||
#!/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
|
||||
+15
-68
@@ -4,8 +4,8 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VM_DIR="$(dirname "$SCRIPT_DIR")/vm-saves"
|
||||
TMP_DISK="${OMARCHY_VM_DISK:-/tmp/omarchy-iso-boot.qcow2}"
|
||||
TMP_OVMF="${OMARCHY_VM_OVMF_VARS:-/tmp/OVMF_VARS.4m.fd}"
|
||||
TMP_DISK="/tmp/omarchy-iso-boot.qcow2"
|
||||
TMP_OVMF="/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 at $TMP_DISK / $TMP_OVMF"
|
||||
echo "Error: No active VM found in /tmp"
|
||||
echo "Boot a VM first with: omarchy-iso-boot [iso]"
|
||||
exit 1
|
||||
fi
|
||||
@@ -53,44 +53,7 @@ list_vms() {
|
||||
}
|
||||
|
||||
boot_vm() {
|
||||
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
|
||||
local name="$1"
|
||||
|
||||
if [[ -z "$(ls -A "$VM_DIR" 2>/dev/null)" ]]; then
|
||||
echo "No VM snapshots available"
|
||||
@@ -110,34 +73,25 @@ boot_vm() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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
|
||||
if [[ -f "$TMP_DISK" ]] && ! gum confirm "Override existing /tmp VM?"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
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"
|
||||
echo "Copying VM '$name' to /tmp..."
|
||||
cp "$vm_path/omarchy-iso-boot.qcow2" "$TMP_DISK"
|
||||
cp "$vm_path/OVMF_VARS.4m.fd" "$TMP_OVMF"
|
||||
|
||||
gum style --foreground 212 "✓ VM ready. Booting..."
|
||||
|
||||
exec omarchy-iso-boot "${boot_args[@]}" /dev/null reuse
|
||||
exec omarchy-iso-boot /dev/null reuse
|
||||
}
|
||||
|
||||
command="${1:-}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
|
||||
case "$command" in
|
||||
case "${1:-}" in
|
||||
save)
|
||||
save_vm "${1:-}"
|
||||
save_vm "$2"
|
||||
;;
|
||||
boot)
|
||||
boot_vm "$@"
|
||||
boot_vm "$2"
|
||||
;;
|
||||
list)
|
||||
list_vms
|
||||
@@ -147,16 +101,9 @@ case "$command" in
|
||||
"$(gum style --bold 'omarchy-vm') - VM snapshot manager" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Commands:')" \
|
||||
" 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" \
|
||||
" save [name] Save current VM from /tmp" \
|
||||
" boot [name] Boot saved VM (interactive if no name)" \
|
||||
" list List available VMs" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Current VMs:')"
|
||||
list_vms 2>/dev/null || echo " None"
|
||||
|
||||
+63
-216
@@ -2,258 +2,105 @@
|
||||
|
||||
set -e
|
||||
|
||||
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.
|
||||
# Note that these are packages installed to 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 imagemagick neovim nodejs npm tree-sitter-cli
|
||||
pacman --noconfirm -Sy archiso git sudo base-devel jq grub
|
||||
|
||||
# Pre-import the omarchy signing key (so pacman trusts our [omarchy] repo
|
||||
# during the build without keyserver lookups).
|
||||
# Pre-import the omarchy signing key so pacman can verify packages without a keyserver lookup
|
||||
pacman-key --add /builder/omarchy.gpg
|
||||
pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571
|
||||
|
||||
# omarchy-keyring is needed inside the offline mirror too.
|
||||
# Install omarchy-keyring for package verification during build
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Sy omarchy-keyring
|
||||
pacman-key --populate omarchy
|
||||
|
||||
# Append the [omarchy] repo to the container's /etc/pacman.conf so subsequent
|
||||
# tools (notably makepkg in build-omarchy-packages.sh) can resolve omarchy-
|
||||
# only build deps like limine-snapper-sync and limine-mkinitcpio-hook.
|
||||
if ! grep -q '^\[omarchy\]' /etc/pacman.conf; then
|
||||
awk '/^\[omarchy\]/,/^$/' /configs/pacman-online-${OMARCHY_MIRROR}.conf >> /etc/pacman.conf
|
||||
fi
|
||||
|
||||
# Build locations
|
||||
build_cache_dir=/var/cache
|
||||
# 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" "$offline_mirror_dir"
|
||||
mkdir -p $build_cache_dir/
|
||||
mkdir -p $offline_mirror_dir/
|
||||
|
||||
# Seed from the official Arch releng profile.
|
||||
cp -r /archiso/configs/releng/* "$build_cache_dir/"
|
||||
# We base our ISO on the official arch ISO (releng) config
|
||||
cp -r /archiso/configs/releng/* $build_cache_dir/
|
||||
rm "$build_cache_dir/airootfs/etc/motd"
|
||||
|
||||
# We rely on the global CDN; drop reflector.
|
||||
# Avoid using reflector for mirror identification as we are relying on the global CDN
|
||||
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 archiso profile additions.
|
||||
cp -r /configs/* "$build_cache_dir/"
|
||||
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
|
||||
# Bring in our configs
|
||||
cp -r /configs/* $build_cache_dir/
|
||||
|
||||
# Persist OMARCHY_MIRROR so it's available at install time
|
||||
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
|
||||
|
||||
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"
|
||||
# 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"
|
||||
fi
|
||||
|
||||
# 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
|
||||
# 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"
|
||||
|
||||
# Node.js binary for offline mise install.
|
||||
# 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_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"
|
||||
echo "$NODE_SHA /tmp/$NODE_FILENAME" | sha256sum -c -
|
||||
|
||||
# Verify SHA256 checksum
|
||||
echo "$NODE_SHA /tmp/$NODE_FILENAME" | sha256sum -c - || {
|
||||
echo "ERROR: Node.js checksum verification failed!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy to ISO
|
||||
mkdir -p "$build_cache_dir/airootfs/opt/packages/"
|
||||
cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/"
|
||||
|
||||
# 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"
|
||||
# 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"
|
||||
|
||||
# The live ISO boots linux-t2 (see airootfs/etc/mkinitcpio.d/linux-t2.preset), so
|
||||
# stock linux is a second kernel nobody boots: ~147MB of ISO, plus its own archiso
|
||||
# initramfs, copied into both the ISO tree and the size-constrained FAT EFI image.
|
||||
#
|
||||
# It cannot just be deleted — releng's broadcom-wl hard-depends on it, and it is
|
||||
# the only releng package that does, so pacman would drag the kernel straight back
|
||||
# in. broadcom-wl is a prebuilt module for stock linux and cannot load on the
|
||||
# kernel we boot, so it has done nothing since we started booting T2 anyway. The
|
||||
# install is entirely offline and the live environment needs no Wi-Fi driver.
|
||||
#
|
||||
# Anchored so linux-t2 and linux-firmware are untouched.
|
||||
sed -i -E '/^(linux|broadcom-wl)$/d' "$build_cache_dir/packages.x86_64"
|
||||
|
||||
# Build the offline mirror: everything pacstrap might want during the target
|
||||
# install. With --local-source, the omarchy* packages we just built are
|
||||
# already in the mirror and we filter them out below. Without it, pacman -Syw
|
||||
# pulls the published omarchy* from the network mirror like any other package.
|
||||
if [[ -d /omarchy-source ]]; then
|
||||
base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages)
|
||||
else
|
||||
# Pull the same package lists out of the freshly-downloaded Omarchy runtime
|
||||
# package so we don't need a local checkout in the non-local-source path.
|
||||
bootstrap_cache_dir=/tmp/omarchy-pkg-bootstrap
|
||||
rm -rf "$bootstrap_cache_dir" /tmp/offlinedb-bootstrap /tmp/omarchy-pkglists
|
||||
mkdir -p "$bootstrap_cache_dir" /tmp/offlinedb-bootstrap
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw "$OMARCHY_RUNTIME_PACKAGE" --cachedir "$bootstrap_cache_dir" --dbpath /tmp/offlinedb-bootstrap >/dev/null
|
||||
omarchy_pkg=$(find "$bootstrap_cache_dir" -maxdepth 1 -type f -name "$OMARCHY_RUNTIME_PACKAGE-*.pkg.tar.zst" | sort | head -1)
|
||||
if [[ -z $omarchy_pkg ]]; then
|
||||
echo "ERROR: downloaded package for $OMARCHY_RUNTIME_PACKAGE not found in $bootstrap_cache_dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p /tmp/omarchy-pkglists
|
||||
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/omarchy-base.packages usr/share/omarchy/install/omarchy-other.packages
|
||||
base_pkg_lists=(/tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-base.packages /tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-other.packages)
|
||||
fi
|
||||
|
||||
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
|
||||
cp "${base_pkg_lists[0]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
|
||||
cp "${base_pkg_lists[1]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages"
|
||||
|
||||
# Collect every package we want available in the offline mirror.
|
||||
declare -a all_packages
|
||||
mapfile -t all_packages < <(
|
||||
{
|
||||
cat "$build_cache_dir/packages.x86_64"
|
||||
grep -hv '^#\|^$' "${base_pkg_lists[@]}"
|
||||
grep -hv '^#\|^$' /builder/archinstall.packages
|
||||
# Always include the selected Omarchy packages so the target install can
|
||||
# find the runtime and companion packages in the offline mirror.
|
||||
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" "$OMARCHY_NVIM_PACKAGE"
|
||||
} | sort -u
|
||||
)
|
||||
|
||||
# With --local-source we already built these omarchy* packages directly into
|
||||
# the mirror; strip them from the pacman -Syw list so it doesn't try to fetch
|
||||
# the published versions on top.
|
||||
if [[ -n ${LOCAL_OMARCHY_BUILD:-} ]]; then
|
||||
mapfile -t all_packages < <(
|
||||
printf '%s\n' "${all_packages[@]}" |
|
||||
grep -Fxv \
|
||||
-e "$OMARCHY_RUNTIME_PACKAGE" \
|
||||
-e "$OMARCHY_SETTINGS_PACKAGE" \
|
||||
-e "$OMARCHY_NVIM_PACKAGE" || true
|
||||
)
|
||||
fi
|
||||
# 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 '^$'))
|
||||
|
||||
# Download all the packages to the offline mirror inside the ISO
|
||||
mkdir -p /tmp/offlinedb
|
||||
download_offline_packages() {
|
||||
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw \
|
||||
"${all_packages[@]}" --cachedir "$offline_mirror_dir/" --dbpath /tmp/offlinedb --needed
|
||||
}
|
||||
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
|
||||
|
||||
# A repository may occasionally republish a package without changing its
|
||||
# filename. Pacman detects that the persistent cached copy no longer matches
|
||||
# the refreshed repository checksum and deletes it, but still fails the
|
||||
# transaction. Retry once so the now-missing package is downloaded.
|
||||
if ! download_offline_packages; then
|
||||
echo "Offline package download failed; retrying after pacman cleaned invalid cached files..." >&2
|
||||
download_offline_packages
|
||||
fi
|
||||
|
||||
prune_stale_package_versions() {
|
||||
local dir="$1"
|
||||
local pkgfile name version cmp
|
||||
local -a stale=()
|
||||
declare -A newest_file=()
|
||||
declare -A newest_version=()
|
||||
|
||||
while IFS= read -r pkgfile; do
|
||||
read -r name version < <(pacman -Qp "$pkgfile" 2>/dev/null) || continue
|
||||
[[ -n $name && -n $version ]] || continue
|
||||
|
||||
if [[ -z ${newest_version[$name]+x} ]]; then
|
||||
newest_version[$name]="$version"
|
||||
newest_file[$name]="$pkgfile"
|
||||
continue
|
||||
fi
|
||||
|
||||
cmp=$(vercmp "$version" "${newest_version[$name]}")
|
||||
if (( cmp > 0 )); then
|
||||
stale+=("${newest_file[$name]}")
|
||||
newest_version[$name]="$version"
|
||||
newest_file[$name]="$pkgfile"
|
||||
else
|
||||
stale+=("$pkgfile")
|
||||
fi
|
||||
done < <(find "$dir" -maxdepth 1 -type f -name '*.pkg.tar.*' ! -name '*.sig' -print | sort)
|
||||
|
||||
if (( ${#stale[@]} > 0 )); then
|
||||
echo "Pruning stale package versions from offline mirror:"
|
||||
for pkgfile in "${stale[@]}"; do
|
||||
echo " $(basename "$pkgfile")"
|
||||
rm -f "$pkgfile" "$pkgfile.sig"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Rebuild the offline repo db from scratch so size/checksum/depends entries
|
||||
# always reflect the current package files. The persistent cache can contain
|
||||
# multiple versions of the same package; repo-add keeps the last one it sees,
|
||||
# which is lexical glob order, not version order (e.g. pkgrel -9 can override
|
||||
# -15). Prune to one newest version per package before indexing.
|
||||
prune_stale_package_versions "$offline_mirror_dir"
|
||||
rm -f "$offline_mirror_dir"/offline.db* "$offline_mirror_dir"/offline.files*
|
||||
repo-add "$offline_mirror_dir/offline.db.tar.gz" "$offline_mirror_dir/"*.pkg.tar.zst
|
||||
|
||||
# mkarchiso expects the mirror at /var/cache/omarchy/mirror/offline inside the
|
||||
# container (the airootfs path); symlink rather than duplicate.
|
||||
# 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
|
||||
mkdir -p /var/cache/omarchy/mirror
|
||||
ln -sf "$offline_mirror_dir" /var/cache/omarchy/mirror/offline
|
||||
ln -s "$offline_mirror_dir" "/var/cache/omarchy/mirror/offline"
|
||||
|
||||
# Live ISO uses the same offline pacman.conf.
|
||||
cp "$build_cache_dir/pacman-offline.conf" "$build_cache_dir/airootfs/etc/pacman.conf"
|
||||
# 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"
|
||||
|
||||
# Build the ISO.
|
||||
mkarchiso -v -w "$build_cache_dir/work/" -o /out/ "$build_cache_dir/"
|
||||
# Finally, we assemble the entire ISO
|
||||
mkarchiso -v -w "$build_cache_dir/work/" -o "/out/" "$build_cache_dir/"
|
||||
|
||||
# Match host UID/GID on output.
|
||||
if [[ -n $HOST_UID && -n $HOST_GID ]]; then
|
||||
chown -R "$HOST_UID:$HOST_GID" /out/
|
||||
# Fix ownership of output files to match host user
|
||||
if [ -n "$HOST_UID" ] && [ -n "$HOST_GID" ]; then
|
||||
chown -R "$HOST_UID:$HOST_GID" /out/
|
||||
fi
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/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|^| |'
|
||||
@@ -1,18 +0,0 @@
|
||||
# mkinitcpio preset file for the 'linux-t2' package on archiso
|
||||
#
|
||||
# The filename is the pkgbase, and that is load-bearing. Kernel packages ship
|
||||
# nothing in /boot — they only install usr/lib/modules/<kver>/vmlinuz — so
|
||||
# mkinitcpio's alpm hook sources /etc/mkinitcpio.d/<pkgbase>.preset and copies
|
||||
# THAT package's kernel to whatever ALL_kver names (is_kernelcopy in
|
||||
# /usr/share/libalpm/scripts/mkinitcpio). Named linux.preset, this pointed the
|
||||
# stock kernel at the T2 filenames, so the ISO booted stock Arch while every
|
||||
# boot entry said linux-t2 and T2 Macs got no keyboard or trackpad.
|
||||
#
|
||||
# releng ships its own linux.preset for the stock kernel; leave it alone.
|
||||
|
||||
PRESETS=('archiso')
|
||||
|
||||
ALL_kver='/boot/vmlinuz-linux-t2'
|
||||
archiso_config='/etc/mkinitcpio.conf.d/archiso.conf'
|
||||
|
||||
archiso_image="/boot/initramfs-linux-t2.img"
|
||||
@@ -0,0 +1,9 @@
|
||||
# mkinitcpio preset file for the 'linux' package on archiso
|
||||
|
||||
PRESETS=('archiso')
|
||||
|
||||
ALL_kver='/boot/vmlinuz-linux-t2'
|
||||
archiso_config='/etc/mkinitcpio.conf.d/archiso.conf'
|
||||
|
||||
archiso_image="/boot/initramfs-linux-t2.img"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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,110 +1,264 @@
|
||||
#!/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
|
||||
|
||||
[[ $(tty) == /dev/tty1 ]] || exit 0
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
run_configurator() {
|
||||
set_tokyo_night_colors
|
||||
./configurator
|
||||
export OMARCHY_USER="$(jq -r '.users[0].username' user_credentials.json)"
|
||||
}
|
||||
|
||||
# Tokyo Night palette so the live VT matches the installed look.
|
||||
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() {
|
||||
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
|
||||
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
|
||||
}
|
||||
set_tokyo_night_colors
|
||||
|
||||
mkdir -p /var/log
|
||||
touch "$OMARCHY_INSTALL_LOG_FILE"
|
||||
install_disk() {
|
||||
jq -er 'first(.disk_config.device_modifications[]? | select(.wipe == true) | .device)' user_configuration.json
|
||||
}
|
||||
|
||||
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
|
||||
cleanup_install_disk() {
|
||||
local disk="$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 "================================"
|
||||
if [[ -z "$disk" || ! -b "$disk" ]]; then
|
||||
echo "Could not determine install disk for cleanup" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Cleaning up existing holders on install disk: $disk"
|
||||
|
||||
# Ensure that no mounts exist from past install attempts.
|
||||
findmnt -R /mnt >/dev/null && umount -R /mnt || true
|
||||
|
||||
# Turn off swap and unmount anything backed by the selected disk, including
|
||||
# device-mapper children from a previous install. Active LVM/swap holders can
|
||||
# prevent the kernel from re-reading the partition table after archinstall
|
||||
# wipes and recreates it.
|
||||
while read -r dev; do
|
||||
[[ -b "$dev" ]] || continue
|
||||
|
||||
swapoff "$dev" 2>/dev/null || true
|
||||
|
||||
while read -r target; do
|
||||
[[ -n "$target" ]] || continue
|
||||
umount "$target" 2>/dev/null || true
|
||||
done < <(findmnt -rn -S "$dev" -o TARGET 2>/dev/null || true)
|
||||
done < <(lsblk -rnpo PATH "$disk")
|
||||
|
||||
# Deactivate any LVM volume groups whose physical volumes live on the selected
|
||||
# disk. This is the common case when replacing Fedora/Alma/RHEL installs.
|
||||
while read -r dev type; do
|
||||
[[ "$type" == "disk" || "$type" == "part" || "$type" == "crypt" ]] || continue
|
||||
|
||||
while read -r vg; do
|
||||
[[ -n "$vg" ]] || continue
|
||||
vgchange -an "$vg" 2>/dev/null || true
|
||||
done < <(pvs --noheadings -o vg_name "$dev" 2>/dev/null | awk '{$1=$1; print}' | sort -u)
|
||||
done < <(lsblk -rnpo PATH,TYPE "$disk")
|
||||
|
||||
# Close any LUKS mappings stacked on the selected disk after filesystems and
|
||||
# swap have been released.
|
||||
while read -r dev type; do
|
||||
[[ "$type" == "crypt" ]] || continue
|
||||
cryptsetup close "$dev" 2>/dev/null || true
|
||||
done < <(lsblk -rnpo PATH,TYPE "$disk")
|
||||
|
||||
blockdev --flushbufs "$disk" 2>/dev/null || true
|
||||
partprobe "$disk" 2>/dev/null || true
|
||||
udevadm settle || true
|
||||
}
|
||||
|
||||
install_base_system() {
|
||||
# Initialize and populate the keyring
|
||||
pacman-key --init
|
||||
pacman-key --populate archlinux
|
||||
pacman-key --populate omarchy
|
||||
|
||||
# Sync the offline database so pacman can find packages
|
||||
pacman -Sy --noconfirm
|
||||
|
||||
cleanup_install_disk "$(install_disk)"
|
||||
|
||||
# Workarounds for archinstall 4.2 regressions under Python 3.14:
|
||||
# 1. sync_log_to_install_medium: `self.target / absolute_logfile` drops
|
||||
# self.target because the RHS is absolute, so Path.copy() raises EINVAL
|
||||
# (source == target).
|
||||
# 2. _add_limine_bootloader: `Path.copy(efi_dir_path)` raises IsADirectoryError
|
||||
# because 3.14's Path.copy treats target as a literal path, not a directory
|
||||
# (shutil.copy used to auto-append the source filename).
|
||||
sed -i \
|
||||
-e 's|logfile_target = self\.target / absolute_logfile$|logfile_target = self.target / absolute_logfile.relative_to("/")|' \
|
||||
-e 's|(limine_path / file)\.copy(efi_dir_path)|(limine_path / file).copy(efi_dir_path / file)|' \
|
||||
-e "s|(limine_path / 'limine-bios.sys')\.copy(boot_limine_path)|(limine_path / 'limine-bios.sys').copy(boot_limine_path / 'limine-bios.sys')|" \
|
||||
/usr/lib/python3.14/site-packages/archinstall/lib/installer.py
|
||||
|
||||
# Install using files generated by the ./configurator
|
||||
# Skip NTP and WKD sync since we're offline (keyring is pre-populated in ISO)
|
||||
archinstall \
|
||||
--config user_configuration.json \
|
||||
--creds user_credentials.json \
|
||||
--silent \
|
||||
--skip-ntp \
|
||||
--skip-wkd \
|
||||
--skip-wifi-check
|
||||
|
||||
# Archinstall unmounts the ESP when it finishes. Omarchy's boot finalizer
|
||||
# needs the generated Limine config and EFI artifacts available under /boot.
|
||||
if ! mountpoint -q /mnt/boot; then
|
||||
arch-chroot /mnt mount /boot
|
||||
fi
|
||||
|
||||
# The installed fstab keeps the ESP root-only, but Omarchy finalization runs
|
||||
# as the target user and must discover the Limine config before using sudo to
|
||||
# replace it. Temporarily allow reads and directory traversal during install.
|
||||
boot_device=$(findmnt -nro SOURCE --target /mnt/boot)
|
||||
umount /mnt/boot
|
||||
mount -t vfat -o rw,fmask=0022,dmask=0022 "$boot_device" /mnt/boot
|
||||
|
||||
if ! arch-chroot -u "$OMARCHY_USER" /mnt test -x /boot; then
|
||||
echo "Target user cannot access the mounted ESP" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# After archinstall sets up the base system but before our installer runs,
|
||||
# we need to ensure the offline pacman.conf is in place
|
||||
cp /etc/pacman.conf /mnt/etc/pacman.conf
|
||||
|
||||
# Mount the offline mirror so it's accessible in the chroot
|
||||
mkdir -p /mnt/var/cache/omarchy/mirror/offline
|
||||
mount --bind /var/cache/omarchy/mirror/offline /mnt/var/cache/omarchy/mirror/offline
|
||||
|
||||
# Mount the packages dir so it's accessible in the chroot
|
||||
mkdir -p /mnt/opt/packages
|
||||
mount --bind /opt/packages /mnt/opt/packages
|
||||
|
||||
# No need to ask for sudo during the installation (omarchy itself responsible for removing after install)
|
||||
mkdir -p /mnt/etc/sudoers.d
|
||||
cat >/mnt/etc/sudoers.d/99-omarchy-installer <<EOF
|
||||
root ALL=(ALL:ALL) NOPASSWD: ALL
|
||||
%wheel ALL=(ALL:ALL) NOPASSWD: ALL
|
||||
$OMARCHY_USER ALL=(ALL:ALL) NOPASSWD: ALL
|
||||
EOF
|
||||
chmod 440 /mnt/etc/sudoers.d/99-omarchy-installer
|
||||
|
||||
# Copy the local omarchy repo to the user's home directory
|
||||
mkdir -p /mnt/home/$OMARCHY_USER/.local/share/
|
||||
cp -r /root/omarchy /mnt/home/$OMARCHY_USER/.local/share/
|
||||
|
||||
chown -R 1000:1000 /mnt/home/$OMARCHY_USER/.local/
|
||||
|
||||
# Ensure all necessary scripts are executable
|
||||
find /mnt/home/$OMARCHY_USER/.local/share/omarchy -type f -path "*/bin/*" -exec chmod +x {} \;
|
||||
chmod +x /mnt/home/$OMARCHY_USER/.local/share/omarchy/boot.sh 2>/dev/null || true
|
||||
find /mnt/home/$OMARCHY_USER/.local/share/omarchy/default/waybar -type f -name "*.sh" -exec chmod +x {} \; 2>/dev/null || true
|
||||
}
|
||||
|
||||
configure_login_for_unencrypted_install() {
|
||||
if [[ $(<user_encrypt_installation.txt) != "false" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Unencrypted installs must stop at SDDM so the user password is entered
|
||||
# before reaching the desktop. Omarchy's normal encrypted path may autologin
|
||||
# because the disk password was already entered at boot.
|
||||
#
|
||||
# Keep the Omarchy SDDM theme and seed SDDM's last user/session state so
|
||||
# first boot looks like the SDDM screen shown after logging out of Omarchy.
|
||||
mkdir -p /mnt/etc/sddm.conf.d
|
||||
rm -f /mnt/etc/sddm.conf.d/autologin.conf
|
||||
cat >/mnt/etc/sddm.conf.d/99-omarchy-login.conf <<EOF
|
||||
[Theme]
|
||||
Current=omarchy
|
||||
|
||||
[Users]
|
||||
RememberLastUser=true
|
||||
RememberLastSession=true
|
||||
EOF
|
||||
|
||||
mkdir -p /mnt/var/lib/sddm
|
||||
cat >/mnt/var/lib/sddm/state.conf <<EOF
|
||||
[Last]
|
||||
Session=omarchy.desktop
|
||||
User=$OMARCHY_USER
|
||||
EOF
|
||||
|
||||
rm -f /mnt/etc/systemd/system/getty@tty1.service.d/autologin.conf
|
||||
arch-chroot /mnt chown sddm:sddm /var/lib/sddm /var/lib/sddm/state.conf >/dev/null 2>&1 || true
|
||||
arch-chroot /mnt systemctl enable sddm.service >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
chroot_bash() {
|
||||
HOME=/home/$OMARCHY_USER \
|
||||
arch-chroot -u $OMARCHY_USER /mnt/ \
|
||||
env OMARCHY_CHROOT_INSTALL=1 \
|
||||
OMARCHY_USER_NAME="$(<user_full_name.txt)" \
|
||||
OMARCHY_USER_EMAIL="$(<user_email_address.txt)" \
|
||||
OMARCHY_MIRROR="$OMARCHY_MIRROR" \
|
||||
USER="$OMARCHY_USER" \
|
||||
HOME="/home/$OMARCHY_USER" \
|
||||
/bin/bash "$@"
|
||||
}
|
||||
|
||||
if [[ $(tty) == "/dev/tty1" ]]; then
|
||||
use_omarchy_helpers
|
||||
run_configurator
|
||||
install_arch
|
||||
install_omarchy
|
||||
fi
|
||||
|
||||
# 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,48 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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}"
|
||||
# Rely on Omarchy for styling and error handling
|
||||
source "$OMARCHY_INSTALL/helpers/all.sh"
|
||||
|
||||
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")"
|
||||
}
|
||||
# Ensure tzupdate is available for guessing timezone
|
||||
command -v tzupdate >/dev/null || sudo pacman -Sy --noconfirm tzupdate
|
||||
|
||||
abort() {
|
||||
gum style "${1:-Aborted installation}"
|
||||
@@ -54,15 +16,10 @@ abort() {
|
||||
step() {
|
||||
clear_logo
|
||||
echo
|
||||
gum style --padding "0 0 0 $PADDING_LEFT" "$1"
|
||||
gum style "$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
|
||||
@@ -277,447 +234,6 @@ 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..."
|
||||
|
||||
@@ -749,60 +265,7 @@ disk_form() {
|
||||
disk=$(echo "$selected_display" | awk '{print $1}')
|
||||
}
|
||||
|
||||
# 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
|
||||
}
|
||||
disk_form
|
||||
|
||||
confirm_disk_overwrite() {
|
||||
local mode="encrypted"
|
||||
@@ -811,10 +274,10 @@ confirm_disk_overwrite() {
|
||||
while true; do
|
||||
clear_logo
|
||||
echo
|
||||
say "Everything will be overwritten. There is no recovery possible."
|
||||
gum style "Everything will be overwritten. There is no recovery possible."
|
||||
|
||||
if [[ $mode == "encrypted" ]]; then
|
||||
say --foreground 8 "Press Ctrl+C for unencrypted install."
|
||||
gum style --foreground 8 "Press Ctrl+C for unencrypted install."
|
||||
affirmative="Yes, install"
|
||||
else
|
||||
affirmative="Yes, install without encryption"
|
||||
@@ -842,45 +305,44 @@ confirm_disk_overwrite() {
|
||||
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 "$@"
|
||||
while ! confirm_disk_overwrite; do
|
||||
disk_form
|
||||
done
|
||||
|
||||
clear
|
||||
|
||||
write_user_files
|
||||
# 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
|
||||
|
||||
# Setup partition layout
|
||||
disk_size=$(lsblk -bdno SIZE "$disk")
|
||||
@@ -895,7 +357,12 @@ 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))
|
||||
|
||||
kernel_choice=$(detect_kernel)
|
||||
# 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
|
||||
|
||||
if [[ $encrypt_installation == true ]]; then
|
||||
disk_encryption_config=$(cat <<-_EOF_
|
||||
@@ -905,7 +372,7 @@ if [[ $encrypt_installation == true ]]; then
|
||||
"lvm_volumes": [],
|
||||
"iter_time": 2000,
|
||||
"partitions": [ "8c2c2b92-1070-455d-b76a-56263bab24aa" ],
|
||||
"encryption_password": $(echo -n "$password" | jq -Rsa)
|
||||
"encryption_password": $password_escaped
|
||||
}
|
||||
_EOF_
|
||||
)
|
||||
@@ -919,22 +386,14 @@ cat <<-_EOF_ >user_configuration.json
|
||||
"archinstall-language": "English",
|
||||
"auth_config": {},
|
||||
"audio_config": { "audio": "pipewire" },
|
||||
"bootloader_config": { "bootloader": "Limine", "uki": false, "removable": false },
|
||||
"bootloader": "Limine",
|
||||
"custom_commands": [],
|
||||
"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": {
|
||||
"btrfs_options": {
|
||||
"snapshot_config": {
|
||||
"type": "Snapper"
|
||||
}
|
||||
},
|
||||
"config_type": "default_layout",
|
||||
"device_modifications": [
|
||||
{
|
||||
@@ -1020,8 +479,7 @@ cat <<-_EOF_ >user_configuration.json
|
||||
"base-devel",
|
||||
"git",
|
||||
"omarchy-keyring",
|
||||
"$OMARCHY_SETTINGS_PACKAGE",
|
||||
"$OMARCHY_RUNTIME_PACKAGE"
|
||||
"snapper"
|
||||
],
|
||||
"profile_config": {
|
||||
"gfx_driver": null,
|
||||
@@ -1033,5 +491,20 @@ cat <<-_EOF_ >user_configuration.json
|
||||
_EOF_
|
||||
|
||||
if [[ $1 == "dry" ]]; then
|
||||
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 user_configuration.json user_credentials.json user_full_name.txt user_email_address.txt user_encrypt_installation.txt
|
||||
fi
|
||||
@@ -1,602 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Foreground Omarchy ISO install UI supervisor.
|
||||
#
|
||||
# This process is the only owner of the visible install UI. It launches the
|
||||
# actual installer as a non-interactive child, sends child output to the support
|
||||
# log, renders progress while the child runs, then renders the final installed
|
||||
# time + reboot prompt itself. There is deliberately no background dashboard.
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <log-file> <state-file> -- <installer command...>" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
(( $# >= 4 )) || usage
|
||||
LOG_FILE="$1"
|
||||
STATE_FILE="$2"
|
||||
shift 2
|
||||
[[ ${1:-} == "--" ]] || usage
|
||||
shift
|
||||
(( $# > 0 )) || usage
|
||||
INSTALL_CMD=("$@")
|
||||
|
||||
TTY_PATH="${OMARCHY_DASHBOARD_TTY:-/dev/tty}"
|
||||
LOGO_PATH="${OMARCHY_PATH:-/usr/share/omarchy}/logo.txt"
|
||||
CSI=$'\033['
|
||||
RESET="${CSI}0m"
|
||||
DIM="${CSI}2m"
|
||||
HIDE_CURSOR="${CSI}?25l"
|
||||
SHOW_CURSOR="${CSI}?25h"
|
||||
CLEAR="${CSI}2J${CSI}H"
|
||||
CLEAR_LINE="${CSI}2K"
|
||||
CLEAR_TO_END="${CSI}J"
|
||||
GREEN="${CSI}32m"
|
||||
WHITE="${CSI}37m"
|
||||
RED="${CSI}31m"
|
||||
DARK="${CSI}90m"
|
||||
|
||||
# The dashboard owns the final reboot prompt now, so keep the gum confirm
|
||||
# styling that used to come from install/helpers/presentation.sh.
|
||||
export GUM_CONFIRM_PROMPT_FOREGROUND="${GUM_CONFIRM_PROMPT_FOREGROUND:-6}"
|
||||
export GUM_CONFIRM_SELECTED_FOREGROUND="${GUM_CONFIRM_SELECTED_FOREGROUND:-0}"
|
||||
export GUM_CONFIRM_SELECTED_BACKGROUND="${GUM_CONFIRM_SELECTED_BACKGROUND:-2}"
|
||||
export GUM_CONFIRM_UNSELECTED_FOREGROUND="${GUM_CONFIRM_UNSELECTED_FOREGROUND:-7}"
|
||||
export GUM_CONFIRM_UNSELECTED_BACKGROUND="${GUM_CONFIRM_UNSELECTED_BACKGROUND:-0}"
|
||||
|
||||
child_pid=""
|
||||
child_pgid=""
|
||||
|
||||
tips=(
|
||||
"Super + Space opens the Omarchy menu for apps, settings, and more"
|
||||
"Super + K shows all the key bindings"
|
||||
"Super is the Windows or command key on your keyboard"
|
||||
"Use Xournal++ to sign PDFs"
|
||||
"Share files with phones and laptops using LocalSend"
|
||||
"Turn any website into an app with Install > Web App in the menu"
|
||||
"Super + Return opens a terminal, Super + Shift + Return the browser"
|
||||
"Edit images with Pinta, videos with Kdenlive, docs with LibreOffice"
|
||||
"Super + Ctrl + Print grabs text off the screen with OCR"
|
||||
"Print takes a screenshot, Alt + Print records the screen"
|
||||
"Set a reminder with Super + Ctrl + R"
|
||||
"Switch themes from Style > Theme in the Omarchy menu"
|
||||
"Double-click the menu bar to make it transparent"
|
||||
"Run a full Windows VM via Install > Windows in the menu"
|
||||
"Super + Ctrl + V opens the clipboard manager"
|
||||
"Super + 1 through 0 switches workspaces, add Shift to bring the window"
|
||||
"Super + Print picks a color from anywhere on screen"
|
||||
"Keep the system fresh with Update in the Omarchy menu"
|
||||
)
|
||||
|
||||
cleanup() {
|
||||
printf '%s%s' "$RESET" "$SHOW_CURSOR" >"$TTY_PATH" 2>/dev/null || true
|
||||
}
|
||||
|
||||
dashboard_log() {
|
||||
printf '[dashboard] %s\n' "$*" >>"$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
stop_child() {
|
||||
[[ -n ${child_pid:-} ]] || return 0
|
||||
if kill -0 "$child_pid" 2>/dev/null; then
|
||||
if [[ -n ${child_pgid:-} ]]; then
|
||||
kill -- -"$child_pgid" 2>/dev/null || true
|
||||
fi
|
||||
kill "$child_pid" 2>/dev/null || true
|
||||
for _ in {1..20}; do
|
||||
kill -0 "$child_pid" 2>/dev/null || break
|
||||
sleep 0.05
|
||||
done
|
||||
if kill -0 "$child_pid" 2>/dev/null; then
|
||||
[[ -n ${child_pgid:-} ]] && kill -9 -- -"$child_pgid" 2>/dev/null || true
|
||||
kill -9 "$child_pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
wait "$child_pid" 2>/dev/null || true
|
||||
}
|
||||
|
||||
interrupt() {
|
||||
local code="$1"
|
||||
stop_child
|
||||
cleanup
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
trap 'interrupt 130' INT
|
||||
trap 'interrupt 143' TERM
|
||||
|
||||
term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; }
|
||||
term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; }
|
||||
|
||||
repeat() {
|
||||
local char="$1" count="$2" out="" i
|
||||
for ((i=0; i<count; i++)); do out+="$char"; done
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
logo_width() {
|
||||
if [[ -f $LOGO_PATH ]]; then
|
||||
awk '{ if (length > max) max = length } END { print max+0 }' "$LOGO_PATH"
|
||||
else
|
||||
printf '81\n'
|
||||
fi
|
||||
}
|
||||
|
||||
logo_height() {
|
||||
if [[ -f $LOGO_PATH ]]; then
|
||||
wc -l <"$LOGO_PATH"
|
||||
else
|
||||
printf '1\n'
|
||||
fi
|
||||
}
|
||||
|
||||
LOGO_WIDTH="$(logo_width)"
|
||||
LOGO_HEIGHT="$(logo_height)"
|
||||
(( LOGO_WIDTH > 0 )) || LOGO_WIDTH=81
|
||||
(( LOGO_HEIGHT > 0 )) || LOGO_HEIGHT=1
|
||||
CONTENT_WIDTH="$LOGO_WIDTH"
|
||||
DYNAMIC_ROW=$((LOGO_HEIGHT + 3))
|
||||
LAST_PCT=1
|
||||
|
||||
left_padding() {
|
||||
local width="${1:-$CONTENT_WIDTH}" cols pad
|
||||
cols="$(term_cols)"
|
||||
pad=$(( (cols - width) / 2 ))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf '%*s' "$pad" ''
|
||||
}
|
||||
|
||||
visible_len() {
|
||||
local text="$1"
|
||||
text="$(printf '%b' "$text" | sed -E $'s/\x1b\\[[0-9;?]*[A-Za-z]//g')"
|
||||
printf '%s' "${#text}"
|
||||
}
|
||||
|
||||
blank_line() { printf '\r%s\n' "$CLEAR_LINE"; }
|
||||
|
||||
line_at() {
|
||||
local width="${1:-$CONTENT_WIDTH}" indent="${2:-0}"
|
||||
shift 2
|
||||
printf '\r%s%s%*s' "$CLEAR_LINE" "$(left_padding "$width")" "$indent" ''
|
||||
printf '%b' "$*"
|
||||
}
|
||||
|
||||
center() {
|
||||
local text="$1" width="${2:-$CONTENT_WIDTH}" len inner_pad
|
||||
len="$(visible_len "$text")"
|
||||
inner_pad=$(( (width - len) / 2 ))
|
||||
(( inner_pad < 0 )) && inner_pad=0
|
||||
printf '\r%s%s%*s%b%s\n' "$CLEAR_LINE" "$(left_padding "$width")" "$inner_pad" '' "$text" "$RESET"
|
||||
}
|
||||
|
||||
render_logo() {
|
||||
local pad
|
||||
pad="$(left_padding "$LOGO_WIDTH")"
|
||||
if [[ -f $LOGO_PATH ]]; then
|
||||
while IFS= read -r line; do
|
||||
printf '\r%s%s%b%s%b\n' "$CLEAR_LINE" "$pad" "$GREEN" "$line" "$RESET"
|
||||
done <"$LOGO_PATH"
|
||||
else
|
||||
center "${GREEN}Omarchy${RESET}" "$CONTENT_WIDTH"
|
||||
fi
|
||||
}
|
||||
|
||||
progress_bar() {
|
||||
local pct="$1" width="${2:-34}" filled empty
|
||||
(( pct < 0 )) && pct=0
|
||||
(( pct > 100 )) && pct=100
|
||||
filled=$(( pct * width / 100 ))
|
||||
empty=$(( width - filled ))
|
||||
printf '%s%s%s%s%s' "$WHITE" "$(repeat █ "$filled")" "$DARK" "$(repeat ░ "$empty")" "$RESET"
|
||||
}
|
||||
|
||||
install_progress() {
|
||||
local phase finished phase_started elapsed span active pct
|
||||
local start=1 end=99 expected=90
|
||||
local -a state
|
||||
|
||||
[[ -f $STATE_FILE ]] || { printf '1'; return; }
|
||||
|
||||
mapfile -t state < <(jq -r '
|
||||
(.current_phase // "Starting installation"),
|
||||
((.finished_at // 0) | tonumber? // 0 | floor),
|
||||
((.phase_started_at // .started_at // 0) | tonumber? // 0 | floor)
|
||||
' "$STATE_FILE" 2>/dev/null)
|
||||
(( ${#state[@]} == 3 )) || { printf '1'; return; }
|
||||
phase="${state[0]}" finished="${state[1]}" phase_started="${state[2]}"
|
||||
|
||||
if (( finished > 0 )) || [[ $phase == "Installation complete" ]]; then
|
||||
printf '100'
|
||||
return
|
||||
fi
|
||||
|
||||
# Phase weights: start%, end%, expected seconds in phase.
|
||||
case "$phase" in
|
||||
"Starting installation") start=1 end=2 expected=2 ;;
|
||||
"Preparing live environment") start=2 end=6 expected=5 ;;
|
||||
"Verifying protected mounts") start=6 end=8 expected=3 ;;
|
||||
"Installing Arch + Omarchy") start=8 end=86 expected=95 ;;
|
||||
"Installing base system") start=8 end=84 expected=85 ;;
|
||||
"Configuring protected boot") start=84 end=86 expected=8 ;;
|
||||
"Configuring hibernation") start=86 end=88 expected=5 ;;
|
||||
"Configuring system") start=88 end=93 expected=10 ;;
|
||||
"Finalizing Limine boot") start=93 end=96 expected=8 ;;
|
||||
"Finalizing user") start=96 end=98 expected=10 ;;
|
||||
"Finalizing in chroot") start=88 end=98 expected=10 ;;
|
||||
"Configuring login") start=98 end=99 expected=3 ;;
|
||||
"Configuring DNS resolver") start=99 end=100 expected=2 ;;
|
||||
"Validating protected boot setup") start=99 end=100 expected=5 ;;
|
||||
"Validating boot setup") start=99 end=100 expected=5 ;;
|
||||
esac
|
||||
|
||||
(( phase_started > 0 )) || phase_started=$EPOCHSECONDS
|
||||
elapsed=$(( EPOCHSECONDS - phase_started ))
|
||||
(( elapsed < 0 )) && elapsed=0
|
||||
span=$(( end - start ))
|
||||
(( span < 0 )) && span=0
|
||||
(( expected < 1 )) && expected=1
|
||||
active=$(( elapsed * span / expected ))
|
||||
(( active > span - 1 )) && active=$(( span - 1 ))
|
||||
(( active < 0 )) && active=0
|
||||
pct=$(( start + active ))
|
||||
(( pct > 100 )) && pct=100
|
||||
printf '%s' "$pct"
|
||||
}
|
||||
|
||||
# One tip per ~8 seconds (frames tick every 0.5s), in order, so each stays
|
||||
# readable and none are skipped when progress jumps.
|
||||
current_tip() {
|
||||
local frame="$1"
|
||||
printf '%s' "${tips[$(( frame / 16 % ${#tips[@]} ))]}"
|
||||
}
|
||||
|
||||
render_static() {
|
||||
printf '%s' "$CLEAR"
|
||||
blank_line
|
||||
render_logo
|
||||
blank_line
|
||||
}
|
||||
|
||||
render_dynamic() {
|
||||
local frame="$1" pct
|
||||
pct="$(install_progress)"
|
||||
[[ $pct =~ ^[0-9]+$ ]] || pct=$LAST_PCT
|
||||
(( pct > 100 )) && pct=100
|
||||
(( pct < LAST_PCT )) && pct=$LAST_PCT
|
||||
LAST_PCT=$pct
|
||||
|
||||
printf '%s%d;1H' "$CSI" "$DYNAMIC_ROW"
|
||||
center "Installing Omarchy" "$CONTENT_WIDTH"
|
||||
blank_line
|
||||
line_at "$CONTENT_WIDTH" $(( (CONTENT_WIDTH - 34) / 2 )) ""
|
||||
progress_bar "$pct" 34
|
||||
printf '\n'
|
||||
blank_line
|
||||
center "${DIM}Tip:${RESET} ${GREEN}$(current_tip "$frame")${RESET}" "$CONTENT_WIDTH"
|
||||
printf '%s' "$CLEAR_TO_END"
|
||||
}
|
||||
|
||||
install_duration() {
|
||||
local path line duration
|
||||
|
||||
if [[ -f $STATE_FILE ]]; then
|
||||
duration="$(jq -r '
|
||||
((.started_at // 0) | tonumber? // 0) as $started
|
||||
| ((.finished_at // 0) | tonumber? // 0) as $finished
|
||||
| if $started <= 0 or $finished <= 0 or $finished < $started then empty
|
||||
else (($finished - $started) | round) as $secs
|
||||
| if $secs >= 3600 then "\($secs / 3600 | floor)h \($secs % 3600 / 60 | floor)m \($secs % 60)s"
|
||||
else "\($secs / 60 | floor)m \($secs % 60)s"
|
||||
end
|
||||
end
|
||||
' "$STATE_FILE" 2>/dev/null || true)"
|
||||
[[ -n $duration ]] && { printf '%s' "$duration"; return 0; }
|
||||
fi
|
||||
|
||||
for path in /mnt/var/log/omarchy-install.log "$LOG_FILE"; do
|
||||
[[ -f $path ]] || continue
|
||||
for prefix in Total Omarchy; do
|
||||
line="$(grep "^$prefix:" "$path" 2>/dev/null | tail -n 1 || true)"
|
||||
[[ -n $line ]] || continue
|
||||
duration="${line#*:}"
|
||||
duration="${duration#${duration%%[![:space:]]*}}"
|
||||
[[ -n $duration ]] && { printf '%s' "$duration"; return 0; }
|
||||
done
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
render_finish() {
|
||||
local duration effect_canvas_width
|
||||
duration="$(install_duration || true)"
|
||||
duration="${duration:-Complete}"
|
||||
effect_canvas_width="$(term_cols)"
|
||||
(( effect_canvas_width > 1 )) && effect_canvas_width=$((effect_canvas_width - 1))
|
||||
|
||||
{
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
|
||||
blank_line
|
||||
render_logo
|
||||
blank_line
|
||||
center "Installed Omarchy in ${duration}" "$CONTENT_WIDTH"
|
||||
blank_line
|
||||
} >"$TTY_PATH"
|
||||
|
||||
if command -v tte >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 && [[ -f $LOGO_PATH ]]; then
|
||||
# TTE reuse mode restores a saved cursor before moving up over its input.
|
||||
# Save this anchor so the effect redraws the same logo rows shown above.
|
||||
printf '%s%d;1H\0337' "$CSI" "$((LOGO_HEIGHT + 2))" >"$TTY_PATH"
|
||||
timeout 8s tte -i "$LOGO_PATH" \
|
||||
--canvas-width "$effect_canvas_width" \
|
||||
--anchor-text c \
|
||||
--frame-rate 260 \
|
||||
--reuse-canvas \
|
||||
laseretch \
|
||||
>"$TTY_PATH" 2>/dev/null || true
|
||||
printf '%s%d;1H' "$CSI" "$((LOGO_HEIGHT + 8))" >"$TTY_PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
reboot_prompt() {
|
||||
local prompt_pad
|
||||
|
||||
# Position the cursor one blank line below the title so the button sits right there.
|
||||
printf '%s%d;1H%s' "$CSI" "$((LOGO_HEIGHT + 5))" "$CLEAR_LINE" >"$TTY_PATH"
|
||||
|
||||
prompt_pad=$(( ( $(term_cols) - 18 ) / 2 ))
|
||||
(( prompt_pad < 0 )) && prompt_pad=0
|
||||
gum confirm \
|
||||
--padding "0 0 0 $prompt_pad" \
|
||||
--show-help=false \
|
||||
--default \
|
||||
--affirmative "Reboot Now" \
|
||||
--negative "" \
|
||||
"" <"$TTY_PATH" >"$TTY_PATH" 2>&1
|
||||
}
|
||||
|
||||
truncate_for_width() {
|
||||
local text="$1" width="$2"
|
||||
if (( ${#text} > width )); then
|
||||
printf '%s…' "${text:0:$((width - 1))}"
|
||||
else
|
||||
printf '%s' "$text"
|
||||
fi
|
||||
}
|
||||
|
||||
render_failure_log_tail() {
|
||||
local rows lines width line tail_log label
|
||||
rows=$(term_size)
|
||||
rows=${rows%% *}
|
||||
lines=$((rows - LOGO_HEIGHT - 18))
|
||||
(( lines < 6 )) && lines=6
|
||||
(( lines > 14 )) && lines=14
|
||||
width=$((CONTENT_WIDTH - 4))
|
||||
(( width < 40 )) && width=40
|
||||
|
||||
tail_log="${OMARCHY_FAILURE_TAIL_LOG:-}"
|
||||
if [[ -z $tail_log ]]; then
|
||||
if [[ -f /mnt/var/log/omarchy-install.log ]]; then
|
||||
tail_log=/mnt/var/log/omarchy-install.log
|
||||
else
|
||||
tail_log="$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
[[ -f $tail_log ]] || return 0
|
||||
|
||||
label="Last log lines:"
|
||||
[[ $tail_log == /mnt/* ]] && label="Last target log lines:"
|
||||
line_at "$CONTENT_WIDTH" 2 "${DIM}${label}${RESET}"
|
||||
printf '\n'
|
||||
tail -n "$lines" "$tail_log" 2>/dev/null | while IFS= read -r line; do
|
||||
line_at "$CONTENT_WIDTH" 2 "$(truncate_for_width "$line" "$width")"
|
||||
printf '\n'
|
||||
done
|
||||
}
|
||||
|
||||
find_log_uploader() {
|
||||
local candidate
|
||||
for candidate in omarchy-upload-log /usr/local/bin/omarchy-upload-log /usr/bin/omarchy-upload-log /mnt/usr/bin/omarchy-upload-log; do
|
||||
if [[ $candidate == */* ]]; then
|
||||
[[ -x $candidate ]] && { printf '%s' "$candidate"; return 0; }
|
||||
elif command -v "$candidate" >/dev/null 2>&1; then
|
||||
command -v "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
prompt_enter() {
|
||||
printf '\nPress Enter to continue…' >"$TTY_PATH"
|
||||
IFS= read -r _ <"$TTY_PATH" || true
|
||||
}
|
||||
|
||||
upload_failure_log() {
|
||||
local uploader
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
|
||||
if uploader=$(find_log_uploader); then
|
||||
"$uploader" install >"$TTY_PATH" 2>&1 || true
|
||||
else
|
||||
{
|
||||
echo "No log uploader is available in this environment."
|
||||
echo "Install log: $LOG_FILE"
|
||||
[[ -f /mnt/var/log/omarchy-install.log ]] && echo "Target log: /mnt/var/log/omarchy-install.log"
|
||||
} >"$TTY_PATH"
|
||||
fi
|
||||
prompt_enter
|
||||
}
|
||||
|
||||
view_failure_log() {
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
|
||||
if command -v less >/dev/null 2>&1 && [[ -f $LOG_FILE ]]; then
|
||||
less "$LOG_FILE" <"$TTY_PATH" >"$TTY_PATH" 2>&1 || true
|
||||
else
|
||||
tail -n 200 "$LOG_FILE" >"$TTY_PATH" 2>&1 || true
|
||||
prompt_enter
|
||||
fi
|
||||
}
|
||||
|
||||
render_failure() {
|
||||
local status="$1" state_summary="${2:-}" line
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
|
||||
blank_line
|
||||
render_logo
|
||||
blank_line
|
||||
center "${RED}Omarchy installation stopped${RESET}" "$CONTENT_WIDTH"
|
||||
center "Installer exited with status $status" "$CONTENT_WIDTH"
|
||||
|
||||
if [[ -n $state_summary ]]; then
|
||||
blank_line
|
||||
while IFS= read -r line; do
|
||||
[[ -n $line ]] || continue
|
||||
center "${DIM}$(truncate_for_width "$line" "$CONTENT_WIDTH")${RESET}" "$CONTENT_WIDTH"
|
||||
done <<<"$state_summary"
|
||||
fi
|
||||
|
||||
blank_line
|
||||
render_failure_log_tail
|
||||
blank_line
|
||||
center "${DIM}Get help at https://omarchy.org/discord${RESET}" "$CONTENT_WIDTH"
|
||||
blank_line
|
||||
}
|
||||
|
||||
failure_menu() {
|
||||
local choice uploader
|
||||
|
||||
[[ ${OMARCHY_UI_FAILURE_ACTION:-} == "exit" ]] && return 0
|
||||
|
||||
while true; do
|
||||
uploader=""
|
||||
uploader=$(find_log_uploader || true)
|
||||
if [[ -n $uploader ]]; then
|
||||
choice=$(gum choose \
|
||||
--height 6 \
|
||||
--header "What would you like to do?" \
|
||||
"Upload log for support" \
|
||||
"View full log" \
|
||||
"Drop to shell" \
|
||||
"Reboot" \
|
||||
"Power off" \
|
||||
<"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell"
|
||||
else
|
||||
choice=$(gum choose \
|
||||
--height 5 \
|
||||
--header "What would you like to do?" \
|
||||
"View full log" \
|
||||
"Drop to shell" \
|
||||
"Reboot" \
|
||||
"Power off" \
|
||||
<"$TTY_PATH" 2>"$TTY_PATH") || choice="Drop to shell"
|
||||
fi
|
||||
|
||||
case "$choice" in
|
||||
"Upload log for support")
|
||||
upload_failure_log
|
||||
render_failure "${failure_status:-1}" "${failure_summary:-}" >"$TTY_PATH" 2>/dev/null || true
|
||||
;;
|
||||
"View full log")
|
||||
view_failure_log
|
||||
render_failure "${failure_status:-1}" "${failure_summary:-}" >"$TTY_PATH" 2>/dev/null || true
|
||||
;;
|
||||
"Reboot")
|
||||
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
|
||||
;;
|
||||
"Power off")
|
||||
poweroff 2>/dev/null || systemctl poweroff 2>/dev/null || true
|
||||
;;
|
||||
"Drop to shell"|"")
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
sanitize_child_output() {
|
||||
# Archinstall is only a library subsystem in Omarchy's installer. Its own
|
||||
# post-install success/reboot banner can be buffered until process exit and
|
||||
# appear after an Omarchy failure, which makes the unified log contradictory.
|
||||
# Keep that third-party banner out of Omarchy's support log; the raw
|
||||
# archinstall log is still uploaded separately by omarchy-upload-log.
|
||||
sed -u \
|
||||
-e $'s/\x1b\\[[0-9;?]*[A-Za-z]//g' \
|
||||
-e '/^Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers$/d' \
|
||||
-e '/^Enabling service systemd-timesyncd$/d' \
|
||||
-e '/^Setting password for root$/d' \
|
||||
-e '/^Updating \/mnt\/etc\/fstab$/d' \
|
||||
-e '/^Syncing the system\.\.\.$/d' \
|
||||
-e '/^Installation completed without any errors\.$/d' \
|
||||
-e '/^Log files temporarily available at \/var\/log\/archinstall\.$/d' \
|
||||
-e '/^You may reboot when ready\.$/d'
|
||||
}
|
||||
|
||||
launch_child() {
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
# Child is non-interactive. All visible UI belongs to this supervisor.
|
||||
PYTHONUNBUFFERED=1 setsid "${INSTALL_CMD[@]}" </dev/null > >(sanitize_child_output >>"$LOG_FILE") 2>&1 &
|
||||
child_pid=$!
|
||||
child_pgid=$child_pid
|
||||
}
|
||||
|
||||
[[ -e $TTY_PATH ]] || exit 2
|
||||
printf '%s' "$HIDE_CURSOR" >"$TTY_PATH"
|
||||
render_static >"$TTY_PATH"
|
||||
launch_child
|
||||
|
||||
frame=0
|
||||
while jobs -pr | grep -qx "$child_pid"; do
|
||||
render_dynamic "$frame" >"$TTY_PATH" 2>/dev/null || true
|
||||
frame=$((frame + 1))
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
set +e
|
||||
wait "$child_pid"
|
||||
status=$?
|
||||
set -e
|
||||
child_pid=""
|
||||
child_pgid=""
|
||||
|
||||
dashboard_log "installer child exited with status $status"
|
||||
|
||||
if (( status != 0 )); then
|
||||
failure_status="$status"
|
||||
failure_summary=""
|
||||
if [[ -f $STATE_FILE ]]; then
|
||||
failure_summary="$(jq -r '
|
||||
(.current_phase // empty | "last installer phase: \(.)"),
|
||||
((.phases // []) | map(select(.status == "failed")) | last // empty
|
||||
| "failed phase: \(.name): \(.error)")
|
||||
' "$STATE_FILE" 2>/dev/null || true)"
|
||||
if [[ -n $failure_summary ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ -n $line ]] && dashboard_log "$line"
|
||||
done <<<"$failure_summary"
|
||||
fi
|
||||
fi
|
||||
render_failure "$failure_status" "$failure_summary" >"$TTY_PATH" 2>/dev/null || true
|
||||
failure_menu
|
||||
exit "$failure_status"
|
||||
fi
|
||||
|
||||
set +e
|
||||
render_finish
|
||||
finish_render_status=$?
|
||||
set -e
|
||||
if (( finish_render_status != 0 )); then
|
||||
dashboard_log "finish screen render failed with status $finish_render_status; using fallback"
|
||||
{
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR"
|
||||
printf '\nFinished installing\n\n'
|
||||
} >"$TTY_PATH" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if reboot_prompt; then
|
||||
if [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
printf '%s' "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
|
||||
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/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.
@@ -1,246 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,112 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,95 +0,0 @@
|
||||
"""Omarchy install orchestrator.
|
||||
|
||||
Single tool that owns the full install phase ordering, with archinstall used as
|
||||
a library subsystem (not as the top-level installer).
|
||||
|
||||
The live-ISO wrapper consumes CLI args and passes configuration paths via
|
||||
OMARCHY_INSTALL_* environment variables before Python starts. This keeps
|
||||
archinstall's import-time CLI parsing from seeing Omarchy-specific flags.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from .context import InstallContext
|
||||
from .phases import PhaseError, run
|
||||
from .ui import error, info
|
||||
|
||||
|
||||
def build_phases(ctx: InstallContext):
|
||||
"""Phase order. Each entry is (display name, callable taking InstallContext).
|
||||
|
||||
The ordering is the whole point of this orchestrator: package-install
|
||||
hooks (limine-mkinitcpio-hook, in particular) and useradd happen at
|
||||
points where their prerequisites are guaranteed to be in place.
|
||||
|
||||
Full-disk and protected installs use the same phase sequence. The
|
||||
configurator only changes the JSON input: full-disk asks archinstall to
|
||||
create/mount the layout, while protected provides an already-mounted target
|
||||
and the partition details Omarchy needs for boot/fstab generation.
|
||||
"""
|
||||
from .phases_impl import (
|
||||
prepare_live,
|
||||
prepare_install_target,
|
||||
arch_install_system,
|
||||
configure_hibernation,
|
||||
run_system_finalizer,
|
||||
finalize_limine_boot,
|
||||
run_chroot_finalizer,
|
||||
configure_dns_resolver,
|
||||
configure_login,
|
||||
validate_boot,
|
||||
)
|
||||
|
||||
return [
|
||||
("Preparing live environment", prepare_live),
|
||||
("Preparing install target", prepare_install_target),
|
||||
("Installing Arch + Omarchy", arch_install_system),
|
||||
("Configuring hibernation", configure_hibernation),
|
||||
("Configuring system", run_system_finalizer),
|
||||
("Finalizing Limine boot", finalize_limine_boot),
|
||||
("Finalizing user", run_chroot_finalizer),
|
||||
("Configuring login", configure_login),
|
||||
("Configuring DNS resolver", configure_dns_resolver),
|
||||
("Validating boot setup", validate_boot),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
ctx = InstallContext.from_env()
|
||||
except RuntimeError as e:
|
||||
error(f"Configuration error: {e}")
|
||||
return 2
|
||||
|
||||
info(f"Installing Omarchy for {ctx.username} → {ctx.target}")
|
||||
|
||||
from .phases_impl import (
|
||||
cleanup_bind_mounts,
|
||||
cleanup_protected_state,
|
||||
cleanup_target_hook_masks,
|
||||
)
|
||||
|
||||
success = False
|
||||
try:
|
||||
try:
|
||||
run(ctx, build_phases(ctx))
|
||||
success = True
|
||||
except PhaseError:
|
||||
error("Installation halted.")
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
error("Installation interrupted.")
|
||||
return 130
|
||||
|
||||
info("Installation complete.")
|
||||
return 0
|
||||
finally:
|
||||
cleanup_bind_mounts(ctx)
|
||||
cleanup_target_hook_masks(ctx)
|
||||
if not success:
|
||||
cleanup_protected_state(ctx)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,78 +0,0 @@
|
||||
"""Phase state machine. Each phase is a (name, callable) pair; callables take
|
||||
the InstallContext and either return cleanly or raise to abort the install."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from .context import InstallContext
|
||||
from .ui import error, info
|
||||
|
||||
|
||||
PhaseFn = Callable[[InstallContext], None]
|
||||
|
||||
|
||||
class PhaseError(Exception):
|
||||
"""Raised when a phase fails. Wrapped with the phase name."""
|
||||
|
||||
|
||||
def run(ctx: InstallContext, phases: list[tuple[str, PhaseFn]]) -> None:
|
||||
ctx.state_dir.mkdir(parents=True, exist_ok=True)
|
||||
state_path = ctx.state_dir / "state.json"
|
||||
state = {
|
||||
"started_at": time.time(),
|
||||
"total_phases": len(phases),
|
||||
"current_index": 0,
|
||||
"current_phase": "Starting installation",
|
||||
"phases": [],
|
||||
}
|
||||
_write_state(state_path, state)
|
||||
|
||||
for index, (name, fn) in enumerate(phases):
|
||||
state["current_index"] = index
|
||||
state["current_phase"] = name
|
||||
state["phase_started_at"] = time.time()
|
||||
_write_state(state_path, state)
|
||||
|
||||
info(f"› {name}")
|
||||
started = time.time()
|
||||
try:
|
||||
fn(ctx)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
elapsed = time.time() - started
|
||||
state["phases"].append({
|
||||
"name": name,
|
||||
"status": "failed",
|
||||
"elapsed": elapsed,
|
||||
"error": str(exc),
|
||||
})
|
||||
_write_state(state_path, state)
|
||||
|
||||
error(f"Phase '{name}' failed after {elapsed:.1f}s: {exc}")
|
||||
traceback.print_exc()
|
||||
raise PhaseError(f"phase {name} failed: {exc}") from exc
|
||||
|
||||
elapsed = time.time() - started
|
||||
state["phases"].append({"name": name, "status": "ok", "elapsed": elapsed})
|
||||
_write_state(state_path, state)
|
||||
|
||||
state["current_index"] = max(len(phases) - 1, 0)
|
||||
state["current_phase"] = "Installation complete"
|
||||
state["finished_at"] = time.time()
|
||||
_write_state(state_path, state)
|
||||
|
||||
timing_path = ctx.target / "var" / "log" / "omarchy-install-timing.json"
|
||||
timing_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
_write_state(timing_path, state)
|
||||
|
||||
|
||||
def _write_state(path: Path, state: dict) -> None:
|
||||
# Dashboard polls this file while phases update it. Write atomically so the
|
||||
# reader never observes a truncated/partial JSON document and resets UI.
|
||||
tmp = path.with_name(f".{path.name}.tmp")
|
||||
tmp.write_text(json.dumps(state, indent=2, default=str))
|
||||
tmp.replace(path)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,28 +0,0 @@
|
||||
"""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 initramfs_async=0
|
||||
options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=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 initramfs_async=0
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=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 initramfs_async=0
|
||||
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux-t2 archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on xe.enable_panel_replay=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 initramfs_async=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
|
||||
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 initramfs_async=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
|
||||
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux-t2.img
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,6 @@ 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 = Never
|
||||
SigLevel = Optional TrustAll
|
||||
Server = file:///var/cache/omarchy/mirror/offline/
|
||||
@@ -30,6 +30,5 @@ 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,9 +27,8 @@ Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
SigLevel = Optional TrustAll
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
Server = https://pkgs.omarchy.org/rc/$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,6 +30,5 @@ 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
|
||||
+3
-21
@@ -12,33 +12,15 @@ bootmodes=('bios.syslinux' 'uefi.grub')
|
||||
arch="x86_64"
|
||||
pacman_conf="pacman-offline.conf"
|
||||
airootfs_image_type="squashfs"
|
||||
# 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)'
|
||||
)
|
||||
airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M')
|
||||
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"
|
||||
["/root/configurator"]="0:0:755"
|
||||
["/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"
|
||||
["/root/configurator"]="0:0:755"
|
||||
["/var/cache/omarchy/mirror/offline/"]="0:0:775"
|
||||
["/usr/local/bin/omarchy-upload-log"]="0:0:755"
|
||||
)
|
||||
@@ -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 initramfs_async=0
|
||||
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% archiso_nbd_srv=${pxeserver} cms_verify=y
|
||||
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 initramfs_async=0
|
||||
APPEND archisobasedir=%INSTALL_DIR% archiso_nfs_srv=${pxeserver}:/run/archiso/bootmnt cms_verify=y
|
||||
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 initramfs_async=0
|
||||
APPEND archisobasedir=%INSTALL_DIR% archiso_http_srv=http://${pxeserver}/ cms_verify=y
|
||||
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 initramfs_async=0
|
||||
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% quiet splash xe.enable_panel_replay=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 initramfs_async=0
|
||||
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on xe.enable_panel_replay=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 ISO architecture guard** must allow supported architectures or scope any x86_64-only checks behind an explicit guard.
|
||||
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).
|
||||
4. **archinstall + Limine** must work end-to-end on aarch64. archinstall supports it; Limine supports aarch64 UEFI. Worth a manual verification before committing to this bootloader path on ARM.
|
||||
|
||||
---
|
||||
@@ -76,9 +76,9 @@ aarch64 has no BIOS — only UEFI. Drop the syslinux path entirely on ARM.
|
||||
|
||||
The cleanest cut: keep one `grub.cfg` shared, strip the x86 shell/memtest fragments at build time when `OMARCHY_ARCH=aarch64`. Don't fork the file.
|
||||
|
||||
### 6. mkinitcpio preset — `configs/airootfs/etc/mkinitcpio.d/linux-t2.preset`
|
||||
### 6. mkinitcpio preset — `configs/airootfs/etc/mkinitcpio.d/linux.preset`
|
||||
|
||||
Names `vmlinuz-linux-t2` and `initramfs-linux-t2.img`. The filename is the pkgbase and the alpm hook keys off it, so aarch64 does not rename this file — it simply does not ship it, and releng's own `linux.preset` covers the stock kernel. Drop `linux-t2` from `arch_packages` on aarch64 and the boot entries point at `vmlinuz-linux` / `initramfs-linux.img`.
|
||||
References `vmlinuz-linux-t2` and `initramfs-linux-t2.img` (lines 5, 8). For aarch64, swap to `vmlinuz-linux` / `initramfs-linux.img`. Generate the file at build time from a template, keyed on arch.
|
||||
|
||||
### 7. Configurator — `configs/airootfs/root/configurator`
|
||||
|
||||
@@ -109,9 +109,9 @@ Both scripts hardcode the OVMF path; small, mechanical fix.
|
||||
### 10. Release script — `bin/omarchy-iso-release:52`
|
||||
|
||||
```bash
|
||||
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*${OMARCHY_ARCH}-"$ISO_REF".iso | head -n1)
|
||||
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*x86_64-"$INSTALLER_REF".iso | head -n1)
|
||||
```
|
||||
Hardcoded `x86_64` glob. Take an arch arg, or do `*${OMARCHY_ARCH}-${ISO_REF}.iso`. The ISO filename already contains arch (set by archiso from `profiledef.sh`), so this just needs the glob parameterized.
|
||||
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.
|
||||
|
||||
### 11. CI — `.github/workflows/nightly-build.yml`
|
||||
|
||||
@@ -145,7 +145,7 @@ Code:
|
||||
- `configs/efiboot/loader/loader.conf` — per-arch default entry
|
||||
- `configs/efiboot/loader/entries/01-archiso-x86_64-linux.conf` — generate aarch64 sibling at build time
|
||||
- `configs/grub/grub.cfg`, `loopback.cfg` — strip x86-only fragments on aarch64
|
||||
- `configs/airootfs/etc/mkinitcpio.d/linux-t2.preset` — omit on aarch64 (drop linux-t2 there; releng's linux.preset covers the stock kernel)
|
||||
- `configs/airootfs/etc/mkinitcpio.d/linux.preset` — template by arch (drop linux-t2 on aarch64)
|
||||
- `configs/airootfs/root/configurator` — branch archinstall JSON mirror list on `uname -m`; optionally guard lspci T2 probe
|
||||
- `.github/workflows/nightly-build.yml` — matrix x86_64/aarch64
|
||||
|
||||
|
||||
@@ -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_ISO_REF` suffix).
|
||||
- Rename output ISO with `-oem` suffix (in addition to existing `-$OMARCHY_INSTALLER_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