feat(installer): sshd opt-in, gsettings replay, live-medium motd stamp
Tests / tests (push) Successful in 2m50s
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:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user