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