Files
compiler-explorer/etc/scripts/find-node
Matt Godbolt (bot acct) eee2e8bfd9 Improve node discovery: version-manager aware, single source of truth (#8845)
## What

Reworks the bespoke `etc/scripts/find-node` and its `Makefile`
interaction so node discovery works out of the box on Linux and macOS,
is optionally friendly to version managers, and stays sympathetic to
people who just use system node.

### Before
- Only ever looked at `PATH`, plus a vestigial
`/opt/compiler-explorer/node` hardcode (an affordance for one dev's
local setup, unused by the live site).
- Duplicated the required node version in three places (`find-node` had
`22` twice; `.node-version`; `package.json` `engines`).
- Silently fell back *past* an unusable `NODE_DIR` to whatever else it
found.
- nvm users got whatever default was active, not the version this repo
pins.
- Nagged on any version that wasn't exactly `22.x`, even newer ones.

### After — clear, permissive resolution order
1. **`$NODE_DIR/bin/node`** — explicit override, now authoritative. If
it's set but missing/too old you get a clear error instead of a silent
fallback.
2. **`node`/`nodejs` on `PATH`** — covers system node *and* any version
manager with shell integration (fnm, asdf, nodenv, volta, an active
nvm). Zero extra steps on a well-configured machine.
3. **Manager rescue** — only when PATH node is absent or below the
minimum: source `nvm` and ask for the pinned version (the one manager
that's a shell function rather than a PATH binary), then try
`fnm`/`nodenv`/`asdf`. Each branch is inert unless that tool is
installed, so nobody is pushed onto a manager.

Other changes:
- **Single source of truth**: the minimum major now comes solely from
`.node-version` (the same file the managers read). Newer majors are
always fine — the "only tested against v22.x" warning is dropped.
- **`Makefile`**: `.node-bin` now also depends on `.node-version`, so
bumping the pin re-resolves node instead of serving a stale cache. The
lazy-dotfile pattern (so failures abort `make` and `make help` pays
nothing) is kept.
- **`README`**: documents the PATH-first + manager-rescue behaviour.

## Testing
- `shellcheck` clean.
- Verified: PATH discovery, valid/bogus `NODE_DIR`, nvm rescue (shadowed
`node`+`nodejs` with v18 shims → skipped them and resolved v22.22 via
nvm), too-old fallthrough, no-node/no-manager failure message, and `make
info` end-to-end.
- `fnm` branch is logically straightforward but was not exercised (no
fnm on the test box) — worth a smoke test if you have one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: mattgodbolt-molty <mattgodbolt-molty@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Matt Godbolt <matt@godbolt.org>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-02 19:50:58 +01:00

121 lines
4.4 KiB
Bash
Executable File

#!/bin/sh
# Locate a usable node interpreter and write its path to $1 (or stdout).
#
# Resolution order, stopping at the first that satisfies the minimum version:
# 1. $NODE_DIR/bin/node -- explicit override; authoritative (errors if too old)
# 2. node / nodejs on PATH -- system node, and any version manager with shell
# integration (fnm, asdf, nodenv, volta, an active nvm)
# 3. a detected version manager, honouring .node-version -- the rescue path for
# nvm's off-PATH default, or a manager without `cd` auto-switching
#
# The minimum version lives in .node-version (the same file version managers read)
# so there's a single source of truth. We compare the full major.minor.patch so a
# node that satisfies .node-version also satisfies package.json's `engines` gate;
# anything newer is fine. Version-manager *lookups* use the major only, since nvm/fnm
# take a "22" prefix and resolve the newest installed 22.x -- which we then validate.
set -e
OUT="$1"
# shellcheck disable=SC1007 # `CDPATH= cd` is the intended empty-assignment idiom
ROOT="$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd -P)"
MIN="$(sed 's/^v//; q' "${ROOT}/.node-version")" # full minimum, e.g. 22.12.0
MIN_MAJOR="${MIN%%.*}" # major only, for manager prefix lookups
# Full version (major.minor.patch) of the node at $1, or empty if it can't be run.
node_version() {
"$1" --version 2>/dev/null | sed 's/^v//'
}
# True if dotted-numeric version $1 is >= $2. Missing components count as 0, and any
# non-numeric suffix (a prerelease tag) is dropped -- good enough for node's scheme.
semver_ge() {
awk -v a="$1" -v b="$2" 'BEGIN {
na = split(a, x, "."); nb = split(b, y, ".");
n = (na > nb) ? na : nb;
for (i = 1; i <= n; i++) {
xi = (i <= na) ? x[i] + 0 : 0;
yi = (i <= nb) ? y[i] + 0 : 0;
if (xi > yi) exit 0;
if (xi < yi) exit 1;
}
exit 0;
}'
}
# True if $1 is an executable node of at least the minimum version.
usable() {
[ -n "$1" ] && [ -x "$1" ] || return 1
_v="$(node_version "$1")"
[ -n "${_v}" ] && semver_ge "${_v}" "${MIN}"
}
# Echo the first usable node discovered on PATH or via a version manager.
discover() {
for _cand in node nodejs; do
_p="$(command -v "${_cand}" 2>/dev/null || true)"
if usable "${_p}"; then echo "${_p}"; return 0; fi
done
# nvm is a shell function, not a PATH binary, so source it and ask for the
# pinned major (a prefix like "22" resolves to the latest installed 22.x, which
# usable() then version-checks). This is the case plain PATH discovery can't cover.
_nvm="${NVM_DIR:-${HOME}/.nvm}/nvm.sh"
if [ -s "${_nvm}" ]; then
# shellcheck disable=SC1090 # nvm.sh location is user-dependent
_p="$(. "${_nvm}" >/dev/null 2>&1 && nvm which "${MIN_MAJOR}" 2>/dev/null || true)"
if usable "${_p}"; then echo "${_p}"; return 0; fi
fi
# Other managers expose a binary that honours the local .node-version.
if command -v fnm >/dev/null 2>&1; then
_p="$(fnm exec --using "${MIN_MAJOR}" -- node -p 'process.execPath' 2>/dev/null || true)"
if usable "${_p}"; then echo "${_p}"; return 0; fi
fi
if command -v nodenv >/dev/null 2>&1; then
_p="$(nodenv which node 2>/dev/null || true)"
if usable "${_p}"; then echo "${_p}"; return 0; fi
fi
if command -v asdf >/dev/null 2>&1; then
_p="$(asdf which node 2>/dev/null || true)"
if usable "${_p}"; then echo "${_p}"; return 0; fi
fi
return 1
}
fail() {
echo >&2 "$@"
echo >&2
echo >&2 "Compiler Explorer needs node ${MIN} or newer (see .node-version)."
echo >&2 "Install it from https://nodejs.org/, or:"
echo >&2 " - point NODE_DIR at an existing install (we use \$NODE_DIR/bin/node), or"
echo >&2 " - select it with your version manager (e.g. 'nvm use', 'fnm use')."
exit 1
}
if [ -n "${NODE_DIR}" ]; then
# Explicit override: honour it, and complain clearly if it's unusable rather
# than silently falling back to something else.
NODE="${NODE_DIR}/bin/node"
if [ ! -x "${NODE}" ]; then
fail "NODE_DIR is set to '${NODE_DIR}' but '${NODE}' is not executable."
fi
_v="$(node_version "${NODE}")"
if [ -z "${_v}" ] || ! semver_ge "${_v}" "${MIN}"; then
fail "NODE_DIR points at node v${_v:-?}, but ${MIN} or newer is required."
fi
else
NODE="$(discover || true)"
if [ -z "${NODE}" ]; then
fail "Unable to find node ${MIN} or newer on your PATH."
fi
fi
if [ -n "${OUT}" ]; then
echo "${NODE}" >"${OUT}"
else
echo "${NODE}"
fi