feat(iso): build end-4 deps in-image, ship end4-pC as default shell
Tests / tests (push) Successful in 2m35s

- customize_airootfs.sh stages dots-hyprland at /opt, runs ./setup
  install-deps as a throwaway builder, harvests the built packages into
  /opt/localrepo so pacstrap resolves them offline
- image ships its own [local-aur] (file:///opt/localrepo); CI and
  localrepo/update.sh keep that copy in sync; .gitignore covers work/,
  out/ and airootfs/opt/localrepo/
- alvin-install runs ./setup install --skip-alldeps on the target
- clone pctrade/end4-pC into /etc/skel/.config/quickshell, patch qsConfig
  ii -> end4-pC in the staged variables.lua (guarded + idempotent), and
  restore the fork after setup rsync --delete's ~/.config/quickshell
- ship user presets in skel, point fish `q` alias at end4-pC
- AGENTS.md: document the fourth package source and new gotchas
This commit is contained in:
alvinlollo
2026-09-27 10:31:49 +10:00
parent 5ffd5d0d82
commit 83ed87f6a9
11 changed files with 1779 additions and 6 deletions
+175
View File
@@ -57,6 +57,181 @@ if [[ ! -d "${NVIM_DATA}/lazy/lazy.nvim" ]]; then
https://github.com/folke/lazy.nvim.git "${NVIM_DATA}/lazy/lazy.nvim"
fi
# ── end-4 dots-hyprland, staged for alvin-install (avoids a clone at install) ─
DOTS_REPO="/opt/dots-hyprland"
if [[ ! -d "${DOTS_REPO}/.git" ]]; then
echo "[customize] Cloning end-4 dots-hyprland..."
git clone --depth=1 https://github.com/end-4/dots-hyprland.git "${DOTS_REPO}"
git -C "${DOTS_REPO}" submodule update --init --depth=1
fi
# ── end4-pC: the default quickshell config, shipped in skel ──────────────────
# pctrade/end4-pC is a fork of the ii shell config. It lives next to ii in
# ~/.config/quickshell and does not modify it; which one runs is decided by
# qsConfig (patched just below). The repo has no submodules.
if [[ ! -d "${SKEL}/.config/quickshell/end4-pC" ]]; then
echo "[customize] Cloning end4-pC quickshell config into skel..."
git clone --depth=1 https://github.com/pctrade/end4-pC.git \
"${SKEL}/.config/quickshell/end4-pC"
fi
# ── Default quickshell config: ii -> end4-pC ─────────────────────────────────
# alvin-install copies this staged repo onto the target and runs `./setup
# install`, whose rsync writes hypr/hyprland/variables.lua into the user's
# home - so patching the staged copy is what sets qsConfig on the installed
# system. Re-running is a no-op; anything other than ii/end4-pC aborts.
VARIABLES_LUA="${DOTS_REPO}/dots/.config/hypr/hyprland/variables.lua"
if grep -Fq 'hl.env("qsConfig", "ii")' "${VARIABLES_LUA}"; then
echo "[customize] Setting qsConfig to end4-pC..."
sed -i 's|hl\.env("qsConfig", "ii")|hl.env("qsConfig", "end4-pC")|' "${VARIABLES_LUA}"
fi
if ! grep -Fq 'hl.env("qsConfig", "end4-pC")' "${VARIABLES_LUA}"; then
echo "[customize] qsConfig is neither ii nor end4-pC in ${VARIABLES_LUA}" >&2
exit 1
fi
# ============================================================================
# Pre-install end-4 dependencies into the image (network heavy, once per ISO)
# ============================================================================
# ./setup install-deps resolves and *builds* every AUR/source dependency the
# dotfiles need (quickshell, MicroTeX, theming, fonts, ...). Doing it here lets
# alvin-install run its end-4 step with --skip-alldeps, so an install only
# copies files - no AUR clones, no makepkg, no dependency resolution.
#
# The script refuses to run as root, hence the throwaway builder account.
# ============================================================================
BUILD_USER="ii-builder"
if ! id -u "${BUILD_USER}" &>/dev/null; then
useradd -m -u 1500 "${BUILD_USER}"
fi
echo "${BUILD_USER} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/99-ii-builder
chmod 440 /etc/sudoers.d/99-ii-builder
# makepkg writes src/, pkg/ and *.pkg.tar.zst into the PKGBUILD directories,
# so the tree must be owned by the account that runs it.
chown -R "${BUILD_USER}:${BUILD_USER}" "${DOTS_REPO}"
# -f: never prompt (ask=false); -s: skip "pacman -Syu";
# --skip-plasmaintg: optional Firefox widget that pulls 410 pkgs / 526 MiB of
# KDE packages onto a non-KDE system;
# stdin: answer end-4's x() failure prompt with "e", so a failed command
# aborts the build instead of blocking forever without a TTY.
# shellcheck disable=SC2024 # the redirect is meant for the child, not sudo
if ! sudo -u "${BUILD_USER}" env HOME="/home/${BUILD_USER}" \
GIT_TERMINAL_PROMPT=0 \
bash -c "cd ${DOTS_REPO} && ./setup install-deps -f -s --skip-plasmaintg" \
< <(yes e); then
echo "[customize] end-4 ./setup install-deps failed" >&2
exit 1
fi
# ── Harvest the packages install-deps just built into the in-image local repo ─
# The live /etc/pacman.conf points [local-aur] at /opt/localrepo; without the
# harvested copies pacstrap cannot resolve these packages at install time and
# would silently drop them from the target system.
mkdir -p /opt/localrepo
shopt -s nullglob
# <name> <version> from a package's embedded .PKGINFO (update.sh does the same)
pkg_meta() {
tar -xOf "$1" .PKGINFO 2>/dev/null |
awk -F' = ' '/^pkgname = /{n=$2} /^pkgver = /{v=$2} END{print n, v}'
}
mapfile -d '' -t built_pkgs < <(
find "${DOTS_REPO}" "/home/${BUILD_USER}/.cache/yay" \
-name '*.pkg.tar.zst' -print0 2>/dev/null
)
declare -A built_file=() built_ver=()
for p in "${built_pkgs[@]}"; do
read -r n v < <(pkg_meta "$p")
[[ -n "${n:-}" && -n "${v:-}" ]] || continue
built_file["$n"]="$p"
built_ver["$n"]="$v"
done
# One entry per package name, and the entry must be the version the image
# actually installed - a stale copy with the same name but another version
# would let pacstrap install something else than what was tested.
for existing in /opt/localrepo/*.pkg.tar.zst; do
[[ -e "$existing" ]] || continue
read -r n v < <(pkg_meta "$existing")
[[ -n "${n:-}" ]] || continue
if [[ -n "${built_ver[$n]:-}" ]]; then
echo "[customize] Replacing localrepo ${n} ${v} with built ${built_ver[$n]}"
rm -f "$existing"
fi
done
if ((${#built_file[@]} > 0)); then
echo "[customize] Harvesting ${#built_file[@]} package(s) into /opt/localrepo"
cp -f -- "${built_file[@]}" /opt/localrepo/
# makepkg -Afsi leaves its archives next to the PKGBUILD. They are already
# harvested (and installed in this image); keeping them in the dots tree
# would ship a second copy inside /opt/dots-hyprland, which alvin-install
# copies to ~/.dots-hyprland on every installed system.
find "${DOTS_REPO}" -type f -name '*.pkg.tar.zst' -delete
fi
# Rebuild the repo database (CI/update.sh ship a pre-built one alongside the
# packages) and refresh the live system's copy of it. No network needed: the
# remote repos were synced during pacstrap.
local_pkgs=(/opt/localrepo/*.pkg.tar.zst)
if ((${#local_pkgs[@]} > 0)); then
echo "[customize] Rebuilding local-aur db (${#local_pkgs[@]} packages)..."
repo-add -R /opt/localrepo/local-aur.db.tar.gz "${local_pkgs[@]}"
mkdir -p /var/lib/pacman/sync
cp -fL /opt/localrepo/local-aur.db /var/lib/pacman/sync/local-aur.db
if [[ -f /opt/localrepo/local-aur.files ]]; then
cp -fL /opt/localrepo/local-aur.files /var/lib/pacman/sync/local-aur.files
fi
fi
# ── makepkg leftovers + throwaway account do not belong in the ISO ──────────
if [[ -d "${DOTS_REPO}/sdata/dist-arch" ]]; then
find "${DOTS_REPO}/sdata/dist-arch" -mindepth 2 -maxdepth 2 -type d \
\( -name src -o -name pkg \) -prune -exec rm -rf {} +
fi
rm -f /etc/sudoers.d/99-ii-builder
if ! userdel -r "${BUILD_USER}" 2>/dev/null; then
rm -rf "/home/${BUILD_USER:?}"
userdel "${BUILD_USER}"
fi
chown -R root:root "${DOTS_REPO}"
# ── Google Sans Flex into skel: install_google_sans_flex() early-returns once
# fc-list sees the font, so the install step needs no network ──────────────
FONT_DIR="${SKEL}/.local/share/fonts/illogical-impulse-google-sans-flex"
if [[ ! -f "${FONT_DIR}/.git/HEAD" ]]; then
echo "[customize] Pre-installing Google Sans Flex into skel..."
rm -rf "${FONT_DIR}"
git clone --depth=1 --recurse-submodules --shallow-submodules \
https://github.com/end-4/google-sans-flex "${FONT_DIR}"
fi
# ── uv into skel: CPython 3.12 + the end-4 requirements wheel cache ──────────
# install-python-packages() runs `uv venv -p 3.12` and `uv pip install -r ...`
# during setup. Arch has no python312 package (uv would download one) and the
# pinned requirements are ~250 MiB of wheels, so warm both into skel: the venv
# is then created and filled straight from the image.
if command -v uv &>/dev/null; then
echo "[customize] Pre-warming uv (CPython 3.12 + requirements cache)..."
warm_venv="/tmp/uv-warm-venv"
rm -rf "${warm_venv}"
UV_CACHE_DIR="${SKEL}/.cache/uv" \
UV_PYTHON_INSTALL_DIR="${SKEL}/.local/share/uv/python" \
uv venv -p 3.12 "${warm_venv}"
UV_CACHE_DIR="${SKEL}/.cache/uv" \
UV_PYTHON_INSTALL_DIR="${SKEL}/.local/share/uv/python" \
uv pip install --python "${warm_venv}/bin/python" \
-r "${DOTS_REPO}/sdata/uv/requirements.txt"
rm -rf "${warm_venv}"
else
echo "[customize] uv not found - skipping python/wheel pre-warm."
fi
# ── Fish: ensure default shell is set for the live user ──────────────────────
# The live user (usually 'archlinux' or 'root') gets fish as default shell
if command -v fish &>/dev/null; then