fix(installer): bootstrap Node.js on a greenfield host
tools/install.sh required node and npm and installed neither. Measured on a snapshot-reverted Debian 13 image with no node, npm or git: the run stopped at `require_cmd node` with "Required command not found: node", exit 1, nothing installed, and no indication of how to proceed. Adds ensure_node() to preflight. It fetches an official Node.js release into $HOME/.mosaic/node, verifies it against that release's SHASUMS256.txt, and refuses rather than degrades when the entry is missing or the checksum does not match. sha256sum on Linux, shasum on macOS. .tar.gz over the smaller .tar.xz because gzip is universally present and xz is not — a minimal image is the case this exists to handle. No-op when a suitable node is already on PATH, so it never fights an operator's nvm/fnm/distro node. MOSAIC_SKIP_NODE_BOOTSTRAP=1 declines the download and fails with instructions instead. Inlined rather than factored into a sibling file because this script is fetched standalone by curl and has nothing to source.
This commit is contained in:
@@ -565,8 +565,151 @@ install_next_cli_from_registry() {
|
|||||||
ok "Installed @next packages: CLI ${installed_cli}, gateway ${installed_gateway}"
|
ok "Installed @next packages: CLI ${installed_cli}, gateway ${installed_gateway}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ─── node bootstrap ───────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Nothing on a greenfield host installs Node.js, yet this installer and the CLI
|
||||||
|
# it installs both hard-require it. Measured on a clean Debian 13 image: the
|
||||||
|
# installer stopped at `require_cmd node` with "Required command not found" and
|
||||||
|
# nothing was installed, with no hint of how to proceed.
|
||||||
|
#
|
||||||
|
# Inlined rather than factored into a sibling file on purpose: this script is
|
||||||
|
# fetched standalone by curl and has nothing to source.
|
||||||
|
#
|
||||||
|
# No-op when a suitable node is already on PATH, so it never fights an
|
||||||
|
# operator's nvm/fnm/distro node.
|
||||||
|
|
||||||
|
NODE_ROOT="${MOSAIC_NODE_ROOT:-$HOME/.mosaic/node}"
|
||||||
|
NODE_BOOTSTRAP_VERSION="${MOSAIC_NODE_VERSION:-v22.23.2}"
|
||||||
|
NODE_MIN_MAJOR="${MOSAIC_NODE_MIN_MAJOR:-20}"
|
||||||
|
NODE_DIST_BASE="${MOSAIC_NODE_DIST_BASE:-https://nodejs.org/dist}"
|
||||||
|
|
||||||
|
# Major version of the node at $1, or empty if it will not run.
|
||||||
|
node_major_of() {
|
||||||
|
local candidate="$1" version
|
||||||
|
version="$("$candidate" -e 'process.stdout.write(process.versions.node)' 2>/dev/null)" || return 0
|
||||||
|
printf '%s' "${version%%.*}"
|
||||||
|
}
|
||||||
|
|
||||||
|
node_is_suitable() {
|
||||||
|
local major
|
||||||
|
major="$(node_major_of "$1")"
|
||||||
|
[[ -n "$major" ]] && [[ "$major" -ge "$NODE_MIN_MAJOR" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
install_node() {
|
||||||
|
local node_os node_arch tarball release_url work_dir extracted target node_bin
|
||||||
|
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux) node_os="linux" ;;
|
||||||
|
Darwin) node_os="darwin" ;;
|
||||||
|
*) fail "Unsupported OS '$(uname -s)'. Install Node.js >= $NODE_MIN_MAJOR manually."; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) node_arch="x64" ;;
|
||||||
|
aarch64|arm64) node_arch="arm64" ;;
|
||||||
|
armv7l) node_arch="armv7l" ;;
|
||||||
|
*) fail "Unsupported architecture '$(uname -m)'. Install Node.js >= $NODE_MIN_MAJOR manually."; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# .tar.gz rather than the smaller .tar.xz: gzip is universally present, xz is
|
||||||
|
# not, and a minimal image is exactly the case this exists to handle.
|
||||||
|
tarball="node-${NODE_BOOTSTRAP_VERSION}-${node_os}-${node_arch}.tar.gz"
|
||||||
|
release_url="${NODE_DIST_BASE}/${NODE_BOOTSTRAP_VERSION}"
|
||||||
|
|
||||||
|
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-node-XXXXXX")"
|
||||||
|
|
||||||
|
info "Installing Node.js $NODE_BOOTSTRAP_VERSION ($node_os-$node_arch) to $NODE_ROOT…"
|
||||||
|
|
||||||
|
if ! curl -fsSL "${release_url}/${tarball}" -o "$work_dir/$tarball"; then
|
||||||
|
fail "Download failed: ${release_url}/${tarball}"
|
||||||
|
rm -rf "$work_dir"; return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! curl -fsSL "${release_url}/SHASUMS256.txt" -o "$work_dir/SHASUMS256.txt"; then
|
||||||
|
fail "Could not fetch SHASUMS256.txt; refusing to install an unverified runtime."
|
||||||
|
rm -rf "$work_dir"; return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keep only our artifact's line, so a missing entry is an error not a pass.
|
||||||
|
if ! grep " ${tarball}\$" "$work_dir/SHASUMS256.txt" >"$work_dir/expected.sha256"; then
|
||||||
|
fail "$tarball has no entry in SHASUMS256.txt; refusing to install."
|
||||||
|
rm -rf "$work_dir"; return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! (cd "$work_dir" && verify_sha256 expected.sha256); then
|
||||||
|
fail "Checksum mismatch for $tarball; refusing to install."
|
||||||
|
rm -rf "$work_dir"; return 1
|
||||||
|
fi
|
||||||
|
ok "Checksum verified"
|
||||||
|
|
||||||
|
tar xzf "$work_dir/$tarball" -C "$work_dir"
|
||||||
|
extracted="$work_dir/node-${NODE_BOOTSTRAP_VERSION}-${node_os}-${node_arch}"
|
||||||
|
if [[ ! -x "$extracted/bin/node" ]]; then
|
||||||
|
fail "Extracted archive has no bin/node"
|
||||||
|
rm -rf "$work_dir"; return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$NODE_ROOT"
|
||||||
|
target="$NODE_ROOT/$NODE_BOOTSTRAP_VERSION"
|
||||||
|
rm -rf "$target.incoming"
|
||||||
|
mv "$extracted" "$target.incoming"
|
||||||
|
rm -rf "$target"
|
||||||
|
mv "$target.incoming" "$target"
|
||||||
|
ln -sfn "$NODE_BOOTSTRAP_VERSION" "$NODE_ROOT/current"
|
||||||
|
rm -rf "$work_dir"
|
||||||
|
|
||||||
|
node_bin="$NODE_ROOT/current/bin"
|
||||||
|
if ! node_is_suitable "$node_bin/node"; then
|
||||||
|
fail "Installed node at $node_bin/node did not run"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PATH="$node_bin:$PATH"
|
||||||
|
ok "Node.js $(node -v) installed with npm $(npm -v 2>/dev/null || echo '?')"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_node() {
|
||||||
|
if command -v node &>/dev/null && node_is_suitable node; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A previous run may have installed one that is not on this shell's PATH.
|
||||||
|
if node_is_suitable "$NODE_ROOT/current/bin/node"; then
|
||||||
|
export PATH="$NODE_ROOT/current/bin:$PATH"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${MOSAIC_SKIP_NODE_BOOTSTRAP:-0}" == "1" ]]; then
|
||||||
|
fail "No suitable Node.js and MOSAIC_SKIP_NODE_BOOTSTRAP=1; refusing to download."
|
||||||
|
echo " Install Node.js >= $NODE_MIN_MAJOR yourself, then re-run this script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd curl
|
||||||
|
require_cmd tar
|
||||||
|
|
||||||
|
# sha256sum on Linux, shasum on macOS. Verification is not optional: without a
|
||||||
|
# checksum this would install an unauthenticated runtime.
|
||||||
|
if command -v sha256sum &>/dev/null; then
|
||||||
|
verify_sha256() { sha256sum -c --status "$1"; }
|
||||||
|
elif command -v shasum &>/dev/null; then
|
||||||
|
verify_sha256() { shasum -a 256 -c --status "$1"; }
|
||||||
|
else
|
||||||
|
fail "sha256sum or shasum required to verify the Node.js download"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! install_node; then
|
||||||
|
fail "Could not bootstrap Node.js. Install Node.js >= $NODE_MIN_MAJOR and re-run."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ─── preflight ────────────────────────────────────────────────────────────────
|
# ─── preflight ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
ensure_node
|
||||||
require_cmd node
|
require_cmd node
|
||||||
require_cmd npm
|
require_cmd npm
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user