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:
@@ -54,13 +54,50 @@ install them into the target without the network.
|
|||||||
| `bootstrap_packages` | Minimal build-time packages (`base`, `arch-install-scripts`) |
|
| `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/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/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/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/` |
|
| `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
|
## 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` 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.
|
- `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.
|
- 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).
|
- 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.
|
- `packages.x86_64` is sorted alphabetically. Keep it sorted when adding/removing entries.
|
||||||
- `airootfs/opt/localrepo/` is a build artifact (gitignored) — never commit it.
|
- `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).
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ AlvinArch/
|
|||||||
6. [System Services & Networking](#system-services--networking)
|
6. [System Services & Networking](#system-services--networking)
|
||||||
7. [Local Repository (localrepo/)](#local-repository-localrepo)
|
7. [Local Repository (localrepo/)](#local-repository-localrepo)
|
||||||
8. [Boot Configuration](#boot-configuration)
|
8. [Boot Configuration](#boot-configuration)
|
||||||
9. [Build & Test](#build--test)
|
9. [Automated Installer (alvin-install)](#automated-installer-alvin-install)
|
||||||
10. [Planned Additions](#planned-additions)
|
10. [Build & Test](#build--test)
|
||||||
11. [Troubleshooting](#troubleshooting)
|
11. [Planned Additions](#planned-additions)
|
||||||
|
12. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ AlvinArch/
|
|||||||
### Key Features
|
### Key Features
|
||||||
|
|
||||||
- **Hyprland + end-4** — Wayland compositor with illogical-impulse dotfiles (quickshell, bibata cursors, microtex)
|
- **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
|
- **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
|
- **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
|
- **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
|
│ │ └── mkinitcpio.conf.d/ # archiso initramfs config
|
||||||
│ ├── usr/
|
│ ├── usr/
|
||||||
│ │ ├── lib/os-release # usr-merge os-release
|
│ │ ├── 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)
|
│ │ ├── local/share/livecd-sound/ # ALSA config (asound.conf.in)
|
||||||
│ │ └── share/pacman/keyrings/ # CachyOS GPG keyring + trust data
|
│ │ └── share/pacman/keyrings/ # CachyOS GPG keyring + trust data
|
||||||
│ └── root/
|
│ └── 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
|
## Build & Test
|
||||||
|
|
||||||
### Building the ISO
|
### Building the ISO
|
||||||
|
|||||||
@@ -28,9 +28,13 @@ install -m 0644 /root/.live-pacman.conf /etc/pacman.conf
|
|||||||
rm -f /root/.live-pacman.conf
|
rm -f /root/.live-pacman.conf
|
||||||
|
|
||||||
# Build stamp staged by profiledef.sh (absent for non-git, non-CI builds):
|
# 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
|
if [[ -f /root/.iso-build-stamp ]]; then
|
||||||
install -m 0644 /root/.iso-build-stamp /etc/alvin-build-stamp
|
install -m 0644 /root/.iso-build-stamp /etc/alvin-build-stamp
|
||||||
|
cat /etc/alvin-build-stamp >> /etc/motd
|
||||||
rm -f /root/.iso-build-stamp
|
rm -f /root/.iso-build-stamp
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ UNL0KR="false"
|
|||||||
TPM_STATUS=""
|
TPM_STATUS=""
|
||||||
LOGIN_MANAGER="none"
|
LOGIN_MANAGER="none"
|
||||||
AUTOLOGIN=""
|
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=""
|
TPM_DISCLAIMER=""
|
||||||
TARGET_DB_SYNCED="false"
|
TARGET_DB_SYNCED="false"
|
||||||
|
|
||||||
@@ -76,6 +81,40 @@ target_owner() {
|
|||||||
arch-chroot "$TARGET" sh -c "id -u '$USERNAME' && id -g '$USERNAME'" | paste -sd:
|
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
|
# 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
|
# 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
|
# 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"
|
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() {
|
screen_summary() {
|
||||||
clear
|
clear
|
||||||
header "Installation Summary"
|
header "Installation Summary"
|
||||||
@@ -398,6 +466,7 @@ screen_summary() {
|
|||||||
"Locale: ${LOCALE}" \
|
"Locale: ${LOCALE}" \
|
||||||
"Timezone: ${TIMEZONE}" \
|
"Timezone: ${TIMEZONE}" \
|
||||||
"Login mgr: ${LOGIN_MANAGER}" \
|
"Login mgr: ${LOGIN_MANAGER}" \
|
||||||
|
"SSH server: $([[ "$SSHD" == "true" ]] && echo enabled || echo skipped)" \
|
||||||
"SSH keys: ${ssh_line}" \
|
"SSH keys: ${ssh_line}" \
|
||||||
"Git: ${git_line}"
|
"Git: ${git_line}"
|
||||||
|
|
||||||
@@ -528,6 +597,17 @@ install_pacstrap() {
|
|||||||
$found || pkgs+=("$p")
|
$found || pkgs+=("$p")
|
||||||
done
|
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)..."
|
info "Installing ${#pkgs[@]} packages (this may take a while)..."
|
||||||
log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped"
|
log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped"
|
||||||
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm 2>>"$LOG"
|
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm 2>>"$LOG"
|
||||||
@@ -607,6 +687,25 @@ install_network() {
|
|||||||
ok "NetworkManager enabled."
|
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() {
|
install_user() {
|
||||||
info "Creating user..."
|
info "Creating user..."
|
||||||
|
|
||||||
@@ -832,6 +931,73 @@ install_end4_dots() {
|
|||||||
fi
|
fi
|
||||||
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
|
# 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
|
# to be unusable in this chroot; anything else it had to skip is worth
|
||||||
# telling the user about.
|
# telling the user about.
|
||||||
@@ -902,8 +1068,24 @@ install_end4_dots() {
|
|||||||
if $configs_ok; then
|
if $configs_ok; then
|
||||||
[[ -x "${TARGET}/usr/bin/hyprctl" && -x "${TARGET}/usr/bin/quickshell" ]] \
|
[[ -x "${TARGET}/usr/bin/hyprctl" && -x "${TARGET}/usr/bin/quickshell" ]] \
|
||||||
|| configs_ok=false
|
|| 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
|
fi
|
||||||
|
|
||||||
if [[ -d /etc/skel/.config/hypr/custom ]]; then
|
if [[ -d /etc/skel/.config/hypr/custom ]]; then
|
||||||
@@ -1614,21 +1796,7 @@ EOF
|
|||||||
ok "Login set: manager=${LOGIN_MANAGER}, autologin=${AUTOLOGIN}."
|
ok "Login set: manager=${LOGIN_MANAGER}, autologin=${AUTOLOGIN}."
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Build stamp ───────────────────────────────────────────────────────────────
|
# ── Cleanup ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
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."
|
|
||||||
}
|
|
||||||
|
|
||||||
install_cleanup() {
|
install_cleanup() {
|
||||||
info "Cleaning up..."
|
info "Cleaning up..."
|
||||||
@@ -1637,10 +1805,21 @@ install_cleanup() {
|
|||||||
log "Installation complete."
|
log "Installation complete."
|
||||||
ok "Installation complete!"
|
ok "Installation complete!"
|
||||||
echo ""
|
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 \
|
gum style --foreground 10 --align center \
|
||||||
"Arch Linux installed successfully." \
|
"Arch Linux installed successfully." \
|
||||||
"" \
|
"" \
|
||||||
"Login manager: ${LOGIN_MANAGER} Autologin: ${AUTOLOGIN:-disabled}" \
|
"Login manager: ${LOGIN_MANAGER} Autologin: ${AUTOLOGIN:-disabled}" \
|
||||||
|
"SSH server: ${ssh_state}" \
|
||||||
"TPM2 auto-unlock: ${TPM_STATUS:-n/a} unl0kr: ${UNL0KR}"
|
"TPM2 auto-unlock: ${TPM_STATUS:-n/a} unl0kr: ${UNL0KR}"
|
||||||
if [[ -n "${TPM_DISCLAIMER}" ]]; then
|
if [[ -n "${TPM_DISCLAIMER}" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
@@ -1671,6 +1850,7 @@ main() {
|
|||||||
screen_timezone
|
screen_timezone
|
||||||
screen_locale
|
screen_locale
|
||||||
screen_login_manager
|
screen_login_manager
|
||||||
|
screen_sshd
|
||||||
screen_summary
|
screen_summary
|
||||||
|
|
||||||
install_partition
|
install_partition
|
||||||
@@ -1683,6 +1863,7 @@ main() {
|
|||||||
install_locale
|
install_locale
|
||||||
install_hostname
|
install_hostname
|
||||||
install_network
|
install_network
|
||||||
|
install_sshd
|
||||||
install_user
|
install_user
|
||||||
install_git_ssh
|
install_git_ssh
|
||||||
install_end4_dots
|
install_end4_dots
|
||||||
@@ -1692,7 +1873,6 @@ main() {
|
|||||||
install_unl0kr
|
install_unl0kr
|
||||||
install_login_manager
|
install_login_manager
|
||||||
install_autologin
|
install_autologin
|
||||||
install_motd
|
|
||||||
install_cleanup
|
install_cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -27,8 +27,9 @@ file_permissions=(
|
|||||||
|
|
||||||
# ── Build stamp (staged file, never committed) ────────────────────────────────
|
# ── Build stamp (staged file, never committed) ────────────────────────────────
|
||||||
# airootfs/root/.iso-build-stamp lands at /root/.iso-build-stamp on the ISO;
|
# 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
|
# customize_airootfs.sh keeps it as /etc/alvin-build-stamp and appends it to
|
||||||
# appends it to the installed system's /etc/motd. Only stamp builds from a git
|
# 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.
|
# checkout or CI — a random source-tarball build would record a bogus commit.
|
||||||
# Sourced by mkarchiso under `set -eu`, hence every expansion is defaulted.
|
# Sourced by mkarchiso under `set -eu`, hence every expansion is defaulted.
|
||||||
_stamp_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
_stamp_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|||||||
Reference in New Issue
Block a user