Improve ISO installer flow and VM tooling
This commit is contained in:
8 files changed
+807
-246
No files matched your search
+119
-12
@@ -2,28 +2,135 @@
|
||||
|
||||
set -e
|
||||
|
||||
ISO="$1"
|
||||
REUSE="$2"
|
||||
ISO=""
|
||||
REUSE=""
|
||||
SSH_HOST="${OMARCHY_VM_SSH_HOST:-127.0.0.1}"
|
||||
SSH_PORT="${OMARCHY_VM_SSH_PORT:-2222}"
|
||||
NETWORK="${OMARCHY_VM_NETWORK:-1}"
|
||||
DISK="${OMARCHY_VM_DISK:-/tmp/omarchy-iso-boot.qcow2}"
|
||||
OVMF_VARS="${OMARCHY_VM_OVMF_VARS:-/tmp/OVMF_VARS.4m.fd}"
|
||||
EXTRA_QEMU_ARGS=()
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: omarchy-iso-boot [options] [release/omarchy.iso] [reuse]
|
||||
|
||||
Options:
|
||||
--reuse Reuse the existing VM disk
|
||||
--ssh-port PORT Forward host PORT to guest port 22 (default: $SSH_PORT)
|
||||
--ssh-host HOST Host address for SSH forwarding (default: $SSH_HOST)
|
||||
--no-network Boot without a virtual NIC
|
||||
--disk PATH VM disk path (default: $DISK)
|
||||
--ovmf-vars PATH OVMF vars path (default: $OVMF_VARS)
|
||||
-h, --help Show this help
|
||||
-- Pass the remaining arguments directly to QEMU
|
||||
|
||||
SSH access after starting sshd in the guest:
|
||||
ssh -p $SSH_PORT root@localhost
|
||||
scp -P $SSH_PORT /path/to/system-manifest root@localhost:/tmp/
|
||||
USAGE
|
||||
}
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--reuse|reuse)
|
||||
REUSE="reuse"
|
||||
shift
|
||||
;;
|
||||
--ssh-port)
|
||||
SSH_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-port=*)
|
||||
SSH_PORT="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ssh-host)
|
||||
SSH_HOST="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-host=*)
|
||||
SSH_HOST="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--no-network|--no-net)
|
||||
NETWORK=0
|
||||
shift
|
||||
;;
|
||||
--disk)
|
||||
DISK="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--disk=*)
|
||||
DISK="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
--ovmf-vars)
|
||||
OVMF_VARS="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ovmf-vars=*)
|
||||
OVMF_VARS="${1#*=}"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
EXTRA_QEMU_ARGS+=("$@")
|
||||
break
|
||||
;;
|
||||
*)
|
||||
if [[ -z $ISO ]]; then
|
||||
ISO="$1"
|
||||
else
|
||||
echo "Unexpected argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z $ISO && -t 0 ]]; then
|
||||
ISO=$(ls -t release/*.iso 2>/dev/null | gum choose --header "Select ISO to boot")
|
||||
if gum confirm --default=false "Reuse existing disk?"; then
|
||||
REUSE="reuse"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ISO" ]; then
|
||||
echo "Usage: omarchy-iso-boot [release/omarchy.iso] [reuse]"
|
||||
echo " SSH available at: ssh -p 2222 root@localhost (after installing openssh)"
|
||||
if [[ -z $ISO ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $DISK || -z $OVMF_VARS ]]; then
|
||||
echo "Disk and OVMF vars paths must not be empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
omarchy-pkg-add qemu-full edk2-ovmf
|
||||
|
||||
DISK="/tmp/omarchy-iso-boot.qcow2"
|
||||
if [[ ! -f $DISK || $REUSE != "reuse" ]]; then
|
||||
qemu-img create -f qcow2 "$DISK" 40G
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd /tmp/OVMF_VARS.4m.fd
|
||||
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd "$OVMF_VARS"
|
||||
fi
|
||||
|
||||
NET_ARGS=()
|
||||
if [[ $NETWORK != "0" && $NETWORK != "false" && $NETWORK != "no" ]]; then
|
||||
if [[ -n $SSH_PORT && $SSH_PORT != "0" ]]; then
|
||||
NET_ARGS=(-netdev "user,id=net0,hostfwd=tcp:${SSH_HOST}:${SSH_PORT}-:22" -device virtio-net-pci,netdev=net0)
|
||||
echo "SSH forwarding enabled: ssh -p $SSH_PORT root@localhost"
|
||||
echo "SCP example: scp -P $SSH_PORT /path/to/system-manifest root@localhost:/tmp/"
|
||||
else
|
||||
NET_ARGS=(-netdev user,id=net0 -device virtio-net-pci,netdev=net0)
|
||||
echo "Networking enabled without SSH port forwarding."
|
||||
fi
|
||||
else
|
||||
echo "Networking disabled."
|
||||
fi
|
||||
|
||||
# Boot from ISO before we've installed, then boot from drive after
|
||||
@@ -32,7 +139,7 @@ qemu-system-x86_64 \
|
||||
-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=/tmp/OVMF_VARS.4m.fd \
|
||||
-drive if=pflash,format=raw,file="$OVMF_VARS" \
|
||||
-drive file="$DISK",format=qcow2,if=none,id=drive0 \
|
||||
-device virtio-blk-pci,drive=drive0,bootindex=1 \
|
||||
-drive file="$ISO",media=cdrom,if=none,format=raw,id=cdrom0 \
|
||||
@@ -40,6 +147,6 @@ qemu-system-x86_64 \
|
||||
-device virtio-vga-gl \
|
||||
-display gtk,gl=on \
|
||||
-usb -device usb-tablet \
|
||||
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
|
||||
-device virtio-net-pci,netdev=net0 \
|
||||
-boot menu=on
|
||||
"${NET_ARGS[@]}" \
|
||||
-boot menu=on \
|
||||
"${EXTRA_QEMU_ARGS[@]}"
|
||||
+68
-15
@@ -4,8 +4,8 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
VM_DIR="$(dirname "$SCRIPT_DIR")/vm-saves"
|
||||
TMP_DISK="/tmp/omarchy-iso-boot.qcow2"
|
||||
TMP_OVMF="/tmp/OVMF_VARS.4m.fd"
|
||||
TMP_DISK="${OMARCHY_VM_DISK:-/tmp/omarchy-iso-boot.qcow2}"
|
||||
TMP_OVMF="${OMARCHY_VM_OVMF_VARS:-/tmp/OVMF_VARS.4m.fd}"
|
||||
|
||||
mkdir -p "$VM_DIR"
|
||||
|
||||
@@ -18,7 +18,7 @@ save_vm() {
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TMP_DISK" || ! -f "$TMP_OVMF" ]]; then
|
||||
echo "Error: No active VM found in /tmp"
|
||||
echo "Error: No active VM found at $TMP_DISK / $TMP_OVMF"
|
||||
echo "Boot a VM first with: omarchy-iso-boot [iso]"
|
||||
exit 1
|
||||
fi
|
||||
@@ -53,7 +53,44 @@ list_vms() {
|
||||
}
|
||||
|
||||
boot_vm() {
|
||||
local name="$1"
|
||||
local name=""
|
||||
local boot_args=()
|
||||
local active_disk="$TMP_DISK"
|
||||
local active_ovmf="$TMP_OVMF"
|
||||
|
||||
if [[ $# -gt 0 && ${1:-} != --* ]]; then
|
||||
name="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--disk)
|
||||
active_disk="${2:-}"
|
||||
boot_args+=("$1" "${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--disk=*)
|
||||
active_disk="${1#*=}"
|
||||
boot_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
--ovmf-vars)
|
||||
active_ovmf="${2:-}"
|
||||
boot_args+=("$1" "${2:-}")
|
||||
shift 2
|
||||
;;
|
||||
--ovmf-vars=*)
|
||||
active_ovmf="${1#*=}"
|
||||
boot_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
boot_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$(ls -A "$VM_DIR" 2>/dev/null)" ]]; then
|
||||
echo "No VM snapshots available"
|
||||
@@ -73,25 +110,34 @@ boot_vm() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$TMP_DISK" ]] && ! gum confirm "Override existing /tmp VM?"; then
|
||||
if [[ -z $active_disk || -z $active_ovmf ]]; then
|
||||
echo "Error: active disk and OVMF vars paths must not be empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f $active_disk ]] && ! gum confirm "Override existing active VM disk at $active_disk?"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Copying VM '$name' to /tmp..."
|
||||
cp "$vm_path/omarchy-iso-boot.qcow2" "$TMP_DISK"
|
||||
cp "$vm_path/OVMF_VARS.4m.fd" "$TMP_OVMF"
|
||||
echo "Copying VM '$name' to $active_disk..."
|
||||
mkdir -p "$(dirname "$active_disk")" "$(dirname "$active_ovmf")"
|
||||
cp "$vm_path/omarchy-iso-boot.qcow2" "$active_disk"
|
||||
cp "$vm_path/OVMF_VARS.4m.fd" "$active_ovmf"
|
||||
|
||||
gum style --foreground 212 "✓ VM ready. Booting..."
|
||||
|
||||
exec omarchy-iso-boot /dev/null reuse
|
||||
exec omarchy-iso-boot "${boot_args[@]}" /dev/null reuse
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
command="${1:-}"
|
||||
[[ $# -gt 0 ]] && shift
|
||||
|
||||
case "$command" in
|
||||
save)
|
||||
save_vm "$2"
|
||||
save_vm "${1:-}"
|
||||
;;
|
||||
boot)
|
||||
boot_vm "$2"
|
||||
boot_vm "$@"
|
||||
;;
|
||||
list)
|
||||
list_vms
|
||||
@@ -101,9 +147,16 @@ case "${1:-}" in
|
||||
"$(gum style --bold 'omarchy-vm') - VM snapshot manager" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Commands:')" \
|
||||
" save [name] Save current VM from /tmp" \
|
||||
" boot [name] Boot saved VM (interactive if no name)" \
|
||||
" list List available VMs" \
|
||||
" save [name] Save current active VM" \
|
||||
" boot [name] [boot-options] Boot saved VM (interactive if no name)" \
|
||||
" list List available VMs" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Boot options:')" \
|
||||
" --ssh-port PORT Forward host PORT to guest SSH (default: 2222)" \
|
||||
" --ssh-host HOST Host address for SSH forwarding (default: 127.0.0.1)" \
|
||||
" --disk PATH Active disk path, useful for parallel VMs" \
|
||||
" --ovmf-vars PATH Active OVMF vars path, useful for parallel VMs" \
|
||||
" --no-network Boot without a virtual NIC" \
|
||||
"" \
|
||||
"$(gum style --foreground 245 'Current VMs:')"
|
||||
list_vms 2>/dev/null || echo " None"
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local smoke test for the foreground installer UI supervisor.
|
||||
# Runs without rebuilding the ISO.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DASHBOARD="$ROOT/configs/airootfs/usr/local/bin/omarchy-install-dashboard"
|
||||
AUTOMATED="$ROOT/configs/airootfs/root/.automated_script.sh"
|
||||
ORCH="$ROOT/configs/airootfs/usr/share/omarchy-iso/orchestrator"
|
||||
OMARCHY_PATH_FOR_TEST="${OMARCHY_PATH_FOR_TEST:-$ROOT/../omarchy-installer}"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
strip_ansi() {
|
||||
python - "$1" <<'PY'
|
||||
from pathlib import Path
|
||||
import re, sys
|
||||
s = Path(sys.argv[1]).read_text(errors="ignore")
|
||||
s = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", s)
|
||||
sys.stdout.write(s)
|
||||
PY
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1" needle="$2" stripped
|
||||
stripped="$tmp/stripped.$(basename "$file")"
|
||||
strip_ansi "$file" >"$stripped"
|
||||
if ! grep -Fq "$needle" "$stripped"; then
|
||||
echo "Expected $file to contain: $needle" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local file="$1" needle="$2" stripped
|
||||
stripped="$tmp/stripped.$(basename "$file")"
|
||||
strip_ansi "$file" >"$stripped"
|
||||
if grep -Fq "$needle" "$stripped"; then
|
||||
echo "Expected $file not to contain: $needle" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
write_state_py='import json, sys, time, pathlib
|
||||
path=pathlib.Path(sys.argv[1]); phase=sys.argv[2]
|
||||
state={"started_at":time.time(),"current_phase":phase,"phase_started_at":time.time(),"phases":[]}
|
||||
tmp=path.with_name(".state.json.tmp")
|
||||
tmp.write_text(json.dumps(state))
|
||||
tmp.replace(path)'
|
||||
|
||||
success_worker="$tmp/success-worker"
|
||||
cat >"$success_worker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
state="\$1"; log="\$2"
|
||||
mkdir -p "\$(dirname "\$state")" "\$(dirname "\$log")"
|
||||
python - "\$state" "Preparing live environment" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
python - "\$state" "Installing Arch + Omarchy" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.4
|
||||
python - "\$state" "Validating boot setup" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
cat >>"\$log" <<'LOG'
|
||||
=== Installation Time Summary ===
|
||||
Omarchy: 1m 40s
|
||||
=================================
|
||||
LOG
|
||||
EOF
|
||||
chmod +x "$success_worker"
|
||||
|
||||
no_duration_worker="$tmp/no-duration-worker"
|
||||
cat >"$no_duration_worker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
state="\$1"
|
||||
mkdir -p "\$(dirname "\$state")"
|
||||
python - "\$state" "Validating boot setup" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
EOF
|
||||
chmod +x "$no_duration_worker"
|
||||
|
||||
fail_worker="$tmp/fail-worker"
|
||||
cat >"$fail_worker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
state="\$1"
|
||||
mkdir -p "\$(dirname "\$state")"
|
||||
python - "\$state" "Finalizing in chroot" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
echo "Syncing the system..."
|
||||
echo "Installation completed without any errors."
|
||||
printf '\033[?25h'
|
||||
echo "You may reboot when ready."
|
||||
sleep 0.2
|
||||
exit 7
|
||||
EOF
|
||||
chmod +x "$fail_worker"
|
||||
|
||||
success_state="$tmp/success-state.json"
|
||||
success_log="$tmp/success.log"
|
||||
success_capture="$tmp/success.capture"
|
||||
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
|
||||
"$DASHBOARD '$success_log' '$success_state' -- '$success_worker' '$success_state' '$success_log'" \
|
||||
"$success_capture" >/dev/null
|
||||
assert_contains "$success_capture" "Installing Omarchy"
|
||||
assert_contains "$success_capture" "Installed in 1m 40s"
|
||||
|
||||
no_duration_state="$tmp/no-duration-state.json"
|
||||
no_duration_log="$tmp/no-duration.log"
|
||||
no_duration_capture="$tmp/no-duration.capture"
|
||||
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
|
||||
"$DASHBOARD '$no_duration_log' '$no_duration_state' -- '$no_duration_worker' '$no_duration_state'" \
|
||||
"$no_duration_capture" >/dev/null
|
||||
assert_contains "$no_duration_capture" "Finished installing"
|
||||
|
||||
fail_state="$tmp/fail-state.json"
|
||||
fail_log="$tmp/fail.log"
|
||||
fail_capture="$tmp/fail.capture"
|
||||
set +e
|
||||
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
|
||||
"$DASHBOARD '$fail_log' '$fail_state' -- '$fail_worker' '$fail_state'" \
|
||||
"$fail_capture" >/dev/null
|
||||
fail_status=$?
|
||||
set -e
|
||||
if [[ $fail_status -ne 7 ]]; then
|
||||
echo "Expected failure worker status 7, got $fail_status" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_contains "$fail_log" "Installation completed without any errors."
|
||||
assert_contains "$fail_log" "[dashboard] installer child exited with status 7"
|
||||
assert_contains "$fail_log" "[dashboard] last installer phase: Finalizing in chroot"
|
||||
python - "$fail_log" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
if "\x1b[?25h" in Path(sys.argv[1]).read_text(errors="ignore"):
|
||||
raise SystemExit("CSI cursor sequence leaked into log")
|
||||
PY
|
||||
assert_not_contains "$fail_capture" "Installed in"
|
||||
assert_not_contains "$fail_capture" "Finished installing"
|
||||
|
||||
bash -n "$DASHBOARD" "$AUTOMATED"
|
||||
python -m py_compile "$ORCH"/*.py
|
||||
git -C "$ROOT" diff --check
|
||||
|
||||
if rg -n 'dashboard\.pid|dashboard\.stop|OMARCHY_INSTALL_DASHBOARD|_stop_install_dashboard|_dashboard_|killpg|current_phase.*Finishing|confirm\(|Reboot now' \
|
||||
"$AUTOMATED" "$DASHBOARD" "$ORCH"; then
|
||||
echo "Forbidden legacy dashboard lifecycle pattern found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq '/usr/local/bin/omarchy-install-dashboard \' "$AUTOMATED"; then
|
||||
echo "Expected .automated_script.sh to call dashboard in foreground" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Eq 'omarchy-install-dashboard .*&' "$AUTOMATED"; then
|
||||
echo "Dashboard must not be launched in background" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "installer UI flow smoke test passed"
|
||||
@@ -1,20 +1,276 @@
|
||||
#!/bin/bash
|
||||
|
||||
# The configurator normally runs under this small PTY proxy so Ctrl-T can work
|
||||
# as a global tools shortcut even while gum owns the terminal in raw mode. The
|
||||
# child configurator keeps stdout on the existing stream (so generated config and
|
||||
# logs behave normally), while stderr and /dev/tty traffic are relayed through the
|
||||
# PTY. Pressing Ctrl-T kills the current configurator screen, opens Tools, then
|
||||
# starts the configurator over at keyboard selection.
|
||||
if [[ -z "${OMARCHY_CONFIGURATOR_INNER:-}" && -z "${OMARCHY_CONFIGURATOR_NO_KEY_PROXY:-}" ]] && { [[ -t 0 ]] || [[ -t 2 ]]; }; then
|
||||
OMARCHY_CONFIGURATOR_PYTHON=$(command -v python3 2>/dev/null || command -v python 2>/dev/null || true)
|
||||
if [[ -n "$OMARCHY_CONFIGURATOR_PYTHON" ]]; then
|
||||
export OMARCHY_CONFIGURATOR_SCRIPT
|
||||
OMARCHY_CONFIGURATOR_SCRIPT=$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || printf '%s' "${BASH_SOURCE[0]}")
|
||||
exec "$OMARCHY_CONFIGURATOR_PYTHON" - "$@" <<'PY'
|
||||
import errno
|
||||
import fcntl
|
||||
import os
|
||||
import pty
|
||||
import select
|
||||
import signal
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
|
||||
CTRL_T = b"\x14"
|
||||
SCRIPT = os.environ["OMARCHY_CONFIGURATOR_SCRIPT"]
|
||||
ARGS = sys.argv[1:]
|
||||
|
||||
try:
|
||||
tty_fd = os.open("/dev/tty", os.O_RDWR | os.O_NOCTTY)
|
||||
except OSError:
|
||||
env = os.environ.copy()
|
||||
env["OMARCHY_CONFIGURATOR_INNER"] = "1"
|
||||
os.execvpe("bash", ["bash", SCRIPT] + ARGS, env)
|
||||
|
||||
stdout_fd = os.dup(1)
|
||||
original_tty = termios.tcgetattr(tty_fd)
|
||||
current_master = None
|
||||
current_pgid = None
|
||||
|
||||
|
||||
def restore_tty():
|
||||
try:
|
||||
termios.tcsetattr(tty_fd, termios.TCSADRAIN, original_tty)
|
||||
except termios.error:
|
||||
pass
|
||||
|
||||
|
||||
def write_tty(data):
|
||||
if data:
|
||||
os.write(tty_fd, data)
|
||||
|
||||
|
||||
def copy_window_size(master):
|
||||
try:
|
||||
size = fcntl.ioctl(tty_fd, termios.TIOCGWINSZ, b"\0" * 8)
|
||||
fcntl.ioctl(master, termios.TIOCSWINSZ, size)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def on_winch(signum, frame):
|
||||
if current_master is not None:
|
||||
copy_window_size(current_master)
|
||||
if current_pgid is not None:
|
||||
try:
|
||||
os.killpg(current_pgid, signal.SIGWINCH)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def on_termination(signum, frame):
|
||||
if current_pgid is not None:
|
||||
try:
|
||||
os.killpg(current_pgid, signum)
|
||||
except OSError:
|
||||
pass
|
||||
restore_tty()
|
||||
raise SystemExit(128 + signum)
|
||||
|
||||
|
||||
def exit_code(status):
|
||||
if os.WIFEXITED(status):
|
||||
return os.WEXITSTATUS(status)
|
||||
if os.WIFSIGNALED(status):
|
||||
return 128 + os.WTERMSIG(status)
|
||||
return 1
|
||||
|
||||
|
||||
def spawn_child(tools_only=False):
|
||||
global current_master, current_pgid
|
||||
|
||||
pid, master = pty.fork()
|
||||
if pid == 0:
|
||||
os.dup2(stdout_fd, 1)
|
||||
try:
|
||||
os.close(stdout_fd)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
env = os.environ.copy()
|
||||
env["OMARCHY_CONFIGURATOR_INNER"] = "1"
|
||||
env["OMARCHY_CONFIGURATOR_CTRL_T"] = "1"
|
||||
if tools_only:
|
||||
env["OMARCHY_CONFIGURATOR_TOOLS_ONLY"] = "1"
|
||||
else:
|
||||
env.pop("OMARCHY_CONFIGURATOR_TOOLS_ONLY", None)
|
||||
|
||||
os.execvpe("bash", ["bash", SCRIPT] + ARGS, env)
|
||||
|
||||
os.set_blocking(master, False)
|
||||
current_master = master
|
||||
current_pgid = pid
|
||||
copy_window_size(master)
|
||||
return pid, master
|
||||
|
||||
|
||||
def wait_for_child(pid, timeout):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
done, status = os.waitpid(pid, os.WNOHANG)
|
||||
except ChildProcessError:
|
||||
return 0
|
||||
if done == pid:
|
||||
return exit_code(status)
|
||||
time.sleep(0.05)
|
||||
return None
|
||||
|
||||
|
||||
def terminate_child(pid):
|
||||
for sig, timeout in ((signal.SIGTERM, 0.5), (signal.SIGKILL, 0.2)):
|
||||
try:
|
||||
os.killpg(pid, sig)
|
||||
except ProcessLookupError:
|
||||
return 0
|
||||
except OSError:
|
||||
try:
|
||||
os.kill(pid, sig)
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
code = wait_for_child(pid, timeout)
|
||||
if code is not None:
|
||||
return code
|
||||
return 130
|
||||
|
||||
|
||||
def drain_master(master):
|
||||
while True:
|
||||
try:
|
||||
data = os.read(master, 4096)
|
||||
except BlockingIOError:
|
||||
return
|
||||
except OSError:
|
||||
return
|
||||
if not data:
|
||||
return
|
||||
write_tty(data)
|
||||
|
||||
|
||||
def relay_child(tools_only=False):
|
||||
global current_master, current_pgid
|
||||
|
||||
pid, master = spawn_child(tools_only)
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
done, status = os.waitpid(pid, os.WNOHANG)
|
||||
except ChildProcessError:
|
||||
drain_master(master)
|
||||
return 0
|
||||
if done == pid:
|
||||
drain_master(master)
|
||||
return exit_code(status)
|
||||
|
||||
try:
|
||||
readable, _, _ = select.select([tty_fd, master], [], [], 0.1)
|
||||
except InterruptedError:
|
||||
continue
|
||||
|
||||
if tty_fd in readable:
|
||||
try:
|
||||
data = os.read(tty_fd, 1024)
|
||||
except OSError:
|
||||
data = b""
|
||||
if not data:
|
||||
continue
|
||||
|
||||
if not tools_only and CTRL_T in data:
|
||||
before = data.split(CTRL_T, 1)[0]
|
||||
if before:
|
||||
try:
|
||||
os.write(master, before)
|
||||
except OSError:
|
||||
pass
|
||||
terminate_child(pid)
|
||||
drain_master(master)
|
||||
return "tools"
|
||||
|
||||
try:
|
||||
os.write(master, data)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if master in readable:
|
||||
try:
|
||||
data = os.read(master, 4096)
|
||||
except BlockingIOError:
|
||||
continue
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.EIO:
|
||||
raise
|
||||
data = b""
|
||||
if data:
|
||||
write_tty(data)
|
||||
finally:
|
||||
current_master = None
|
||||
current_pgid = None
|
||||
try:
|
||||
os.close(master)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
signal.signal(signal.SIGWINCH, on_winch)
|
||||
signal.signal(signal.SIGHUP, on_termination)
|
||||
signal.signal(signal.SIGTERM, on_termination)
|
||||
|
||||
# Use raw-ish input so Ctrl-T/Ctrl-C can be intercepted and relayed, but keep
|
||||
# the real terminal's output processing intact. Full raw mode disables ONLCR,
|
||||
# causing gum/logo newlines to stair-step across the screen.
|
||||
tty_attrs = termios.tcgetattr(tty_fd)
|
||||
tty_attrs[0] &= ~(termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON)
|
||||
tty_attrs[2] |= termios.CS8
|
||||
tty_attrs[3] &= ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
|
||||
tty_attrs[6][termios.VMIN] = 1
|
||||
tty_attrs[6][termios.VTIME] = 0
|
||||
termios.tcsetattr(tty_fd, termios.TCSADRAIN, tty_attrs)
|
||||
|
||||
try:
|
||||
while True:
|
||||
result = relay_child(tools_only=False)
|
||||
if result == "tools":
|
||||
relay_child(tools_only=True)
|
||||
continue
|
||||
raise SystemExit(result)
|
||||
finally:
|
||||
restore_tty()
|
||||
try:
|
||||
os.close(stdout_fd)
|
||||
os.close(tty_fd)
|
||||
except OSError:
|
||||
pass
|
||||
PY
|
||||
fi
|
||||
fi
|
||||
unset OMARCHY_CONFIGURATOR_PYTHON
|
||||
|
||||
# Minimal live-ISO presentation helpers. Keep these local to the ISO: the
|
||||
# installed Omarchy package owns target setup helpers, while the ISO owns the
|
||||
# installer UI that runs before any target packages exist.
|
||||
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||
LOGO_PATH="${LOGO_PATH:-$OMARCHY_PATH/logo.txt}"
|
||||
|
||||
TERM_HEIGHT=${LINES:-24}
|
||||
TERM_WIDTH=${COLUMNS:-80}
|
||||
if [[ -e /dev/tty ]]; then
|
||||
TERM_SIZE=$(stty size 2>/dev/null </dev/tty || true)
|
||||
fi
|
||||
if [[ -n ${TERM_SIZE:-} ]]; then
|
||||
TERM_HEIGHT=${TERM_SIZE%% *}
|
||||
TERM_WIDTH=${TERM_SIZE##* }
|
||||
else
|
||||
TERM_HEIGHT=${LINES:-24}
|
||||
TERM_WIDTH=${COLUMNS:-80}
|
||||
if [[ $TERM_SIZE =~ ^([0-9]+)[[:space:]]+([0-9]+)$ ]] && (( BASH_REMATCH[1] > 0 && BASH_REMATCH[2] > 0 )); then
|
||||
TERM_HEIGHT=${BASH_REMATCH[1]}
|
||||
TERM_WIDTH=${BASH_REMATCH[2]}
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f $LOGO_PATH ]]; then
|
||||
@@ -60,10 +316,20 @@ abort() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
tools_hint() {
|
||||
[[ -n "${OMARCHY_CONFIGURATOR_CTRL_T:-}" && -z "${OMARCHY_CONFIGURATOR_TOOLS_ONLY:-}" ]] || return 0
|
||||
|
||||
# Draw the shortcut as a fixed footer instead of in the form body. Keep the
|
||||
# cursor where the active gum widget expects it; do not print a newline from
|
||||
# the bottom row or the VT will scroll.
|
||||
printf "\0337\033[%s;1H\033[2K%s\033[38;5;8mCtrl-T Tools\033[0m\0338" "$TERM_HEIGHT" "$PADDING_LEFT_SPACES"
|
||||
}
|
||||
|
||||
step() {
|
||||
clear_logo
|
||||
echo
|
||||
gum style --padding "0 0 0 $PADDING_LEFT" "$1"
|
||||
tools_hint
|
||||
echo
|
||||
}
|
||||
|
||||
@@ -80,8 +346,8 @@ notice() {
|
||||
}
|
||||
|
||||
# Tools menu — recovery-style actions kept off the main install path.
|
||||
# Inlines its own disk picker so the launcher can run before disk_form is
|
||||
# defined further down in the script.
|
||||
# Inlines its own disk picker so Ctrl-T can open it before disk_form is defined
|
||||
# further down in the script.
|
||||
tools_menu() {
|
||||
while true; do
|
||||
step "Tools"
|
||||
@@ -126,28 +392,10 @@ tools_menu() {
|
||||
done
|
||||
}
|
||||
|
||||
# Landing screen: Enter installs, Ctrl-T opens the tools menu.
|
||||
launcher_form() {
|
||||
while true; do
|
||||
step "Install Omarchy"
|
||||
say "Press Enter to install Omarchy on this machine."
|
||||
echo
|
||||
say --foreground 8 "Ctrl-T Tools"
|
||||
|
||||
local key
|
||||
IFS= read -rsn1 key || true
|
||||
case "$key" in
|
||||
""|$'\n'|$'\r')
|
||||
return 0
|
||||
;;
|
||||
$'\x14')
|
||||
tools_menu
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
launcher_form
|
||||
if [[ -n "${OMARCHY_CONFIGURATOR_TOOLS_ONLY:-}" ]]; then
|
||||
tools_menu
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# STEP 1: KEYBOARD LAYOUT
|
||||
|
||||
|
||||
@@ -32,8 +32,17 @@ CLEAR="${CSI}2J${CSI}H"
|
||||
CLEAR_LINE="${CSI}2K"
|
||||
CLEAR_TO_END="${CSI}J"
|
||||
GREEN="${CSI}32m"
|
||||
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=""
|
||||
|
||||
@@ -206,6 +215,7 @@ weights = [
|
||||
('Finalizing user', 96, 98, 10),
|
||||
('Finalizing in chroot', 88, 98, 10),
|
||||
('Configuring login', 98, 99, 3),
|
||||
('Configuring DNS resolver', 99, 100, 2),
|
||||
('Validating protected boot setup', 99, 100, 5),
|
||||
('Validating boot setup', 99, 100, 5),
|
||||
]
|
||||
@@ -268,6 +278,30 @@ render_dynamic() {
|
||||
|
||||
install_duration() {
|
||||
local path line duration
|
||||
|
||||
if [[ -f $STATE_FILE ]]; then
|
||||
duration="$(python - "$STATE_FILE" <<'PY' 2>/dev/null || true
|
||||
import json, sys
|
||||
try:
|
||||
state = json.load(open(sys.argv[1]))
|
||||
started = float(state.get("started_at") or 0)
|
||||
finished = float(state.get("finished_at") or 0)
|
||||
except Exception:
|
||||
raise SystemExit(0)
|
||||
if started <= 0 or finished <= 0 or finished < started:
|
||||
raise SystemExit(0)
|
||||
seconds = int(round(finished - started))
|
||||
hours, remainder = divmod(seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
if hours:
|
||||
print(f"{hours}h {minutes}m {seconds}s", end="")
|
||||
else:
|
||||
print(f"{minutes}m {seconds}s", end="")
|
||||
PY
|
||||
)"
|
||||
[[ -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
|
||||
@@ -313,10 +347,185 @@ reboot_prompt() {
|
||||
"" <"$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.
|
||||
setsid "${INSTALL_CMD[@]}" </dev/null > >(sed -u 's/\x1b\[[0-9;?]*[A-Za-z]//g' >>"$LOG_FILE") 2>&1 &
|
||||
PYTHONUNBUFFERED=1 setsid "${INSTALL_CMD[@]}" </dev/null > >(sanitize_child_output >>"$LOG_FILE") 2>&1 &
|
||||
child_pid=$!
|
||||
child_pgid=$child_pid
|
||||
}
|
||||
@@ -343,8 +552,10 @@ child_pgid=""
|
||||
dashboard_log "installer child exited with status $status"
|
||||
|
||||
if (( status != 0 )); then
|
||||
failure_status="$status"
|
||||
failure_summary=""
|
||||
if [[ -f $STATE_FILE ]]; then
|
||||
state_summary="$(python - "$STATE_FILE" <<'PY' 2>/dev/null || true
|
||||
failure_summary="$(python - "$STATE_FILE" <<'PY' 2>/dev/null || true
|
||||
import json, sys
|
||||
try:
|
||||
state = json.load(open(sys.argv[1]))
|
||||
@@ -359,14 +570,15 @@ for item in reversed(state.get("phases", [])):
|
||||
break
|
||||
PY
|
||||
)"
|
||||
if [[ -n ${state_summary:-} ]]; then
|
||||
if [[ -n $failure_summary ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ -n $line ]] && dashboard_log "$line"
|
||||
done <<<"$state_summary"
|
||||
done <<<"$failure_summary"
|
||||
fi
|
||||
fi
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH" 2>/dev/null || true
|
||||
exit "$status"
|
||||
render_failure "$failure_status" "$failure_summary" >"$TTY_PATH" 2>/dev/null || true
|
||||
failure_menu
|
||||
exit "$failure_status"
|
||||
fi
|
||||
|
||||
set +e
|
||||
|
||||
@@ -34,6 +34,7 @@ from the start.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
@@ -144,6 +145,63 @@ 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_users(method) -> bool:
|
||||
"""Return whether a bound archinstall method accepts the users 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 "users" in (*positional, *kwonly)
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -36,6 +36,7 @@ def build_phases(ctx: InstallContext):
|
||||
run_system_finalizer,
|
||||
finalize_limine_boot,
|
||||
run_chroot_finalizer,
|
||||
configure_dns_resolver,
|
||||
configure_login,
|
||||
validate_boot,
|
||||
)
|
||||
@@ -49,6 +50,7 @@ def build_phases(ctx: InstallContext):
|
||||
("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),
|
||||
]
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Phase ordering (full-disk and protected/pre-mounted):
|
||||
run_system_finalizer → arch-chroot root omarchy-setup-system
|
||||
finalize_limine_boot → final Limine config/UKI build after hardware drop-ins
|
||||
run_chroot_finalizer → arch-chroot -u user omarchy-setup-user
|
||||
configure_login → sddm autologin for unencrypted installs
|
||||
configure_login → sddm state + encrypted-install autologin
|
||||
validate_boot → assert UKI / limine.conf / kernel cmdline are sane
|
||||
"""
|
||||
|
||||
@@ -182,6 +182,10 @@ def arch_install_system(ctx: InstallContext) -> None:
|
||||
if config.auth_config and config.auth_config.users:
|
||||
installer.create_users(config.auth_config.users)
|
||||
|
||||
if config.app_config:
|
||||
info("› installing archinstall application selections")
|
||||
arch.install_applications(installer, config)
|
||||
|
||||
info("› installing Omarchy runtime + omarchy-base.packages")
|
||||
installer.add_additional_packages(_runtime_package_list(ctx))
|
||||
finally:
|
||||
@@ -901,6 +905,29 @@ def _ensure_finalizer_log_started(ctx: InstallContext) -> tuple[str, int]:
|
||||
return ctx.state["omarchy_start_time"], ctx.state["omarchy_start_epoch"]
|
||||
|
||||
|
||||
def _target_user_env(ctx: InstallContext, user: str) -> list[str]:
|
||||
home = f"/home/{user}"
|
||||
shell = "/bin/bash"
|
||||
passwd = ctx.target / "etc" / "passwd"
|
||||
|
||||
try:
|
||||
for line in passwd.read_text(errors="ignore").splitlines():
|
||||
fields = line.split(":")
|
||||
if len(fields) >= 7 and fields[0] == user:
|
||||
home = fields[5] or home
|
||||
shell = fields[6] or shell
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return [
|
||||
f"HOME={home}",
|
||||
f"USER={user}",
|
||||
f"LOGNAME={user}",
|
||||
f"SHELL={shell}",
|
||||
]
|
||||
|
||||
|
||||
def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str | None = None) -> None:
|
||||
_prepare_target_setup(ctx)
|
||||
omarchy_start_time, omarchy_start_epoch = _ensure_finalizer_log_started(ctx)
|
||||
@@ -929,6 +956,7 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
|
||||
f"OMARCHY_USER_EMAIL={ctx.email}",
|
||||
f"OMARCHY_MIRROR={mirror_channel}",
|
||||
"OMARCHY_INSTALL_LOG_FILE=/var/log/omarchy-install.log",
|
||||
"OMARCHY_LOG_TO_STDOUT=1",
|
||||
]
|
||||
if _install_debug_enabled():
|
||||
env_extras.append("OMARCHY_INSTALL_DEBUG=1")
|
||||
@@ -937,6 +965,7 @@ def _run_target_setup_command(ctx: InstallContext, cmd: list[str], *, user: str
|
||||
chroot_cmd = ["arch-chroot"]
|
||||
if user:
|
||||
chroot_cmd += ["-u", user]
|
||||
env_extras.extend(_target_user_env(ctx, user))
|
||||
chroot_cmd += [str(ctx.target), "env", "--unset=XDG_RUNTIME_DIR", *env_extras, *cmd]
|
||||
|
||||
try:
|
||||
@@ -1069,31 +1098,54 @@ def run_chroot_finalizer(ctx: InstallContext) -> None:
|
||||
user=ctx.username,
|
||||
)
|
||||
|
||||
|
||||
def configure_dns_resolver(ctx: InstallContext) -> None:
|
||||
"""Put the installed system in systemd-resolved stub mode.
|
||||
|
||||
Arch's systemd-resolved docs explicitly say not to create this symlink from
|
||||
inside arch-chroot because /etc/resolv.conf may be a bind mount from the
|
||||
live environment. Do it from the ISO against /mnt instead.
|
||||
"""
|
||||
resolv_conf = ctx.target / "etc" / "resolv.conf"
|
||||
target = "../run/systemd/resolve/stub-resolv.conf"
|
||||
|
||||
if resolv_conf.is_symlink() and os.readlink(resolv_conf) == target:
|
||||
return
|
||||
|
||||
info("› configuring /etc/resolv.conf for systemd-resolved")
|
||||
resolv_conf.parent.mkdir(parents=True, exist_ok=True)
|
||||
resolv_conf.unlink(missing_ok=True)
|
||||
resolv_conf.symlink_to(target)
|
||||
|
||||
|
||||
def _read_omarchy_mirror() -> str:
|
||||
p = Path("/root/omarchy_mirror")
|
||||
return p.read_text().strip() if p.exists() else "stable"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# configure_login: sddm autologin for unencrypted installs only (encrypted
|
||||
# installs already get a LUKS unlock prompt, no need for sddm autologin).
|
||||
# configure_login: seed SDDM's last user/session for the password-only Omarchy
|
||||
# greeter. Encrypted installs autologin because the LUKS prompt is the auth
|
||||
# boundary; unencrypted installs leave SDDM as the auth screen.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def configure_login(ctx: InstallContext) -> None:
|
||||
if ctx.encrypt:
|
||||
return
|
||||
|
||||
sddm_dir = ctx.target / "etc" / "sddm.conf.d"
|
||||
sddm_dir.mkdir(parents=True, exist_ok=True)
|
||||
(sddm_dir / "99-omarchy-login.conf").write_text(
|
||||
"[Theme]\nCurrent=omarchy\n\n"
|
||||
"[Users]\nRememberLastUser=true\nRememberLastSession=true\n"
|
||||
)
|
||||
(sddm_dir / "autologin.conf").write_text(
|
||||
"[Autologin]\n"
|
||||
f"User={ctx.username}\n"
|
||||
"Session=omarchy.desktop\n"
|
||||
)
|
||||
|
||||
autologin_conf = sddm_dir / "autologin.conf"
|
||||
if ctx.encrypt:
|
||||
autologin_conf.write_text(
|
||||
"[Autologin]\n"
|
||||
f"User={ctx.username}\n"
|
||||
"Session=omarchy.desktop\n"
|
||||
)
|
||||
else:
|
||||
autologin_conf.unlink(missing_ok=True)
|
||||
|
||||
state_dir = ctx.target / "var" / "lib" / "sddm"
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in new issue
Block a user