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
+184 -1
View File
@@ -28,6 +28,7 @@ log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG"; }
cleanup() {
set +e
rm -f "${TARGET}/etc/sudoers.d/99-end4-install" 2>/dev/null
umount -R "$TARGET" 2>/dev/null
cryptsetup close cryptroot 2>/dev/null
true
@@ -443,7 +444,11 @@ Include = /etc/pacman.d/cachyos-mirrorlist
REPO
fi
sed -i '/\[local-aur\]/,/Include/d' "${TARGET}/etc/pacman.conf" 2>/dev/null || true
# [local-aur] lives only on the live medium (file:///opt/localrepo). The
# target's pacman.conf does not normally contain it - pacstrap is run
# without -P, so it keeps the stock file shipped by the pacman package -
# but drop it anyway in case that ever changes.
sed -i '/^\[local-aur\]$/,/^$/d' "${TARGET}/etc/pacman.conf" 2>/dev/null || true
ok "CachyOS repo configured."
}
@@ -520,6 +525,183 @@ install_git() {
ok "Git configured for ${USERNAME}."
}
install_end4_dots() {
if [[ ! -d /opt/dots-hyprland ]]; then
warn "No staged dots-hyprland at /opt/dots-hyprland — skipping end-4 dotfiles."
return
fi
info "Installing end-4 dotfiles (this may take a while)..."
local home_in_target="/home/${USERNAME}"
local repo_in_target="${home_in_target}/.dots-hyprland"
local repo_on_host="${TARGET}${repo_in_target}"
local dots_ok=true
local setups_done=true
cp -a /opt/dots-hyprland "$repo_on_host"
chown -R "${USERNAME}:${USERNAME}" "$repo_on_host"
echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" > "${TARGET}/etc/sudoers.d/99-end4-install"
chmod 440 "${TARGET}/etc/sudoers.d/99-end4-install"
# Everything appended to $LOG from here belongs to this attempt; used
# further down to report which steps end-4 had to skip.
local log_mark=0
if [[ -f "$LOG" ]]; then
log_mark=$(wc -l < "$LOG")
fi
# stdin feeds "i" to end-4's x() [R/e:i] failure prompt, which otherwise
# blocks forever on a TTY whose prompt text is redirected to $LOG.
# Its systemd lines cannot work from inside this chroot: the medium's /run
# is bind-mounted in, so `systemctl --machine=…@.host --user enable … --now`
# has no target session (and --now would start services on the live medium
# anyway) - they fail on every install. "i" lets the run carry on with the
# venv, groups and config sync instead of aborting at the first such line;
# the usable part of that step is re-applied below and every skip is
# reported afterwards.
# --skip-alldeps: every dependency was already installed at ISO build time
# (customize_airootfs.sh runs ./setup install-deps), so this only copies
# configs, sets up services and builds the python venv from the warm cache.
if ! arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env HOME="$home_in_target" bash -c \
"cd ${repo_in_target} && ./setup install -f -s --skip-alldeps" \
< <(yes i) >>"$LOG" 2>&1; then
warn "end-4 setup failed, retrying files only (see $LOG)."
setups_done=false
arch-chroot "$TARGET" runuser -u "$USERNAME" -- \
env HOME="$home_in_target" bash -c \
"cd ${repo_in_target} && ./setup install -f -s --skip-alldeps --skip-allsetups" \
< <(yes i) >>"$LOG" 2>&1 || dots_ok=false
fi
rm -f "${TARGET}/etc/sudoers.d/99-end4-install"
if ! arch-chroot "$TARGET" id -nG "$USERNAME" | grep -qw input; then
arch-chroot "$TARGET" groupadd -f i2c
arch-chroot "$TARGET" usermod -aG video,i2c,input "$USERNAME"
warn "end-4 group setup was missing and has been applied."
fi
# ── steps end-4 cannot take from inside the chroot ───────────────────────
# `enable` alone is a pure file operation and needs no running session, so
# redo those here - without --now, since the target is not booted yet.
local mods_conf="${TARGET}/etc/modules-load.d/i2c-dev.conf"
if [[ ! -f "$mods_conf" ]]; then
mkdir -p "$(dirname "$mods_conf")"
echo i2c-dev > "$mods_conf"
fi
if [[ -f "${TARGET}/usr/lib/systemd/system/bluetooth.service" ]]; then
arch-chroot "$TARGET" systemctl enable bluetooth.service >>"$LOG" 2>&1 \
|| warn "Could not enable bluetooth.service (see $LOG)."
fi
if [[ -f "${TARGET}/usr/lib/systemd/user/ydotool.service" ]]; then
arch-chroot "$TARGET" runuser -u "$USERNAME" -- env HOME="$home_in_target" \
systemctl --user enable ydotool.service >>"$LOG" 2>&1 \
|| warn "Could not enable ydotool.service for ${USERNAME} (see $LOG)."
fi
# Everything end-4 answered "i" to. systemctl/gsettings are the lines known
# to be unusable in this chroot; anything else it had to skip is worth
# telling the user about.
local -a ignored=()
if ((log_mark > 0)); then
mapfile -t ignored < <(
sed -n "$((log_mark + 1)),\$p" "$LOG" | sed $'s/\x1b\\[[0-9;]*m//g' |
sed -n 's/^.*Command "\(.*\)" has failed but ignored by user\..*$/\1/p'
)
fi
if ((${#ignored[@]} > 0)); then
log "end-4 skipped ${#ignored[@]} step(s): ${ignored[*]}"
local step
local -a skipped=()
for step in "${ignored[@]}"; do
case "$step" in
*systemctl* | *gsettings*) ;;
*) skipped+=("$step") ;;
esac
done
if ((${#skipped[@]} > 0)); then
warn "end-4 had to skip: ${skipped[*]} (see $LOG)"
fi
fi
if ! $setups_done; then
warn "end-4 setup did not complete — rerun as ${USERNAME}: cd ~/.dots-hyprland && ./setup install-setups"
fi
# end-4's install step rsync --delete's the whole ~/.config/quickshell
# directory (sdata/subcmd-install/3.files-legacy.sh), which drops the
# end4-pC fork skel put into the user's home - restore it afterwards.
local fork_src="/etc/skel/.config/quickshell/end4-pC"
local fork_dst="${TARGET}${home_in_target}/.config/quickshell/end4-pC"
if [[ -d "$fork_src" ]]; then
mkdir -p "$(dirname "$fork_dst")"
rm -rf "$fork_dst"
cp -a "$fork_src" "$fork_dst"
chown -R "${USERNAME}:${USERNAME}" "$fork_dst"
fi
# Same for fish: end-4 syncs ~/.config/fish with rsync --delete, keeping
# only what upstream ships (auto-Hypr.fish, config.fish, fish_variables),
# which drops skel's extras (fisher/nvm functions, completions,
# fish_plugins) and restores its own `alias q 'qs -c ii'`. The default
# quickshell config on this ISO is end4-pC, so put the extras back and
# repoint that one alias.
local fish_src="/etc/skel/.config/fish"
local fish_dst="${TARGET}${home_in_target}/.config/fish"
if [[ -d "$fish_src" && -d "$fish_dst" ]]; then
rsync -a --exclude config.fish --exclude fish_variables \
"$fish_src/" "$fish_dst/"
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"
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)."
fi
chown -R "${USERNAME}:${USERNAME}" "$fish_dst"
fi
local configs_ok=true
[[ -e "${TARGET}${home_in_target}/.config/hypr/hyprland.lua" \
&& -d "${TARGET}${home_in_target}/.config/quickshell/ii" \
&& -d "${TARGET}${home_in_target}/.config/quickshell/end4-pC" ]] || configs_ok=false
# Files alone are not enough: with --skip-alldeps the dependencies must
# have come from the ISO. hyprland and quickshell are installed at ISO
# build time, so their binaries missing here means pacstrap dropped them
# (they were not resolvable in the live repos). The venv is created from
# 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.
if $configs_ok; then
[[ -x "${TARGET}/usr/bin/hyprctl" && -x "${TARGET}/usr/bin/quickshell" ]] \
|| configs_ok=false
[[ -x "${TARGET}${home_in_target}/.local/state/quickshell/.venv/bin/python" ]] \
|| configs_ok=false
fi
if [[ -d /etc/skel/.config/hypr/custom ]]; then
cp -rfT /etc/skel/.config/hypr/custom "${TARGET}${home_in_target}/.config/hypr/custom"
chown -R "${USERNAME}:${USERNAME}" "${TARGET}${home_in_target}/.config/hypr/custom"
fi
# Presets ship with absolute paths from the build machine; repoint them at
# the home that was just created (the images they name are not shipped).
local presets="${TARGET}${home_in_target}/.config/illogical-impulse/presets"
local preset
for preset in "$presets"/*.json; do
[[ -f "$preset" ]] || continue
sed -i "s|/home/alvin/|/home/${USERNAME}/|g" "$preset"
done
if $dots_ok && $configs_ok; then
log "end-4 dotfiles installed for ${USERNAME} (setups_done=${setups_done} skipped=${#ignored[@]})."
ok "end-4 dotfiles installed."
else
log "end-4 dotfiles incomplete for ${USERNAME} (setup_exit_ok=${dots_ok} configs=${configs_ok} setups=${setups_done} skipped=${#ignored[@]})."
warn "end-4 dotfiles incomplete (see $LOG) — as ${USERNAME}, run: cd ~/.dots-hyprland && ./setup install"
fi
}
install_mkinitcpio() {
info "Generating initramfs..."
@@ -597,6 +779,7 @@ main() {
install_network
install_user
install_git
install_end4_dots
install_mkinitcpio
install_bootloader
install_cleanup