Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6f00018d4
|
||
|
|
e377f9313a
|
||
|
|
00c89f23f0
|
||
|
|
3b1e7d1982
|
||
|
|
6a708c0303
|
||
|
|
0cdaa6f82d
|
||
|
|
5735859179
|
@@ -53,7 +53,7 @@ install them into the target without the network.
|
|||||||
| `pacman.conf` | Repos: cachyos → core → extra → multilib → chaotic-aur → local-aur |
|
| `pacman.conf` | Repos: cachyos → core → extra → multilib → chaotic-aur → local-aur |
|
||||||
| `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`); also what pacstrap resolves the target from, so `[cachyos]` must stay ahead of `[core]` like in `pacman.conf` |
|
||||||
| `airootfs/usr/local/bin/alvin-install` | Installer: partitions, pacstrap, LUKS (systemd initramfs **or** experimental unl0kr + optional TPM2 unlock), user (fish shell), git identity + GitHub SSH keys, login manager (none/greetd/sddm/gdm) + autologin, optional sshd (default on), end-4 dotfiles (`--skip-alldeps`), boot |
|
| `airootfs/usr/local/bin/alvin-install` | Installer: partitions, pacstrap, LUKS (systemd initramfs **or** experimental unl0kr + optional TPM2 unlock), user (fish shell), 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/` |
|
||||||
@@ -74,7 +74,7 @@ The airootfs overlay ships it as `/usr/local/bin/alvin-install` (mode `0755` for
|
|||||||
locale, login manager, sshd, summary) then a fixed order of `install_*` steps
|
locale, login manager, sshd, summary) then a fixed order of `install_*` steps
|
||||||
(partition → LUKS → format/mount → pacstrap → fstab → repos → timezone → locale →
|
(partition → LUKS → format/mount → pacstrap → fstab → repos → timezone → locale →
|
||||||
hostname → network → sshd → user → git/SSH → end-4 dots → mkinitcpio → bootloader →
|
hostname → network → sshd → user → git/SSH → end-4 dots → mkinitcpio → bootloader →
|
||||||
TPM/unl0kr → login manager → autologin → cleanup). Screens and install steps are
|
TPM/unl0kr → login manager (none if unl0kr succeeded; with unl0kr selected the login screen only asks for a fallback) → autologin → update (reflector + keyrings + `pacman -Syu`, warn-only) → cleanup, which offers an optional `arch-chroot` shell into the target before unmounting; the `EXIT` trap offers the same shell when a step after pacstrap fails or the install is interrupted (INT/TERM re-exit with 128+N so signals count as failure). Screens and install steps are
|
||||||
independent — edit one without
|
independent — edit one without
|
||||||
touching the other. An `EXIT` trap unmounts `$TARGET` and closes `cryptroot` on any exit.
|
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
|
- **Package list**: `install_pacstrap` builds the *installed* system's list from the medium's
|
||||||
@@ -97,6 +97,16 @@ yamllint -d "{extends: relaxed, rules: {document-start: disable, line-length: di
|
|||||||
`packages.x86_64` syntax: one package per line, `[a-zA-Z0-9._@+\-]+` only, no trailing
|
`packages.x86_64` syntax: one package per line, `[a-zA-Z0-9._@+\-]+` only, no trailing
|
||||||
whitespace.
|
whitespace.
|
||||||
|
|
||||||
|
A **pre-commit hook** (`githooks/pre-commit`) runs the same ShellCheck pass on every
|
||||||
|
commit, so CI failures surface before you push. Enable it once per clone:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath githooks
|
||||||
|
```
|
||||||
|
|
||||||
|
It skips linting (with a warning) when `shellcheck` is missing rather than blocking the
|
||||||
|
commit; CI remains the enforcer. Bypass it with `git commit --no-verify`.
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
- 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.
|
- 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.
|
||||||
@@ -106,13 +116,14 @@ whitespace.
|
|||||||
- The **image** ships its own `[local-aur]` at `file:///opt/localrepo` (see `.live-pacman.conf`). It holds `localrepo/` (copied by CI/`update.sh`) plus everything `install-deps` built. pacstrap runs without `-P`, so the target keeps the stock `/etc/pacman.conf`; `alvin-install` strips `[local-aur]` from it anyway as a safety net, because `/opt/localrepo` exists only on the medium.
|
- The **image** ships its own `[local-aur]` at `file:///opt/localrepo` (see `.live-pacman.conf`). It holds `localrepo/` (copied by CI/`update.sh`) plus everything `install-deps` built. pacstrap runs without `-P`, so the target keeps the stock `/etc/pacman.conf`; `alvin-install` strips `[local-aur]` from it anyway as a safety net, because `/opt/localrepo` exists only on the medium.
|
||||||
- `alvin-install` runs end-4 with `--skip-alldeps` — dependencies must come from the ISO. It feeds `yes i` to end-4's failure prompt: inside the installer chroot the medium's `/run` is bind-mounted in, so end-4's `systemctl --machine=…@.host --user enable … --now` lines can never work (and `--now` would start services on the live medium). `install_end4_dots` re-applies the enable-only part (bluetooth, ydotool, i2c module) outside the chroot and reports every step end-4 had to skip. If the end-4 step reports "incomplete", check that `hyprctl`/`quickshell`/the venv were resolvable (harvest) before re-running anything.
|
- `alvin-install` runs end-4 with `--skip-alldeps` — dependencies must come from the ISO. It feeds `yes i` to end-4's failure prompt: inside the installer chroot the medium's `/run` is bind-mounted in, so end-4's `systemctl --machine=…@.host --user enable … --now` lines can never work (and `--now` would start services on the live medium). `install_end4_dots` re-applies the enable-only part (bluetooth, ydotool, i2c module) outside the chroot and reports every step end-4 had to skip. If the end-4 step reports "incomplete", check that `hyprctl`/`quickshell`/the venv were resolvable (harvest) before re-running anything.
|
||||||
- `/etc/skel/.cache/uv` + `/etc/skel/.local/share/uv/python` are pre-warmed by the build (~460 MB: CPython 3.12 and the pinned wheel cache) so end-4's `uv venv`/`uv pip install` at install time needs no network.
|
- `/etc/skel/.cache/uv` + `/etc/skel/.local/share/uv/python` are pre-warmed by the build (~460 MB: CPython 3.12 and the pinned wheel cache) so end-4's `uv venv`/`uv pip install` at install time needs no network.
|
||||||
- **end4-pC** (`pctrade/end4-pC`) is the default quickshell config: cloned into `/etc/skel/.config/quickshell/end4-pC` at build time, selected by patching `hl.env("qsConfig", "ii")` → `"end4-pC"` in the **staged** `/opt/dots-hyprland/dots/.config/hypr/hyprland/variables.lua` (there is no `hypr/hyprland/` in this repo — `./setup install` copies it to the installed system). `alvin-install` re-copies end4-pC into the user's home *after* `./setup install`, because end-4's installer `rsync --delete`s all of `~/.config/quickshell`.
|
- **end4-pC** (`pctrade/end4-pC`) is the default quickshell config: cloned into `/etc/skel/.config/quickshell/end4-pC` at build time, selected by patching `hl.env("qsConfig", "ii")` → `"end4-pC"` in the **staged** `/opt/dots-hyprland/dots/.config/hypr/hyprland/variables.lua` (there is no `hypr/hyprland/` in this repo — `./setup install` copies it to the installed system). `alvin-install` re-copies end4-pC into the user's home *after* `./setup install`, because end-4's installer `rsync --delete`s all of `~/.config/quickshell`. The build also rewrites the fork's hard-coded `QUICKSHELL_CONFIG_NAME="ii"` in its colour/wallpaper/kvantum scripts to `end4-pC` (otherwise they act on `~/.config/quickshell/ii`), and overwrites its `assets/images/default_wallpaper.png` (applied by `FirstRunExperience.qml` on first login) with `airootfs/etc/skel/Pictures/Wallpapers/Dynamic-Wallpapers/Dark/japan-street-Dark.png`.
|
||||||
- `airootfs/etc/skel/.config/illogical-impulse/presets/` carries the user's presets (`Laptop.json`, `Personal-Performance.json`, `Personal-Performance.zip`). They embed `/home/alvin/...` paths, so wallpapers/screenshots need fixing up after install.
|
- `airootfs/etc/skel/.config/illogical-impulse/presets/` carries the user's presets (`Laptop.json`, `Personal-Performance.json`, `Personal-Performance.zip`). They embed `/home/alvin/...` paths, so wallpapers/screenshots need fixing up after install.
|
||||||
- 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).
|
||||||
- **quickshell pin / Qt 6.12**: end-4 pins quickshell to `41651d7` (Sep 25), which predates upstream fix `5d5d498` (Oct 3, *"use MOC includes instead of opaque pointer definitions — … Qt 6.12 compatibility"*). Arch shipped qt6 6.12.0 on Oct 8 and the old pin died with `Meta Types must be fully defined` in the moc build of quickshell-hyprland-ipc (failed CI run 137). `customize_airootfs.sh` therefore rewrites end-4's `_commit` to `11ca60be` after cloning — but **only** while it still matches `^_commit='41651d7`, so once end-4 bumps their own pin the guard goes inert and theirs wins. `localrepo/check-quickshell-pin.sh` (weekly `pin-check.yaml` job) fails CI if either pin goes stale or loses the fix; it warns instead of failing when end-4's known-stale pin is covered by the override.
|
- **quickshell pin / Qt 6.12**: end-4 pins quickshell to `41651d7` (Sep 25), which predates upstream fix `5d5d498` (Oct 3, *"use MOC includes instead of opaque pointer definitions — … Qt 6.12 compatibility"*). Arch shipped qt6 6.12.0 on Oct 8 and the old pin died with `Meta Types must be fully defined` in the moc build of quickshell-hyprland-ipc (failed CI run 137). `customize_airootfs.sh` therefore rewrites end-4's `_commit` to `11ca60be` after cloning — but **only** while it still matches `^_commit='41651d7`, so once end-4 bumps their own pin the guard goes inert and theirs wins. `localrepo/check-quickshell-pin.sh` (weekly `pin-check.yaml` job) fails CI if either pin goes stale or loses the fix; it warns instead of failing when end-4's known-stale pin is covered by the override.
|
||||||
- **MicroTeX flags**: the image's `makepkg.conf` is CachyOS's (`-march=native -O3`, LTO). GCC 16.2 hit an internal compiler error (segfault) on MicroTeX's `gtkmm_main.cpp` with it (failed CI run 155; not reproducible locally), so `customize_airootfs.sh` patches end-4's `illogical-impulse-microtex-git` PKGBUILD to `options=(!lto)` + `-O2` (our `localrepo/end4-pkgbuilds/microtex-git` carries the same).
|
- **MicroTeX flags**: the image's `makepkg.conf` is CachyOS's (`-march=native -O3`, LTO). GCC 16.2 hit an internal compiler error (segfault) on MicroTeX's `gtkmm_main.cpp` with it (failed CI run 155; not reproducible locally), so `customize_airootfs.sh` patches end-4's `illogical-impulse-microtex-git` PKGBUILD to `options=(!lto)` + `-O2` (our `localrepo/end4-pkgbuilds/microtex-git` carries the same).
|
||||||
- **CAP_SETFCAP on the runner**: without it pacstrap drops `security.capability` xattrs (newuidmap/newgidmap, snap-confine, dumpcap, kwin_wayland, ...) from the live ISO with only a warning. `build-iso.yaml` fails the job early if the runner lacks it — fix the runner, don't remove the check.
|
- **CAP_SETFCAP on the runner**: without it pacstrap drops `security.capability` xattrs (newuidmap/newgidmap, snap-confine, dumpcap, kwin_wayland, ...) from the live ISO with only a warning. `build-iso.yaml` fails the job early if the runner lacks it — fix the runner, don't remove the check.
|
||||||
- **yay SIGSEGV on exit**: yay 13 (libalpm via purego/`dyalpm`) can segfault in `alpm_release()` *after* a finished transaction (failed CI run at end-4's `illogical-impulse-kde` step), which aborted `install-deps`. `customize_airootfs.sh` puts a temporary `yay` shim first on the builder's `PATH`: a failed plain `yay -S <pkgs>` counts as success only if `pacman -T` confirms every target is installed (it logs `[yay-shim]`). Every other failure keeps yay's exit code.
|
- **yay SIGSEGV on exit**: yay 13 (libalpm via purego/`dyalpm`) can segfault in `alpm_release()` *after* a finished transaction (failed CI run at end-4's `illogical-impulse-kde` step), which aborted `install-deps`. `customize_airootfs.sh` puts a temporary `yay` shim first on the builder's `PATH`: a failed plain `yay -S <pkgs>` counts as success only if `pacman -T` confirms every target is installed (it logs `[yay-shim]`). Every other failure keeps yay's exit code.
|
||||||
|
- **gvfs / dead `~/.gvfs`**: `install_end4_dots` replays end-4's gsettings writes via `dbus-run-session` as the target user. A session bus D-Bus-activates gvfsd → gvfsd-fuse mounts `~/.gvfs` and dies with the bus, leaving a dead FUSE mount that root can't stat, so the home's catch-all `chown -R` failed (`cannot access '/mnt/home/<user>/.gvfs': Permission denied`) and aborted the install before the bootloader. The replay therefore runs with `GIO_USE_VFS=local GVFS_DISABLE_FUSE=1`, and the function unmounts anything mounted under the home before that chown — keep both.
|
||||||
- mono's `cert-sync` runs before `ca-certificates.crt` exists during pacstrap, so `customize_airootfs.sh` (live) and `install_pacstrap` (target) re-run it.
|
- mono's `cert-sync` runs before `ca-certificates.crt` exists during pacstrap, so `customize_airootfs.sh` (live) and `install_pacstrap` (target) re-run it.
|
||||||
- `/etc/skel/.zshrc` is owned by `grml-zsh-config`, so ours is staged as `airootfs/root/.zshrc-skel` and installed by `customize_airootfs.sh` after pacstrap — never add files under `airootfs/` at paths a package owns, or pacstrap aborts with "exists in filesystem".
|
- `/etc/skel/.zshrc` is owned by `grml-zsh-config`, so ours is staged as `airootfs/root/.zshrc-skel` and installed by `customize_airootfs.sh` after pacstrap — never add files under `airootfs/` at paths a package owns, or pacstrap aborts with "exists in filesystem".
|
||||||
- `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.
|
||||||
|
|||||||
@@ -384,11 +384,25 @@ sudo mkarchiso -v -w work -o out/ .
|
|||||||
|
|
||||||
### CI Checks (must pass before merge)
|
### CI Checks (must pass before merge)
|
||||||
|
|
||||||
- **ShellCheck**: all `.sh` files checked with `shellcheck -x -s bash`
|
- **ShellCheck**: all `.sh` files, plus extensionless bash scripts in `airootfs/usr/local/bin/`, checked with `shellcheck -x -s bash`
|
||||||
- **yamllint**: all `.yml`/`.yaml` files (relaxed config)
|
- **yamllint**: all `.yml`/`.yaml` files (relaxed config)
|
||||||
- **packages.x86_64 syntax**: one package per line, no trailing whitespace, `[a-zA-Z0-9._@+\-]+` only
|
- **packages.x86_64 syntax**: one package per line, no trailing whitespace, `[a-zA-Z0-9._@+\-]+` only
|
||||||
- **Package repo check**: every package in `packages.x86_64` must be resolvable via `pacman -Si` in allowed repos (`cachyos`, `extra`, `multilib`, `chaotic-aur`), OR have a pre-built `.pkg.tar.zst` in `localrepo/`, OR appear in `localrepo/build-end4-deps.sh`
|
- **Package repo check**: every package in `packages.x86_64` must be resolvable via `pacman -Si` in allowed repos (`cachyos`, `extra`, `multilib`, `chaotic-aur`), OR have a pre-built `.pkg.tar.zst` in `localrepo/`, OR appear in `localrepo/build-end4-deps.sh`
|
||||||
|
|
||||||
|
### Git Hooks
|
||||||
|
|
||||||
|
A pre-commit hook (`githooks/pre-commit`) runs the ShellCheck pass on every commit, so lint failures surface before you push instead of in CI.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable once per clone
|
||||||
|
git config core.hooksPath githooks
|
||||||
|
|
||||||
|
# Bypass the hook for a single commit
|
||||||
|
git commit --no-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
If `shellcheck` is not installed the hook warns and skips rather than blocking the commit; CI remains the enforcer.
|
||||||
|
|
||||||
### Recommended VirtualBox Settings (for testing)
|
### Recommended VirtualBox Settings (for testing)
|
||||||
|
|
||||||
- RAM: 4GB minimum, 8GB recommended
|
- RAM: 4GB minimum, 8GB recommended
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.4 MiB |
@@ -33,6 +33,14 @@ LocalFileSigLevel = Optional
|
|||||||
# - URLs will have $arch replaced by the name of the architecture
|
# - URLs will have $arch replaced by the name of the architecture
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# [cachyos] first, same as the build's pacman.conf: pacstrap installs the
|
||||||
|
# target from this file, so with [core] ahead the target got Arch builds of
|
||||||
|
# everything cachyos also ships (pacman -2 vs the live -4 -> libalpm
|
||||||
|
# "unknown key '%INSTALLED_DB%'" warnings) and flipped them on the first -Syu.
|
||||||
|
[cachyos]
|
||||||
|
SigLevel = Required DatabaseOptional
|
||||||
|
Include = /etc/pacman.d/cachyos-mirrorlist
|
||||||
|
|
||||||
[core]
|
[core]
|
||||||
Include = /etc/pacman.d/mirrorlist
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
@@ -42,10 +50,6 @@ Include = /etc/pacman.d/mirrorlist
|
|||||||
[multilib]
|
[multilib]
|
||||||
Include = /etc/pacman.d/mirrorlist
|
Include = /etc/pacman.d/mirrorlist
|
||||||
|
|
||||||
[cachyos]
|
|
||||||
SigLevel = Required DatabaseOptional
|
|
||||||
Include = /etc/pacman.d/cachyos-mirrorlist
|
|
||||||
|
|
||||||
[chaotic-aur]
|
[chaotic-aur]
|
||||||
SigLevel = Required DatabaseOptional
|
SigLevel = Required DatabaseOptional
|
||||||
Include = /etc/pacman.d/chaotic-mirrorlist
|
Include = /etc/pacman.d/chaotic-mirrorlist
|
||||||
|
|||||||
@@ -134,6 +134,29 @@ if [[ ! -d "${SKEL}/.config/quickshell/end4-pC" ]]; then
|
|||||||
git clone --depth=1 https://github.com/pctrade/end4-pC.git \
|
git clone --depth=1 https://github.com/pctrade/end4-pC.git \
|
||||||
"${SKEL}/.config/quickshell/end4-pC"
|
"${SKEL}/.config/quickshell/end4-pC"
|
||||||
fi
|
fi
|
||||||
|
# The fork's colour/wallpaper/kvantum scripts still hard-code
|
||||||
|
# QUICKSHELL_CONFIG_NAME="ii", so they'd read and write ~/.config/quickshell/ii
|
||||||
|
# instead of end4-pC. Point them at the fork. Re-running is a no-op.
|
||||||
|
mapfile -t PC_II_SCRIPTS < <(grep -rl --exclude-dir=.git \
|
||||||
|
'^QUICKSHELL_CONFIG_NAME="ii"$' "${SKEL}/.config/quickshell/end4-pC" || true)
|
||||||
|
if (( ${#PC_II_SCRIPTS[@]} )); then
|
||||||
|
echo "[customize] Pointing ${#PC_II_SCRIPTS[@]} end4-pC scripts at end4-pC..."
|
||||||
|
sed -i 's/^QUICKSHELL_CONFIG_NAME="ii"$/QUICKSHELL_CONFIG_NAME="end4-pC"/' \
|
||||||
|
"${PC_II_SCRIPTS[@]}"
|
||||||
|
fi
|
||||||
|
# Default wallpaper: end4-pC's first run (services/FirstRunExperience.qml)
|
||||||
|
# feeds assets/images/default_wallpaper.png to switchwall.sh, so replace that
|
||||||
|
# image with the one shipped in skel's ~/Pictures/Wallpapers.
|
||||||
|
DEFAULT_WALL="${SKEL}/Pictures/Wallpapers/Dynamic-Wallpapers/Dark/japan-street-Dark.png"
|
||||||
|
PC_DEFAULT_WALL="${SKEL}/.config/quickshell/end4-pC/assets/images/default_wallpaper.png"
|
||||||
|
if [[ ! -f "${DEFAULT_WALL}" ]]; then
|
||||||
|
echo "[customize] Default wallpaper ${DEFAULT_WALL} is missing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! cmp -s "${DEFAULT_WALL}" "${PC_DEFAULT_WALL}"; then
|
||||||
|
echo "[customize] Setting end4-pC default wallpaper to ${DEFAULT_WALL##*/}..."
|
||||||
|
install -Dm644 "${DEFAULT_WALL}" "${PC_DEFAULT_WALL}"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Default quickshell config: ii -> end4-pC ─────────────────────────────────
|
# ── Default quickshell config: ii -> end4-pC ─────────────────────────────────
|
||||||
# alvin-install copies this staged repo onto the target and runs `./setup
|
# alvin-install copies this staged repo onto the target and runs `./setup
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ readonly ESP_SIZE="1GiB"
|
|||||||
readonly TARGET="/mnt"
|
readonly TARGET="/mnt"
|
||||||
readonly -a PKGS_EXCLUDE=(archiso mkinitcpio-archiso mkinitcpio-nfs-utils syslinux memtest86+)
|
readonly -a PKGS_EXCLUDE=(archiso mkinitcpio-archiso mkinitcpio-nfs-utils syslinux memtest86+)
|
||||||
|
|
||||||
skip() { local x; for x in "${PKGS_EXCLUDE[@]}"; do [[ "$x" == "$1" ]] && return 0; done; return 1; }
|
skip() {
|
||||||
|
local x
|
||||||
|
for x in "${PKGS_EXCLUDE[@]}"; do [[ "$x" == "$1" ]] && return 0; done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
DISK=""
|
DISK=""
|
||||||
KEYMAP=""
|
KEYMAP=""
|
||||||
@@ -27,6 +31,9 @@ GH_USERNAME=""
|
|||||||
UNL0KR="false"
|
UNL0KR="false"
|
||||||
TPM_STATUS=""
|
TPM_STATUS=""
|
||||||
LOGIN_MANAGER="none"
|
LOGIN_MANAGER="none"
|
||||||
|
# true when unl0kr was picked: no login manager is installed if unl0kr
|
||||||
|
# succeeds, and LOGIN_MANAGER only holds the fallback for when it doesn't.
|
||||||
|
LOGIN_MANAGER_SKIP="false"
|
||||||
AUTOLOGIN=""
|
AUTOLOGIN=""
|
||||||
# SSHD is the choice made on the sshd screen; SSHD_APPLIED is only flipped once
|
# 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
|
# the target's sshd.service has actually been enabled, so the final banner can
|
||||||
@@ -35,20 +42,75 @@ SSHD="true"
|
|||||||
SSHD_APPLIED="false"
|
SSHD_APPLIED="false"
|
||||||
TPM_DISCLAIMER=""
|
TPM_DISCLAIMER=""
|
||||||
TARGET_DB_SYNCED="false"
|
TARGET_DB_SYNCED="false"
|
||||||
|
UPDATE_STATUS="not run"
|
||||||
|
# Flipped once pacstrap succeeds: from then on a failed step still leaves a
|
||||||
|
# chroot-able system, so the EXIT trap offers a repair shell into it.
|
||||||
|
TARGET_INSTALLED="false"
|
||||||
|
# Teardown state: a signal arriving while cleanup() runs must not skip the
|
||||||
|
# unmount - the first one only aborts what is on screen, a second forces exit.
|
||||||
|
CLEANUP_RUNNING="false"
|
||||||
|
SIGNALS_SEEN="0"
|
||||||
|
|
||||||
log() { echo "[$(date '+%H:%M:%S')] $*" >>"$LOG"; }
|
log() { echo "[$(date '+%H:%M:%S')] $*" >>"$LOG"; }
|
||||||
|
|
||||||
cleanup() {
|
# Temporary NOPASSWD drop-ins some install steps create; never leave them
|
||||||
set +e
|
# behind (EXIT) or expose them to the user's post-install chroot shell.
|
||||||
|
remove_temp_sudoers() {
|
||||||
rm -f "${TARGET}/etc/sudoers.d/99-end4-install" \
|
rm -f "${TARGET}/etc/sudoers.d/99-end4-install" \
|
||||||
"${TARGET}/etc/sudoers.d/99-ssh-import-id" \
|
"${TARGET}/etc/sudoers.d/99-ssh-import-id" \
|
||||||
"${TARGET}/etc/sudoers.d/99-unl0kr-install" 2>/dev/null
|
"${TARGET}/etc/sudoers.d/99-unl0kr-install" 2>/dev/null || true
|
||||||
umount -R "$TARGET" 2>/dev/null
|
}
|
||||||
cryptsetup close cryptroot 2>/dev/null
|
|
||||||
|
cleanup() {
|
||||||
|
local rc=$?
|
||||||
|
CLEANUP_RUNNING="true"
|
||||||
|
set +e
|
||||||
|
trap - ERR
|
||||||
|
# A later step failed but the base system is on disk: let the user fix
|
||||||
|
# it from inside before everything is unmounted.
|
||||||
|
if ((rc != 0)) && [[ "$TARGET_INSTALLED" == "true" && -t 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
warn "Installation failed after the base system was installed."
|
||||||
|
offer_chroot "The installed system is still mounted. Chroot into it to fix things?"
|
||||||
|
fi
|
||||||
|
remove_temp_sudoers
|
||||||
|
# A repair shell can leave mounts behind, and the LUKS mapping may still
|
||||||
|
# be open - report a failure instead of hiding it, so the user knows the
|
||||||
|
# target was not cleanly torn down before rebooting.
|
||||||
|
if mountpoint -q "$TARGET" 2>/dev/null; then
|
||||||
|
if ! umount -R "$TARGET"; then
|
||||||
|
log "Cleanup: umount -R $TARGET failed"
|
||||||
|
warn "Some mounts under ${TARGET} are busy - unmounting failed (released at reboot)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -e /dev/mapper/cryptroot ]] && ! cryptsetup close cryptroot 2>/dev/null; then
|
||||||
|
log "Cleanup: cryptsetup close cryptroot failed"
|
||||||
|
warn "cryptroot is still open - it is released at reboot."
|
||||||
|
fi
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Signal handler. Without one, bash runs the EXIT trap with $? = 0 when the
|
||||||
|
# shell itself is killed (SIGINT between commands, SIGTERM from a shutdown),
|
||||||
|
# and cleanup()'s rc != 0 test would read that as success and skip the repair
|
||||||
|
# shell. Re-exit with the conventional 128+N status so an interrupted install
|
||||||
|
# takes the same path as a failed step. Note: a SIGINT delivered while a
|
||||||
|
# foreground command is running is consumed together with that command (bash
|
||||||
|
# then suppresses both errexit and this handler) - that case is unchanged.
|
||||||
|
on_signal() {
|
||||||
|
local status="$1"
|
||||||
|
if [[ "$CLEANUP_RUNNING" == "true" && "$SIGNALS_SEEN" == "0" ]]; then
|
||||||
|
SIGNALS_SEEN="1"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "" >&2
|
||||||
|
echo "Interrupted." >&2
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap 'on_signal 130' INT
|
||||||
|
trap 'on_signal 143' TERM
|
||||||
|
|
||||||
# ── error surfacing ───────────────────────────────────────────────────────────
|
# ── error surfacing ───────────────────────────────────────────────────────────
|
||||||
# Step output normally ends up in $LOG only. These helpers make sure a failure
|
# Step output normally ends up in $LOG only. These helpers make sure a failure
|
||||||
# is ALSO shown on screen, paged through `gum pager` so long outputs stay
|
# is ALSO shown on screen, paged through `gum pager` so long outputs stay
|
||||||
@@ -60,7 +122,11 @@ trap cleanup EXIT
|
|||||||
page_output() {
|
page_output() {
|
||||||
local ctx="$1"
|
local ctx="$1"
|
||||||
if [[ -c /dev/tty && -t 1 ]]; then
|
if [[ -c /dev/tty && -t 1 ]]; then
|
||||||
{ gum style --foreground 11 --bold "$ctx"; echo ""; cat; } | gum pager || true
|
{
|
||||||
|
gum style --foreground 11 --bold "$ctx"
|
||||||
|
echo ""
|
||||||
|
cat
|
||||||
|
} | gum pager || true
|
||||||
else
|
else
|
||||||
gum style --foreground 9 --bold "$ctx" || true
|
gum style --foreground 9 --bold "$ctx" || true
|
||||||
cat >&2 || true
|
cat >&2 || true
|
||||||
@@ -91,15 +157,21 @@ page_log_since() {
|
|||||||
# partial output) and - on failure - page the lines it added so the error
|
# partial output) and - on failure - page the lines it added so the error
|
||||||
# reaches the user, not just the log. Returns the command's exit status.
|
# reaches the user, not just the log. Returns the command's exit status.
|
||||||
# Usage: run_logged [-s SPINNER [-t TITLE]] "what failed" cmd [args...]
|
# Usage: run_logged [-s SPINNER [-t TITLE]] "what failed" cmd [args...]
|
||||||
# -s show a gum spinner while cmd runs: dot for linear commands, jump for
|
# -s show a gum spinner while cmd runs (always dot, for a consistent look).
|
||||||
# ones that compile. Frames render on stderr (the terminal), never $LOG.
|
# Frames render on stderr (the terminal), never $LOG.
|
||||||
# -t spinner title; omit when an info line already says what is running.
|
# -t spinner title; omit when an info line already says what is running.
|
||||||
run_logged() {
|
run_logged() {
|
||||||
local ctx="" spinner="" title="" rc=0 mark=0 total=0
|
local ctx="" spinner="" title="" rc=0 mark=0 total=0
|
||||||
while [[ "${1:-}" == -[st] ]]; do
|
while [[ "${1:-}" == -[st] ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-s) spinner="$2"; shift 2 ;;
|
-s)
|
||||||
-t) title="$2"; shift 2 ;;
|
spinner="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-t)
|
||||||
|
title="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
ctx="$1"
|
ctx="$1"
|
||||||
@@ -123,8 +195,8 @@ run_logged() {
|
|||||||
if [[ -n "$title" ]]; then
|
if [[ -n "$title" ]]; then
|
||||||
spin+=(--title "$title")
|
spin+=(--title "$title")
|
||||||
fi
|
fi
|
||||||
"${spin[@]}" -- sh -c '"$@" 2>&1' sh "$@" \
|
"${spin[@]}" -- sh -c '"$@" 2>&1' sh "$@" |
|
||||||
| tee -a "$LOG" >/dev/null || rc=$?
|
tee -a "$LOG" >/dev/null || rc=$?
|
||||||
else
|
else
|
||||||
"$@" 2>&1 | tee -a "$LOG" >/dev/null || rc=$?
|
"$@" 2>&1 | tee -a "$LOG" >/dev/null || rc=$?
|
||||||
fi
|
fi
|
||||||
@@ -153,9 +225,15 @@ on_err() {
|
|||||||
}
|
}
|
||||||
trap 'on_err "$LINENO"' ERR
|
trap 'on_err "$LINENO"' ERR
|
||||||
|
|
||||||
(( EUID == 0 )) || { echo "Run as root" >&2; exit 1; }
|
((EUID == 0)) || {
|
||||||
|
echo "Run as root" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
die() { gum style --foreground 9 --bold "ERROR: $*"; exit 1; }
|
die() {
|
||||||
|
gum style --foreground 9 --bold "ERROR: $*"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
info() { gum style --foreground 12 --bold "$*"; }
|
info() { gum style --foreground 12 --bold "$*"; }
|
||||||
ok() { gum style --foreground 10 --bold "$*"; }
|
ok() { gum style --foreground 10 --bold "$*"; }
|
||||||
warn() { gum style --foreground 11 --bold "$*"; }
|
warn() { gum style --foreground 11 --bold "$*"; }
|
||||||
@@ -274,8 +352,8 @@ screen_keymap() {
|
|||||||
"Use US keyboard layout (us)?"; then
|
"Use US keyboard layout (us)?"; then
|
||||||
KEYMAP="us"
|
KEYMAP="us"
|
||||||
else
|
else
|
||||||
KEYMAP=$(localectl list-keymaps | gum filter --header "+ Keymap" --placeholder "Search keymaps...") \
|
KEYMAP=$(localectl list-keymaps | gum filter --header "+ Keymap" --placeholder "Search keymaps...") ||
|
||||||
|| die "No keymap selected."
|
die "No keymap selected."
|
||||||
fi
|
fi
|
||||||
[[ -z "$KEYMAP" ]] && die "No keymap selected."
|
[[ -z "$KEYMAP" ]] && die "No keymap selected."
|
||||||
loadkeys "$KEYMAP" 2>/dev/null || true
|
loadkeys "$KEYMAP" 2>/dev/null || true
|
||||||
@@ -352,8 +430,8 @@ screen_disk() {
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
local choice
|
local choice
|
||||||
choice=$(gum choose --header "+ Target disk (live USB hidden)" "${disks[@]}") \
|
choice=$(gum choose --header "+ Target disk (live USB hidden)" "${disks[@]}") ||
|
||||||
|| die "No disk selected."
|
die "No disk selected."
|
||||||
DISK=$(echo "$choice" | awk '{print $1}')
|
DISK=$(echo "$choice" | awk '{print $1}')
|
||||||
|
|
||||||
gum confirm --affirmative "Wipe ${DISK}" --negative "Go back" \
|
gum confirm --affirmative "Wipe ${DISK}" --negative "Go back" \
|
||||||
@@ -419,8 +497,8 @@ screen_filesystem() {
|
|||||||
clear
|
clear
|
||||||
header "Filesystem"
|
header "Filesystem"
|
||||||
echo ""
|
echo ""
|
||||||
FILESYSTEM=$(gum choose --header "+ Root filesystem" "ext4" "btrfs") \
|
FILESYSTEM=$(gum choose --header "+ Root filesystem" "ext4" "btrfs") ||
|
||||||
|| die "No filesystem selected."
|
die "No filesystem selected."
|
||||||
log "Filesystem: $FILESYSTEM"
|
log "Filesystem: $FILESYSTEM"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,8 +507,8 @@ screen_username() {
|
|||||||
header "Create User"
|
header "Create User"
|
||||||
echo ""
|
echo ""
|
||||||
local input
|
local input
|
||||||
input=$(gum input --placeholder "username" --char-limit 32 --header "+ Username") \
|
input=$(gum input --placeholder "username" --char-limit 32 --header "+ Username") ||
|
||||||
|| die "No username entered."
|
die "No username entered."
|
||||||
[[ -z "$input" ]] && die "Username cannot be empty."
|
[[ -z "$input" ]] && die "Username cannot be empty."
|
||||||
valid_username "$input" || die "Invalid username. Use lowercase letters, digits, _ or -."
|
valid_username "$input" || die "Invalid username. Use lowercase letters, digits, _ or -."
|
||||||
USERNAME="$input"
|
USERNAME="$input"
|
||||||
@@ -458,11 +536,11 @@ screen_hostname() {
|
|||||||
clear
|
clear
|
||||||
header "Hostname"
|
header "Hostname"
|
||||||
echo ""
|
echo ""
|
||||||
HOSTNAME=$(gum input --placeholder "archlinux" --char-limit 63 --header "+ Hostname") \
|
HOSTNAME=$(gum input --placeholder "archlinux" --char-limit 63 --header "+ Hostname") ||
|
||||||
|| die "No hostname entered."
|
die "No hostname entered."
|
||||||
[[ -z "$HOSTNAME" ]] && HOSTNAME="archlinux"
|
[[ -z "$HOSTNAME" ]] && HOSTNAME="archlinux"
|
||||||
[[ "$HOSTNAME" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,62})$ && "$HOSTNAME" != *- ]] \
|
[[ "$HOSTNAME" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,62})$ && "$HOSTNAME" != *- ]] ||
|
||||||
|| die "Invalid hostname. Use letters, digits and '-' (max 63, no leading/trailing '-')."
|
die "Invalid hostname. Use letters, digits and '-' (max 63, no leading/trailing '-')."
|
||||||
log "Hostname: $HOSTNAME"
|
log "Hostname: $HOSTNAME"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,8 +555,8 @@ screen_git() {
|
|||||||
echo ""
|
echo ""
|
||||||
GH_USERNAME=$(gum input --placeholder "octocat (empty to skip)" \
|
GH_USERNAME=$(gum input --placeholder "octocat (empty to skip)" \
|
||||||
--header "+ GitHub username for SSH keys (optional)") || die "Git configuration cancelled."
|
--header "+ GitHub username for SSH keys (optional)") || die "Git configuration cancelled."
|
||||||
GIT_NAME=$(gum input --value "$USERNAME" --header "+ Git name (Enter keeps '${USERNAME}')") \
|
GIT_NAME=$(gum input --value "$USERNAME" --header "+ Git name (Enter keeps '${USERNAME}')") ||
|
||||||
|| die "Git configuration cancelled."
|
die "Git configuration cancelled."
|
||||||
GIT_EMAIL=$(gum input --placeholder "user@example.com (empty to skip)" \
|
GIT_EMAIL=$(gum input --placeholder "user@example.com (empty to skip)" \
|
||||||
--header "+ Git email (optional)") || die "Git configuration cancelled."
|
--header "+ Git email (optional)") || die "Git configuration cancelled."
|
||||||
log "Git: name='${GIT_NAME}' email='${GIT_EMAIL}' github='${GH_USERNAME}'"
|
log "Git: name='${GIT_NAME}' email='${GIT_EMAIL}' github='${GH_USERNAME}'"
|
||||||
@@ -488,8 +566,8 @@ screen_timezone() {
|
|||||||
clear
|
clear
|
||||||
header "Timezone"
|
header "Timezone"
|
||||||
echo ""
|
echo ""
|
||||||
TIMEZONE=$(timedatectl list-timezones | gum filter --header "+ Timezone" --placeholder "Search...") \
|
TIMEZONE=$(timedatectl list-timezones | gum filter --header "+ Timezone" --placeholder "Search...") ||
|
||||||
|| die "No timezone selected."
|
die "No timezone selected."
|
||||||
log "Timezone: $TIMEZONE"
|
log "Timezone: $TIMEZONE"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,12 +578,17 @@ screen_locale() {
|
|||||||
local locales
|
local locales
|
||||||
locales=$(sed -n 's/^#\?\([a-z][a-z]*_[A-Z][A-Z]\.UTF-8\) UTF-8.*/\1/p' /etc/locale.gen | sort -u)
|
locales=$(sed -n 's/^#\?\([a-z][a-z]*_[A-Z][A-Z]\.UTF-8\) UTF-8.*/\1/p' /etc/locale.gen | sort -u)
|
||||||
[[ -z "$locales" ]] && locales="en_US.UTF-8"
|
[[ -z "$locales" ]] && locales="en_US.UTF-8"
|
||||||
LOCALE=$(echo "$locales" | gum filter --header "+ Locale" --placeholder "Search...") \
|
LOCALE=$(echo "$locales" | gum filter --header "+ Locale" --placeholder "Search...") ||
|
||||||
|| die "No locale selected."
|
die "No locale selected."
|
||||||
log "Locale: $LOCALE"
|
log "Locale: $LOCALE"
|
||||||
}
|
}
|
||||||
|
|
||||||
screen_login_manager() {
|
screen_login_manager() {
|
||||||
|
if [[ "$LUKS" == "true" && "$UNL0KR" == "true" ]]; then
|
||||||
|
screen_unl0kr_login
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
LOGIN_MANAGER_SKIP="false"
|
||||||
clear
|
clear
|
||||||
header "Login Manager"
|
header "Login Manager"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -522,6 +605,33 @@ screen_login_manager() {
|
|||||||
log "Login manager: $LOGIN_MANAGER"
|
log "Login manager: $LOGIN_MANAGER"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# unl0kr already asks for the passphrase on screen at boot, so a login
|
||||||
|
# manager on top would be a second prompt. Skip it, but collect a fallback
|
||||||
|
# now: install_unl0kr can still fall back to the systemd initramfs, and the
|
||||||
|
# installer must not stop to ask mid-install.
|
||||||
|
screen_unl0kr_login() {
|
||||||
|
LOGIN_MANAGER_SKIP="true"
|
||||||
|
clear
|
||||||
|
header "Login Manager (unl0kr)"
|
||||||
|
echo ""
|
||||||
|
gum style --foreground 15 \
|
||||||
|
"unl0kr is selected, so no login manager will be installed:" \
|
||||||
|
"after the LUKS passphrase is entered with unl0kr, the system logs in on" \
|
||||||
|
"tty1 and Hyprland starts automatically (autologin is asked during install)." \
|
||||||
|
"" \
|
||||||
|
"If unl0kr fails to install, the installer falls back to the standard" \
|
||||||
|
"passphrase prompt. Pick the login manager to use in that case:" \
|
||||||
|
"" \
|
||||||
|
" none - plain tty login, Hyprland starts automatically" \
|
||||||
|
" greetd - lightweight TUI greeter (tuigreet)" \
|
||||||
|
" sddm - Qt display manager (already installed)" \
|
||||||
|
" gdm - GNOME display manager (large download)"
|
||||||
|
echo ""
|
||||||
|
LOGIN_MANAGER=$(gum choose --header "+ Fallback login manager (Enter = none)" \
|
||||||
|
"none" "greetd" "sddm" "gdm") || die "No fallback login manager selected."
|
||||||
|
log "Login manager: skipped for unl0kr (fallback: $LOGIN_MANAGER)"
|
||||||
|
}
|
||||||
|
|
||||||
screen_sshd() {
|
screen_sshd() {
|
||||||
clear
|
clear
|
||||||
header "SSH Server"
|
header "SSH Server"
|
||||||
@@ -571,6 +681,11 @@ screen_summary() {
|
|||||||
ssh_line="gh:${GH_USERNAME}"
|
ssh_line="gh:${GH_USERNAME}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local login_line="${LOGIN_MANAGER}"
|
||||||
|
if [[ "$LOGIN_MANAGER_SKIP" == "true" ]]; then
|
||||||
|
login_line="none - unl0kr (fallback: ${LOGIN_MANAGER})"
|
||||||
|
fi
|
||||||
|
|
||||||
gum style --foreground 15 --border normal --border-foreground 8 \
|
gum style --foreground 15 --border normal --border-foreground 8 \
|
||||||
--padding "0 2" --width 60 \
|
--padding "0 2" --width 60 \
|
||||||
"Disk: ${DISK}" \
|
"Disk: ${DISK}" \
|
||||||
@@ -582,7 +697,7 @@ screen_summary() {
|
|||||||
"Keymap: ${KEYMAP}" \
|
"Keymap: ${KEYMAP}" \
|
||||||
"Locale: ${LOCALE}" \
|
"Locale: ${LOCALE}" \
|
||||||
"Timezone: ${TIMEZONE}" \
|
"Timezone: ${TIMEZONE}" \
|
||||||
"Login mgr: ${LOGIN_MANAGER}" \
|
"Login mgr: ${login_line}" \
|
||||||
"SSH server: $([[ "$SSHD" == "true" ]] && echo enabled || echo skipped)" \
|
"SSH server: $([[ "$SSHD" == "true" ]] && echo enabled || echo skipped)" \
|
||||||
"SSH keys: ${ssh_line}" \
|
"SSH keys: ${ssh_line}" \
|
||||||
"Git: ${git_line}"
|
"Git: ${git_line}"
|
||||||
@@ -675,8 +790,8 @@ install_pacstrap() {
|
|||||||
log "Pacstrap: ${pkglist} missing, falling back to pacman -Q"
|
log "Pacstrap: ${pkglist} missing, falling back to pacman -Q"
|
||||||
mapfile -t src_lines < <(pacman -Q 2>>"$LOG" || true)
|
mapfile -t src_lines < <(pacman -Q 2>>"$LOG" || true)
|
||||||
fi
|
fi
|
||||||
(( ${#src_lines[@]} > 0 )) \
|
((${#src_lines[@]} > 0)) ||
|
||||||
|| die "No package list: ${pkglist} is missing and pacman -Q returned nothing."
|
die "No package list: ${pkglist} is missing and pacman -Q returned nothing."
|
||||||
|
|
||||||
# Parse only the first field, then drop names not resolvable in the
|
# Parse only the first field, then drop names not resolvable in the
|
||||||
# live session's repos (incl. [local-aur] = /opt/localrepo).
|
# live session's repos (incl. [local-aur] = /opt/localrepo).
|
||||||
@@ -740,8 +855,8 @@ install_pacstrap() {
|
|||||||
log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped"
|
log "Pacstrap: ${#pkgs[@]} packages, ${#dropped[@]} skipped"
|
||||||
run_logged -s dot -t "Installing ${#pkgs[@]} packages (this may take a while)" \
|
run_logged -s dot -t "Installing ${#pkgs[@]} packages (this may take a while)" \
|
||||||
"pacstrap failed" \
|
"pacstrap failed" \
|
||||||
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm \
|
pacstrap "$TARGET" "${pkgs[@]}" --noconfirm ||
|
||||||
|| die "pacstrap failed - the target system is incomplete (output above, full log: $LOG)."
|
die "pacstrap failed - the target system is incomplete (output above, full log: $LOG)."
|
||||||
|
|
||||||
# mono's post_install runs cert-sync before the post-transaction hook has
|
# mono's post_install runs cert-sync before the post-transaction hook has
|
||||||
# generated ca-certificates.crt, leaving mono's certificate store empty.
|
# generated ca-certificates.crt, leaving mono's certificate store empty.
|
||||||
@@ -750,6 +865,7 @@ install_pacstrap() {
|
|||||||
>>"$LOG" 2>&1 || warn "cert-sync failed - mono apps may reject TLS certificates."
|
>>"$LOG" 2>&1 || warn "cert-sync failed - mono apps may reject TLS certificates."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
TARGET_INSTALLED="true"
|
||||||
ok "Packages installed."
|
ok "Packages installed."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,8 +891,8 @@ install_cachyos_repo() {
|
|||||||
fi
|
fi
|
||||||
# chaotic-aur packages (yay, ...) came from the ISO - without the repo
|
# chaotic-aur packages (yay, ...) came from the ISO - without the repo
|
||||||
# they turn foreign and never update.
|
# they turn foreign and never update.
|
||||||
if ! grep -q '^\[chaotic-aur\]' "$conf" 2>/dev/null \
|
if ! grep -q '^\[chaotic-aur\]' "$conf" 2>/dev/null &&
|
||||||
&& [[ -f "${TARGET}/etc/pacman.d/chaotic-mirrorlist" ]]; then
|
[[ -f "${TARGET}/etc/pacman.d/chaotic-mirrorlist" ]]; then
|
||||||
printf '\n[chaotic-aur]\nInclude = /etc/pacman.d/chaotic-mirrorlist\n' >>"$conf"
|
printf '\n[chaotic-aur]\nInclude = /etc/pacman.d/chaotic-mirrorlist\n' >>"$conf"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -800,7 +916,9 @@ install_locale() {
|
|||||||
info "Setting locale..."
|
info "Setting locale..."
|
||||||
|
|
||||||
arch-chroot "$TARGET" sed -i "s/^#${LOCALE}/${LOCALE}/" /etc/locale.gen
|
arch-chroot "$TARGET" sed -i "s/^#${LOCALE}/${LOCALE}/" /etc/locale.gen
|
||||||
arch-chroot "$TARGET" locale-gen
|
run_logged -s dot -t "Generating locales" "locale-gen failed" \
|
||||||
|
arch-chroot "$TARGET" locale-gen ||
|
||||||
|
die "locale-gen failed (output above, full log: $LOG)."
|
||||||
|
|
||||||
echo "LANG=${LOCALE}" >"${TARGET}/etc/locale.conf"
|
echo "LANG=${LOCALE}" >"${TARGET}/etc/locale.conf"
|
||||||
echo "KEYMAP=${KEYMAP}" >"${TARGET}/etc/vconsole.conf"
|
echo "KEYMAP=${KEYMAP}" >"${TARGET}/etc/vconsole.conf"
|
||||||
@@ -888,12 +1006,12 @@ install_git_ssh() {
|
|||||||
local git_name="${GIT_NAME:-$USERNAME}"
|
local git_name="${GIT_NAME:-$USERNAME}"
|
||||||
run_logged "git config user.name failed" \
|
run_logged "git config user.name failed" \
|
||||||
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
||||||
git config --global user.name "$git_name" \
|
git config --global user.name "$git_name" ||
|
||||||
|| warn "git config user.name failed (output above)."
|
warn "git config user.name failed (output above)."
|
||||||
run_logged "git config user.email failed" \
|
run_logged "git config user.email failed" \
|
||||||
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
||||||
git config --global user.email "$GIT_EMAIL" \
|
git config --global user.email "$GIT_EMAIL" ||
|
||||||
|| warn "git config user.email failed (output above)."
|
warn "git config user.email failed (output above)."
|
||||||
ok "Git configured for ${USERNAME} (${git_name} <${GIT_EMAIL}>)."
|
ok "Git configured for ${USERNAME} (${git_name} <${GIT_EMAIL}>)."
|
||||||
log "Git: ${git_name} <${GIT_EMAIL}>"
|
log "Git: ${git_name} <${GIT_EMAIL}>"
|
||||||
else
|
else
|
||||||
@@ -931,14 +1049,15 @@ install_git_ssh() {
|
|||||||
else
|
else
|
||||||
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >"${TARGET}/etc/sudoers.d/99-ssh-import-id"
|
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >"${TARGET}/etc/sudoers.d/99-ssh-import-id"
|
||||||
chmod 440 "${TARGET}/etc/sudoers.d/99-ssh-import-id"
|
chmod 440 "${TARGET}/etc/sudoers.d/99-ssh-import-id"
|
||||||
run_logged "yay -Ss ssh-import-id found nothing" \
|
run_logged -s dot -t "Searching the AUR for ssh-import-id" \
|
||||||
|
"yay -Ss ssh-import-id found nothing" \
|
||||||
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
||||||
env HOME="$home_in_target" yay -Ss ssh-import-id \
|
env HOME="$home_in_target" yay -Ss ssh-import-id ||
|
||||||
|| warn "yay -Ss ssh-import-id found nothing (output above)."
|
warn "yay -Ss ssh-import-id found nothing (output above)."
|
||||||
if run_logged -s dot -t "Installing ssh-import-id" \
|
if run_logged -s dot -t "Installing ssh-import-id" \
|
||||||
"pacman -S python-distro failed" \
|
"pacman -S python-distro failed" \
|
||||||
arch-chroot "$TARGET" pacman -S --noconfirm python-distro \
|
arch-chroot "$TARGET" pacman -S --noconfirm python-distro &&
|
||||||
&& run_logged -s jump -t "Installing ssh-import-id" \
|
run_logged -s dot -t "Installing ssh-import-id" \
|
||||||
"yay failed to install ssh-import-id" \
|
"yay failed to install ssh-import-id" \
|
||||||
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
||||||
env HOME="$home_in_target" yay -S --noconfirm ssh-import-id; then
|
env HOME="$home_in_target" yay -S --noconfirm ssh-import-id; then
|
||||||
@@ -951,7 +1070,7 @@ install_git_ssh() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$sid_ok" == "true" ]]; then
|
if [[ "$sid_ok" == "true" ]]; then
|
||||||
if run_logged "ssh-import-id failed" \
|
if run_logged -s dot -t "Importing GitHub SSH keys" "ssh-import-id failed" \
|
||||||
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
||||||
env HOME="$home_in_target" ssh-import-id "gh:${GH_USERNAME}"; then
|
env HOME="$home_in_target" ssh-import-id "gh:${GH_USERNAME}"; then
|
||||||
ok "SSH keys imported via ssh-import-id."
|
ok "SSH keys imported via ssh-import-id."
|
||||||
@@ -1046,11 +1165,11 @@ install_end4_dots() {
|
|||||||
# --skip-alldeps: every dependency was already installed at ISO build time
|
# --skip-alldeps: every dependency was already installed at ISO build time
|
||||||
# (customize_airootfs.sh runs ./setup install-deps), so this only copies
|
# (customize_airootfs.sh runs ./setup install-deps), so this only copies
|
||||||
# configs, sets up services and builds the python venv from the warm cache.
|
# configs, sets up services and builds the python venv from the warm cache.
|
||||||
# Both attempts run under a jump spinner (this step compiles the venv and
|
# Both attempts run under a dot spinner (this step compiles the venv and
|
||||||
# pip bits): --show-output + the sh stderr-merge keep every line flowing
|
# pip bits): --show-output + the sh stderr-merge keep every line flowing
|
||||||
# into $LOG, the frames render on the terminal, and stdin still feeds the
|
# into $LOG, the frames render on the terminal, and stdin still feeds the
|
||||||
# `yes i` process substitution through to ./setup.
|
# `yes i` process substitution through to ./setup.
|
||||||
if ! gum spin --show-output --spinner jump -- \
|
if ! gum spin --show-output -s dot -- \
|
||||||
sh -c '"$@" 2>&1' sh \
|
sh -c '"$@" 2>&1' sh \
|
||||||
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
||||||
env HOME="$home_in_target" bash -c \
|
env HOME="$home_in_target" bash -c \
|
||||||
@@ -1058,7 +1177,7 @@ install_end4_dots() {
|
|||||||
< <(yes i) >>"$LOG"; then
|
< <(yes i) >>"$LOG"; then
|
||||||
warn "end-4 setup failed, retrying files only (see $LOG)."
|
warn "end-4 setup failed, retrying files only (see $LOG)."
|
||||||
setups_done=false
|
setups_done=false
|
||||||
gum spin --show-output --spinner jump -- \
|
gum spin --show-output -s dot -- \
|
||||||
sh -c '"$@" 2>&1' sh \
|
sh -c '"$@" 2>&1' sh \
|
||||||
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
||||||
env HOME="$home_in_target" bash -c \
|
env HOME="$home_in_target" bash -c \
|
||||||
@@ -1088,8 +1207,8 @@ install_end4_dots() {
|
|||||||
|
|
||||||
if [[ -f "${TARGET}/usr/lib/systemd/system/bluetooth.service" ]]; then
|
if [[ -f "${TARGET}/usr/lib/systemd/system/bluetooth.service" ]]; then
|
||||||
run_logged "Could not enable bluetooth.service" \
|
run_logged "Could not enable bluetooth.service" \
|
||||||
arch-chroot "$TARGET" systemctl enable bluetooth.service \
|
arch-chroot "$TARGET" systemctl enable bluetooth.service ||
|
||||||
|| warn "Could not enable bluetooth.service (output above)."
|
warn "Could not enable bluetooth.service (output above)."
|
||||||
fi
|
fi
|
||||||
# `systemctl --user` cannot work in this chroot either: it needs
|
# `systemctl --user` cannot work in this chroot either: it needs
|
||||||
# /run/user/UID/bus, which does not exist until the target boots (it only
|
# /run/user/UID/bus, which does not exist until the target boots (it only
|
||||||
@@ -1101,9 +1220,9 @@ install_end4_dots() {
|
|||||||
# appended to $LOG and paged to the user (as the bluetooth block's
|
# appended to $LOG and paged to the user (as the bluetooth block's
|
||||||
# run_logged does), never kept in the log alone.
|
# run_logged does), never kept in the log alone.
|
||||||
local yd_rc=0 yd_err=""
|
local yd_rc=0 yd_err=""
|
||||||
yd_err=$( { mkdir -p "$yd_wants" \
|
yd_err=$({ mkdir -p "$yd_wants" &&
|
||||||
&& ln -sfn /usr/lib/systemd/user/ydotool.service "$yd_wants/ydotool.service"; } 2>&1 ) \
|
ln -sfn /usr/lib/systemd/user/ydotool.service "$yd_wants/ydotool.service"; } 2>&1) ||
|
||||||
|| yd_rc=$?
|
yd_rc=$?
|
||||||
if ((yd_rc == 0)); then
|
if ((yd_rc == 0)); then
|
||||||
log "ydotool.service enabled for ${USERNAME} (symlinked in user unit dir)."
|
log "ydotool.service enabled for ${USERNAME} (symlinked in user unit dir)."
|
||||||
else
|
else
|
||||||
@@ -1123,8 +1242,8 @@ install_end4_dots() {
|
|||||||
# throwaway session bus as the target user. gsettings exits 0 even when
|
# 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
|
# the commit fails, so read the key back in a second session — only a
|
||||||
# matching value counts as persisted.
|
# matching value counts as persisted.
|
||||||
if ((log_mark > 0)) \
|
if ((log_mark > 0)) &&
|
||||||
&& [[ -x "${TARGET}/usr/bin/dbus-run-session" && -x "${TARGET}/usr/bin/gsettings" ]]; then
|
[[ -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 gs_err gs_rc
|
local gs_line gs_schema gs_key gs_val gs_got gs_norm gs_raw gs_err gs_rc
|
||||||
# Snapshot and parse first (the loop below appends to $LOG), keeping
|
# Snapshot and parse first (the loop below appends to $LOG), keeping
|
||||||
# log order: a key set twice replays chronologically so the last
|
# log order: a key set twice replays chronologically so the last
|
||||||
@@ -1155,6 +1274,8 @@ install_end4_dots() {
|
|||||||
gs_val=${BASH_REMATCH[3]}
|
gs_val=${BASH_REMATCH[3]}
|
||||||
# env -i: root's env is exactly what broke dconf; rebuild it for
|
# env -i: root's env is exactly what broke dconf; rebuild it for
|
||||||
# the target user with only what gsettings/dbus need.
|
# the target user with only what gsettings/dbus need.
|
||||||
|
# GIO_USE_VFS=local: otherwise the session bus activates gvfsd, whose
|
||||||
|
# gvfsd-fuse leaves a dead ~/.gvfs mount that breaks chown -R below.
|
||||||
# gsettings exits 0 even when the commit fails, so both the exit
|
# gsettings exits 0 even when the commit fails, so both the exit
|
||||||
# status AND the read-back below decide success; its output is
|
# status AND the read-back below decide success; its output is
|
||||||
# kept in $LOG and paged on failure.
|
# kept in $LOG and paged on failure.
|
||||||
@@ -1164,6 +1285,7 @@ install_end4_dots() {
|
|||||||
env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \
|
env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \
|
||||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \
|
||||||
XDG_CONFIG_HOME="${home_in_target}/.config" \
|
XDG_CONFIG_HOME="${home_in_target}/.config" \
|
||||||
|
GIO_USE_VFS=local GVFS_DISABLE_FUSE=1 \
|
||||||
dbus-run-session -- \
|
dbus-run-session -- \
|
||||||
gsettings set "$gs_schema" "$gs_key" "$gs_val" 2>&1) || gs_rc=$?
|
gsettings set "$gs_schema" "$gs_key" "$gs_val" 2>&1) || gs_rc=$?
|
||||||
if [[ -n "$gs_err" ]]; then
|
if [[ -n "$gs_err" ]]; then
|
||||||
@@ -1174,6 +1296,7 @@ install_end4_dots() {
|
|||||||
env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \
|
env -i HOME="$home_in_target" USER="$USERNAME" LOGNAME="$USERNAME" \
|
||||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \
|
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin \
|
||||||
XDG_CONFIG_HOME="${home_in_target}/.config" \
|
XDG_CONFIG_HOME="${home_in_target}/.config" \
|
||||||
|
GIO_USE_VFS=local GVFS_DISABLE_FUSE=1 \
|
||||||
dbus-run-session -- \
|
dbus-run-session -- \
|
||||||
gsettings get "$gs_schema" "$gs_key" 2>>"$LOG") || gs_got=""
|
gsettings get "$gs_schema" "$gs_key" 2>>"$LOG") || gs_got=""
|
||||||
# Normalize the GVariant read-back: strings come back quoted
|
# Normalize the GVariant read-back: strings come back quoted
|
||||||
@@ -1249,15 +1372,15 @@ install_end4_dots() {
|
|||||||
"$fish_src/" "$fish_dst/"
|
"$fish_src/" "$fish_dst/"
|
||||||
if [[ -f "${fish_dst}/config.fish" ]]; then
|
if [[ -f "${fish_dst}/config.fish" ]]; then
|
||||||
sed -i "s/alias q 'qs -c ii'/alias q 'qs -c end4-pC'/" "${fish_dst}/config.fish"
|
sed -i "s/alias q 'qs -c ii'/alias q 'qs -c end4-pC'/" "${fish_dst}/config.fish"
|
||||||
grep -qF "alias q 'qs -c end4-pC'" "${fish_dst}/config.fish" \
|
grep -qF "alias q 'qs -c end4-pC'" "${fish_dst}/config.fish" ||
|
||||||
|| warn "fish 'q' alias still points at the upstream ii config (see $LOG)."
|
warn "fish 'q' alias still points at the upstream ii config (see $LOG)."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local configs_ok=true
|
local configs_ok=true
|
||||||
[[ -e "${TARGET}${home_in_target}/.config/hypr/hyprland.lua" \
|
[[ -e "${TARGET}${home_in_target}/.config/hypr/hyprland.lua" &&
|
||||||
&& -d "${TARGET}${home_in_target}/.config/quickshell/ii" \
|
-d "${TARGET}${home_in_target}/.config/quickshell/ii" &&
|
||||||
&& -d "${TARGET}${home_in_target}/.config/quickshell/end4-pC" ]] || configs_ok=false
|
-d "${TARGET}${home_in_target}/.config/quickshell/end4-pC" ]] || configs_ok=false
|
||||||
# Files alone are not enough: with --skip-alldeps the dependencies must
|
# Files alone are not enough: with --skip-alldeps the dependencies must
|
||||||
# have come from the ISO. hyprland and quickshell are installed at ISO
|
# have come from the ISO. hyprland and quickshell are installed at ISO
|
||||||
# build time, so their binaries missing here means pacstrap dropped them
|
# build time, so their binaries missing here means pacstrap dropped them
|
||||||
@@ -1265,26 +1388,15 @@ install_end4_dots() {
|
|||||||
# the image's uv cache at install time, so a missing python means the
|
# the image's uv cache at install time, so a missing python means the
|
||||||
# python step of end-4's setup never got to run.
|
# python step of end-4's setup never got to run.
|
||||||
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
|
||||||
|
|
||||||
# uv writes bin/python as an absolute symlink into the chroot
|
# uv's bin/python is a chain of absolute symlinks (the venv link AND
|
||||||
# (/home/.../uv/python/...). Tested verbatim from the live medium
|
# uv's cpython-3.12-... dir link) into /home/$USERNAME, which exists
|
||||||
# that link dangles - /home/$USERNAME exists only under $TARGET -
|
# only under $TARGET - resolve and run it inside the chroot instead.
|
||||||
# so walk the chain here and re-root absolute hops under $TARGET
|
arch-chroot "$TARGET" \
|
||||||
# (relative hops stay inside the venv and need no repointing).
|
"${home_in_target}/.local/state/quickshell/.venv/bin/python" -B -c '' \
|
||||||
local venv_probe="${TARGET}${home_in_target}/.local/state/quickshell/.venv/bin/python"
|
>>"$LOG" 2>&1 || configs_ok=false
|
||||||
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
|
||||||
@@ -1304,6 +1416,19 @@ install_end4_dots() {
|
|||||||
# fork/fish/hypr restores, and the presets sed -i just rewrote through a
|
# fork/fish/hypr restores, and the presets sed -i just rewrote through a
|
||||||
# temp file) left root-owned files in the home — one pass fixes them all.
|
# temp file) left root-owned files in the home — one pass fixes them all.
|
||||||
# Numeric ids again: this chown also runs on the medium, not in the chroot.
|
# Numeric ids again: this chown also runs on the medium, not in the chroot.
|
||||||
|
# A dead FUSE mount (gvfs ~/.gvfs, kio-fuse, ...) makes chown -R fail with
|
||||||
|
# EACCES even as root; unmount everything under the home first. The home
|
||||||
|
# is no mount point itself, so filter the full mount table by prefix
|
||||||
|
# (findmnt -R would match nothing); sort -r unmounts deepest first. The
|
||||||
|
# || true keeps an empty match from tripping the inherited ERR trap,
|
||||||
|
# whose message would otherwise be read as a mount path.
|
||||||
|
local mnt
|
||||||
|
while read -r mnt; do
|
||||||
|
umount "$mnt" 2>/dev/null || umount -l "$mnt" 2>/dev/null || true
|
||||||
|
log "unmounted stray mount in home: $mnt"
|
||||||
|
done < <(findmnt -rno TARGET 2>/dev/null |
|
||||||
|
awk -v p="${TARGET}${home_in_target}/" 'index($0, p) == 1' | sort -r || true)
|
||||||
|
rmdir "${TARGET}${home_in_target}/.gvfs" 2>/dev/null || true
|
||||||
chown -R "$owner" "${TARGET}${home_in_target}"
|
chown -R "$owner" "${TARGET}${home_in_target}"
|
||||||
|
|
||||||
if $dots_ok && $configs_ok; then
|
if $dots_ok && $configs_ok; then
|
||||||
@@ -1340,7 +1465,9 @@ install_mkinitcpio() {
|
|||||||
info "Generating initramfs configuration..."
|
info "Generating initramfs configuration..."
|
||||||
|
|
||||||
if [[ "$LUKS" != "true" ]]; then
|
if [[ "$LUKS" != "true" ]]; then
|
||||||
arch-chroot "$TARGET" mkinitcpio -P
|
run_logged -s dot -t "Generating initramfs" "mkinitcpio -P failed" \
|
||||||
|
arch-chroot "$TARGET" mkinitcpio -P ||
|
||||||
|
die "mkinitcpio -P failed (output above, full log: $LOG)."
|
||||||
ok "initramfs generated."
|
ok "initramfs generated."
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
@@ -1374,7 +1501,9 @@ HOOKS
|
|||||||
# Mode A: systemd initramfs with sd-encrypt (matches the rd.luks.name
|
# Mode A: systemd initramfs with sd-encrypt (matches the rd.luks.name
|
||||||
# kernel cmdline written by install_bootloader).
|
# kernel cmdline written by install_bootloader).
|
||||||
write_mode_a_hooks
|
write_mode_a_hooks
|
||||||
arch-chroot "$TARGET" mkinitcpio -P
|
run_logged -s dot -t "Generating initramfs" "mkinitcpio -P failed" \
|
||||||
|
arch-chroot "$TARGET" mkinitcpio -P ||
|
||||||
|
die "mkinitcpio -P failed (output above, full log: $LOG)."
|
||||||
|
|
||||||
ok "initramfs generated."
|
ok "initramfs generated."
|
||||||
}
|
}
|
||||||
@@ -1465,8 +1594,8 @@ install_bootloader() {
|
|||||||
esp_partnum=${esp_partnum:-1}
|
esp_partnum=${esp_partnum:-1}
|
||||||
# Match the loader path, not just the label: a stale ALVIN entry left by
|
# Match the loader path, not just the label: a stale ALVIN entry left by
|
||||||
# a previous install on another disk would satisfy a bare label match.
|
# a previous install on another disk would satisfy a bare label match.
|
||||||
boot_entry=$(efibootmgr -v 2>/dev/null \
|
boot_entry=$(efibootmgr -v 2>/dev/null |
|
||||||
| grep -iF '\EFI\ALVIN\grubx64.efi' | head -n1) || boot_entry=""
|
grep -iF '\EFI\ALVIN\grubx64.efi' | head -n1) || boot_entry=""
|
||||||
if [[ -z "$boot_entry" ]]; then
|
if [[ -z "$boot_entry" ]]; then
|
||||||
warn "No ALVIN entry in the firmware NVRAM - creating one now."
|
warn "No ALVIN entry in the firmware NVRAM - creating one now."
|
||||||
if ! run_logged "efibootmgr could not create the NVRAM boot entry" \
|
if ! run_logged "efibootmgr could not create the NVRAM boot entry" \
|
||||||
@@ -1474,8 +1603,8 @@ install_bootloader() {
|
|||||||
-l '\EFI\ALVIN\grubx64.efi'; then
|
-l '\EFI\ALVIN\grubx64.efi'; then
|
||||||
die "Could not create the firmware NVRAM boot entry (output above). Refusing to finish: the install stops before grub-mkconfig runs, so \\EFI\\BOOT\\BOOTX64.EFI exists but is not bootable yet. Check UEFI setup (NVRAM write access) and re-run."
|
die "Could not create the firmware NVRAM boot entry (output above). Refusing to finish: the install stops before grub-mkconfig runs, so \\EFI\\BOOT\\BOOTX64.EFI exists but is not bootable yet. Check UEFI setup (NVRAM write access) and re-run."
|
||||||
fi
|
fi
|
||||||
boot_entry=$(efibootmgr -v 2>/dev/null \
|
boot_entry=$(efibootmgr -v 2>/dev/null |
|
||||||
| grep -iF '\EFI\ALVIN\grubx64.efi' | head -n1) || boot_entry=""
|
grep -iF '\EFI\ALVIN\grubx64.efi' | head -n1) || boot_entry=""
|
||||||
if [[ -z "$boot_entry" ]]; then
|
if [[ -z "$boot_entry" ]]; then
|
||||||
die "NVRAM boot entry still missing after efibootmgr -c. Refusing to finish: the install stops before grub-mkconfig runs, so the system is not bootable yet - check UEFI setup and re-run."
|
die "NVRAM boot entry still missing after efibootmgr -c. Refusing to finish: the install stops before grub-mkconfig runs, so the system is not bootable yet - check UEFI setup and re-run."
|
||||||
fi
|
fi
|
||||||
@@ -1560,7 +1689,7 @@ install_tpm() {
|
|||||||
# after, so the secret exists only for the duration of this call.
|
# after, so the secret exists only for the duration of this call.
|
||||||
export PASSWORD="$PASS"
|
export PASSWORD="$PASS"
|
||||||
local cenrc=0
|
local cenrc=0
|
||||||
run_logged "systemd-cryptenroll failed" \
|
run_logged -s dot -t "Enrolling the TPM2 unlock key" "systemd-cryptenroll failed" \
|
||||||
systemd-cryptenroll --tpm2-device=auto \
|
systemd-cryptenroll --tpm2-device=auto \
|
||||||
--tpm2-pcrs=0+7 "$(part 2)" || cenrc=$?
|
--tpm2-pcrs=0+7 "$(part 2)" || cenrc=$?
|
||||||
unset PASSWORD
|
unset PASSWORD
|
||||||
@@ -1575,8 +1704,8 @@ install_tpm() {
|
|||||||
luks_uuid=$(blkid -s UUID -o value "$(part 2)")
|
luks_uuid=$(blkid -s UUID -o value "$(part 2)")
|
||||||
write_mode_a_cmdline "$luks_uuid" "rd.luks.options=${luks_uuid}=tpm2-device=auto"
|
write_mode_a_cmdline "$luks_uuid" "rd.luks.options=${luks_uuid}=tpm2-device=auto"
|
||||||
run_logged -s dot "grub-mkconfig failed (rd.luks.options)" \
|
run_logged -s dot "grub-mkconfig failed (rd.luks.options)" \
|
||||||
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg \
|
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg ||
|
||||||
|| warn "grub-mkconfig failed after adding rd.luks.options (output above) - TPM2 unlock will not activate at boot."
|
warn "grub-mkconfig failed after adding rd.luks.options (output above) - TPM2 unlock will not activate at boot."
|
||||||
ok "TPM2 auto-unlock enrolled and activated (rd.luks.options, PCR 0+7)."
|
ok "TPM2 auto-unlock enrolled and activated (rd.luks.options, PCR 0+7)."
|
||||||
else
|
else
|
||||||
TPM_STATUS="failed"
|
TPM_STATUS="failed"
|
||||||
@@ -1621,11 +1750,11 @@ unl0kr_fallback_mode_a() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
run_logged -s dot "mkinitcpio -P failed (unl0kr fallback)" \
|
run_logged -s dot "mkinitcpio -P failed (unl0kr fallback)" \
|
||||||
arch-chroot "$TARGET" mkinitcpio -P \
|
arch-chroot "$TARGET" mkinitcpio -P ||
|
||||||
|| warn "mkinitcpio -P failed during unl0kr fallback (output above)."
|
warn "mkinitcpio -P failed during unl0kr fallback (output above)."
|
||||||
run_logged -s dot "grub-mkconfig failed (unl0kr fallback)" \
|
run_logged -s dot "grub-mkconfig failed (unl0kr fallback)" \
|
||||||
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg \
|
arch-chroot "$TARGET" grub-mkconfig -o /boot/grub/grub.cfg ||
|
||||||
|| warn "grub-mkconfig failed during unl0kr fallback (output above)."
|
warn "grub-mkconfig failed during unl0kr fallback (output above)."
|
||||||
warn "The system will ask for the LUKS passphrase at boot - test a reboot before relying on it."
|
warn "The system will ask for the LUKS passphrase at boot - test a reboot before relying on it."
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1641,7 +1770,7 @@ install_unl0kr() {
|
|||||||
# yay runs as the new user and needs passwordless sudo for this one call.
|
# yay runs as the new user and needs passwordless sudo for this one call.
|
||||||
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >"${TARGET}/etc/sudoers.d/99-unl0kr-install"
|
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >"${TARGET}/etc/sudoers.d/99-unl0kr-install"
|
||||||
chmod 440 "${TARGET}/etc/sudoers.d/99-unl0kr-install"
|
chmod 440 "${TARGET}/etc/sudoers.d/99-unl0kr-install"
|
||||||
run_logged -s jump -t "Building unl0kr from the AUR (this may take a while)" \
|
run_logged -s dot -t "Building unl0kr from the AUR (this may take a while)" \
|
||||||
"yay failed to build/install unl0kr" \
|
"yay failed to build/install unl0kr" \
|
||||||
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
|
||||||
env HOME="$home_in_target" yay -S --noconfirm unl0kr || build_rc=$?
|
env HOME="$home_in_target" yay -S --noconfirm unl0kr || build_rc=$?
|
||||||
@@ -1655,8 +1784,8 @@ install_unl0kr() {
|
|||||||
# Record what the AUR actually gave us - advertised and PKGBUILD versions
|
# Record what the AUR actually gave us - advertised and PKGBUILD versions
|
||||||
# have disagreed before, so trust pacman, not the web page.
|
# have disagreed before, so trust pacman, not the web page.
|
||||||
local built_ver
|
local built_ver
|
||||||
built_ver=$(arch-chroot "$TARGET" pacman -Qi unl0kr 2>/dev/null \
|
built_ver=$(arch-chroot "$TARGET" pacman -Qi unl0kr 2>/dev/null |
|
||||||
| awk -F': ' '/^Version/{print $2}') || built_ver=""
|
awk -F': ' '/^Version/{print $2}') || built_ver=""
|
||||||
log "unl0kr built version: ${built_ver:-unknown}"
|
log "unl0kr built version: ${built_ver:-unknown}"
|
||||||
|
|
||||||
# The package must ship busybox initcpio hooks (its prepare() strips the
|
# The package must ship busybox initcpio hooks (its prepare() strips the
|
||||||
@@ -1681,6 +1810,34 @@ install_unl0kr() {
|
|||||||
page_log_tail "unl0kr hook file output (q to quit)" 40 || true
|
page_log_tail "unl0kr hook file output (q to quit)" 40 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# unl0kr parses /usr/share/unl0kr/unl0kr.conf first; the package doesn't
|
||||||
|
# ship it, so inih fails to open it and unl0kr prints "Ignoring invalid
|
||||||
|
# config file" on every boot. An empty file parses cleanly - FILES+= puts
|
||||||
|
# it in the image (the hook only adds /etc/unl0kr.conf). Our own
|
||||||
|
# /etc/unl0kr.conf pins the dark theme instead of trusting the PKGBUILD's
|
||||||
|
# sed on the upstream (light) default.
|
||||||
|
mkdir -p "${TARGET}/usr/share/unl0kr"
|
||||||
|
: >"${TARGET}/usr/share/unl0kr/unl0kr.conf"
|
||||||
|
cat >"${TARGET}/etc/unl0kr.conf" <<'UNL0KR'
|
||||||
|
[general]
|
||||||
|
animations=true
|
||||||
|
|
||||||
|
[keyboard]
|
||||||
|
autohide=false
|
||||||
|
layout=us
|
||||||
|
popovers=true
|
||||||
|
|
||||||
|
[textarea]
|
||||||
|
obscured=true
|
||||||
|
|
||||||
|
[theme]
|
||||||
|
default=breezy-dark
|
||||||
|
alternate=breezy-light
|
||||||
|
UNL0KR
|
||||||
|
echo 'FILES+=(/usr/share/unl0kr/unl0kr.conf)' \
|
||||||
|
>>"${TARGET}/etc/mkinitcpio.conf.d/alvin.conf"
|
||||||
|
log "unl0kr: wrote /etc/unl0kr.conf (breezy-dark) + empty /usr/share/unl0kr/unl0kr.conf"
|
||||||
|
|
||||||
# Touchscreen/input stack for the initramfs: mkinitcpio fails the build
|
# Touchscreen/input stack for the initramfs: mkinitcpio fails the build
|
||||||
# on a MODULES entry it cannot resolve, and -P applies the list to every
|
# on a MODULES entry it cannot resolve, and -P applies the list to every
|
||||||
# preset - so a module may enter MODULES only if EVERY installed kernel
|
# preset - so a module may enter MODULES only if EVERY installed kernel
|
||||||
@@ -1746,9 +1903,9 @@ install_unl0kr() {
|
|||||||
# perfectly good unl0kr install on every run.
|
# perfectly good unl0kr install on every run.
|
||||||
local -a expect=() missing=()
|
local -a expect=() missing=()
|
||||||
local linux_kver mi m_norm mods_section
|
local linux_kver mi m_norm mods_section
|
||||||
linux_kver=$(arch-chroot "$TARGET" pacman -Ql linux 2>/dev/null \
|
linux_kver=$(arch-chroot "$TARGET" pacman -Ql linux 2>/dev/null |
|
||||||
| sed -n 's|.* /usr/lib/modules/\([^/]*\)/modules\.builtin.*|\1|p' \
|
sed -n 's|.* /usr/lib/modules/\([^/]*\)/modules\.builtin.*|\1|p' |
|
||||||
| head -n1) || linux_kver=""
|
head -n1) || linux_kver=""
|
||||||
for m in "${keep[@]+"${keep[@]}"}"; do
|
for m in "${keep[@]+"${keep[@]}"}"; do
|
||||||
mi=$(arch-chroot "$TARGET" modinfo -k "$linux_kver" "$m" 2>/dev/null || true)
|
mi=$(arch-chroot "$TARGET" modinfo -k "$linux_kver" "$m" 2>/dev/null || true)
|
||||||
if ! grep -q '(builtin)' <<<"$mi"; then
|
if ! grep -q '(builtin)' <<<"$mi"; then
|
||||||
@@ -1756,8 +1913,8 @@ install_unl0kr() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
log "unl0kr image check: kernel=${linux_kver:-unknown} expect=${expect[*]:-none}"
|
log "unl0kr image check: kernel=${linux_kver:-unknown} expect=${expect[*]:-none}"
|
||||||
mods_section=$(awk '/==> Included modules/{f=1;next} f&&/^==>/{exit} f' <<< "$analysis" \
|
mods_section=$(awk '/==> Included modules/{f=1;next} f&&/^==>/{exit} f' <<<"$analysis" |
|
||||||
| tr -s ' \t' '\n' | tr '_' '-')
|
tr -s ' \t' '\n' | tr '_' '-')
|
||||||
for m in "${expect[@]+"${expect[@]}"}"; do
|
for m in "${expect[@]+"${expect[@]}"}"; do
|
||||||
m_norm=${m//_/-}
|
m_norm=${m//_/-}
|
||||||
grep -qxF -- "$m_norm" <<<"$mods_section" || missing+=("$m")
|
grep -qxF -- "$m_norm" <<<"$mods_section" || missing+=("$m")
|
||||||
@@ -1807,8 +1964,8 @@ install_none_profile() {
|
|||||||
owner=$(target_owner)
|
owner=$(target_owner)
|
||||||
|
|
||||||
local shell_name
|
local shell_name
|
||||||
shell_name=$(arch-chroot "$TARGET" getent passwd "$USERNAME" \
|
shell_name=$(arch-chroot "$TARGET" getent passwd "$USERNAME" |
|
||||||
| awk -F: '{print $NF}') || shell_name="/bin/bash"
|
awk -F: '{print $NF}') || shell_name="/bin/bash"
|
||||||
|
|
||||||
local profile=""
|
local profile=""
|
||||||
case "$shell_name" in
|
case "$shell_name" in
|
||||||
@@ -1838,8 +1995,8 @@ FISH
|
|||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
if [[ -f "${TARGET}${home_in_target}/.bash_profile" \
|
if [[ -f "${TARGET}${home_in_target}/.bash_profile" ||
|
||||||
|| ! -f "${TARGET}${home_in_target}/.profile" ]]; then
|
! -f "${TARGET}${home_in_target}/.profile" ]]; then
|
||||||
profile="${home_in_target}/.bash_profile"
|
profile="${home_in_target}/.bash_profile"
|
||||||
else
|
else
|
||||||
profile="${home_in_target}/.profile"
|
profile="${home_in_target}/.profile"
|
||||||
@@ -1870,6 +2027,21 @@ FISH
|
|||||||
log "tty profile: ${profile} (exec Hyprland on VT1)"
|
log "tty profile: ${profile} (exec Hyprland on VT1)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Runs after install_unl0kr, which resets UNL0KR to false when it falls
|
||||||
|
# back to the systemd initramfs - so UNL0KR still being true here means
|
||||||
|
# unl0kr really is in the image and no login manager is wanted.
|
||||||
|
resolve_login_manager() {
|
||||||
|
[[ "$LOGIN_MANAGER_SKIP" == "true" ]] || return 0
|
||||||
|
if [[ "$UNL0KR" == "true" ]]; then
|
||||||
|
LOGIN_MANAGER="none"
|
||||||
|
info "unl0kr is installed - skipping the login manager (tty1 + Hyprland)."
|
||||||
|
log "Login manager: none (unl0kr installed)"
|
||||||
|
else
|
||||||
|
warn "unl0kr fell back - using the fallback login manager: ${LOGIN_MANAGER}."
|
||||||
|
log "Login manager: fallback ${LOGIN_MANAGER} (unl0kr not installed)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
install_login_manager() {
|
install_login_manager() {
|
||||||
info "Configuring login manager: ${LOGIN_MANAGER}..."
|
info "Configuring login manager: ${LOGIN_MANAGER}..."
|
||||||
|
|
||||||
@@ -1879,8 +2051,8 @@ install_login_manager() {
|
|||||||
for u in greetd sddm gdm; do
|
for u in greetd sddm gdm; do
|
||||||
if [[ -f "${TARGET}/usr/lib/systemd/system/${u}.service" ]]; then
|
if [[ -f "${TARGET}/usr/lib/systemd/system/${u}.service" ]]; then
|
||||||
run_logged "Could not disable ${u}.service" \
|
run_logged "Could not disable ${u}.service" \
|
||||||
arch-chroot "$TARGET" systemctl disable "$u.service" \
|
arch-chroot "$TARGET" systemctl disable "$u.service" ||
|
||||||
|| warn "Could not disable ${u}.service (output above)."
|
warn "Could not disable ${u}.service (output above)."
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
rm -f "${TARGET}/etc/systemd/system/getty@tty1.service.d/autologin.conf"
|
rm -f "${TARGET}/etc/systemd/system/getty@tty1.service.d/autologin.conf"
|
||||||
@@ -1910,8 +2082,8 @@ install_login_manager() {
|
|||||||
# this version does not (ArchWiki: user = "greeter").
|
# this version does not (ArchWiki: user = "greeter").
|
||||||
if ! arch-chroot "$TARGET" id greeter >/dev/null 2>&1; then
|
if ! arch-chroot "$TARGET" id greeter >/dev/null 2>&1; then
|
||||||
run_logged "Could not create the greeter user" \
|
run_logged "Could not create the greeter user" \
|
||||||
arch-chroot "$TARGET" useradd -r -M -s /usr/bin/nologin greeter \
|
arch-chroot "$TARGET" useradd -r -M -s /usr/bin/nologin greeter ||
|
||||||
|| warn "Could not create the greeter user (output above)."
|
warn "Could not create the greeter user (output above)."
|
||||||
fi
|
fi
|
||||||
mkdir -p "${TARGET}/etc/greetd"
|
mkdir -p "${TARGET}/etc/greetd"
|
||||||
cat >"${TARGET}/etc/greetd/config.toml" <<'TOML'
|
cat >"${TARGET}/etc/greetd/config.toml" <<'TOML'
|
||||||
@@ -1975,6 +2147,19 @@ TOML
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
ensure_hyprland_session
|
ensure_hyprland_session
|
||||||
|
# gdm (autologin included) starts the session stored in the user's
|
||||||
|
# AccountsService record; without one it picks gnome.desktop, which
|
||||||
|
# the gdm install drags in. Pin Hyprland.
|
||||||
|
local as_dir="${TARGET}/var/lib/AccountsService/users"
|
||||||
|
mkdir -p "$as_dir"
|
||||||
|
cat >"${as_dir}/${USERNAME}" <<'ACCOUNT'
|
||||||
|
[User]
|
||||||
|
Session=hyprland
|
||||||
|
SessionType=wayland
|
||||||
|
SystemAccount=false
|
||||||
|
ACCOUNT
|
||||||
|
chmod 600 "${as_dir}/${USERNAME}"
|
||||||
|
log "gdm: AccountsService session pinned to hyprland"
|
||||||
if run_logged "Could not enable gdm.service" \
|
if run_logged "Could not enable gdm.service" \
|
||||||
arch-chroot "$TARGET" systemctl enable gdm.service; then
|
arch-chroot "$TARGET" systemctl enable gdm.service; then
|
||||||
run_logged "Could not disable getty@tty1.service" \
|
run_logged "Could not disable getty@tty1.service" \
|
||||||
@@ -2122,6 +2307,53 @@ EOF
|
|||||||
|
|
||||||
# ── Cleanup ───────────────────────────────────────────────────────────────────
|
# ── Cleanup ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Last install step: the system is already bootable, so every failure here
|
||||||
|
# only warns. Re-ranks the Arch mirrorlist (the shipped one is whatever the
|
||||||
|
# ISO build had), refreshes the keyrings first so newer packages' signatures
|
||||||
|
# verify, then -Syu. That also moves packages pacstrap took from [core] onto
|
||||||
|
# the [cachyos] builds the target's pacman.conf now prefers. A new kernel
|
||||||
|
# triggers the mkinitcpio hook with the final HOOKS, and grub.cfg names
|
||||||
|
# /vmlinuz-linux etc. without versions, so the bootloader stays valid.
|
||||||
|
install_update() {
|
||||||
|
info "Updating mirrors and the installed system..."
|
||||||
|
sync_target_dbs
|
||||||
|
|
||||||
|
local ml="${TARGET}/etc/pacman.d/mirrorlist"
|
||||||
|
if [[ -x "${TARGET}/usr/bin/reflector" ]]; then
|
||||||
|
cp -a "$ml" "${ml}.alvin-bak" 2>/dev/null || true
|
||||||
|
if run_logged -s dot -t "Ranking Arch mirrors (reflector)" "reflector failed" \
|
||||||
|
arch-chroot "$TARGET" reflector --protocol https --age 12 --latest 20 \
|
||||||
|
--sort rate --save /etc/pacman.d/mirrorlist; then
|
||||||
|
log "Update: mirrorlist re-ranked by reflector (backup: mirrorlist.alvin-bak)"
|
||||||
|
else
|
||||||
|
[[ -s "${ml}.alvin-bak" ]] && cp -a "${ml}.alvin-bak" "$ml"
|
||||||
|
warn "reflector failed (output above) - keeping the shipped mirrorlist."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Update: reflector not installed in target - mirrorlist left as shipped"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a keyrings=()
|
||||||
|
mapfile -t keyrings < <(arch-chroot "$TARGET" pacman -Qq \
|
||||||
|
archlinux-keyring cachyos-keyring chaotic-keyring 2>/dev/null || true)
|
||||||
|
if ((${#keyrings[@]} > 0)); then
|
||||||
|
run_logged -s dot -t "Updating keyrings" "keyring update failed" \
|
||||||
|
arch-chroot "$TARGET" pacman -Sy --needed --noconfirm "${keyrings[@]}" ||
|
||||||
|
warn "Keyring update failed (output above) - trying the upgrade anyway."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if run_logged -s dot -t "Updating the installed system (pacman -Syu)" \
|
||||||
|
"pacman -Syu failed" \
|
||||||
|
arch-chroot "$TARGET" pacman -Syu --noconfirm; then
|
||||||
|
UPDATE_STATUS="up to date"
|
||||||
|
log "Update: pacman -Syu complete"
|
||||||
|
ok "Installed system is up to date."
|
||||||
|
else
|
||||||
|
UPDATE_STATUS="FAILED - run sudo pacman -Syu after boot"
|
||||||
|
warn "pacman -Syu failed (output above) - the system boots; run 'sudo pacman -Syu' after first login."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
install_cleanup() {
|
install_cleanup() {
|
||||||
info "Cleaning up..."
|
info "Cleaning up..."
|
||||||
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
|
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
|
||||||
@@ -2143,7 +2375,7 @@ install_cleanup() {
|
|||||||
"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}" \
|
"SSH server: ${ssh_state} System update: ${UPDATE_STATUS}" \
|
||||||
"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 ""
|
||||||
@@ -2160,11 +2392,33 @@ install_cleanup() {
|
|||||||
gum pager <"$LOG" || true
|
gum pager <"$LOG" || true
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
offer_chroot
|
||||||
# Last line on screen after the optional log view - the instruction to
|
# Last line on screen after the optional log view - the instruction to
|
||||||
# reboot must be the final thing the user reads.
|
# reboot must be the final thing the user reads.
|
||||||
echo "Run 'reboot' to boot into your new system."
|
echo "Run 'reboot' to boot into your new system."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Optional shell inside the installed system. $TARGET (and cryptroot) are
|
||||||
|
# still mounted here - cleanup() only tears them down afterwards.
|
||||||
|
# Also called from the EXIT trap when a step after pacstrap failed ($1 then
|
||||||
|
# carries the failure prompt).
|
||||||
|
offer_chroot() {
|
||||||
|
gum confirm --affirmative "Open chroot" --negative "No thanks" \
|
||||||
|
"${1:-Chroot into the installed system to make changes before rebooting?}" ||
|
||||||
|
return 0
|
||||||
|
remove_temp_sudoers
|
||||||
|
# Copy first so the log is readable from inside (/var/log/alvin-install.log).
|
||||||
|
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
|
||||||
|
info "Entering ${TARGET} - type 'exit' to leave and finish."
|
||||||
|
log "Chroot: interactive shell opened"
|
||||||
|
# The user's last command may fail; that must not trip errexit/ERR.
|
||||||
|
arch-chroot "$TARGET" /bin/bash || true
|
||||||
|
log "Chroot: shell closed"
|
||||||
|
# Keep the copy in the target current with the lines logged above.
|
||||||
|
cp "$LOG" "${TARGET}/var/log/alvin-install.log" 2>/dev/null || true
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
@@ -2205,8 +2459,10 @@ main() {
|
|||||||
install_bootloader
|
install_bootloader
|
||||||
install_tpm
|
install_tpm
|
||||||
install_unl0kr
|
install_unl0kr
|
||||||
|
resolve_login_manager
|
||||||
install_login_manager
|
install_login_manager
|
||||||
install_autologin
|
install_autologin
|
||||||
|
install_update
|
||||||
install_cleanup
|
install_cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# pre-commit hook: run ShellCheck on every shell script in the repo.
|
||||||
|
#
|
||||||
|
# This mirrors the CI check in .gitea/workflows/tests.yaml so the local
|
||||||
|
# lint and the CI lint cannot drift apart. Enable it in a clone with:
|
||||||
|
#
|
||||||
|
# git config core.hooksPath githooks
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
echo "pre-commit: shellcheck not found on PATH, skipping lint." >&2
|
||||||
|
echo "pre-commit: install it with 'pacman -S shellcheck'; CI still enforces it." >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Same file set as CI: *.sh plus the extensionless bash scripts in
|
||||||
|
# airootfs/usr/local/bin picked up by their shebang.
|
||||||
|
# find's stderr is dropped because localrepo/end4-pkgbuilds/*/pkg is left
|
||||||
|
# at mode d--x--x--x by makepkg and prints "Permission denied" locally.
|
||||||
|
sh_files=$(
|
||||||
|
{
|
||||||
|
find . -name '*.sh' -not -path './.git/*' 2>/dev/null
|
||||||
|
find airootfs/usr/local/bin -type f ! -name '*.sh' \
|
||||||
|
-exec grep -lE '^#!(/usr/bin/env bash|/bin/bash)' {} + 2>/dev/null
|
||||||
|
} | sed '/^$/d' | sort -u
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ -z "$sh_files" ]; then
|
||||||
|
echo "pre-commit: no shell scripts found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
while IFS= read -r f; do
|
||||||
|
if shellcheck -x -s bash "$f"; then
|
||||||
|
echo " ok $f"
|
||||||
|
else
|
||||||
|
echo " FAIL $f" >&2
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done <<< "$sh_files"
|
||||||
|
|
||||||
|
if [ "$failed" -ne 0 ]; then
|
||||||
|
echo "" >&2
|
||||||
|
echo "pre-commit: ShellCheck failed. Fix the issues above, or bypass with:" >&2
|
||||||
|
echo " git commit --no-verify" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user