feat(installer): sshd opt-in, gsettings replay, live-medium motd stamp
Tests / tests (push) Successful in 2m50s

- alvin-install: new sshd screen (default on) + install step. openssh is
  guaranteed into the pacstrap list when requested, and the final banner
  reports whether the enable actually succeeded (SSHD_APPLIED) instead of
  what was selected. The prompt warns that password login uses the same
  password as LUKS, and only mentions the GitHub key import when a
  username was given.
- alvin-install: replay the `gsettings set` commands end-4 logged during
  its setup run with a clean env and a throwaway session bus as the target
  user, then read each key back in a second session to confirm it persisted
  (gvariant_unescape normalizes both '...' and "..." GVariant output). The
  venv check now walks the uv bin/python symlink chain, re-rooting
  absolute hops under $TARGET, since that link dangles on the live medium.
- build: append the build stamp to the live /etc/motd (keeps the installed
  system's motd untouched) and drop alvin-install's install_motd step.
- docs: installer sections in README/AGENTS.md, sshd notes, corrected line
  counts.
This commit is contained in:
alvinlollo
2026-10-07 21:55:30 +11:00
parent 1bb6f8dddd
commit 59d82f06f5
5 changed files with 312 additions and 29 deletions
+198 -18
View File
@@ -28,6 +28,11 @@ UNL0KR="false"
TPM_STATUS=""
LOGIN_MANAGER="none"
AUTOLOGIN=""
# SSHD is the choice made on the sshd screen; SSHD_APPLIED is only flipped once
# the target's sshd.service has actually been enabled, so the final banner can
# report reality instead of the request.
SSHD="true"
SSHD_APPLIED="false"
TPM_DISCLAIMER=""
TARGET_DB_SYNCED="false"
@@ -76,6 +81,40 @@ target_owner() {
arch-chroot "$TARGET" sh -c "id -u '$USERNAME' && id -g '$USERNAME'" | paste -sd:
}
# `gsettings get` prints GVariant text: '...' normally, "..." when the value
# contains an apostrophe, with \n \t \\ \' \" escapes inside. Reduce that to
# the plain string the original `gsettings set` was called with, so a read-back
# can be compared against the replayed command. One left-to-right pass: doing
# global replacements instead would corrupt sequences like \\n (literal
# backslash + n must not become a newline).
gvariant_unescape() {
local s=$1 out="" i n c
if [[ "$s" == \'*\' || "$s" == \"*\" ]]; then
s=${s:1:${#s}-2}
n=${#s}
for ((i = 0; i < n; i++)); do
c=${s:i:1}
if [[ "$c" == "\\" && $((i + 1)) -lt $n ]]; then
i=$((i + 1))
case "${s:i:1}" in
n) out+=$'\n' ;;
t) out+=$'\t' ;;
r) out+=$'\r' ;;
a) out+=$'\a' ;;
b) out+=$'\b' ;;
f) out+=$'\f' ;;
v) out+=$'\v' ;;
*) out+="${s:i:1}" ;;
esac
else
out+=$c
fi
done
s=$out
fi
printf '%s' "$s"
}
# Refresh the target's package databases once before the first target-side
# pacman/yay call (cachyos is appended to the target's pacman.conf after
# pacstrap, so its db is not there yet; keyrings are re-populated as a
@@ -366,6 +405,35 @@ screen_login_manager() {
log "Login manager: $LOGIN_MANAGER"
}
screen_sshd() {
clear
header "SSH Server"
echo ""
local -a ask_lines=(
"Enable the SSH server (sshd) in the installed system?"
""
" - Starts at boot (systemctl enable sshd)"
" - Arch's default config allows password login, and this password"
" is the same as your LUKS passphrase - enable it only on networks"
" you trust, or harden sshd afterwards (keys-only auth, firewall)."
)
# The GitHub import is optional (screen_git), so only mention it when the
# user actually gave a username - and it happens later, during install.
if [[ -n "$GH_USERNAME" ]]; then
ask_lines+=(" - GitHub SSH keys for ${USERNAME} are imported during install")
fi
gum style --foreground 15 "${ask_lines[@]}"
echo ""
# --default pre-selects the affirmative so Enter accepts the default.
if gum confirm --default --affirmative "Enable sshd" --negative "Skip" \
"Enable the SSH server?"; then
SSHD="true"
else
SSHD="false"
fi
log "SSH server: ${SSHD}"
}
screen_summary() {
clear
header "Installation Summary"
@@ -398,6 +466,7 @@ screen_summary() {
"Locale: ${LOCALE}" \
"Timezone: ${TIMEZONE}" \
"Login mgr: ${LOGIN_MANAGER}" \
"SSH server: $([[ "$SSHD" == "true" ]] && echo enabled || echo skipped)" \
"SSH keys: ${ssh_line}" \
"Git: ${git_line}"
@@ -528,6 +597,17 @@ install_pacstrap() {
$found || pkgs+=("$p")
done
# sshd was requested but openssh is not in the medium's pkglist (or was
# filtered out above) — without this, install_sshd's enable would fail.
if [[ "$SSHD" == "true" ]]; then
local found_ssh=false
for existing in "${pkgs[@]+"${pkgs[@]}"}"; do
[[ "$existing" == "openssh" ]] && found_ssh=true && break
done
$found_ssh || pkgs+=(openssh)
log "Pacstrap: openssh guaranteed in package list (sshd requested)"
fi
info "Installing ${#pkgs[@]} packages (this may take a while)..."
log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped"
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm 2>>"$LOG"
@@ -607,6 +687,25 @@ install_network() {
ok "NetworkManager enabled."
}
install_sshd() {
[[ "$SSHD" != "true" ]] && return 0
info "Enabling SSH server..."
if [[ ! -x "${TARGET}/usr/bin/sshd" ]]; then
warn "openssh is missing from the target - cannot enable sshd."
log "SSH server: enable skipped (sshd binary missing in target)"
return 0
fi
if arch-chroot "$TARGET" systemctl enable sshd.service >>"$LOG" 2>&1; then
SSHD_APPLIED="true"
log "SSH server: enabled"
ok "sshd enabled (starts at boot)."
else
warn "Could not enable sshd.service (see $LOG)."
log "SSH server: enable failed"
fi
}
install_user() {
info "Creating user..."
@@ -832,6 +931,73 @@ install_end4_dots() {
fi
fi
# end-4's gsettings calls run under its sudo session inside this chroot,
# so dconf sees root's environment (/run/user/0, /root/.local/...) and no
# session bus: every write fails with "failed to commit changes to
# dconf" while gsettings still exits 0 and reports "finished". Replay any
# gsettings set this run logged, with a clean environment and a
# throwaway session bus as the target user. gsettings exits 0 even when
# the commit fails, so read the key back in a second session — only a
# matching value counts as persisted.
if ((log_mark > 0)) \
&& [[ -x "${TARGET}/usr/bin/dbus-run-session" && -x "${TARGET}/usr/bin/gsettings" ]]; then
local gs_line gs_schema gs_key gs_val gs_got gs_norm gs_raw
# Snapshot and parse first (the loop below appends to $LOG), keeping
# log order: a key set twice replays chronologically so the last
# write wins, exactly as end-4 ran it. Dedupe identical repeats only.
local -a gs_cmds=()
mapfile -t gs_cmds < <(
# Same ANSI-strip as the ignored-list parse below; the setup log
# shows each command once as its "Next command" line (the
# "…finished." echoes carry a prefix and never match ^gsettings).
sed -n "$((log_mark + 1)),\$p" "$LOG" | sed $'s/\x1b\\[[0-9;]*m//g' |
sed -n 's/^\(gsettings set .*\)$/\1/p' | awk '!seen[$0]++'
)
log "gsettings replay: ${#gs_cmds[@]} command(s) parsed from log"
# Parsing is coupled to end-4's v() echo format: if upstream changes
# it, the replay silently does nothing and the original dconf failure
# returns - so distinguish "none ran" from "none recognized".
if ((${#gs_cmds[@]} == 0)); then
gs_raw=$(sed -n "$((log_mark + 1)),\$p" "$LOG" | sed $'s/\x1b\\[[0-9;]*m//g' |
grep -c 'gsettings set' || true)
if ((gs_raw > 0)); then
warn "setup log mentions gsettings ${gs_raw}x but none matched the replay parser (format drift?) - see $LOG"
fi
fi
for gs_line in "${gs_cmds[@]}"; do
[[ "$gs_line" =~ ^gsettings\ set\ ([^[:space:]]+)\ ([^[:space:]]+)\ (.+)$ ]] || continue
gs_schema=${BASH_REMATCH[1]}
gs_key=${BASH_REMATCH[2]}
gs_val=${BASH_REMATCH[3]}
# env -i: root's env is exactly what broke dconf; rebuild it for
# the target user with only what gsettings/dbus need.
if arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \
XDG_CONFIG_HOME="${home_in_target}/.config" \
dbus-run-session -- \
gsettings set "$gs_schema" "$gs_key" "$gs_val" >>"$LOG" 2>&1; then
gs_got=$(arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \
XDG_CONFIG_HOME="${home_in_target}/.config" \
dbus-run-session -- \
gsettings get "$gs_schema" "$gs_key" 2>>"$LOG") || gs_got=""
# Normalize the GVariant read-back: strings come back quoted
# ('...' normally, "..." when the value contains an apostrophe)
# with \n \\ \' \" escapes, bools/ints bare ('true' vs true).
gs_norm=$(gvariant_unescape "$gs_got")
if [[ "$gs_norm" == "$gs_val" ]]; then
log "gsettings persisted: ${gs_schema} ${gs_key} = ${gs_val}"
else
warn "gsettings ${gs_schema} ${gs_key} did not persist (read back: ${gs_got:-nothing}, see $LOG)."
fi
else
warn "Could not replay gsettings ${gs_schema} ${gs_key} (see $LOG)."
fi
done
fi
# Everything end-4 answered "i" to. systemctl/gsettings are the lines known
# to be unusable in this chroot; anything else it had to skip is worth
# telling the user about.
@@ -902,8 +1068,24 @@ install_end4_dots() {
if $configs_ok; then
[[ -x "${TARGET}/usr/bin/hyprctl" && -x "${TARGET}/usr/bin/quickshell" ]] \
|| configs_ok=false
[[ -x "${TARGET}${home_in_target}/.local/state/quickshell/.venv/bin/python" ]] \
|| configs_ok=false
# uv writes bin/python as an absolute symlink into the chroot
# (/home/.../uv/python/...). Tested verbatim from the live medium
# that link dangles - /home/$USERNAME exists only under $TARGET -
# so walk the chain here and re-root absolute hops under $TARGET
# (relative hops stay inside the venv and need no repointing).
local venv_probe="${TARGET}${home_in_target}/.local/state/quickshell/.venv/bin/python"
local venv_link hops=0
while [[ -L "$venv_probe" && $hops -lt 10 ]]; do
venv_link=$(readlink "$venv_probe")
if [[ "$venv_link" == /* ]]; then
venv_probe="${TARGET}${venv_link}"
else
venv_probe="$(dirname "$venv_probe")/$venv_link"
fi
hops=$((hops + 1))
done
[[ -x "$venv_probe" ]] || configs_ok=false
fi
if [[ -d /etc/skel/.config/hypr/custom ]]; then
@@ -1614,21 +1796,7 @@ EOF
ok "Login set: manager=${LOGIN_MANAGER}, autologin=${AUTOLOGIN}."
}
# ── Build stamp ───────────────────────────────────────────────────────────────
install_motd() {
# Only stamped media carry a stamp; without one (local non-git build)
# the installed system keeps whatever motd the target packages shipped.
local stamp="/etc/alvin-build-stamp"
if [[ ! -f "$stamp" ]]; then
log "motd: no build stamp on this medium - target motd left as-is"
return 0
fi
cp /etc/motd "${TARGET}/etc/motd" 2>/dev/null || true
cat "$stamp" >> "${TARGET}/etc/motd"
log "motd: appended build stamp: $(tr -d '\n' < "$stamp")"
ok "Build stamp appended to the installed /etc/motd."
}
# ── Cleanup ───────────────────────────────────────────────────────────────────
install_cleanup() {
info "Cleaning up..."
@@ -1637,10 +1805,21 @@ install_cleanup() {
log "Installation complete."
ok "Installation complete!"
echo ""
# Report what actually happened, not what was asked for: the enable step
# only warns when it cannot work (openssh missing / systemctl failed).
local ssh_state
if [[ "$SSHD" != "true" ]]; then
ssh_state="disabled"
elif [[ "$SSHD_APPLIED" == "true" ]]; then
ssh_state="enabled (starts at boot)"
else
ssh_state="NOT enabled - see ${LOG}"
fi
gum style --foreground 10 --align center \
"Arch Linux installed successfully." \
"" \
"Login manager: ${LOGIN_MANAGER} Autologin: ${AUTOLOGIN:-disabled}" \
"SSH server: ${ssh_state}" \
"TPM2 auto-unlock: ${TPM_STATUS:-n/a} unl0kr: ${UNL0KR}"
if [[ -n "${TPM_DISCLAIMER}" ]]; then
echo ""
@@ -1671,6 +1850,7 @@ main() {
screen_timezone
screen_locale
screen_login_manager
screen_sshd
screen_summary
install_partition
@@ -1683,6 +1863,7 @@ main() {
install_locale
install_hostname
install_network
install_sshd
install_user
install_git_ssh
install_end4_dots
@@ -1692,7 +1873,6 @@ main() {
install_unl0kr
install_login_manager
install_autologin
install_motd
install_cleanup
}