#!/bin/bash # test/entrypoint-test.sh # # Runs inside the docker `test` stage. The repo is mounted read-write at # /dotfiles (see the Makefile's `docker run -v "$PWD":/dotfiles`). # # Two tiers: # 1. DRY PROBES (always, fast, offline) # - `make -n` dry-runs to catch Makefile typos / missing scripts # - `stow --simulate` to prove every stow target resolves # 2. REAL SMOKE (guarded by RUN_SMOKE=1) # - resolves every @pacman entry against the repo DB # - exercises setup/term/setup.sh (pacman + npm) and re-runs it to prove # idempotency # - verifies stow actually creates symlinks into $HOME # AUR + kreation/buntes-monster only run with RUN_FULL=1. # # Env flags (set via `docker run -e`): # RUN_SMOKE=1 run the real pacman+npm install smoke # RUN_FULL=1 also attempt AUR + hypr/kreation/buntes-monster # SKIP_NET=1 pretend no network; skip anything that needs the mirror set -uo pipefail # Resolve the repo root: prefer the container's /dotfiles mount, otherwise # fall back to the directory above this script (a local checkout). if [[ -d /dotfiles ]]; then cd /dotfiles || { echo "FATAL: repo not mounted at /dotfiles"; exit 1; } else cd "$(dirname "${BASH_SOURCE[0]}")/.." || { echo "FATAL: cannot locate repo root"; exit 1; } fi REPO="$(pwd)" # Guard: both resolution paths must find a real checkout. if [[ ! -f "$REPO/Makefile" || ! -d "$REPO/test" ]]; then echo "FATAL: '$REPO' does not look like the dotfiles repo root." >&2 echo " In docker, run with -v \"\$PWD\":/dotfiles ." >&2 exit 1 fi PASS=0 FAIL=0 say_ok() { printf ' ok: %s\n' "$*"; PASS=$((PASS+1)); } say_fail(){ printf ' FAIL: %s\n' "$*"; FAIL=$((FAIL+1)); } header() { printf '\n==== %s ====\n' "$*"; } # Scratch file for captured stderr; kept OUTSIDE the repo mount so it never # leaks into the host workspace (the mount is /dotfiles). ERR_LOG="$(mktemp -t entrypoint.XXXXXX)" cleanup() { rm -f "$ERR_LOG"; } trap cleanup EXIT if command -v git >/dev/null 2>&1 && [[ ! -d .git ]]; then # when mounted from a worktree, .git may be a file; fine. : fi echo "entrypoint-test.sh: running as $(id -un) in $(pwd)" # --------------------------------------------------------------------------- # 1. DRY PROBES - always run, cheap, offline. # --------------------------------------------------------------------------- header "Dry probes" # 1a. Makefile dry runs for every target. # Discover targets dynamically so we never drift from the Makefile. targets=$(grep -oE '^[A-Za-z0-9_-]+:' Makefile | tr -d ':' | grep -v '^test' | sort -u) for target in $targets; do # `make -n` must not error out from missing scripts / dirs. if ( cd "$REPO" && make -n "$target" >/dev/null 2>"$ERR_LOG" ); then say_ok "make -n $target parses" else say_fail "make -n $target (typo? missing script?)" sed 's/^/ /' "$ERR_LOG" 2>/dev/null fi done # 1b. stow --simulate for every package the Makefile stows. while IFS= read -r pkg; do [[ -z "$pkg" ]] && continue if stow --simulate --dir="$REPO" --target="$REPO" "$pkg" 2>"$ERR_LOG"; then say_ok "stow --simulate $pkg resolves" else say_fail "stow --simulate $pkg (non-existent package or conflict)" sed 's/^/ /' "$ERR_LOG" 2>/dev/null fi done < <(grep -P '^\t' Makefile | grep -E '\bstow\b' | awk '{print $NF}' | sort -u) # 1c. run the repo's own lint + probe scripts (they're mounted here). if [[ -x test/lint.sh ]] || [[ -f test/lint.sh ]]; then if bash test/lint.sh >/dev/null 2>&1; then say_ok "test/lint.sh" else say_fail "test/lint.sh (see logs above / rerun outside container)" fi else say_fail "test/lint.sh not present in mount" fi header "Smoke gates" echo " RUN_SMOKE=${RUN_SMOKE:-0} RUN_FULL=${RUN_FULL:-0} SKIP_NET=${SKIP_NET:-0}" # --------------------------------------------------------------------------- # 2. REAL SMOKE (RUN_SMOKE=1) # --------------------------------------------------------------------------- if [[ "${RUN_SMOKE:-0}" == "1" ]]; then header "Real smoke (RUN_SMOKE=1)" # 2a. Allow scripts to use sudo without a password (already NOPASSWD via # the Dockerfile for the wheel group). # # 2b. Override host-daemon commands with no-ops so nothing tries to talk # to systemd/dbus/dbus in the container. mkdir -p "$HOME/bin" cat > "$HOME/bin/systemctl" <<'NOOP' #!/bin/bash echo "[stub] systemctl $*" exit 0 NOOP cat > "$HOME/bin/dbus-update-activation-environment" <<'NOOP' #!/bin/bash echo "[stub] dbus-update-activation-environment $*" exit 0 NOOP cat > "$HOME/bin/kbuildsycoca6" <<'NOOP' #!/bin/bash echo "[stub] kbuildsycoca6 $*" exit 0 NOOP cat > "$HOME/bin/updatedb" <<'NOOP' #!/bin/bash echo "[stub] updatedb $*" exit 0 NOOP # chsh has no functional /etc/passwd in the container; make it a no-op so # set_fish_default_shell doesn't fail (documented in the header comment). cat > "$HOME/bin/chsh" <<'NOOP' #!/bin/bash echo "[stub] chsh $*" exit 0 NOOP # The AUR section is stripped for the smoke, so setup/term/setup.sh only ever # passes repo packages (+ npm) here; route yay through pacman directly. # --noconfirm is needed because the container isn't interactive. cat > "$HOME/bin/yay" <<'NOOP' #!/bin/bash echo "[stub] yay $* (routed through pacman)" sudo pacman -Sy --noconfirm exec sudo pacman --noconfirm "$@" NOOP chmod +x "$HOME/bin/"* export PATH="$HOME/bin:$PATH" # 2c. setup.sh package parsing sanity (mirrors read_packages) for each # setup/$d target. All setup scripts + package lists live in setup/ # (outside the stow packages) and share setup/lib/common.sh. for d in basic term utils hypr kreation; do SETUP_PATH="setup/$d/setup.sh" PKG_PATH="setup/$d/packages.txt" if ( cd "$REPO" && bash -n "$SETUP_PATH" ); then say_ok "$SETUP_PATH syntax OK" else say_fail "$SETUP_PATH syntax error" fi pacman_count=$(awk ' /^@pacman/{s="pacman"; next} /^@aur/{s="aur"; next} /^@npm/{s="npm"; next} /^[[:space:]]*#/{next} /^[[:space:]]*$/{next} s=="pacman"{pc++} s=="aur"{ac++} s=="npm"{nc++} END{printf "%d %d %d", pc,ac,nc} ' "$REPO/$PKG_PATH") read -r PC AC NC <<< "$pacman_count" say_ok "$PKG_PATH -> ${PC} pacman, ${AC} aur, ${NC} npm entries" done # 2c2. Every @pacman entry in every packages.txt must resolve in the # repo DB, using the same resolution as the installer (`pacman -S # --print-format`). Aliases/provides count as resolved (locate -> # plocate, ttf-font-awesome -> woff2-font-awesome), while a typo'd or # AUR-only name (e.g. `rd`) fails here before any install happens. header "package resolvability (@pacman entries)" resolv_fail=0 for d in basic term utils hypr kreation; do PKG_PATH="setup/$d/packages.txt" while IFS= read -r pkg; do [[ -z "$pkg" ]] && continue if pacman -S --print-format '%n' "$pkg" >/dev/null 2>&1; then say_ok "pacman resolves $pkg ($d)" else say_fail "pacman cannot resolve $pkg ($d) - target not found" resolv_fail=1 fi done < <(awk ' BEGIN{s="pacman"} /^@pacman/{s="pacman"; next} /^@aur/{s="skip"; next} /^@npm/{s="skip"; next} /^[[:space:]]*#/{next} /^[[:space:]]*$/{next} s=="pacman"{ sub(/^[^\/]*\//,""); print } ' "$REPO/$PKG_PATH") done # 2d. If network allowed and not skipped, actually install the pacman # and npm sections of setup/term/setup.sh (mirrors `make term` without # AUR; the @aur section stays RUN_FULL-gated since it needs yay/AUR builds). if [[ "${SKIP_NET:-0}" != "1" ]]; then header "make term (pacman + npm packages, AUR skipped)" # Patch packages.txt: keep @pacman and @npm (@npm runs via sudo npm # in the container), drop @aur so we don't hit AUR builds. cp "$REPO/setup/term/packages.txt" "$HOME/packages.txt.full" awk ' /^@aur$/{s="skip"}; /^@npm$/{s="npm"}; /^@pacman$/{s="pacman"} { if (s!="skip" && NF>0 && $1 !~ /^#/) print } ' "$REPO/setup/term/packages.txt" > "$HOME/packages.txt.term-smoke" # pacman-only list for verify_packages.sh (npm packages are not in # the pacman DB). awk ' /^@aur$/{s="skip"}; /^@npm$/{s="skip"}; /^@pacman$/{s="pacman"} { if (s!="skip" && NF>0 && $1 !~ /^#/) print } ' "$REPO/setup/term/packages.txt" > "$HOME/packages.txt.term-only" # Stage a scratch copy of the term setup incl. setup/lib/common.sh # (setup.sh sources it via a path relative to its own location). mkdir -p "$HOME/smoke/term" "$HOME/smoke/lib" cp "$REPO/setup/term/setup.sh" "$HOME/smoke/term/setup.sh" cp "$REPO/setup/lib/common.sh" "$HOME/smoke/lib/common.sh" cp "$HOME/packages.txt.term-smoke" "$HOME/smoke/term/packages.txt" # chsh is stubbed via $HOME/bin so even `chsh` works without a pty. setup_ok=0 if ( cd "$HOME/smoke/term" && bash setup.sh ) ; then say_ok "setup/term/setup.sh (pacman+npm) completed" setup_ok=1 else say_fail "setup/term/setup.sh (pacman+npm) failed (exit $?)" fi # 2d1. Idempotency probe: a second run must also succeed. Catches # steps that break on re-runs over an already-populated state (e.g. # `ya pkg add` failing with "already exists"). if [[ "$setup_ok" == "1" ]]; then if ( cd "$HOME/smoke/term" && bash setup.sh ) ; then say_ok "setup/term/setup.sh re-run: idempotent" else say_fail "setup/term/setup.sh re-run failed (exit $?) - not idempotent" fi else echo " skipping idempotency probe (first run did not complete)" fi # 2d2. Verify that every package is really installed and its binary # runs. Only meaningful if the install itself succeeded. if [[ "$setup_ok" == "1" ]]; then if [[ -x test/verify_packages.sh ]]; then header "verify installed packages" if bash test/verify_packages.sh "$HOME/packages.txt.term-only"; then say_ok "verify_packages.sh: all packages verified" else say_fail "verify_packages.sh: one or more packages failed verification (see above)" fi else say_fail "test/verify_packages.sh missing" fi else echo " skipping package verification (install did not complete)" fi else echo " SKIP_NET=1 -> skipping real package install and verification" fi # 2e. stow --actual into $HOME to prove real symlinks are created. header "stow real symlinks into \$HOME" # setup/term/setup.sh may have just re-created the yazi plugin (`ya pkg add`) # as real files. The repo vendors the same plugin under term/.config/yazi, # so drop the duplicate first, letting stow take ownership of that config. rm -rf "$HOME/.config/yazi" for pkg in term hypr kreation local_ai; do if [[ -d "$REPO/$pkg" ]]; then if stow --dir="$REPO" --target="$HOME" "$pkg" 2>"$ERR_LOG"; then say_ok "stow $pkg into \$HOME" else say_fail "stow $pkg into \$HOME" sed 's/^/ /' "$ERR_LOG" 2>/dev/null fi else say_fail "package dir missing: $pkg" fi done # sanity: representative symlinks must now exist in $HOME. missing="" for link in .config/fish .config/hypr .vst3/MT-PowerDrumKit.vst3; do [[ -e "$HOME/$link" ]] || missing="$missing $link" done if [[ -z "$missing" ]]; then say_ok "stow created representative \$HOME symlinks" else say_fail "missing expected stow symlinks:$missing" fi # 2f. kreation package: verify the VST path is stow-able and present. header "kreation VST" if [[ -f "$REPO/kreation/.vst3/MT-PowerDrumKit.vst3/Contents/x86_64-linux/MT-PowerDrumKit.so" ]]; then say_ok "MT-PowerDrumKit.vst3 binary present" else say_fail "MT-PowerDrumKit.vst3 binary missing" fi else echo " RUN_SMOKE not set -> skipping real installs & symlink creation." fi # --------------------------------------------------------------------------- # 3. FULL (RUN_FULL=1) - opt-in heavy path (AUR/npm/hypr/kreation) # --------------------------------------------------------------------------- if [[ "${RUN_FULL:-0}" == "1" ]] && [[ "${SKIP_NET:-0}" != "1" ]]; then header "Full smoke (RUN_FULL=1)" if ( cd "$REPO" && make buntes-monster ); then say_ok "make buntes-monster" else say_fail "make buntes-monster (exit $?)" fi else echo " RUN_FULL not set (or SKIP_NET) -> skipping full/hypr/kreation install." fi # --------------------------------------------------------------------------- # Summary & exit code # --------------------------------------------------------------------------- echo echo "==============================================" echo "entrypoint-test.sh: $PASS passed, $FAIL failed" echo "==============================================" # Exit non-zero if anything failed. [[ $FAIL -eq 0 ]]