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
+40 -3
View File
@@ -54,13 +54,50 @@ install them into the target without the network.
| `bootstrap_packages` | Minimal build-time packages (`base`, `arch-install-scripts`) |
| `airootfs/root/customize_airootfs.sh` | Runs inside chroot during build: keyrings, skel dotfiles (incl. end4-pC), qsConfig patch, end-4 `install-deps`, package harvest, uv/font pre-warm |
| `airootfs/root/.live-pacman.conf` | `/etc/pacman.conf` of the built image (adds `[local-aur]` = `file:///opt/localrepo`) |
| `airootfs/usr/local/bin/alvin-install` | Installer: partitions, pacstrap, LUKS (systemd initramfs **or** experimental unl0kr + optional TPM2 unlock), user, git identity + GitHub SSH keys, login manager (none/greetd/sddm/gdm) + autologin, end-4 dotfiles (`--skip-alldeps`), build-stamp motd, boot |
| `airootfs/usr/local/bin/alvin-install` | Installer: partitions, pacstrap, LUKS (systemd initramfs **or** experimental unl0kr + optional TPM2 unlock), user, git identity + GitHub SSH keys, login manager (none/greetd/sddm/gdm) + autologin, optional sshd (default on), end-4 dotfiles (`--skip-alldeps`), boot |
| `localrepo/build-end4-deps.sh` | Non-interactive AUR builds for end-4 dependencies |
| `localrepo/update.sh` | Regenerates localrepo DB, syncs names to packages.x86_64, syncs `airootfs/opt/localrepo/` |
## alvin-install (installer inside the ISO)
Source: `airootfs/usr/local/bin/alvin-install` (~1,880 lines, **no `.sh` extension**).
The airootfs overlay ships it as `/usr/local/bin/alvin-install` (mode `0755` forced in
`profiledef.sh` `file_permissions`), and `/etc/motd` advertises it on the live medium.
- **Invoke**: as root from the live session. Hard requirements: `gum` and UEFI —
`check_prereqs` exits if `/sys/firmware/efi` is missing. Log: `/var/log/alvin-install.log`
(copied into the target at the end; `ERR` trap prints the failing line).
- **Structure**: `main()` runs 16 `screen_*` TUI functions (collect answers: welcome, keymap,
network, disk, luks, unl0kr, filesystem, username, password, hostname, git, timezone,
locale, login manager, sshd, summary) then a fixed order of `install_*` steps
(partition → LUKS → format/mount → pacstrap → fstab → repos → timezone → locale →
hostname → network → sshd → user → git/SSH → end-4 dots → mkinitcpio → bootloader →
TPM/unl0kr → login manager → autologin → cleanup). Screens and install steps are
independent — edit one without
touching the other. An `EXIT` trap unmounts `$TARGET` and closes `cryptroot` on any exit.
- **Package list**: `install_pacstrap` builds the *installed* system's list from the medium's
`/run/archiso/bootmnt/arch/pkglist.x86_64.txt` (i.e. `pacman -Q` of the live ISO), so
`packages.x86_64` changes flow into installed systems. It drops `PKGS_EXCLUDE`
(`archiso mkinitcpio-archiso mkinitcpio-nfs-utils syslinux memtest86+`) plus names not
resolvable in the live repos, then always appends hardcoded essentials
(`base linux linux-firmware linux-cachyos networkmanager grub efibootmgr sudo git …`).
- **CI never lints it**: the shellcheck step globs only `*.sh`. Check manually after edits:
`shellcheck -x -s bash airootfs/usr/local/bin/alvin-install`
- end-4 quirks (`--skip-alldeps`, `yes i`, end4-pC re-copy) are in [Gotchas](#gotchas) below.
## Verify a single change locally (mirrors CI)
```bash
shellcheck -x -s bash <script>.sh # plus the alvin-install command above
yamllint -d "{extends: relaxed, rules: {document-start: disable, line-length: disable, new-line-at-end-of-file: disable}}" <file>.yaml
```
`packages.x86_64` syntax: one package per line, `[a-zA-Z0-9._@+\-]+` only, no trailing
whitespace.
## Gotchas
- The profile root is the **repo root** (not a `releng/` subdirectory). The CI build-iso workflow uses `mkarchiso -v -w work -o out/ releng` but this is a naming convention — the actual profile files live at the repo root.
- The profile root is the **repo root** (not a `releng/` subdirectory). CI builds with `mkarchiso -v -w "$PWD/work" -o "$PWD/out" -- "$PWD"` (`.gitea/workflows/build-iso.yaml`), and the local build command above does the same — never pass a `releng` path.
- `customize_airootfs.sh` clones git repos (oh-my-zsh, lazy.nvim, plugins, end-4 dots) into `/etc/skel` and `/opt` during the ISO build. These require network access at build time but make the live ISO work offline.
- `customize_airootfs.sh` runs `./setup install-deps -f -s --skip-plasmaintg` as the throwaway user `ii-builder` (end-4's scripts refuse to run as root) and aborts the build if it fails. `--skip-plasmaintg` is deliberate: that optional Firefox widget pulls 410 packages / 526 MiB of KDE.
- The `[local-aur]` repo path in `pacman.conf` (`file:///home/alvin/alvinarch/localrepo`) is for local builds only. The CI does not use this path.
@@ -72,4 +109,4 @@ install them into the target without the network.
- End-4 metapackages are built from `localrepo/end4-pkgbuilds/` (quickshell-git, bibata-modern-classic-bin, microtex-git).
- `packages.x86_64` is sorted alphabetically. Keep it sorted when adding/removing entries.
- `airootfs/opt/localrepo/` is a build artifact (gitignored) — never commit it.
- **Build stamp flow**: `profiledef.sh` (sourced by mkarchiso) stages the gitignored `airootfs/root/.iso-build-stamp` when a `.git` dir exists **or** `CI`/`GITEA_ACTIONS` is set — line reads `AlvinArch build <date> | commit <date> | <4-char hash>` (falls back to `commit unknown` if `git -c safe.directory` can't read the repo; without `.git`/CI no file is staged). `customize_airootfs.sh` installs it as `/etc/alvin-build-stamp` on the live medium, and `alvin-install`'s `install_motd` copies the live `/etc/motd` banner to the target and appends the stamp. If the medium has no stamp, the installed system gets no motd from us.
- **Build stamp flow**: `profiledef.sh` (sourced by mkarchiso) stages the gitignored `airootfs/root/.iso-build-stamp` when a `.git` dir exists **or** `CI`/`GITEA_ACTIONS` is set — line reads `AlvinArch build <date> | commit <date> | <4-char hash>` (falls back to `commit unknown` if `git -c safe.directory` can't read the repo; without `.git`/CI no file is staged). `customize_airootfs.sh` keeps it as `/etc/alvin-build-stamp` on the live medium **and appends it to the live `/etc/motd`**, so the booted ISO identifies its build. The installed system's motd is never touched by us — `alvin-install` has no motd step (an earlier `install_motd` that copied the banner + stamp into the target was removed on purpose).
+66 -5
View File
@@ -37,9 +37,10 @@ AlvinArch/
6. [System Services & Networking](#system-services--networking)
7. [Local Repository (localrepo/)](#local-repository-localrepo)
8. [Boot Configuration](#boot-configuration)
9. [Build & Test](#build--test)
10. [Planned Additions](#planned-additions)
11. [Troubleshooting](#troubleshooting)
9. [Automated Installer (alvin-install)](#automated-installer-alvin-install)
10. [Build & Test](#build--test)
11. [Planned Additions](#planned-additions)
12. [Troubleshooting](#troubleshooting)
---
@@ -50,7 +51,7 @@ AlvinArch/
### Key Features
- **Hyprland + end-4** — Wayland compositor with illogical-impulse dotfiles (quickshell, bibata cursors, microtex)
- **Automated installer (`alvin-install`)** — gum TUI: LUKS with systemd initramfs or experimental touchscreen unl0kr, optional TPM2 auto-unlock, GitHub SSH key import, login manager choice (none/greetd/sddm/gdm), autologin, and a build stamp in `/etc/motd`
- **Automated installer (`alvin-install`)** — gum TUI: LUKS with systemd initramfs or experimental touchscreen unl0kr, optional TPM2 auto-unlock, GitHub SSH key import, login manager choice (none/greetd/sddm/gdm), optional sshd (on by default), autologin; the build stamp identifies the live ISO via its `/etc/motd`
- **LazyVim** — Pre-configured Neovim with lazy.nvim and curated plugins
- **Fish + Zsh + Starship** — Dual shell setup with oh-my-zsh and a cross-shell prompt
- **Local AUR repo** — Pre-built packages (end-4 deps, wallust, grub-customizer, etc.) that aren't in official repos
@@ -138,7 +139,7 @@ AlvinArch/
│ │ └── mkinitcpio.conf.d/ # archiso initramfs config
│ ├── usr/
│ │ ├── lib/os-release # usr-merge os-release
│ │ ├── local/bin/ # choose-mirror, Installation_guide, livecd-sound
│ │ ├── local/bin/ # alvin-install, choose-mirror, Installation_guide, livecd-sound
│ │ ├── local/share/livecd-sound/ # ALSA config (asound.conf.in)
│ │ └── share/pacman/keyrings/ # CachyOS GPG keyring + trust data
│ └── root/
@@ -305,6 +306,66 @@ Legacy BIOS boot with custom splash screen. Includes PXE network boot config.
---
## Automated Installer (alvin-install)
The ISO ships an interactive installer at `/usr/local/bin/alvin-install` (source:
`airootfs/usr/local/bin/alvin-install`, ~1,880 lines of bash; the live `/etc/motd`
advertises it). It partitions, encrypts, installs, and fully configures a system —
including the end-4 dotfiles — without network access to this repo.
### Running it
```bash
alvin-install # as root on the live medium (root autologins on tty1)
```
Requirements: **UEFI boot** (it exits if `/sys/firmware/efi` is missing — BIOS/CSM
is not supported by the installer) and `gum`. A network connection is required and
verified by pinging `archlinux.org` before any disk is touched; the network screen
can open `nmtui` for WiFi. Every step is logged to `/var/log/alvin-install.log`,
which is copied into the installed system.
### What it asks (gum TUI screens)
Keymap → network → target disk (the live USB is hidden from the list) → LUKS
encryption (optional; **the LUKS passphrase is the same as your login password**)
→ experimental unl0kr touchscreen unlock → filesystem (`ext4`/`btrfs`) →
username/password/hostname → git identity + GitHub username (both optional) →
timezone/locale → login manager → SSH server (enabled by default) → summary.
### What it does
1. GPT partitioning (1 GiB ESP) on the chosen disk, optional LUKS2 on the rest,
format + mount at `/mnt`
2. `pacstrap` with the package list derived from the medium's
`pkglist.x86_64.txt` (`pacman -Q` of the live ISO) — `archiso`, `syslinux`,
`memtest86+` etc. are dropped, hardcoded essentials (kernel, GRUB,
NetworkManager, cryptsetup, microcode) always appended
3. User account, sudo, git config, GitHub SSH keys (`ssh-import-id`, falling
back to `github.com/<user>.keys`)
4. end-4 dotfiles from the staged `/opt/dots-hyprland` via
`./setup install -f -s --skip-alldeps`
5. Initramfs in one of two modes: **Mode A** — systemd initramfs + `sd-encrypt`
(default); **Mode B** — busybox hooks + unl0kr touchscreen unlock
(experimental, mutually exclusive with TPM2; falls back to Mode A on failure)
6. Optional **TPM2 auto-unlock** (`systemd-cryptenroll`, PCR 0+7; passphrase
slot kept as fallback; warns if Secure Boot is off)
7. GRUB (UEFI, boot entry `ALVIN`), login manager — `none` (plain tty, Hyprland
autostarts), `greetd`+tuigreet, `sddm`, or `gdm` — falls back to `none` only
if the **package** can't be resolved/installed; a failed `systemctl enable`
keeps `getty@tty1` as the login path (the summary still reports the chosen
manager). Optional autologin (forced off when TPM2 is enrolled), and sshd
enabled at boot when selected (default: on) — Arch's stock config allows
**password login with the same password as LUKS**, so only accept the default
on networks you trust (or harden `sshd_config` afterwards). The final banner
reports whether the enable actually succeeded, not just what was selected
On failure an `EXIT` trap unmounts `/mnt` and closes the `cryptroot` mapping, so
re-running is safe. If the end-4 step reports "incomplete", see the repository's
`AGENTS.md` Gotchas for the chroot/systemd limitations that cause it.
---
## Build & Test
### Building the ISO
+5 -1
View File
@@ -28,9 +28,13 @@ install -m 0644 /root/.live-pacman.conf /etc/pacman.conf
rm -f /root/.live-pacman.conf
# Build stamp staged by profiledef.sh (absent for non-git, non-CI builds):
# keep it where alvin-install looks for it, drop the staging copy.
# keep the stamp file itself as a reference, drop the staging copy, and
# stamp the live medium's motd so the booted ISO identifies its build (the
# installed system keeps the stock package motd - alvin-install never
# writes /etc/motd).
if [[ -f /root/.iso-build-stamp ]]; then
install -m 0644 /root/.iso-build-stamp /etc/alvin-build-stamp
cat /etc/alvin-build-stamp >> /etc/motd
rm -f /root/.iso-build-stamp
fi
+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
}
+3 -2
View File
@@ -27,8 +27,9 @@ file_permissions=(
# ── Build stamp (staged file, never committed) ────────────────────────────────
# airootfs/root/.iso-build-stamp lands at /root/.iso-build-stamp on the ISO;
# customize_airootfs.sh installs it as /etc/alvin-build-stamp, and alvin-install
# appends it to the installed system's /etc/motd. Only stamp builds from a git
# customize_airootfs.sh keeps it as /etc/alvin-build-stamp and appends it to
# the live medium's /etc/motd (the installed system's motd is left alone).
# Only stamp builds from a git
# checkout or CI — a random source-tarball build would record a bogus commit.
# Sourced by mkarchiso under `set -eu`, hence every expansion is defaulted.
_stamp_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"