- Replace the 644/755 chmod sweep (which also undid its own chmod 700) with go-w,a+rX so non-.sh executables keep +x. - Install an oh-my-zsh .zshrc into /etc/skel after pacstrap. It is staged as /root/.zshrc-skel because grml-zsh-config owns /etc/skel/.zshrc and an airootfs copy would abort pacstrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
379 lines
17 KiB
Bash
379 lines
17 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# The chroot's /dev is the kernel's single shared devtmpfs, which only gets the
|
|
# fd/stdin/stdout/stderr symlinks at boot (from mkinitcpio's init). If the
|
|
# runner environment lacks them, process substitution (< <(...)) and upstream
|
|
# tools (pacman-key) fail with "… /dev/fd/63: No such file or directory".
|
|
# Self-heal here; the CI build-iso workflow does the same before pacstrap so
|
|
# that mkinitcpio's hook (which pipes through /dev/stdin) works too.
|
|
for _link in "fd:/proc/self/fd" "stdin:/proc/self/fd/0" \
|
|
"stdout:/proc/self/fd/1" "stderr:/proc/self/fd/2"; do
|
|
[[ -e "/dev/${_link%%:*}" || -L "/dev/${_link%%:*}" ]] ||
|
|
ln -sfn "${_link#*:}" "/dev/${_link%%:*}"
|
|
done
|
|
unset _link
|
|
|
|
# Populate the keyring in the built root from the keyring bundles
|
|
# (/usr/share/pacman/keyrings, incl. the copied cachyos keyring).
|
|
# This mirrors the guide; it runs inside the image you are making.
|
|
# pacman-key --populate needs no network.
|
|
pacman-key --init
|
|
pacman-key --populate archlinux cachyos chaotic
|
|
|
|
# Restore the strict live /etc/pacman.conf after pacstrap installs the
|
|
# 'pacman' package, which ships a stock /etc/pacman.conf and would otherwise
|
|
# overwrite this directory's configured version.
|
|
install -m 0644 /root/.live-pacman.conf /etc/pacman.conf
|
|
rm -f /root/.live-pacman.conf
|
|
|
|
# Build stamp staged by profiledef.sh (absent for non-git, non-CI builds):
|
|
# keep the stamp file itself as a reference, drop the staging copy, and
|
|
# stamp the live medium's motd so the booted ISO identifies its build (the
|
|
# installed system keeps the stock package motd - alvin-install never
|
|
# writes /etc/motd).
|
|
if [[ -f /root/.iso-build-stamp ]]; then
|
|
install -m 0644 /root/.iso-build-stamp /etc/alvin-build-stamp
|
|
cat /etc/alvin-build-stamp >> /etc/motd
|
|
rm -f /root/.iso-build-stamp
|
|
fi
|
|
|
|
# ============================================================================
|
|
# Pre-install dotfile dependencies into /etc/skel so new users get them
|
|
# instantly on first login (no network required).
|
|
# ============================================================================
|
|
|
|
SKEL="/etc/skel"
|
|
|
|
# ── Oh-My-Zsh ────────────────────────────────────────────────────────────────
|
|
if [[ ! -d "${SKEL}/.oh-my-zsh" ]]; then
|
|
echo "[customize] Cloning Oh-My-Zsh into skel..."
|
|
git clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git "${SKEL}/.oh-my-zsh"
|
|
fi
|
|
|
|
# Oh-My-Zsh custom plugins
|
|
ZSH_CUSTOM="${SKEL}/.oh-my-zsh/custom"
|
|
mkdir -p "${ZSH_CUSTOM}/plugins"
|
|
|
|
declare -A OMZ_PLUGINS=(
|
|
[zsh-history-substring-search]="https://github.com/zsh-users/zsh-history-substring-search.git"
|
|
[zsh-autosuggestions]="https://github.com/zsh-users/zsh-autosuggestions.git"
|
|
[zsh-eza]="https://github.com/z-shell/zsh-eza.git"
|
|
[fzf-zsh-plugin]="https://github.com/unixorn/fzf-zsh-plugin.git"
|
|
[zsh-syntax-highlighting]="https://github.com/zsh-users/zsh-syntax-highlighting.git"
|
|
[zsh-autocomplete]="https://github.com/marlonrichert/zsh-autocomplete.git"
|
|
)
|
|
|
|
for plugin in "${!OMZ_PLUGINS[@]}"; do
|
|
target="${ZSH_CUSTOM}/plugins/${plugin}"
|
|
if [[ ! -d "${target}" ]]; then
|
|
echo "[customize] Cloning OMZ plugin: ${plugin}"
|
|
git clone --depth=1 "${OMZ_PLUGINS[${plugin}]}" "${target}"
|
|
fi
|
|
done
|
|
|
|
# grml-zsh-config owns /etc/skel/.zshrc, so shipping ours in
|
|
# airootfs/etc/skel would make pacstrap abort ("exists in filesystem").
|
|
# Stage it under /root and overwrite the package's copy here, after pacstrap.
|
|
if [[ -f /root/.zshrc-skel ]]; then
|
|
install -m 0644 /root/.zshrc-skel "${SKEL}/.zshrc"
|
|
rm -f /root/.zshrc-skel
|
|
fi
|
|
|
|
# ── LazyVim (lazy.nvim bootstraps on first launch, but pre-clone the plugin) ─
|
|
NVIM_DATA="${SKEL}/.local/share/nvim"
|
|
mkdir -p "${NVIM_DATA}"
|
|
if [[ ! -d "${NVIM_DATA}/lazy/lazy.nvim" ]]; then
|
|
echo "[customize] Pre-cloning lazy.nvim for LazyVim..."
|
|
git clone --filter=blob:none --branch=stable \
|
|
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
|
|
|
|
# end-4 pins quickshell to 41651d7 (Sep 25), which predates quickshell's
|
|
# Qt 6.12 fix (5d5d498, Oct 3: "use MOC includes instead of opaque pointer
|
|
# definitions - Fixes linking issues and Qt 6.12 compatibility"). Arch
|
|
# shipped qt6 6.12.0 into extra on Oct 8; with the old pin qmetatype.h's
|
|
# "Meta Types must be fully defined" static_assert kills the moc build of
|
|
# quickshell-hyprland-ipc (failed CI run 137) and the whole ISO build with
|
|
# it. Force the fixed commit, but ONLY while end-4 still carries the old
|
|
# pin - once they bump it themselves the guard stops matching and their
|
|
# pin wins untouched. localrepo/check-quickshell-pin.sh (weekly CI)
|
|
# verifies both this pin and end-4's stay Qt 6.12-compatible.
|
|
QS_PKG="${DOTS_REPO}/sdata/dist-arch/illogical-impulse-quickshell-git/PKGBUILD"
|
|
if [[ -f "${QS_PKG}" ]] && grep -q "^_commit='41651d7" "${QS_PKG}"; then
|
|
echo "[customize] Bumping end-4 quickshell pin to 11ca60be (Qt 6.12 fix)..."
|
|
sed -i "s|^_commit='41651d7[^']*'|_commit='11ca60be22b063478ed9586ca1d7f92f0f261caf'|" "${QS_PKG}"
|
|
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
|
|
|
|
# Processes still running as BUILD_USER make userdel exit 8 and abort the
|
|
# build. end-4's ./setup is the usual source: sudo_init_keepalive forks
|
|
# `(sleep 60; sudo true) &` and its EXIT trap kills only the subshell,
|
|
# orphaning the sleep child for up to a minute. Mirrors shadow's
|
|
# user_busy_processes(): match real/effective/saved uid AND require the
|
|
# process to live inside this build chroot - the runner shares our PID
|
|
# namespace, and a host process could carry the same uid.
|
|
#
|
|
# mkarchiso runs this script via arch-chroot, which mounts proc *before*
|
|
# `unshare --fork --pid`: /proc entry numbers belong to the outer pid
|
|
# namespace, while kill(2) resolves pids in our (nested) namespace. Use the
|
|
# last NSpid field - the pid in our own namespace - for signalling; keep the
|
|
# /proc entry number for logging, since that is what userdel reports.
|
|
nspid_fields() {
|
|
awk -F'\t' '/^NSpid:/{ $1=""; sub(/^ +/, ""); print; exit }' "$1"
|
|
}
|
|
|
|
builder_procs() {
|
|
local uid broot st p key real eff saved kpid cmd
|
|
local -a nsf self_nsf=()
|
|
uid=$(id -u "${BUILD_USER}") || return 0
|
|
broot=$(stat -Lc '%d:%i' /)
|
|
read -r -a self_nsf < <(nspid_fields /proc/self/status)
|
|
for st in /proc/[0-9]*/status; do
|
|
[[ -r "$st" ]] || continue
|
|
real=""; eff=""; saved=""
|
|
while read -r key real eff saved _; do
|
|
[[ $key == "Uid:" ]] && break
|
|
done < "$st" || continue
|
|
[[ $real == "$uid" || $eff == "$uid" || $saved == "$uid" ]] || continue
|
|
p=${st#/proc/}; p=${p%/status}
|
|
# Stat of /proc/<pid>/root needs CAP_SYS_PTRACE (or same-uid): exactly
|
|
# what shadow's userdel uses, so a process we cannot see here is one
|
|
# userdel cannot flag either. Compare by device+inode; skip on failure.
|
|
[[ $(stat -Lc '%d:%i' "/proc/$p/root" 2>/dev/null) == "$broot" ]] || continue
|
|
# NSpid chain length must equal ours, i.e. the process must live in our
|
|
# pid namespace - only then is its last field killable from here.
|
|
nsf=()
|
|
read -r -a nsf < <(nspid_fields "/proc/$p/status") || continue
|
|
if ((${#nsf[@]} > 0 && ${#nsf[@]} == ${#self_nsf[@]})); then
|
|
kpid=${nsf[${#nsf[@]} - 1]}
|
|
else
|
|
kpid="-"
|
|
fi
|
|
cmd=$(tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null)
|
|
printf '%s\t%s\t%s\n' "$p" "$kpid" "$cmd"
|
|
done
|
|
return 0
|
|
}
|
|
|
|
sweep_builder_procs() {
|
|
local sig=$1 p kpid cmd kerr
|
|
local -a pids=()
|
|
while IFS=$'\t' read -r p kpid cmd; do
|
|
[[ -n $p ]] || continue
|
|
if [[ -z $kpid || $kpid == "-" ]]; then
|
|
echo "[customize] ${BUILD_USER} process outside our pid namespace (cannot signal): pid $p: $cmd"
|
|
continue
|
|
fi
|
|
echo "[customize] ${BUILD_USER} process ($sig): pid $p: $cmd"
|
|
pids+=("$kpid")
|
|
done < <(builder_procs)
|
|
if ((${#pids[@]} > 0)); then
|
|
# Surface signal failures (they used to be swallowed, hiding a
|
|
# pid-namespace mismatch as a silent no-op sweep).
|
|
if ! kerr=$(kill -"$sig" "${pids[@]}" 2>&1); then
|
|
[[ -n $kerr ]] && printf '[customize] kill -%s: %s\n' "$sig" "$kerr"
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
sweep_builder_procs TERM
|
|
sleep 2
|
|
sweep_builder_procs KILL
|
|
sleep 1
|
|
|
|
if ! userdel -r "${BUILD_USER}"; then
|
|
rm -rf "/home/${BUILD_USER:?}"
|
|
if ! userdel "${BUILD_USER}"; then
|
|
echo "[customize] FATAL: cannot remove user ${BUILD_USER}; leftover processes:" >&2
|
|
builder_procs >&2 || true
|
|
exit 1
|
|
fi
|
|
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: default shell for the live root user ───────────────────────────────
|
|
# (alvin-install creates the installed user with fish as well.)
|
|
if command -v fish &>/dev/null; then
|
|
echo "[customize] Setting fish as root's shell on the live medium..."
|
|
chsh -s /usr/bin/fish root 2>/dev/null || true
|
|
fi
|
|
|
|
# ── Ensure skel directories have correct permissions ─────────────────────────
|
|
# Only take away group/other write and make everything readable: forcing
|
|
# files to 644 stripped +x from executables that are not *.sh (python/fish
|
|
# helpers in the cloned quickshell configs).
|
|
chmod -R go-w,a+rX "${SKEL}/.config" "${SKEL}/.local" 2>/dev/null || true
|
|
find "${SKEL}/.config" -name '*.sh' -exec chmod 755 {} + 2>/dev/null || true
|
|
|
|
echo "[customize] Dotfile pre-installation complete." |