Improve ISO installer flow and VM tooling
This commit is contained in:
8 files changed
+807
-246
No files matched your search
@@ -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