build: pin toolchain via make bootstrap + tool-versions/preflight (repo-local .tools/)
Make targets now resolve a repo-local, gitignored .tools/bin (Hugo extended 0.167.0, Node 22.11.0, lychee 0.24.2) via a recipe-scoped PATH, so machines with old/missing tools can no longer produce error-127 mysteries or version drift. System PATH is never touched. make bootstrap one-time idempotent install (sha256-verified Hugo) make tool-versions pinned vs PATH-first diagnostic table make preflight strict check, wired ahead of build/serve/serve-lan
This commit is contained in:
@@ -3,3 +3,4 @@ linkcheck/
|
||||
resources/_gen/
|
||||
__pycache__/
|
||||
.hermes/
|
||||
.tools/
|
||||
|
||||
@@ -3,12 +3,37 @@ PAGEFIND := npx -y pagefind
|
||||
LYCHEE := lychee
|
||||
OUT := public
|
||||
|
||||
.PHONY: all build index serve run link-check link-fix external-check serve-lan clean
|
||||
# --- Pinned toolchain (repo-local .tools/; gitignored) ---
|
||||
# `make bootstrap` installs these exact versions into .tools/bin. The PATH
|
||||
# prepend below applies to every recipe, so make targets always use the pinned
|
||||
# tools; your system PATH is never modified. `make tool-versions` shows both.
|
||||
HUGO_VERSION := 0.167.0
|
||||
NODE_VERSION := 22.11.0
|
||||
LYCHEE_VERSION := 0.24.2
|
||||
TOOLS_BIN := $(CURDIR)/.tools/bin
|
||||
export PATH := $(TOOLS_BIN):$(PATH)
|
||||
|
||||
.PHONY: all build index serve run link-check link-fix external-check external-fix external-fix-apply serve-lan clean bootstrap tool-versions preflight
|
||||
|
||||
all: index
|
||||
|
||||
# Build Hugo content -> public/
|
||||
build:
|
||||
# ONE-TIME per machine: install the pinned hugo/node/lychee into .tools/bin.
|
||||
# Idempotent (present-at-pin tools are skipped); touches nothing outside .tools/.
|
||||
bootstrap:
|
||||
scripts/bootstrap.sh
|
||||
|
||||
# Diagnostic: pinned (.tools/bin) vs PATH-first (system) tool versions.
|
||||
tool-versions:
|
||||
scripts/tool_versions.sh
|
||||
|
||||
# Strict tool check; runs automatically before all/serve/serve-lan and
|
||||
# fails with a `make bootstrap` hint instead of a bare "error 127".
|
||||
preflight:
|
||||
scripts/tool_versions.sh --strict
|
||||
|
||||
# Build Hugo content -> public/ (preflight first so a missing tool fails fast
|
||||
# and in order even under `make -j`).
|
||||
build: preflight
|
||||
$(HUGO) --gc
|
||||
|
||||
# (Re)build the Pagefind search index from public/ -> public/pagefind/
|
||||
@@ -39,14 +64,32 @@ link-fix:
|
||||
$(PAGEFIND) --site $(OUT)
|
||||
|
||||
# DEAD LINKS -- external (network): scan all http/https links in content. Report-only.
|
||||
# Always exits 0; the report is the point.
|
||||
# Always exits 0; the report is the point. Also saves the full lychee JSON to
|
||||
# linkcheck/external.json so external-fix can act without re-pinging the web.
|
||||
external-check:
|
||||
python3 scripts/check_external_links.py
|
||||
|
||||
# Preview which EXTERNAL links are truly dead (domain/DNS no longer exists) and
|
||||
# would be commented out. Reports only; changes nothing. Run external-check first.
|
||||
external-fix:
|
||||
python3 scripts/fix_dead_external_links.py --report linkcheck/external.json
|
||||
|
||||
# Comment out the truly-dead EXTERNAL links (DNS failed / host unreachable), then
|
||||
# rebuild content + index so they leave the live page AND the search index.
|
||||
# 404s, SSL errors, 5xx, redirects, refused/reset connections are NOT auto-commented
|
||||
# (domain is still alive / ambiguous) -- they go to linkcheck/manual_review_external.txt.
|
||||
external-fix-apply:
|
||||
python3 scripts/fix_dead_external_links.py --report linkcheck/external.json --apply
|
||||
$(HUGO) --gc
|
||||
$(PAGEFIND) --site $(OUT)
|
||||
|
||||
# Serve the BUILT site on all interfaces so another LAN machine can open it:
|
||||
# http://<this-machine-LAN-IP>:8090/ (IP printed at startup; also `hostname -I | head -1`)
|
||||
serve-lan:
|
||||
serve-lan: preflight
|
||||
python3 scripts/serve_lan.py
|
||||
|
||||
# NOTE: clean does NOT remove .tools/ -- the pinned toolchain is expensive to
|
||||
# reinstall and independent of build output. Remove it manually if ever needed.
|
||||
|
||||
clean:
|
||||
rm -rf $(OUT) linkcheck
|
||||
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
# Install the pinned toolchain into the repo-local .tools/bin (gitignored):
|
||||
# hugo extended 0.167.0, node 22.11.0 (provides npx/npm), lychee 0.24.2
|
||||
# Idempotent: a tool already present at the pinned version is skipped, so
|
||||
# re-running with everything installed is an instant no-op. Touches nothing
|
||||
# outside .tools/ and never modifies your system PATH.
|
||||
# Requires: curl, tar, sha256sum, and network on first run.
|
||||
set -eu
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
BIN="$ROOT/.tools/bin"
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
HUGO_VERSION=0.167.0
|
||||
HUGO_SHA256=0163f5c3deddac1f494a1629ddc40c65d18de9d5794facd98f7f96ac2c7d8957
|
||||
NODE_VERSION=22.11.0
|
||||
LYCHEE_VERSION=0.24.2
|
||||
|
||||
case "$(uname -s)/$(uname -m)" in
|
||||
Linux/x86_64) HARCH=amd64; NARCH=x64; RARCH=x86_64 ;;
|
||||
Linux/aarch64) HARCH=arm64; NARCH=arm64; RARCH=aarch64 ;;
|
||||
*) echo "bootstrap: unsupported platform $(uname -s) $(uname -m) (Linux x86_64/aarch64 only)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
command -v curl >/dev/null 2>&1 || { echo "bootstrap: curl is required (downloads the pinned tarballs)" >&2; exit 1; }
|
||||
|
||||
mkdir -p "$BIN"
|
||||
|
||||
# --- hugo extended ---
|
||||
# Release builds report "v<ver>-<40-hex-commit>+extended"; strip the commit so
|
||||
# the pinned comparison is exact.
|
||||
hugo_norm() { "$1" version 2>/dev/null | awk '{print $2}' | sed 's/-[0-9a-f]\{40,\}//'; }
|
||||
if [ -x "$BIN/hugo" ] && [ "$(hugo_norm "$BIN/hugo")" = "v$HUGO_VERSION+extended" ]; then
|
||||
echo "hugo v$HUGO_VERSION+extended: present, skipping"
|
||||
else
|
||||
echo "hugo v$HUGO_VERSION+extended: downloading..."
|
||||
curl -fsSL -o "$TMP/h.tgz" \
|
||||
"https://github.com/gohugoio/hugo/releases/download/v$HUGO_VERSION/hugo_extended_${HUGO_VERSION}_linux-${HARCH}.tar.gz"
|
||||
echo "$HUGO_SHA256 $TMP/h.tgz" | sha256sum -c -
|
||||
tar xzf "$TMP/h.tgz" -C "$TMP"
|
||||
install -m 0755 "$TMP/hugo" "$BIN/hugo"
|
||||
fi
|
||||
|
||||
# --- node (+ npm/npx) ---
|
||||
if [ -x "$BIN/node" ] && [ "$("$BIN/node" --version 2>/dev/null)" = "v$NODE_VERSION" ]; then
|
||||
echo "node v$NODE_VERSION: present, skipping"
|
||||
else
|
||||
echo "node v$NODE_VERSION: downloading..."
|
||||
curl -fsSL -o "$TMP/n.tar.xz" \
|
||||
"https://nodejs.org/dist/v$NODE_VERSION/node-v${NODE_VERSION}-linux-${NARCH}.tar.xz"
|
||||
rm -rf "$ROOT"/.tools/node-v*-linux-*
|
||||
tar xJf "$TMP/n.tar.xz" -C "$ROOT/.tools"
|
||||
for t in node npm npx; do
|
||||
ln -sfn "../node-v${NODE_VERSION}-linux-${NARCH}/bin/$t" "$BIN/$t"
|
||||
done
|
||||
fi
|
||||
|
||||
# --- lychee ---
|
||||
if [ -x "$BIN/lychee" ] && "$BIN/lychee" --version 2>/dev/null | grep -q "$LYCHEE_VERSION"; then
|
||||
echo "lychee $LYCHEE_VERSION: present, skipping"
|
||||
else
|
||||
echo "lychee $LYCHEE_VERSION: downloading..."
|
||||
curl -fsSL -o "$TMP/l.tgz" \
|
||||
"https://github.com/lycheeverse/lychee/releases/download/lychee-v$LYCHEE_VERSION/lychee-${RARCH}-unknown-linux-musl.tar.gz"
|
||||
tar xzf "$TMP/l.tgz" -C "$TMP"
|
||||
install -m 0755 "$TMP/lychee-${RARCH}-unknown-linux-musl/lychee" "$BIN/lychee"
|
||||
fi
|
||||
|
||||
echo "OK: pinned toolchain ready in $BIN (verify with: make tool-versions)"
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/bin/sh
|
||||
# Print a table of tool versions: pinned (.tools/bin -- what make targets use,
|
||||
# because the Makefile prepends it to recipe PATH) vs PATH-first (what your own
|
||||
# shell resolves outside make).
|
||||
#
|
||||
# scripts/tool_versions.sh informational; always exits 0
|
||||
# scripts/tool_versions.sh --strict exit 1 if a pinned tool is missing or
|
||||
# has the wrong version (used by `make
|
||||
# preflight`, which runs before
|
||||
# all/serve/serve-lan)
|
||||
set -u
|
||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||
BIN="$ROOT/.tools/bin"
|
||||
MODE=${1:-}
|
||||
|
||||
HUGO_PIN="v0.167.0+extended"
|
||||
NODE_PIN="v22.11.0"
|
||||
LYCHEE_PIN="0.24.2"
|
||||
|
||||
# PATH with .tools/bin stripped, so "PATH-first" shows what the user's shell
|
||||
# would resolve, even though make exported .tools/bin into our PATH.
|
||||
SYSPATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vx "$BIN" | tr '\n' ':' | sed 's/:$//')
|
||||
|
||||
pinned_ver() {
|
||||
case "$1" in
|
||||
# hugo release builds report "v<ver>-<40-hex-commit>+extended"; strip the commit
|
||||
hugo) [ -x "$BIN/hugo" ] && "$BIN/hugo" version 2>/dev/null | awk '{print $2}' | sed 's/-[0-9a-f]\{40,\}//' ;;
|
||||
node) [ -x "$BIN/node" ] && "$BIN/node" --version 2>/dev/null ;;
|
||||
npx) [ -x "$BIN/npx" ] && "$BIN/npx" --version 2>/dev/null ;;
|
||||
lychee) [ -x "$BIN/lychee" ] && "$BIN/lychee" --version 2>/dev/null | awk '{print $2}' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
first_path() { PATH="$SYSPATH" command -v "$1" 2>/dev/null; }
|
||||
|
||||
first_ver() { # first_ver <tool> <path>
|
||||
[ -n "${2:-}" ] || return 0
|
||||
case "$1" in
|
||||
hugo) "$2" version 2>/dev/null | awk '{print $2}' | sed 's/-[0-9a-f]\{40,\}//' ;;
|
||||
node) "$2" --version 2>/dev/null ;;
|
||||
npx) "$2" --version 2>/dev/null ;;
|
||||
lychee) "$2" --version 2>/dev/null | awk '{print $2}' ;;
|
||||
python3) "$2" --version 2>/dev/null | awk '{print $2}' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ "$MODE" = "--strict" ]; then
|
||||
fail=0
|
||||
[ "$(pinned_ver hugo)" = "$HUGO_PIN" ] || { echo "preflight: pinned hugo $HUGO_PIN missing/wrong in $BIN" >&2; fail=1; }
|
||||
[ "$(pinned_ver node)" = "$NODE_PIN" ] || { echo "preflight: pinned node $NODE_PIN missing/wrong in $BIN" >&2; fail=1; }
|
||||
[ -n "$(pinned_ver npx)" ] || { echo "preflight: pinned npx missing in $BIN" >&2; fail=1; }
|
||||
[ "$(pinned_ver lychee)" = "$LYCHEE_PIN" ] || { echo "preflight: pinned lychee $LYCHEE_PIN missing/wrong in $BIN" >&2; fail=1; }
|
||||
command -v python3 >/dev/null 2>&1 || { echo "preflight: python3 not found on PATH (needed by serve-lan)" >&2; fail=1; }
|
||||
if [ "$fail" = 1 ]; then
|
||||
echo "preflight: fix with: make bootstrap" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "preflight: OK (pinned toolchain present in $BIN)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
row() { # row <tool> <pinned disp> <first disp> <first path>
|
||||
printf '%-8s %-27s %-27s %s\n' "$1" "$2" "$3" "${4:--}"
|
||||
}
|
||||
|
||||
echo "tool pinned (.tools/bin) PATH-first (your shell)"
|
||||
for t in hugo node npx lychee; do
|
||||
pv=$(pinned_ver "$t")
|
||||
fp=$(first_path "$t")
|
||||
fv=$(first_ver "$t" "$fp")
|
||||
[ -n "$pv" ] || pv="(missing)"
|
||||
[ -n "$fv" ] || fv="(not installed)"
|
||||
row "$t" "$pv" "$fv" "$fp"
|
||||
done
|
||||
fp=$(first_path python3); fv=$(first_ver python3 "$fp")
|
||||
[ -n "$fv" ] || fv="(not installed)"
|
||||
row "python3" "(not pinned)" "$fv" "$fp"
|
||||
echo
|
||||
echo "make targets always use the pinned column. 'make bootstrap' installs it."
|
||||
Reference in New Issue
Block a user