From 6fdc64efcd718bf2d2389b2ff663ca4a2c9e27e9 Mon Sep 17 00:00:00 2001 From: ethernet Date: Sat, 1 Aug 2026 22:00:26 -0400 Subject: [PATCH] fix(install): install npm 12 into the vendored Node 26 tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundled-Node bootstrap unpacked the nodejs.org tarball and stopped. Node 26.5.1 bundles npm 11.17.0, one minor below the root package.json's own `engines.npm` floor of >=12 — and .npmrc sets `engine-strict=true`, so that is fatal rather than a warning: npm error code EBADENGINE npm error notsup Required: {"node":">=26.0.0","npm":">=12.0.0"} npm error notsup Actual: {"node":"v26.5.1","npm":"11.17.0"} A brand-new install died at the first `npm ci` with "Desktop workspace npm install failed". CI never saw it because the workflows run an explicit `npm i -g npm@12`; the Python update path recovers through hermes_cli/npm_engine.py, but the installer path had no such rung. _nb_ensure_bundled_npm_range() now upgrades the managed tree's npm into range right after the tarball lands, mirroring upgrade_managed_npm(): - temp cwd, so the checkout's own .npmrc (engine-strict, min-release-age) does not gate the upgrade meant to satisfy it; - npm_config_min_release_age=0, which also neutralises a user ~/.npmrc; - explicit --prefix at the managed tree, because _nb_configure_npm_prefix writes prefix=~/.local into its etc/npmrc and a bare `npm i -g` would install a second npm elsewhere while the managed tree stayed stale. The range is read out of package.json rather than duplicated, so the two cannot drift, with HERMES_NPM_TARGET_RANGE as an override and a >=12.0.0 fallback for a stripped install tree. An already-in-range npm skips the network round-trip. Best-effort: a failed upgrade warns with the manual command and keeps the working Node, since npm_engine.py still covers the EBADENGINE that follows. Verified against a real tree provisioned by this bootstrap: node v26.5.1 / npm 12.0.2, bin/npm and bin/npx still relative-symlinked into the upgraded lib/node_modules/npm, the ~/.local/bin links resolving to 12.0.2 through the tree, and no stray second npm under ~/.local/lib. --- scripts/lib/node-bootstrap.sh | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/scripts/lib/node-bootstrap.sh b/scripts/lib/node-bootstrap.sh index 2f9383430e85..fd48374b4802 100644 --- a/scripts/lib/node-bootstrap.sh +++ b/scripts/lib/node-bootstrap.sh @@ -76,6 +76,93 @@ _nb_node_major() { [[ "$v" =~ ^[0-9]+$ ]] && echo "$v" || echo 0 } +# The npm range the checkout's root package.json demands. Read from the +# manifest rather than duplicated here so the two can never drift; falls back +# to the current floor when the manifest is unreadable (vendored copy of this +# script, stripped install tree). +_nb_npm_range() { + if [ -n "${HERMES_NPM_TARGET_RANGE:-}" ]; then + printf '%s\n' "$HERMES_NPM_TARGET_RANGE" + return 0 + fi + local repo_root manifest range + repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." 2>/dev/null && pwd)" + manifest="$repo_root/package.json" + if [ -r "$manifest" ]; then + # sed, not node: this runs before a usable node is guaranteed. + range=$(sed -n '/"engines"/,/}/p' "$manifest" \ + | sed -n 's/.*"npm"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \ + | head -1) + if [ -n "$range" ]; then + printf '%s\n' "$range" + return 0 + fi + fi + printf '>=12.0.0\n' +} + +# Upgrade the managed tree's bundled npm into the checkout's engines.npm range. +# +# The nodejs.org tarball ships whatever npm that Node major bundles — Node +# 26.5.1 bundles npm 11.17.0, one minor below our own `engines.npm` floor of +# >=12. With `engine-strict=true` in the repo .npmrc that is fatal, not a +# warning, so a brand-new install died at the first `npm ci` with EBADENGINE. +# The Python side recovers through hermes_cli/npm_engine.py; the installer path +# had no such rung, so provision the right npm here instead of reacting later. +# +# Three details are load-bearing, all mirroring upgrade_managed_npm(): +# - a temp cwd, so the checkout's own .npmrc (engine-strict, min-release-age) +# does not gate the very upgrade meant to satisfy it; +# - npm_config_min_release_age=0, which also neutralises a user ~/.npmrc; +# - an explicit --prefix at the managed tree, because +# _nb_configure_npm_prefix wrote prefix=~/.local into its etc/npmrc, and +# without the override this installs a second npm elsewhere while the +# managed tree stays stale. +# +# Best-effort: a failure here leaves a working Node with an old npm, which is +# strictly better than no Node at all, and npm_engine.py still covers the +# EBADENGINE that follows. +_nb_ensure_bundled_npm_range() { + local npm_bin="$HERMES_HOME/node/bin/npm" + [ -x "$npm_bin" ] || return 0 + + local range have want + range="$(_nb_npm_range)" + [ -n "$range" ] || return 0 + + # Skip the network round-trip when the bundled npm already satisfies the + # range. Only the ">=N" shape we actually author is checked; anything more + # exotic falls through to letting npm itself decide. + if [[ "$range" =~ ^\>=([0-9]+) ]]; then + want="${BASH_REMATCH[1]}" + have=$("$npm_bin" --version 2>/dev/null | cut -d. -f1) + if [[ "$have" =~ ^[0-9]+$ ]] && [ "$have" -ge "$want" ]; then + return 0 + fi + fi + + _nb_log "Upgrading bundled npm to satisfy $range..." + local tmp_cwd + tmp_cwd=$(mktemp -d) + if ( + cd "$tmp_cwd" || exit 1 + CI=1 npm_config_min_release_age=0 \ + "$npm_bin" install --global \ + --prefix "$HERMES_HOME/node" \ + "npm@$range" \ + --no-fund --no-audit --progress=false >/dev/null 2>&1 + ); then + rm -rf "$tmp_cwd" + _nb_ok "npm $("$npm_bin" --version 2>/dev/null) installed" + return 0 + fi + + rm -rf "$tmp_cwd" + _nb_warn "Could not upgrade bundled npm to $range — \`npm ci\` may fail with EBADENGINE." + _nb_warn "Fix manually: npm install -g --prefix \"$HERMES_HOME/node\" npm@\"$range\"" + return 1 +} + _nb_have_modern_node() { command -v node >/dev/null 2>&1 || return 1 [ "$(_nb_node_major)" -ge "$HERMES_NODE_MIN_VERSION" ] @@ -232,6 +319,9 @@ _nb_install_bundled_node() { _nb_have_modern_node || return 1 _nb_ok "Node $(node --version) installed to $HERMES_HOME/node/" + # The tarball's bundled npm is usually below the repo's engines.npm floor. + # Best-effort: an old npm still beats no Node. + _nb_ensure_bundled_npm_range || true return 0 }