From 88a09000ea728dbe381492132a4e8894be546454 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Wed, 21 Oct 2026 05:21:26 +0800 Subject: [PATCH 1/3] fix: musl libatomic check, 32-bit ARM package and .nvmrc matching Writing the new docs against vfox 1.0.12 and 1.0.13 turned up these bugs in the hooks. - musl: after the unpack, PostInstall runs `node --version` once and reads the dynamic loader's complaint. The musl builds of 26.8.0 and later need libatomic, which a stock Alpine does not have, so the install used to succeed and the first `node` failed with `Error loading shared library libatomic.so.1`. Now the install stops, nothing is registered, and the message names the package and the command that installs it (`apk add libatomic` on Alpine, `opkg install ...` on OpenWrt). The libstdc++ message names its command too and no longer says the library "is normally installed automatically". Where no command can be run, or node fails for another reason, the install is left alone. - 32-bit ARM Linux: vfox reports `arm` and Node.js names the package `linux-armv7l`. The plugin asked for `linux-arm`, which never existed, so every install on 32-bit ARM ended in "package ... is not listed". - .nvmrc and .node-version under latest_installed and latest_available: releases are matched by their numbers, part by part, and the newest one wins. The text used to be a Lua pattern against vfox's text-sorted list, so `22` picked 22.9.0 over 22.23.3, `22.2` matched 22.23.3, `22.x` matched nothing and a file holding `(` raised `unfinished capture`. `22.x`, `22.x.x` and `22.*` now mean 22, text that is no release number (node, lts/-1, `(`) is returned as it is, and only a leading `v` is dropped, so an `lts/` with a `v` inside works. `^lts-` is escaped: it matched anything starting with `lt` and turned `lts/-1` into `lts//1`. - PreInstall: `get checksum failed` says why, `HTTP 404 from ` or the network error, instead of hiding it. - VFOX_NODEJS_MIRROR may end in several slashes, not only one. The hook tests grow from 28 to 52 cases. The musl check is also covered by running it against Alpine 3.24.1 with and without libstdc++ and libatomic. --- hooks/parse_legacy_file.lua | 51 ++++--- hooks/post_install.lua | 66 +++++++-- hooks/pre_install.lua | 15 +- lib/util.lua | 174 +++++++++++++++++++++- tests/hooks_test.lua | 281 ++++++++++++++++++++++++++++++++++++ 5 files changed, 539 insertions(+), 48 deletions(-) diff --git a/hooks/parse_legacy_file.lua b/hooks/parse_legacy_file.lua index c436d2e..dc6c93c 100644 --- a/hooks/parse_legacy_file.lua +++ b/hooks/parse_legacy_file.lua @@ -64,37 +64,33 @@ function PLUGIN:ParseLegacyFile(ctx) if content == "" then return {} end - function resolve_legacy_version(strategy, query) - local list = {} - - if strategy == "latest_installed" then - list = ctx.getInstalledVersions() - elseif strategy == "latest_available" then - for _, av in pairs(self:Available({})) do - table.insert(list, av.version) - end - else - -- Just return the original query + --- `specified` leaves the text to vfox. The other strategies read it as the + --- start of a release number (`22`, `22.23`, `22.x`) and take the newest + --- release that starts with it. The comparison is by numbers: vfox lists the + --- installed releases as text, where `22.9.0` comes before `22.23.3`, and + --- `22.2` must not match `22.23.3`. Text that is not a release number + --- (`node`, `lts/unknown`, ranges) is returned as it is. + local function resolve_legacy_version(strategy, query) + local prefix = util.version_prefix(query) + if prefix == nil or (strategy ~= "latest_installed" and strategy ~= "latest_available") then return query end - local resolved = "" - for _, item in pairs(list) do - if item:match("^" .. query) then - resolved = item - break + if strategy == "latest_installed" then + local installed = util.newest_matching(ctx.getInstalledVersions(), prefix) + if installed ~= nil then + return installed end - end - - if resolved ~= "" then - return resolved - elseif strategy ~= "latest_available" then -- If no version is installed, fallback to latest_available return resolve_legacy_version("latest_available", query) - else - -- Give up and pretty the query itself - return query end + + local available = {} + for _, av in pairs(self:Available({})) do + table.insert(available, av.version) + end + -- Give up and pretty the query itself + return util.newest_matching(available, prefix) or query end local query = resolve_version(content, ctx.strategy) @@ -106,9 +102,12 @@ function PLUGIN:ParseLegacyFile(ctx) end function resolve_version(query, strategy) - query = string.lower(query:gsub("v", "")) + -- Only a leading `v` goes (v22.23.3): a codename may have one inside. + query = (string.lower(query):gsub("^v", "")) - if query:match("^lts-") then + -- `lts-jod` is `lts/jod`. The `-` is escaped: in a pattern it repeats the + -- character before it, so `^lts-` matched anything that starts with `lt`. + if query:match("^lts%-") then query = query:gsub("-", "/") end diff --git a/hooks/post_install.lua b/hooks/post_install.lua index affcca1..52ab80f 100644 --- a/hooks/post_install.lua +++ b/hooks/post_install.lua @@ -14,9 +14,39 @@ local util = require("util") ---- Fail fast when the musl Node.js binary cannot run: it needs libstdc++, ---- which musl distributions do not install by default. Without this check ---- the install looks successful and only `node --version` fails later with +--- Message for a shared library that the musl build needs and the system +--- lacks, with the command that installs it where the distribution is known. +--- @param soname string e.g. "libatomic.so.1" +--- @return string +local function missing_library(soname) + local message = "Node.js (musl build) requires " + .. util.library_package_hint(soname) + .. ", which was not found on this system." + local command = util.install_command(soname) + if command ~= nil then + message = message .. " Install it with `" .. command .. "` and reinstall." + else + message = message .. " Install it and reinstall." + end + return message .. " See " .. util.ReadmeUrl +end + +--- Where vfox unpacked the release. +--- @param ctx table +--- @return string|nil +local function install_path(ctx) + for _, info in pairs(ctx.sdkInfo or {}) do + if type(info) == "table" and type(info.path) == "string" then + return info.path + end + end + return nil +end + +--- Fail fast when the musl Node.js binary cannot run: it needs libstdc++ and, +--- from 26.8.0 on, libatomic, which musl distributions do not install by +--- default. Without this check the install looks successful and only +--- `node --version` fails later with `Error loading shared library ...` or --- `Error relocating ... symbol not found`. --- See https://github.com/version-fox/vfox-nodejs/issues/29 --- @param ctx table Context information @@ -27,14 +57,28 @@ function PLUGIN:PostInstall(ctx) if not util.is_musl() then return end - if util.has_libstdcxx() then + if not util.has_libstdcxx() then + error(missing_library("libstdc++.so.6")) + end + + -- The dynamic loader names whatever else is missing, so run the binary + -- once instead of keeping a list of libraries and where they live. When + -- nothing can be run here, or node fails for another reason, the install + -- is left alone. + local path = install_path(ctx) + if path == nil or not util.file_exists(path .. "/bin/node") then + return + end + local output, status = util.capture(util.shell_quote(path .. "/bin/node") .. " --version") + if output == nil or status == 0 then return end - error( - "Node.js (musl build) requires " - .. util.libstdcxx_hint() - .. ", which was not found on this system. It is normally installed automatically;" - .. " install it and reinstall. See " - .. util.ReadmeUrl - ) + local soname = output:match("Error loading shared library ([^:%s]+):") + if soname ~= nil then + error(missing_library(soname)) + end + local relocation = output:match("Error relocating [^\n]+") + if relocation ~= nil then + error("Node.js (musl build) cannot run on this system: " .. relocation .. ". See " .. util.ReadmeUrl) + end end diff --git a/hooks/pre_install.lua b/hooks/pre_install.lua index fb51a64..1a620e8 100644 --- a/hooks/pre_install.lua +++ b/hooks/pre_install.lua @@ -49,6 +49,9 @@ function PLUGIN:PreInstall(ctx) arch_type = "x64" elseif RUNTIME.archType == "386" then arch_type = "x86" + elseif RUNTIME.archType == "arm" then + -- vfox reports 32-bit ARM as `arm`, Node.js names the package armv7l. + arch_type = "armv7l" end if RUNTIME.osType == "windows" then ext = ".zip" @@ -93,11 +96,17 @@ function PLUGIN:PreInstall(ctx) filename = util.FileName:format(version, osType, arch_type, ext) end + local sums_url = baseUrl .. "SHASUMS256.txt" local resp, err = http.get({ - url = baseUrl .. "SHASUMS256.txt", + url = sums_url, }) - if err ~= nil or resp.status_code ~= 200 then - error("get checksum failed") + -- Keep the cause: a missing release, a dead mirror and a TLS problem all + -- end here. + if err ~= nil then + error("get checksum failed: " .. tostring(err)) + end + if resp.status_code ~= 200 then + error("get checksum failed: HTTP " .. tostring(resp.status_code) .. " from " .. sums_url) end local checksum = util.get_checksum(resp.body, filename) if checksum ~= nil then diff --git a/lib/util.lua b/lib/util.lua index e0a135a..7037f5d 100644 --- a/lib/util.lua +++ b/lib/util.lua @@ -26,8 +26,9 @@ function UTIL.getBaseUrl() if mirror == "" or mirror == nil then return "https://nodejs.org/dist" end - -- Strip trailing slash to avoid double slashes in URL paths - return mirror:gsub("/$", "") + -- Strip trailing slashes to avoid double slashes in URL paths. The + -- parentheses keep gsub's second result (the count) out of the return. + return (mirror:gsub("/+$", "")) end function UTIL.file_exists(path) @@ -105,20 +106,97 @@ function UTIL.has_libstdcxx() return false end ---- Name of the system package providing libstdc++.so.6, picked by probing ---- the distribution (Alpine, OpenWrt). -function UTIL.libstdcxx_hint() +--- System packages that provide the shared libraries the Node.js musl builds +--- link, by their Alpine (apk) and OpenWrt (opkg) names. libatomic is linked +--- by the musl builds of 26.8.0 and later, the others by every one. +UTIL.MuslLibraries = { + ["libstdc++.so.6"] = { apk = "libstdc++", opkg = "libstdcpp" }, + ["libatomic.so.1"] = { apk = "libatomic", opkg = "libatomic" }, + ["libgcc_s.so.1"] = { apk = "libgcc", opkg = "libgcc" }, +} + +--- "apk" on Alpine, "opkg" on OpenWrt and ImmortalWrt, nil on anything else, +--- picked by probing the distribution. +--- @return string|nil +function UTIL.package_manager() if UTIL.file_exists("/etc/alpine-release") or UTIL.file_exists("/sbin/apk") or UTIL.file_exists("/usr/bin/apk") then - return "the libstdc++ system package" + return "apk" end if UTIL.file_exists("/etc/openwrt_release") or UTIL.file_exists("/bin/opkg") or UTIL.file_exists("/usr/bin/opkg") then - return "the libstdcpp system package" + return "opkg" + end + return nil +end + +--- Phrase naming the system package that provides a shared library, such as +--- "the libatomic system package". Falls back to the library itself. +--- @param soname string e.g. "libatomic.so.1" +--- @return string +function UTIL.library_package_hint(soname) + local package = UTIL.MuslLibraries[soname] + if package == nil then + return "the " .. soname .. " shared library" + end + local manager = UTIL.package_manager() + if manager == "apk" or (manager == nil and package.apk == package.opkg) then + return "the " .. package.apk .. " system package" + end + if manager == "opkg" then + return "the " .. package.opkg .. " system package" + end + return "the " .. package.apk .. " system package (" .. package.opkg .. " on OpenWrt)" +end + +--- Command that installs the package providing a shared library, nil when the +--- distribution or the library is not known. +--- @param soname string e.g. "libatomic.so.1" +--- @return string|nil +function UTIL.install_command(soname) + local package = UTIL.MuslLibraries[soname] + local manager = UTIL.package_manager() + if package == nil or manager == nil then + return nil + end + if manager == "apk" then + return "apk add " .. package.apk end - return "the libstdc++ system package (libstdcpp on OpenWrt)" + return "opkg install " .. package.opkg +end + +--- Quote a string as one word for a POSIX shell. +--- @param text string +--- @return string +function UTIL.shell_quote(text) + return "'" .. text:gsub("'", "'\\''") .. "'" +end + +--- Run a shell command and capture what it prints, stderr included. +--- @param command string +--- @return string|nil output nil when no command can be run here +--- @return number|nil status the exit status, when the runtime reports one +function UTIL.capture(command) + if type(io.popen) ~= "function" then + return nil, nil + end + local started, handle = pcall(io.popen, command .. " 2>&1") + if not started or not handle then + return nil, nil + end + local output = handle:read("*a") or "" + -- vfox's Lua returns the exit status here, standard Lua `true` or nil. + --- @type boolean|number|nil + local closed = handle:close() + if closed == true then + return output, 0 + end + if type(closed) == "number" then + return output, closed + end + return output, nil end --- Whether a PATH entry belongs to vfox: like vfox's own check, one of its @@ -192,6 +270,86 @@ function UTIL.compare_versions(v1o, v2o) return false end +--- The numbers of a release such as "22.23.3". nil unless every part is a +--- number. +--- @param version string +--- @return number[]|nil +function UTIL.version_parts(version) + if type(version) ~= "string" then + return nil + end + local parts = {} + for part in string.gmatch(version .. ".", "([^.]*)%.") do + if not part:match("^%d+$") then + return nil + end + table.insert(parts, tonumber(part)) + end + return parts +end + +--- What a version file asks for, as the numbers a release has to start with: +--- "22" is { 22 }, "22.23" is { 22, 23 }, and "22.x", "22.x.x" and "22.*" are +--- { 22 }. nil for anything else, such as "lts/jod", "node", ">=20" or "x". +--- @param query string +--- @return number[]|nil +function UTIL.version_prefix(query) + if type(query) ~= "string" then + return nil + end + local prefix = {} + local wildcard = false + for part in string.gmatch(query .. ".", "([^.]*)%.") do + if part == "x" or part == "X" or part == "*" then + wildcard = true + elseif wildcard or not part:match("^%d+$") then + return nil + else + table.insert(prefix, tonumber(part)) + end + end + if #prefix == 0 then + return nil + end + return prefix +end + +--- Whether a release starts with the numbers of a prefix, part by part: "22.23.3" +--- starts with { 22 } and { 22, 23 }, not with { 22, 2 }. +--- @param version string +--- @param prefix number[] +--- @return boolean +function UTIL.version_starts_with(version, prefix) + local parts = UTIL.version_parts(version) + if parts == nil or #parts < #prefix then + return false + end + for i, number in ipairs(prefix) do + if parts[i] ~= number then + return false + end + end + return true +end + +--- The newest release that starts with the prefix, compared as numbers. vfox +--- lists installed releases as text, so "22.9.0" comes before "22.23.3" there. +--- @param versions table|nil releases as text, in any order +--- @param prefix number[] +--- @return string|nil +function UTIL.newest_matching(versions, prefix) + local newest = nil + for _, version in pairs(versions or {}) do + if + UTIL.version_starts_with(version, prefix) + and (newest == nil or UTIL.compare_versions({ version = version }, { version = newest })) + then + newest = version + end + end + return newest +end + function UTIL.get_checksum(file_content, file_name) for line in string.gmatch(file_content, "([^\n]*)\n?") do local checksum, name = string.match(line, "(%w+)%s+(%S+)") diff --git a/tests/hooks_test.lua b/tests/hooks_test.lua index f6ca6f8..f6fd448 100644 --- a/tests/hooks_test.lua +++ b/tests/hooks_test.lua @@ -73,6 +73,7 @@ local cases = { { "windows", "386", "win-x86.zip" }, { "windows", "arm64", "win-arm64.zip" }, { "linux", "amd64", "linux-x64.tar.gz" }, + { "linux", "arm", "linux-armv7l.tar.gz" }, { "darwin", "arm64", "darwin-arm64.tar.gz" }, } for _, case in ipairs(cases) do @@ -138,6 +139,13 @@ test("musl unsupported architecture", function() end, "musl") assert(state.requests == 0) end) +test("musl on 32-bit ARM names the armv7l package", function() + RUNTIME = { osType = "linux", archType = "arm", envType = "musl" } + expect_error(function() + PLUGIN:PreInstall({ version = "22.23.3" }) + end, "got armv7l") + assert(state.requests == 0) +end) -- PostInstall guards the musl libstdc++ runtime Dependency. dofile("hooks/post_install.lua") test("post_install skips non-musl systems", function() @@ -162,6 +170,99 @@ test("post_install fails without libstdc++", function() end, "libstdc++") util.file_exists = saved end) +-- PostInstall also runs the unpacked node once: the dynamic loader names any +-- library that is missing, such as libatomic for the musl builds of 26.8.0+. +local function with_util(stubs, fn) + local saved = {} + for name, value in pairs(stubs) do + saved[name] = util[name] + util[name] = value + end + local ok, err = pcall(fn) + for name, value in pairs(saved) do + util[name] = value + end + assert(ok, err) +end +local musl_node = "/vfox/cache/nodejs/v-26.12.0/nodejs-26.12.0/bin/node" +local loader_error = "Error loading shared library libatomic.so.1: No such file or directory (needed by " + .. musl_node + .. ")\nError relocating " + .. musl_node + .. ": __atomic_compare_exchange: symbol not found\n" +--- The error PostInstall raises for a node that prints `output` and exits with `status`, nil when it passes. +local function post_install_error(output, status, opts) + opts = opts or {} + RUNTIME = { osType = "linux", archType = "amd64", envType = "musl" } + local install = { sdkInfo = { [opts.sdk or "nodejs"] = { path = "/vfox/cache/nodejs/v-26.12.0/nodejs-26.12.0" } } } + -- A table field: gopher-lua loses a `local x = nil` that a closure assigns. + local result = {} + with_util({ + has_libstdcxx = function() + return true + end, + file_exists = function(path) + return path == musl_node or (opts.alpine and path == "/etc/alpine-release") + end, + capture = function(command) + state.command = command + return output, status + end, + }, function() + local ok, err = pcall(function() + PLUGIN:PostInstall(install) + end) + if not ok then + result.message = tostring(err) + end + end) + return result.message +end +test("post_install names the library the loader cannot find", function() + local message = post_install_error(loader_error, 127, { alpine = true }) + assert(message ~= nil, "expected an error") + assert(message:find("the libatomic system package", 1, true), message) + assert(message:find("`apk add libatomic`", 1, true), message) + assert(message:find("README.md", 1, true), message) + assert(state.command == "'" .. musl_node .. "' --version", tostring(state.command)) +end) +test("post_install without a known package manager still names the library", function() + local message = post_install_error(loader_error, 127) + assert(message ~= nil and message:find("libatomic", 1, true), tostring(message)) + assert(not message:find("apk add", 1, true), message) +end) +test("post_install finds the release under the name of an alias", function() + local message = post_install_error(loader_error, 127, { sdk = "nodejs-dev" }) + assert(message ~= nil and message:find("libatomic", 1, true), tostring(message)) +end) +test("post_install names a library it has no package for", function() + local message = post_install_error("Error loading shared library libfoo.so.2: No such file or directory\n", 127) + assert(message ~= nil and message:find("the libfoo.so.2 shared library", 1, true), tostring(message)) +end) +test("post_install reports a symbol the libraries lack", function() + local message = post_install_error("Error relocating /x/bin/node: _ZNSt3foo: symbol not found\n", 127) + assert(message ~= nil and message:find("cannot run", 1, true), tostring(message)) + assert(message:find("_ZNSt3foo: symbol not found", 1, true), message) +end) +test("post_install passes when node runs", function() + assert(post_install_error("v26.12.0\n", 0) == nil) +end) +test("post_install leaves the install alone when nothing can be run", function() + assert(post_install_error(nil, nil) == nil) +end) +test("post_install leaves the install alone when node fails for another reason", function() + assert(post_install_error("Segmentation fault\n", 139) == nil) +end) +test("post_install does not run node outside musl", function() + RUNTIME = { osType = "linux", archType = "amd64", envType = "gnu" } + with_util({ + capture = function() + error("node must not be run") + end, + }, function() + PLUGIN:PostInstall({ sdkInfo = { nodejs = { path = "/vfox/nodejs" } } }) + end) +end) test("missing package", function() RUNTIME = { osType = "windows", archType = "arm64" } state.response = { status_code = 200, body = string.rep("a", 64) .. " node-v20.16.0-win-x64.zip\n" } @@ -169,6 +270,22 @@ test("missing package", function() PLUGIN:PreInstall({ version = "20.16.0" }) end, "node-v20.16.0-win-arm64.zip") end) +test("a failed checksum request says why", function() + RUNTIME = { osType = "linux", archType = "amd64" } + state.request_error = + 'Get "https://nodejs.org/dist/v20.16.0/SHASUMS256.txt": x509: certificate signed by unknown authority' + expect_error(function() + PLUGIN:PreInstall({ version = "20.16.0" }) + end, "get checksum failed: Get") + state.request_error = nil + state.response = { status_code = 404 } + local ok, err = pcall(function() + PLUGIN:PreInstall({ version = "20.99.0" }) + end) + assert(not ok, "expected an error") + assert(tostring(err):find("get checksum failed: HTTP 404 from https://", 1, true), tostring(err)) + assert(tostring(err):find("/v20.99.0/SHASUMS256.txt", 1, true), tostring(err)) +end) -- ParseLegacyFile: the legacy file record drives version selection through -- resolve_version (codenames) and resolve_legacy_version (strategies). dofile("hooks/parse_legacy_file.lua") @@ -287,6 +404,81 @@ test("legacy latest_available resolves against the index", function() os.remove(path) assert(result.version == "22.23.3", tostring(result.version)) end) +-- Version files name a release by its numbers, not by its text. +local function installed_list(list) + return function(...) + assert(select("#", ...) == 0, "getInstalledVersions must be called as a plain function") + return list + end +end +local function legacy_version(content, strategy, installed) + local path = write_legacy(content) + local ok, result = pcall(function() + return PLUGIN:ParseLegacyFile({ + filepath = path, + filename = ".nvmrc", + strategy = strategy, + getInstalledVersions = installed_list(installed or {}), + }) + end) + os.remove(path) + assert(ok, result) + return result.version +end +test("legacy latest_installed takes the newest release by number", function() + -- vfox lists installed releases as text, descending: 22.9.0 comes before 22.23.3. + local installed = { "26.12.0", "22.9.0", "22.3.0", "22.23.3", "20.20.2" } + assert(legacy_version("22", "latest_installed", installed) == "22.23.3") + assert(legacy_version("22.3", "latest_installed", installed) == "22.3.0") + assert(legacy_version("lts/jod", "latest_installed", installed) == "22.23.3") + assert(state.requests == 0, "installed releases answer without the index") +end) +test("legacy prefixes are matched by numbers, part by part", function() + state.response = { status_code = 200, body = "index" } + state.decoded = { { version = "v22.23.3" }, { version = "v22.2.1" } } + -- 22.2 is not the start of 22.23.3. Nothing installed matches, so the index answers. + assert(legacy_version("22.2", "latest_installed", { "22.23.3" }) == "22.2.1") + assert(legacy_version("22.2", "latest_installed", { "22.23.3", "22.2.1" }) == "22.2.1") + assert(legacy_version("22.23", "latest_installed", { "22.23.3", "22.2.1" }) == "22.23.3") +end) +test("legacy wildcards stand for the rest of a release number", function() + local installed = { "22.9.0", "22.23.3" } + for _, text in ipairs({ "22.x", "22.X", "22.x.x", "22.*", "v22.x" }) do + assert(legacy_version(text, "latest_installed", installed) == "22.23.3", text) + end +end) +test("legacy text that is not a release number is left alone", function() + local texts = { "node", "stable", "iron", "lts/-1", "lts/(", ">=20", "^22.0.0", "~22.23", "22.x.1", "22..3", "x" } + for _, text in ipairs(texts) do + assert(legacy_version(text, "latest_installed", { "22.23.3" }) == text, text) + end + assert(legacy_version("node", "latest_available") == "node") + assert(state.requests == 0, "text that is no release number needs no index") +end) +test("legacy only drops a leading v", function() + assert(legacy_version("V22.23.3", "specified") == "22.23.3") + assert(legacy_version("v22", "latest_installed", { "22.23.3" }) == "22.23.3") + -- A codename with a v inside keeps it. + state.response = { status_code = 200, body = "index" } + state.decoded = { + { version = "v30.1.0", npm = "11.0.0", lts = "Vanadium" }, + { version = "v30.0.0", npm = "11.0.0", lts = "Vanadium" }, + } + assert(legacy_version("lts/vanadium", "latest_available") == "30.1.0") +end) +test("legacy latest_available takes the newest matching release", function() + state.response = { status_code = 200, body = "index" } + state.decoded = { + { version = "v22.9.0" }, + { version = "v22.23.3" }, + { version = "v22.3.0" }, + { version = "v20.20.2" }, + } + assert(legacy_version("22", "latest_available") == "22.23.3") + assert(legacy_version("22.3", "latest_available") == "22.3.0") + assert(legacy_version("22.x", "latest_available") == "22.23.3") + assert(legacy_version("18", "latest_available") == "18") +end) -- PreUse notes a `node` that vfox keeps ahead of its own paths, such as the -- one `pnpm runtime set node -g` writes to $PNPM_HOME/bin. dofile("hooks/pre_use.lua") @@ -357,5 +549,94 @@ test("shadowing_node on windows", function() assert(util.shadowing_node() == pnpm .. "\\node.exe", tostring(util.shadowing_node())) end) end) +-- lib/util.lua helpers. +test("version_prefix reads what a version file asks for", function() + local accepted = { + ["22"] = { 22 }, + ["22.23"] = { 22, 23 }, + ["22.23.3"] = { 22, 23, 3 }, + ["22.x"] = { 22 }, + ["22.X"] = { 22 }, + ["22.x.x"] = { 22 }, + ["22.23.*"] = { 22, 23 }, + } + for text, want in pairs(accepted) do + local got = util.version_prefix(text) + assert(got ~= nil and #got == #want, text) + for i, number in ipairs(want) do + assert(got[i] == number, text) + end + end + for _, text in ipairs({ "", "x", "*", "node", "lts/jod", ">=20", "22.x.1", "22..3", "22.", ".22", "22.2a", "v22" }) do + assert(util.version_prefix(text) == nil, "should not read " .. text) + end +end) +test("newest_matching compares numbers", function() + local versions = { "22.9.0", "22.3.0", "22.23.3", "20.20.2", "x.y.z" } + assert(util.newest_matching(versions, { 22 }) == "22.23.3") + assert(util.newest_matching(versions, { 22, 3 }) == "22.3.0") + assert(util.newest_matching(versions, { 22, 2 }) == nil) + assert(util.newest_matching(versions, { 18 }) == nil) + assert(util.newest_matching(nil, { 22 }) == nil) +end) +test("getBaseUrl drops trailing slashes", function() + local saved = os.getenv + local mirror = nil + os.getenv = function(key) + if key == "VFOX_NODEJS_MIRROR" then + return mirror + end + return saved(key) + end + local ok, err = pcall(function() + assert(util.getBaseUrl() == "https://nodejs.org/dist") + mirror = "" + assert(util.getBaseUrl() == "https://nodejs.org/dist") + mirror = "https://npmmirror.com/mirrors/node" + assert(util.getBaseUrl() == "https://npmmirror.com/mirrors/node") + mirror = "https://npmmirror.com/mirrors/node/" + assert(util.getBaseUrl() == "https://npmmirror.com/mirrors/node") + mirror = "https://npmmirror.com/mirrors/node//" + assert(util.getBaseUrl() == "https://npmmirror.com/mirrors/node") + assert(select("#", util.getBaseUrl()) == 1, "the count of gsub must not leak out") + end) + os.getenv = saved + assert(ok, err) +end) +test("capture runs a command and returns what it printed", function() + local output, status = util.capture("echo hi") + assert(output == "hi\n", tostring(output)) + assert(status == 0 or status == nil, tostring(status)) +end) +test("shell_quote quotes one word", function() + assert(util.shell_quote("a b") == "'a b'") + assert(util.shell_quote("it's") == "'it'\\''s'") +end) +test("library hints follow the distribution", function() + local function on(existing, fn) + with_util({ + file_exists = function(path) + return existing[path] == true + end, + }, fn) + end + on({ ["/etc/alpine-release"] = true }, function() + assert(util.package_manager() == "apk") + assert(util.library_package_hint("libstdc++.so.6") == "the libstdc++ system package") + assert(util.install_command("libatomic.so.1") == "apk add libatomic") + end) + on({ ["/etc/openwrt_release"] = true }, function() + assert(util.package_manager() == "opkg") + assert(util.library_package_hint("libstdc++.so.6") == "the libstdcpp system package") + assert(util.install_command("libstdc++.so.6") == "opkg install libstdcpp") + end) + on({}, function() + assert(util.package_manager() == nil) + assert(util.library_package_hint("libstdc++.so.6") == "the libstdc++ system package (libstdcpp on OpenWrt)") + assert(util.library_package_hint("libatomic.so.1") == "the libatomic system package") + assert(util.library_package_hint("libfoo.so.2") == "the libfoo.so.2 shared library") + assert(util.install_command("libatomic.so.1") == nil) + end) +end) print(state.passed .. " passed, " .. state.failed .. " failed") assert(state.failed == 0) From 0ad5c6fd6d98ec3ecd551cdacc4b1374fc1d765a Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Wed, 21 Oct 2026 05:22:59 +0800 Subject: [PATCH 2/3] docs: document the plugin the way vfox-java does Rewrite the README and add a page per topic under docs/: versions and names, platforms, .nvmrc and .node-version, npm and global packages, troubleshooting, development and changes. The pages for the mirror, musl, LTS, building from source and E2E gain the details they lacked. The commands, messages and tables were taken from vfox 1.0.12 and 1.0.13 on Ubuntu 26.04 and Alpine 3.24.1. Pages that quote data give the day it was taken (2026-10-21), and docs/development.md has the commands that refresh it. What exists only on main, and not in 0.5.2, is marked "Unreleased" with the 0.5.2 behaviour next to it and listed in docs/changes.md. docs/development.md also lists the gaps that are still open. docs/e2e.md said the Windows suite keeps a Docker Compose runner; #40 replaced it with tests/e2e/windows/run.ps1. --- README.md | 118 +++++++++++++++--- docs/changes.md | 142 +++++++++++++++++++++ docs/development.md | 267 ++++++++++++++++++++++++++++++++++++++++ docs/e2e.md | 11 +- docs/lts.md | 82 +++++++++--- docs/mirror.md | 96 ++++++++++++++- docs/musl.md | 124 +++++++++++++++---- docs/npm.md | 75 +++++++++++ docs/nvmrc.md | 164 ++++++++++++++++++++++++ docs/platforms.md | 147 ++++++++++++++++++++++ docs/source-build.md | 63 +++++++++- docs/troubleshooting.md | 238 +++++++++++++++++++++++++++++++++++ docs/versions.md | 164 ++++++++++++++++++++++++ 13 files changed, 1625 insertions(+), 66 deletions(-) create mode 100644 docs/changes.md create mode 100644 docs/development.md create mode 100644 docs/npm.md create mode 100644 docs/nvmrc.md create mode 100644 docs/platforms.md create mode 100644 docs/troubleshooting.md create mode 100644 docs/versions.md diff --git a/README.md b/README.md index 068ad14..85a066d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ Node.js plugin for [vfox](https://vfox.dev/). +It installs the Node.js release archives published on +[nodejs.org](https://nodejs.org/dist/), or on a [mirror](docs/mirror.md) of it, +and checks every download against the SHA-256 sums published next to it. Each +release comes with its own npm. On Alpine, OpenWrt and other musl systems it +installs the musl builds. vfox reads `.nvmrc` and `.node-version` through it. + ## Install After installing [vfox](https://github.com/version-fox/vfox), install the plugin by running: @@ -10,6 +16,30 @@ After installing [vfox](https://github.com/version-fox/vfox), install the plugin vfox add nodejs ``` +vfox has to be hooked into your shell for `node` to be on the `PATH`, see its +[quick start](https://vfox.dev/guides/quick-start.html). + +### Example + +```bash +vfox add nodejs +vfox install nodejs@24 +vfox use --global nodejs@24 +``` + +You can verify this using the following command: + +```bash +node -v +``` + +`vfox search nodejs` lists every release, with the npm that comes with it and a +mark on the ones of an LTS line. In an interactive terminal it lets you pick one. + +Behavior that only exists on `main` and not in the latest release is marked +"Unreleased" in the pages below. [docs/changes.md](docs/changes.md) lists it, and +[docs/source-build.md](docs/source-build.md) gets you `main`. + ### Example: Ubuntu 26.04 ```bash @@ -28,40 +58,85 @@ not install them. ### Example: Alpine / musl ```bash +apk add bash libstdc++ libatomic vfox add nodejs -vfox install nodejs@26.11.1 -vfox use --global nodejs@26.11.1 +vfox install nodejs@24 +vfox use --global nodejs@24 ``` -You can verify this using the following command: +The Node.js musl builds need `libstdc++`, and the ones of 26.8.0 and later need +`libatomic` as well. A stock Alpine image has neither, and the plugin does not +install system libraries. The install stops with a message that names the +missing package and the command that installs it (`apk add libatomic`; OpenWrt +calls `libstdc++` the `libstdcpp` package). Unreleased: release 0.5.2 checks for +`libstdc++` only, and a missing `libatomic` shows when `node` fails with `Error +loading shared library libatomic.so.1`. musl builds exist for x64 and arm64 +only. musl details (OpenWrt, tarball matrix, mirror fallback, which releases +have a build): see [docs/musl.md](docs/musl.md). + +## Versions and names + +A version is a release number: `24.21.0`, or the start of one, `24.21` and `24`, +which install the newest release of that line. `latest` is the newest release of +any line, LTS or not. There is no `lts` alias, name the line (`nodejs@24`). The +name of an install is the release number, `24.21.0`, and `vfox uninstall` needs +it whole. See [docs/versions.md](docs/versions.md) for details, including what +`vfox use` and `vfox search` do with them. + +## Platforms + +Node.js publishes packages for Linux (x64, arm64, ppc64le, s390x), macOS (x64 and +arm64) and Windows (x64, arm64, and x86 up to 22.x). Not every release has every +one of them: Windows on ARM starts at 19.9.0, Linux arm64 at 4.0.0, Apple silicon +at 16.0.0 (older releases install the x64 package under Rosetta 2). 32-bit ARM +Linux has packages up to 23.11.1. Unreleased: release 0.5.2 cannot install them. +riscv64 and loong64 are not supported. `vfox search` lists every release +whatever your platform is. See [docs/platforms.md](docs/platforms.md) for the +table. + +## npm and global packages + +Packages installed with `npm install -g` belong to the Node.js release that was in +use, and are gone from the others. Node.js 25 and later does not ship Corepack. +See [docs/npm.md](docs/npm.md). -```bash -node -v -``` +## Mirror -musl details (OpenWrt, tarball matrix, mirror fallback): see [docs/musl.md](docs/musl.md). +Set `VFOX_NODEJS_MIRROR` to use a mirror of `nodejs.org/dist`, for example +`https://npmmirror.com/mirrors/node`. See [docs/mirror.md](docs/mirror.md) for +details and for mirrors that were checked. -## Mirror +## `.nvmrc` and `.node-version` -See [docs/mirror.md](docs/mirror.md) for details. +vfox reads these files when you enter a directory, and selects the release they +name if it is installed. What a file can say depends on vfox's +`legacyVersionFile.strategy`: with the default, `specified`, only a full release +number selects something. See [docs/nvmrc.md](docs/nvmrc.md) for details. -## FAQ +## LTS -### Install from source +See [docs/lts.md](docs/lts.md) for the LTS lines, how to install one and how +`lts/` is resolved. -See [docs/source-build.md](docs/source-build.md) for details. +## pnpm -### LTS +pnpm as a package manager works next to this plugin. A `node` that pnpm installs +itself can shadow vfox's. See [docs/pnpm.md](docs/pnpm.md). -See [docs/lts.md](docs/lts.md) for details. +## Troubleshooting -### pnpm +See [docs/troubleshooting.md](docs/troubleshooting.md) for the messages the +plugin prints, proxies and where Node.js is. -See [docs/pnpm.md](docs/pnpm.md) for details. +## Install from source -### Testing +See [docs/source-build.md](docs/source-build.md) for details. -See [docs/e2e.md](docs/e2e.md) for details. +## Development + +See [docs/development.md](docs/development.md) for how the plugin works and how +to change it, [docs/e2e.md](docs/e2e.md) for the container tests, and +[docs/changes.md](docs/changes.md) for what changed in each release. ## Releasing this plugin @@ -77,3 +152,10 @@ the tag. If publication fails, re-run the original failed job to resume it. The workflow follows the shared `@v1` release-tool version. Updating the tool does not release this plugin. See the [shared workflow documentation](https://github.com/version-fox/plugin-manifest-action) for the package contract and first-rollout requirements. + +## Thanks + +- [Node.js](https://nodejs.org/), whose release archives, checksums and version + index the plugin installs from +- [Node.js Unofficial Builds](https://github.com/nodejs/unofficial-builds), where + the musl builds that nodejs.org lacks come from diff --git a/docs/changes.md b/docs/changes.md new file mode 100644 index 0000000..b56aa0a --- /dev/null +++ b/docs/changes.md @@ -0,0 +1,142 @@ +# Changes + +What changed in each release, as a user notices it. GitHub's +[release pages](https://github.com/version-fox/vfox-nodejs/releases) list the +pull requests. The newest release at the time of writing is 0.5.2, and `main` is +ahead of it by everything under "Unreleased". +[source-build.md](source-build.md) gets you those changes now. + +## Unreleased + +- 32-bit ARM Linux installs the `linux-armv7l` package, which Node.js published + up to 23.11.1: `vfox install nodejs@20` works there. vfox's `armv7` build + reports `arm`, and release 0.5.2 asks for `linux-arm`, which does not exist, so + every install on 32-bit ARM ends with `Node.js package + node-v20.19.5-linux-arm.tar.gz is not listed in ...`. From 24 on there is no + package, and the ARMv6 one is never chosen. See + [platforms.md](platforms.md#not-supported). +- On musl the install stops when the unpacked `node` cannot run for lack of a + library. After the unpack the plugin runs `node --version` once, and the dynamic + loader names what is missing. The musl packages of 26.8.0 and later need + `libatomic`, which a stock Alpine does not have: the install ends with + ``Node.js (musl build) requires the libatomic system package, which was not + found on this system. Install it with `apk add libatomic` and reinstall.`` and + nothing is registered. Release 0.5.2 checks for `libstdc++` only, so the + install succeeds and `node` fails with `Error loading shared library + libatomic.so.1`. The message about `libstdc++` names its command too, where + release 0.5.2 says `It is normally installed automatically`. See + [musl.md](musl.md#requirements). +- `.nvmrc` and `.node-version` name a release by its numbers under + `legacyVersionFile.strategy` `latest_installed` and `latest_available`. With + 22.9.0 and 22.23.3 installed, `22` selects 22.23.3, where release 0.5.2 selects + 22.9.0, because vfox lists the installed releases as text. `22.2` no longer + matches 22.23.3. `22.x`, `22.x.x` and `22.*` mean 22, and release 0.5.2 selects + nothing for `22.x`. Only a leading `v` is dropped, so `lts/` works for + a codename with a `v` in it. Text that is no release number (`node`, `(`, + `22[`) is left alone, where release 0.5.2 fails with `unfinished capture at + EOS` and vfox hides the error. See [nvmrc.md](nvmrc.md#how-a-prefix-is-matched). +- A failed request for `SHASUMS256.txt` says why: `get checksum failed: HTTP 404 + from https://nodejs.org/dist/v24.99.0/SHASUMS256.txt`, or the network error, + such as `x509: certificate signed by unknown authority`. Release 0.5.2 prints + `get checksum failed` and nothing else. See + [troubleshooting.md](troubleshooting.md#messages-from-the-plugin). +- `VFOX_NODEJS_MIRROR` may end in several slashes. Release 0.5.2 strips one. + +## 0.5.2 (2026-10-17) + +- `vfox use` prints a note when another `node`, such as the one that `pnpm + runtime set node -g` installs, comes before vfox in `PATH`, so that `node` does + not run the release that was just selected. Before, `vfox use` reported + `Now using` and `node -v` printed another version. See [pnpm.md](pnpm.md) + ([#38](https://github.com/version-fox/vfox-nodejs/pull/38)). +- The Windows e2e jobs run against a home directory with a space in it, which + needs vfox 1.0.13 ([#39](https://github.com/version-fox/vfox-nodejs/pull/39)). + +## 0.5.1 (2026-10-17) + +- Alpine, OpenWrt and other musl systems get the musl packages. Before, the glibc + package was installed and `node` failed with `not found`. Where nodejs.org has + no musl package, the plugin takes it from unofficial-builds.nodejs.org, which is + all of arm64 and most of x64. `VFOX_NODEJS_LIBC` overrides the detection. + An install without `libstdc++` fails with a message that names it. See + [musl.md](musl.md) + ([#31](https://github.com/version-fox/vfox-nodejs/pull/31), + [#29](https://github.com/version-fox/vfox-nodejs/issues/29)). +- `.nvmrc` and `.node-version` work with `legacyVersionFile.strategy` set to + `latest_installed` under vfox 1.x. Releases 0.3.0 to 0.5.0 call + `ctx:getInstalledVersions()`, which vfox 1.x rejects with `expected 0 arguments + for func() []string, got 1`, and vfox hides the error: the file selects + nothing, even `22.23.3`. The default strategy, `specified`, was not affected + ([#30](https://github.com/version-fox/vfox-nodejs/pull/30)). +- Under `latest_available`, `lts` and `lts/` are looked up in the version + index, so a codename newer than the plugin keeps working. Krypton (24) is in the + table that the other strategies use. See [lts.md](lts.md) + ([#31](https://github.com/version-fox/vfox-nodejs/pull/31)). +- The repository has a container e2e suite for Linux, Alpine, Windows and WSL, + and a dev container, see [e2e.md](e2e.md) + ([#30](https://github.com/version-fox/vfox-nodejs/pull/30), + [#33](https://github.com/version-fox/vfox-nodejs/pull/33), + [#34](https://github.com/version-fox/vfox-nodejs/pull/34), + [#37](https://github.com/version-fox/vfox-nodejs/pull/37)). + +## 0.5.0 (2026-09-12) + +- `vfox search nodejs` fails with `Failed to fetch Node.js versions: ...` when the + index cannot be fetched. Before, it listed nothing and vfox cached the empty + list ([#28](https://github.com/version-fox/vfox-nodejs/pull/28), + [#16](https://github.com/version-fox/vfox-nodejs/issues/16)). +- 32-bit x86 asks for the `x86` package. Before it asked for `386`, which does not + exist. An empty architecture from vfox stops the install with `Cannot determine + Node.js package architecture from RUNTIME.archType` + ([#28](https://github.com/version-fox/vfox-nodejs/pull/28), + [#23](https://github.com/version-fox/vfox-nodejs/issues/23)). +- A package that `SHASUMS256.txt` does not list stops the install with `Node.js + package ... is not listed in ...`. Before, vfox went on to a download without + a checksum, which answered 404 + ([#28](https://github.com/version-fox/vfox-nodejs/pull/28)). +- `VFOX_NODEJS_MIRROR` may end in a slash. Before, the URLs had `//` in them + ([#25](https://github.com/version-fox/vfox-nodejs/pull/25)). + +## 0.4.1 (2025-09-29) + +- Node.js 16.0.0 installs the arm64 package on Apple silicon. Before, 16 took the + amd64 one, and only releases before 16 should. Those say `Note: Node.js + versions below 16 do not support Apple Silicon (arm64), using x64 architecture + with Rosetta 2` ([#21](https://github.com/version-fox/vfox-nodejs/pull/21)). + +## 0.4.0 (2025-09-28) + +- `.nvmrc` and `.node-version` follow `legacyVersionFile.strategy`. Before, they + always used `latest_installed`. See [nvmrc.md](nvmrc.md) + ([#17](https://github.com/version-fox/vfox-nodejs/pull/17)). +- `lts` and `lts/*` select the newest LTS line of the table, and `lts/jod` is + known ([#17](https://github.com/version-fox/vfox-nodejs/pull/17)). +- An unknown version says `version not found for provided version `. Before, the hook failed with `cannot perform concat operation between + string and nil` ([#14](https://github.com/version-fox/vfox-nodejs/pull/14)). + +## 0.3.0 (2024-04-22) + +- vfox reads `.nvmrc` and `.node-version`: exact versions, majors and + `lts/`, see [nvmrc.md](nvmrc.md) + ([#7](https://github.com/version-fox/vfox-nodejs/pull/7)). + +## 0.2.x (2024-04) + +- 0.2.2 (2024-04-09): `vfox use nodejs@20` and `nodejs@20.11` find the installed + releases ([#6](https://github.com/version-fox/vfox-nodejs/pull/6)). +- 0.2.1 (2024-04-08): `vfox install nodejs@20` and `nodejs@20.11` install the + newest release of the line. They failed before + ([#5](https://github.com/version-fox/vfox-nodejs/pull/5)). +- 0.2.0 (2024-04-07): `VFOX_NODEJS_MIRROR`, see [mirror.md](mirror.md) + ([#3](https://github.com/version-fox/vfox-nodejs/issues/3)). Apple silicon + installs the amd64 package of Node.js 16 and earlier + ([#4](https://github.com/version-fox/vfox-nodejs/issues/4)). + +## 0.0.x and 0.1.x (2024-03 and 2024-04) + +- 0.0.1 (2024-03-26) is the first release. `vfox search nodejs` failed in it, and + works from 0.0.5. Installing a full release number, with the check of its + SHA-256 sum, works from the start. +- 0.1.1 (2024-04-01) adds the lookup of a line for `vfox use`, which finds + nothing until 0.2.2. diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..4a13157 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,267 @@ +# Development + +## Layout + +```text +metadata.lua plugin name, version, and .node-version and .nvmrc as legacy files +hooks/available.lua vfox search: the version index +hooks/pre_install.lua vfox install: choose the package, its URL and its checksum +hooks/post_install.lua musl: fail when node cannot run, such as without libstdc++ or libatomic +hooks/env_keys.lua PATH: bin on Linux and macOS, the install directory on Windows +hooks/pre_use.lua vfox use: map 24 or 24.21 to an installed release, note a shadowing node +hooks/parse_legacy_file.lua .nvmrc and .node-version +lib/util.lua mirror URL, musl detection and missing libraries, running a command, version comparison and matching, shorthands, PATH probe +tests/hooks_test.lua offline hook tests +tests/e2e/ container tests, see e2e.md +Injection.lua type hints for what vfox injects, never loaded +.devcontainer/ Linux development container, see e2e.md +.github/workflows/ test, lint, license, e2e and publish +``` + +vfox loads `metadata.lua`, calls a hook when a command needs it, and puts +`PLUGIN` and `RUNTIME` (`osType`, `archType`, `version`) in scope, and the +modules `http` and `json`. `osType` and `archType` are Go's `GOOS` and `GOARCH` +of the vfox binary. `RUNTIME.envType` is read when a vfox provides it (none +does yet), see [musl.md](musl.md#detection-and-override). `lib/util.lua` is +loaded with `require("util")`. + +## What runs for a command + +| You run | Hooks | +| --- | --- | +| `vfox search nodejs` | `Available` | +| `vfox install nodejs@24` | `PreInstall` returns the release, URL and checksum, and calls `Available` for `latest` and for a start of a release. vfox downloads, verifies and unpacks, then calls `PostInstall` | +| `vfox use nodejs@24` | `PreUse` returns the installed release to select. `EnvKeys` provides `PATH` | +| a shell hook, `cd` into a directory with `.nvmrc` | `ParseLegacyFile`, then `EnvKeys` | +| `vfox uninstall nodejs@24.21.0` | none, the name has to be exact | + +A few contracts that cost time to find: + +- `PreInstall` returns `version`, `url`, `sha256` and sometimes `note`. + `version` is the name of the install, so a start of a release has to be + resolved to the full release here. vfox only warns when there is no checksum + (`Checksum is not provided`) and goes on to download, so since 0.5.0 the hook + fails when the package is not in `SHASUMS256.txt` instead of returning none. + Notes that the hook prints with `print` show up in vfox's output. +- `Available` raises on a network error or a status other than 200. Since 0.5.0 + it does not return an empty list, which vfox cached. The result is kept in the + global `available_result` for the life of the process, and the tests reset it. + vfox caches the search itself for 12 hours. +- `PreUse` returns `{ version = nil }` for what it cannot map, and vfox answers + `is not installed`. The `ctx.installedSdks` it gets is a table keyed by + release, not an array, which is why `calculate_shorthand` iterates with + `pairs`. +- `ParseLegacyFile` calls `ctx.getInstalledVersions()` with no argument. vfox 1.x + binds it as a plain Go function and rejects the extra argument that + `ctx:getInstalledVersions()` adds. The strategy is `ctx.strategy`. vfox hands + over the installed versions as text sorted descending, so `22.9.0` comes + before `22.23.3`. The test passes the function as a plain function and asserts + that it was called without arguments. +- `PostInstall` gets `ctx.rootPath` and `ctx.sdkInfo`, a table keyed by the name + of the SDK (`nodejs`, or the alias) whose `path` is the unpacked release. + `bin/node` is there by then. The hook walks `ctx.sdkInfo` instead of reading + `ctx.sdkInfo.nodejs`, so an alias works too. `io.popen` and `os.execute` work in + vfox's Lua. `io.popen(...):close()` returns the exit status as a number, not + `true` as in standard Lua, and `util.capture` copes with both. +- `EnvKeys` sets `PATH` only, nothing like `NODE_HOME`. +- vfox removes a leading `v` from what the user types before the hooks see it. + +## Resolving a version + +`PreInstall`: + +1. `latest` becomes the first entry of `Available`, which is sorted newest first + by `util.compare_versions`, numbers one by one. +2. Anything that is not `x.y.z` (`^%d+%.%d+%.%d+$`, `util.is_semver_simple`) is + looked up in `util.calculate_shorthand(Available)`, a table from `24` and + `24.21` to the newest release of each. A miss ends in `version not found for + provided version `. +3. A release that does not exist passes both steps and fails when + `SHASUMS256.txt` answers 404: `get checksum failed`. + +`PreUse` does step 2 against the installed releases. `ParseLegacyFile` has its own +rules, see below. + +## Packages + +`PreInstall` builds the file name `node-v--` +(`util.FileName`) from `RUNTIME`: `amd64` is `x64`, `386` is `x86`, `arm` is +`armv7l`, Windows is `win` and `.zip`. macOS arm64 with a major below 16 takes +`x64`. On Linux `util.is_musl()` adds `-musl` for `x64` and `arm64`, and errors +for other CPUs. It then reads `/v/SHASUMS256.txt`, finds the sum +with `util.get_checksum`, and returns the URL next to the sums. A request that +fails ends in `get checksum failed: ` or `get checksum failed: HTTP + from
`. If a musl package is missing there, it tries +`UnofficialBaseUrl` the same way. The tables of what exists are in +[platforms.md](platforms.md) and [musl.md](musl.md). + +`util.getBaseUrl()` is the mirror: `$VFOX_NODEJS_MIRROR` without trailing +slashes, or `https://nodejs.org/dist`. The unofficial site ignores it. + +`post_install.lua` does nothing outside musl. On musl it fails when no +`libstdc++.so.6` is found in the usual library directories +(`util.has_libstdcxx`). Then it runs the unpacked `bin/node --version` once with +`util.capture`: the dynamic loader names a library that is missing (`Error +loading shared library libatomic.so.1`), and the hook turns that into a message +with the package and, on Alpine and OpenWrt, the command (`util.MuslLibraries` +holds the package names, `util.install_command` builds the command). Any other +failure, and a runtime that cannot run a command, leave the install alone, so a +check that cannot work never blocks an install. See +[musl.md](musl.md#requirements). + +## Legacy files + +`ParseLegacyFile` reads the file, removes all whitespace, and gives the text to +`resolve_version(query, strategy)`: it drops a leading `v`, lowers the case, +turns `lts-x` into `lts/x`, and maps `lts/` to a major, `lts` and +`lts/*` to the highest major in `nodejs_codenames`. Under `latest_available` the +codename comes from the index first (`discover_lts`), and the table is the +fallback when that finds nothing or the request fails. + +`resolve_legacy_version(strategy, query)` then reads the query with +`util.version_prefix`: `22` is `{ 22 }`, `22.23` is `{ 22, 23 }`, and `22.x`, +`22.x.x` and `22.*` are `{ 22 }`. Text that is no such prefix (`node`, +`lts/unknown`, `>=20`) is returned as it is. For a prefix: + +- `specified`: returns the query. +- `latest_installed`: the newest of `ctx.getInstalledVersions()` that starts with + the prefix (`util.newest_matching`, compared as numbers part by part). If none + does, it does what `latest_available` does. +- `latest_available`: the newest of `Available` that starts with the prefix, or + the query itself. + +`vfox` then selects the answer if it is installed. User-facing rules and +examples are in [nvmrc.md](nvmrc.md) and [lts.md](lts.md). + +Unreleased: release 0.5.2 matches the query as a Lua pattern against the start of +each release, in the order vfox lists them, and takes the first one that matches. +See [nvmrc.md](nvmrc.md#how-a-prefix-is-matched). + +## When Node.js promotes a line to LTS + +Each October an even-numbered line gets a codename, see +[lts.md](lts.md#when-nodejs-promotes-a-line). To teach the plugin the new one: + +1. Add ` = ` to `nodejs_codenames` in + `hooks/parse_legacy_file.lua`. +2. Add a case next to `legacy lts krypton maps to its major` in + `tests/hooks_test.lua`. +3. Add a row to the table in [lts.md](lts.md#what-this-plugin-understands), and + change the "24 at the time of writing" sentences in [lts.md](lts.md) and + [nvmrc.md](nvmrc.md). +4. `DefaultLTSVersion` in `tests/e2e/common/common.go` may stay: it only has to + stay inside the table. +5. Release. + +## Tests + +The hook tests run offline under `gopher-lua`, the Lua interpreter vfox embeds: + +```bash +go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/hooks_test.lua +``` + +Run it from the repository root. `tests/hooks_test.lua` stubs `http` and `json` +itself (`package.preload`), sets `RUNTIME` per case, and answers requests from +`state.response`, `state.decoded` and `state.responder`. A case is +`test("name", function() ... end)`, which resets the state and `available_result`. +It covers `Available` and its failures, the package name per platform and the +errors of a failed checksum request, musl and `PostInstall` (with `util.capture`, +`util.file_exists` and `util.has_libstdcxx` stubbed through `with_util`), +`ParseLegacyFile` under each strategy, `PreUse` with the shadowing `node`, and +the helpers of `lib/util.lua`. Add the case a change needs there. + +gopher-lua 1.1.1 loses a `local x = nil` that a closure assigns: the outer scope +still reads `nil`. A test that collects a value from inside `with_util` or +`pcall` has to keep it in a table field. + +To call one hook against the real servers, vfox 1.0.13 and later has +`vfox plugin run`. It does not register the plugin, `--os` and `--arch` answer +for another platform, `--offline` rejects HTTP, and `VFOX_NODEJS_MIRROR` applies: + +```bash +vfox plugin run . Available --json +vfox plugin run . PreInstall --input '{"version":"24"}' --os windows --arch arm64 --json +vfox plugin run . PreUse --input '{"version":"22","installedSdks":{"22.23.3":{"version":"22.23.3"}}}' --json +vfox plugin run . ParseLegacyFile --input '{"filename":".nvmrc","filepath":"/tmp/p/.nvmrc","strategy":"latest_installed","installedVersions":["24.21.0"]}' --json +vfox plugin run . EnvKeys --input '{"path":"C:\\vfox\\nodejs-24.21.0"}' --os windows --arch amd64 --json +``` + +`vfox --debug ` shows what vfox does with a hook, which is how to find +an error it swallows, such as one from `ParseLegacyFile`. + +Lint, license headers and the container tests are in +[e2e.md](e2e.md#local-checks). Lua is formatted with StyLua (`stylua.toml`: 4 +spaces, 120 columns), checked by LuaLS at `Warning` level (`.luarc.json` declares +`PLUGIN` and `RUNTIME`). Source files (Lua, Go, YAML, PowerShell and the +Dockerfiles) carry the license header, Markdown and JSON do not. + +## Refreshing the platform table + +[platforms.md](platforms.md) was taken from nodejs.org. `index.json` lists the +releases, newest first, with the files each has, and this prints for each package +the first and the last release that has it, and how many releases in between do +not: + +```bash +curl -s https://nodejs.org/dist/index.json | jq -r ' + def span($key): + [to_entries[] | select(.value.files | index($key))] as $hits + | if ($hits | length) == 0 then "none" + else "\($hits[-1].value.version) to \($hits[0].value.version), \($hits | length) releases, \($hits[-1].key - $hits[0].key + 1 - ($hits | length)) missing in between" + end; + ("linux-x64", "linux-arm64", "linux-ppc64le", "linux-s390x", "linux-x86", "linux-x64-musl", + "osx-x64-tar", "osx-arm64-tar", "win-x64-zip", "win-x86-zip", "win-arm64-zip") as $key + | "\($key): \(span($key))"' +``` + +The keys are the index's names for the plugin's files (`osx-x64-tar` is +`darwin-x64.tar.gz`, `win-x64-zip` is `win-x64.zip`). The index agrees with the +`SHASUMS256.txt` files, which are what the plugin reads, except for two releases: +it lists 0.11.4 and 0.11.6 for `linux-x64`, `linux-x86` and macOS x64, and they +have no package. For the musl table run the same program against +`https://unofficial-builds.nodejs.org/download/release/index.json` with +`linux-x64-musl` and `linux-arm64-musl`, and against the official index with +`linux-x64-musl`. Each answers for one site, so combine the two. + +## Known gaps + +- 32-bit ARM takes the `linux-armv7l` package, up to 23.11.1. The ARMv6 package + (`linux-armv6l`) is never chosen, and from 24 on nothing exists for 32-bit ARM. +- riscv64 and loong64 only have packages on Unofficial Builds, which the plugin + uses for musl only. +- `PostInstall` runs `node --version` on musl only. On glibc a missing + `libatomic.so.1` (Node.js 25 and later) shows when `node` runs: the README + leaves system libraries to the user. +- The e2e Alpine image gets `libatomic` through `go`, which depends on `gcc`, so + the suite does not cover a stock Alpine without it. The hook tests do. +- `.nvmrc` aliases (`node`, `stable`, `lts/-1`), ranges and comments are not + understood, and `lts` follows a hand-made table, see + [nvmrc.md](nvmrc.md#not-understood). +- `latest_available` downloads `index.json` at every prompt in a directory with a + legacy file. +- The unofficial site is not covered by `VFOX_NODEJS_MIRROR`. +- macOS has no e2e job. + +## Releasing + +A maintainer runs **Actions → Plugin → Run workflow** on the default branch with +the version, without `v`. The README describes what the shared workflow does. +Afterwards `plugins/nodejs.json` in +[vfox-plugins](https://github.com/version-fox/vfox-plugins) shows the version +that `vfox add nodejs` serves. + +The documentation marks what is newer than the last release with "Unreleased". +After a release, move those entries in [changes.md](changes.md) under the new +version and take the remarks out: `grep -rn -i unreleased docs README*`. Also +search for the number of the previous release: a few sentences compare against +it without saying "Unreleased". + +## Writing the docs + +English pages are in `docs/`. Prose is wrapped at 80 columns, tables are not. +Pages that quote data give the day it was taken (the platform and musl tables, +the mirror list, the LTS table, the outputs of `vfox search`), and nothing +checks them. CI does not lint Markdown, so run the commands you add, and the +outputs in a page are the ones they printed. When a change in the plugin makes a +page wrong, change the page in the same pull request. diff --git a/docs/e2e.md b/docs/e2e.md index 7118928..63dd938 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -50,8 +50,9 @@ the Windows filesystem works too. Windows *container* coverage (`tests/e2e/windows`) cannot run in a dev container: the Dev Containers spec does not support Windows container images ([vscode-remote-release#445](https://github.com/microsoft/vscode-remote-release/issues/445)), -and Windows containers only run on a Windows daemon. That suite keeps its -Docker Compose runner on a Windows host; see [Windows 11](#windows-11) below. +and Windows containers only run on a Windows daemon. That suite runs from a +Windows host through `tests/e2e/windows/run.ps1`; see [Windows 11](#windows-11) +below. ## What the suite verifies @@ -141,6 +142,12 @@ flow, proving the plugin selects the `*-musl` tarballs ([issue #29](https://github.com/version-fox/vfox-nodejs/issues/29)). See [docs/musl.md](musl.md) for the tarball matrix. +The image installs `libstdc++` on purpose. It gets `libatomic` only through +`go`, which depends on `gcc`, so the suite does not cover a stock Alpine, where +the musl packages of 26.8.0 and later need `libatomic` and the install stops +without it. The hook tests cover that check (`post_install names the library the +loader cannot find`), see [docs/musl.md](musl.md#requirements). + Set `E2E_LINUX_BASE` to switch the same Linux suite onto the Alpine image: ```bash diff --git a/docs/lts.md b/docs/lts.md index ce7335b..f88cac8 100644 --- a/docs/lts.md +++ b/docs/lts.md @@ -5,6 +5,31 @@ they keep receiving fixes long after odd releases go end-of-life, so most production setups pin an LTS line. Each LTS line has a codename (`argon` for 4, `boron` for 6, …, `krypton` for 24). +## Installing an LTS line + +The plugin has no `lts` alias for `vfox install`: `vfox install nodejs@lts` +stops with `version not found for provided version lts`. Name the line instead, +which installs the newest release of it: + +```bash +vfox install nodejs@24 +``` + +`vfox search nodejs` marks the releases of LTS lines with `(LTS)`, so the newest +LTS release is the first line that has the mark: + +```text +$ vfox search nodejs | grep -m1 '(LTS)' + - 24.22.0 (LTS) [npm 11.19.1] +``` + +Install it with `vfox install nodejs@24.22.0`, or with `nodejs@24` to follow the +line. The mark is the `lts` field of the version index, which Node.js sets from +the day a line enters long-term support: 24.11.0 is the first release of +Krypton with it, 24.10.0 and the ones before have none. `lts/` is +understood in `.nvmrc` and `.node-version` files only, see +[nvmrc.md](nvmrc.md). + ## What this plugin understands Under the `latest_available` strategy the plugin discovers LTS codenames @@ -12,26 +37,48 @@ dynamically from the version index (`index.json` carries the codename in each release's `lts` field), so codenames that postdate this plugin keep working there. Everywhere else — and as the offline fallback — a hardcoded table in `hooks/parse_legacy_file.lua` is used, because `specified` and -`latest_installed` must resolve without network access: - -| Codename | Major | -| -------- | ----- | -| argon | 4 | -| boron | 6 | -| carbon | 8 | -| dubnium | 10 | -| erbium | 12 | -| fermium | 14 | -| gallium | 16 | -| hydrogen | 18 | -| iron | 20 | -| jod | 22 | -| krypton | 24 | +`latest_installed` must resolve without network access. "First LTS release" and +"Last release" are from the index on 2026-10-21, and Krypton is still receiving +releases: + +| Codename | Major | First LTS release | Last release | +| -------- | ----- | ----------------- | ------------ | +| argon | 4 | 4.2.0 | 4.9.1 | +| boron | 6 | 6.9.0 | 6.17.1 | +| carbon | 8 | 8.9.0 | 8.17.0 | +| dubnium | 10 | 10.13.0 | 10.24.1 | +| erbium | 12 | 12.13.0 | 12.22.12 | +| fermium | 14 | 14.15.0 | 14.21.3 | +| gallium | 16 | 16.13.0 | 16.20.2 | +| hydrogen | 18 | 18.12.0 | 18.20.8 | +| iron | 20 | 20.9.0 | 20.20.2 | +| jod | 22 | 22.11.0 | 22.23.3 | +| krypton | 24 | 24.11.0 | 24.22.0 | + +## When Node.js promotes a line + +An even-numbered line enters long-term support each October and gets its +codename then. The [release schedule](https://github.com/nodejs/Release) puts +Node.js 26 on 2026-10-28, with no codename announced yet. + +- Under `latest_available` the plugin finds the new codename in the index the + day it appears. `lts` and `lts/*` then name the newest release of the new + line. +- Under `specified` and `latest_installed` the table above decides, and the + table changes with a release of the plugin. Until then `lts/` + selects nothing, and `lts` and `lts/*` keep meaning the highest line in the + table, 24 at the time of writing. +- `(LTS)` in `vfox search` follows the index, so it changes at once. + +The table is in `hooks/parse_legacy_file.lua`, see +[development.md](development.md#when-nodejs-promotes-a-line-to-lts). ## Examples Given an `.nvmrc` (the same applies to `.node-version`) and -`22.23.3` plus `24.21.0` installed: +`22.23.3` plus `24.21.0` installed. Examples 1 to 4 run under the +`latest_installed` strategy that example 1 sets, see +[nvmrc.md](nvmrc.md#the-strategy-decides) for the strategies: ```sh # 1. LTS codename -> major, then strategy picks the release. @@ -43,7 +90,8 @@ v22.23.3 ``` ```sh -# 2. Bare `lts` (or `lts/*`) means "the highest mapped major", currently 24. +# 2. Bare `lts` (or `lts/*`) means "the highest mapped major", 24 at the time +# of writing. $ printf 'lts\n' > .nvmrc $ cd && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version v24.21.0 diff --git a/docs/mirror.md b/docs/mirror.md index c882dad..96a4f90 100644 --- a/docs/mirror.md +++ b/docs/mirror.md @@ -14,16 +14,104 @@ paths (`/index.json`, `/v/...`) to that base URL. Trailing slashes are handled, so both `https://nodejs.org/dist` and `https://nodejs.org/dist/` work. +## Setting it + +The variable is read from the environment of the process that runs vfox, so set +it where your shell does: + +```bash +# bash, zsh: in ~/.bashrc or ~/.zshrc +export VFOX_NODEJS_MIRROR="https://npmmirror.com/mirrors/node" + +# fish +set -Ux VFOX_NODEJS_MIRROR https://npmmirror.com/mirrors/node + +# for one command +VFOX_NODEJS_MIRROR=https://npmmirror.com/mirrors/node vfox search nodejs +``` + +```powershell +# PowerShell: this session only +$env:VFOX_NODEJS_MIRROR = "https://npmmirror.com/mirrors/node" + +# PowerShell: for the user, in the shells you open later +[Environment]::SetEnvironmentVariable("VFOX_NODEJS_MIRROR", "https://npmmirror.com/mirrors/node", "User") +``` + +An empty value is the same as unset: `https://nodejs.org/dist`. + +## What a mirror has to serve + +The same layout as `nodejs.org/dist`: + +```text +/index.json +/v24.21.0/SHASUMS256.txt +/v24.21.0/node-v24.21.0-linux-x64.tar.gz +``` + +- `index.json` decides what `vfox search nodejs`, `nodejs@latest`, `nodejs@24` and + `nodejs@24.21` see. A mirror that lags behind lists fewer releases, and a + release it lists but does not carry fails at the download. +- The sums in the mirror's own `SHASUMS256.txt` are what the download is checked + against. A mirror whose files differ from its sums ends in `checksum error`. +- Redirects are followed. `https://npmmirror.com/mirrors/node` answers + `index.json` with a redirect, and works. + +## Mirrors that work + +These were checked on 2026-10-21: each served an `index.json` with the same 872 +releases as nodejs.org, the same SHA-256 sum for `node-v24.21.0-linux-x64.tar.gz` +and the package itself, and the musl package of 26.12.0. The list records that +and endorses nothing. A mirror can change or go away without the plugin knowing. + +| Mirror | `VFOX_NODEJS_MIRROR` | +| --- | --- | +| npmmirror | `https://npmmirror.com/mirrors/node` (`https://cdn.npmmirror.com/binaries/node` is the same data) | +| Huawei Cloud | `https://mirrors.huaweicloud.com/nodejs` | +| Tsinghua University (TUNA) | `https://mirrors.tuna.tsinghua.edu.cn/nodejs-release` | +| USTC | `https://mirrors.ustc.edu.cn/node` | +| Alibaba Cloud | `https://mirrors.aliyun.com/nodejs-release` | +| Tencent Cloud | `https://mirrors.cloud.tencent.com/nodejs-release` | +| Nanjing University | `https://mirror.nju.edu.cn/nodejs-release` | +| Beijing Foreign Studies University | `https://mirrors.bfsu.edu.cn/nodejs-release` | + +The Node.js mirror is not the npm registry. Packages that `npm install` fetches +come from the registry in your npm config, for example `npm config set registry +https://registry.npmmirror.com`, see [npm.md](npm.md). + +## Checking that it applies + +A mirror that does not answer fails instead of falling back to nodejs.org, which +is how you can tell the variable is read: + +```bash +VFOX_NODEJS_MIRROR=https://example.invalid/dist vfox search nodejs +# Failed to fetch Node.js versions: Get "https://example.invalid/dist/index.json": ... +``` + +On the same mirror `vfox install nodejs@24.21.0` fails with `get checksum failed: +Get "https://example.invalid/dist/v24.21.0/SHASUMS256.txt": ...` (Unreleased: +release 0.5.2 leaves out what follows `failed`). A mirror that serves no +`index.json` at that address fails the search with +`Failed to fetch Node.js versions: HTTP 404`, see +[troubleshooting.md](troubleshooting.md#network). vfox caches the search for 12 +hours, so after a change run `vfox config cache.availableHookDuration 0` or +expect the old list for a while. + +A proxy is a different setting, and covers the mirror as well, see +[troubleshooting.md](troubleshooting.md#network). + ## musl and unofficial builds `VFOX_NODEJS_MIRROR` only covers `nodejs.org/dist`. It does **not** apply to `https://unofficial-builds.nodejs.org/download/release`, which the plugin uses as a fallback on musl Linux (see [musl](musl.md)): -- Linux musl x64 on a recent release (`26.8+`, `24.20+`, `22.23.3+`): the - official `*-musl` tarball is used, so the mirror applies normally (as long - as the mirror carries it). -- Linux musl arm64 (all releases), or musl x64 on an older release: the +- Linux musl x64 on a release that nodejs.org carries (22.23.3, 24.20.0 to + 24.22.0, 26.8.0 and later): the official `*-musl` tarball is used, so the + mirror applies normally (as long as the mirror carries it). +- Linux musl arm64 (all releases), or musl x64 on any other release: the official index has no such file, so the plugin downloads from `unofficial-builds.nodejs.org` directly, ignoring the mirror. diff --git a/docs/musl.md b/docs/musl.md index 15ac6f0..7fba115 100644 --- a/docs/musl.md +++ b/docs/musl.md @@ -7,43 +7,117 @@ See [issue #29](https://github.com/version-fox/vfox-nodejs/issues/29). This plugin detects musl and installs a musl tarball instead. +## Alpine in a few lines + +```sh +apk add bash libstdc++ libatomic +vfox add nodejs +vfox install nodejs@24 +vfox use --global nodejs@24 +``` + +`vfox use` needs vfox's shell hook, and vfox does not support BusyBox `ash` +(`vfox activate sh` answers `unknown target shell sh`), so run it from bash with +`eval "$(vfox activate bash)"` in place. The two libraries are explained below. + ## Requirements -The musl builds link `libstdc++`, which musl distributions normally install -automatically alongside the rest of the system toolchain, so there is -usually nothing extra to do. If `node --version` ever fails with -`Error relocating ... symbol not found`, the system is missing its -`libstdc++` (`libstdcpp` on OpenWrt) runtime. The plugin's `PostInstall` -hook detects this case and fails the install with a pointer at the README -instead of leaving the cryptic relocation error. - -`libatomic1` is sometimes needed by glibc Node.js binaries on Ubuntu, but it -is **not** a requirement for the supported musl Node.js builds. In particular, -Alpine does not need an extra `apk add libatomic` for this plugin. If another -program on the system explicitly reports a missing `libatomic.so.1`, Alpine's -package is named `libatomic` and can be installed with `apk add libatomic`; -that is separate from Node.js's musl runtime requirement above. +The Node.js musl packages link system libraries that the plugin does not +install. A stock Alpine image has none of them, and the usual toolchain does +not always pull them in: + +| Library | Needed by | Alpine | OpenWrt, ImmortalWrt | +| ------- | --------- | ------ | -------------------- | +| `libstdc++` | every musl package | `apk add libstdc++` | the `libstdcpp` package | +| `libatomic` | the musl packages of 26.8.0 and later | `apk add libatomic` | `libatomic`, not checked | + +The install checks for them, so that it does not look successful while `node +--version` fails later. Right after the unpack the `PostInstall` hook looks for +`libstdc++` in the usual library directories, and then runs `node --version` +once: the dynamic loader names any other library that is missing. The message +names the package and the command that installs it, the install is not +registered (`vfox list nodejs` answers `no available version` when it was the +only one), and after installing the library you run `vfox install` again. Where +no command can be run, or when `node` fails for another reason, the install is +left alone. + +```text +plugin [PostInstall] method error: :: Node.js (musl build) requires the libstdc++ system package, which was not found on this system. Install it with `apk add libstdc++` and reinstall. See https://github.com/version-fox/vfox-nodejs/blob/main/README.md +failed to install nodejs +``` + +```text +plugin [PostInstall] method error: :: Node.js (musl build) requires the libatomic system package, which was not found on this system. Install it with `apk add libatomic` and reinstall. See https://github.com/version-fox/vfox-nodejs/blob/main/README.md +failed to install nodejs +``` + +On OpenWrt the command is `opkg install libstdcpp`. A system that is neither +gets the message without a command. A symbol that the libraries lack, which the +loader reports as `Error relocating ...: symbol not found`, ends in `Node.js +(musl build) cannot run on this system: Error relocating ...`. + +Unreleased: release 0.5.2 looks for `libstdc++` only, and its message says `It +is normally installed automatically; install it and reinstall.` instead of +naming the command. It does not look for `libatomic`: the install succeeds, and +`node` then fails. + +```text +Error loading shared library libatomic.so.1: No such file or directory (needed by /root/.vfox/cache/nodejs/v-26.12.0/nodejs-26.12.0/bin/node) +Error relocating /root/.vfox/cache/nodejs/v-26.12.0/nodejs-26.12.0/bin/node: __atomic_compare_exchange: symbol not found +``` + +Which releases need `libatomic` was checked on Alpine 3.24.1 (x86_64) with only +`libstdc++` installed: 18.20.8, 22.23.3, 24.20.0, 24.22.0, 25.2.0, 25.6.0, +25.9.0, 26.0.0 and 26.5.1 run. 26.8.0, 26.11.1 and 26.12.0 do not. What the +three have in common is that nodejs.org carries them: its musl packages of 26 +start at 26.8.0, and the 26.x before that come from Unofficial Builds, see below. +The packages that nodejs.org carries for 22.23.3, 24.20.0 and 24.22.0 run +without `libatomic`. A system that has `gcc` installed has `libatomic` already, +which is why the e2e image (`go` depends on `gcc`) does not show the problem. + +`libatomic1` is the Debian and Ubuntu name of the same library, which the glibc +packages of Node.js 25 and later link, see [platforms.md](platforms.md#linux). ## Which tarball is installed | Host | Tarball | Source | | ---- | ------- | ------ | | Linux glibc, x64/arm64 | `node-v-linux-x64.tar.gz` etc. | `nodejs.org/dist` | -| Linux musl, x64 | `node-v-linux-x64-musl.tar.gz` | official index when present, else unofficial | -| Linux musl, arm64 | `node-v-linux-arm64-musl.tar.gz` | `unofficial-builds.nodejs.org` (official never ships it) | +| Linux musl, x64 | `node-v-linux-x64-musl.tar.gz` | `nodejs.org/dist` when it has the release, else `unofficial-builds.nodejs.org` | +| Linux musl, arm64 | `node-v-linux-arm64-musl.tar.gz` | `unofficial-builds.nodejs.org` (nodejs.org never ships it) | Notes: -- Official `nodejs.org/dist` only carries `linux-x64-musl` for recent - releases (roughly `26.8+`, `24.20+`, `22.23.3+`) and never carries - `linux-arm64-musl`. Older releases and all `arm64-musl` installs fall back - to `https://unofficial-builds.nodejs.org/download/release` automatically. +- A release that nodejs.org lacks falls back to + `https://unofficial-builds.nodejs.org/download/release` automatically, and + the plugin says so: + `Note: node-v25.9.0-linux-x64-musl.tar.gz is not listed in https://nodejs.org/dist/v25.9.0/, using unofficial musl build`. - Mirror behavior on musl (when the mirror applies, when unofficial is used): see [mirror](mirror.md). - musl builds exist only for `x64` and `arm64`. Other Linux architectures - (e.g. `x86`) error out with a pointer to the README. -- After install the plugin returns a note pointing at the README so the - `libstdc++` requirement is visible in normal `vfox install` output. + (e.g. `x86` or `armv7l`) error out with a pointer to the README. +- Every musl install starts with a note that names the package, and points at + the README: `Note: musl libc detected, installed node-v22.23.3-linux-x64-musl.tar.gz. See ...`. + +## Which releases have a musl build + +As of 2026-10-21, from the `index.json` and `SHASUMS256.txt` of nodejs.org and of +Unofficial Builds: + +| | nodejs.org | Unofficial Builds | +| --- | --- | --- | +| x64 | 22.23.3, 24.20.0 to 24.22.0, and 26.8.0 and later | 8.16.0 and later, but not every release | +| arm64 | none | 20.20.1 and 20.20.2, 22.21.1 and later 22.x, 24.9.0 and later, 25.2.0 and later, and all of 26 | + +- On x64 that makes every release from 12.0.0 on, except 12.11.0, 13.9.0, 13.10.0 + and 19.8.0. Of 8.x, 10.x and 11.x only some have a build, and 9.x and anything + before 8.16.0 have none. +- On arm64 there is nothing for 18, 19, 21 and 23, nor for the lines before + 20.20.1. +- A release without a package ends in `Node.js package node-v9.11.2-linux-x64-musl.tar.gz is not listed in https://nodejs.org/dist/v9.11.2/SHASUMS256.txt`. + Pick another release, or see [platforms.md](platforms.md). +- Both sites change. [development.md](development.md#refreshing-the-platform-table) + shows how to ask again. ## Detection and override @@ -60,3 +134,7 @@ Detection order in `lib/util.lua`: # Force musl selection (debugging only) VFOX_NODEJS_LIBC=musl vfox install nodejs@26.11.1 ``` + +Forcing the other way installs a glibc package that cannot run on musl. On +Alpine, `VFOX_NODEJS_LIBC=gnu vfox install nodejs@20.20.2` installs +`node-v20.20.2-linux-x64.tar.gz`, and running its `node` answers `not found`. diff --git a/docs/npm.md b/docs/npm.md new file mode 100644 index 0000000..3bbcdeb --- /dev/null +++ b/docs/npm.md @@ -0,0 +1,75 @@ +# npm and global packages + +Every release comes with the npm that Node.js ships it with. `vfox search nodejs` +lists that version after the release, and `npm -v` prints it once the release is +in use: + +```bash +vfox install nodejs@24.21.0 +vfox use --global nodejs@24.21.0 +npm -v # 11.19.0 +``` + +## Global packages belong to a release + +npm installs `-g` packages next to the Node.js that runs it. With vfox that is +the directory of the installed release, so each release has its own global +packages and its own commands. They are in the `bin` directory that is on `PATH` +while the release is in use: + +```text +$ vfox use --global nodejs@24.21.0 +$ npm install -g semver +$ npm root -g +/home/me/.vfox/cache/nodejs/v-24.21.0/nodejs-24.21.0/lib/node_modules +$ command -v semver +/home/me/.vfox/sdks/nodejs/bin/semver +$ vfox use --global nodejs@22.23.3 +$ command -v semver +$ # nothing: 22.23.3 has no semver +``` + +- After switching to another release, install what you need again. vfox does + not copy global packages from one release to the next. +- The directory is yours, so `npm install -g` needs no `sudo`. Do not use `sudo` + with it. +- `vfox uninstall nodejs@24.21.0` removes the release with its global + packages. +- Settings are shared in part. The user config (`~/.npmrc`) and the cache + (`~/.npm`) are common to all releases, which is where an npm `registry` setting + belongs. The global config (`npm config get globalconfig`) is + `/etc/npmrc` and belongs to the release. + +## Updating npm + +`npm install -g npm@latest` replaces npm in the release you are using, and in +that one only. On 24.21.0, which comes with npm 11.19.0, `npm install -g npm@10` +leaves `npm -v` at 10.9.9, while every other release keeps its npm. `vfox search` +keeps listing the npm that the release came with. + +## Yarn, pnpm and Corepack + +- Node.js up to 24 comes with Corepack. `corepack enable` adds `yarn`, + `yarnpkg`, `pnpm` and `pnpx` to the `bin` directory of the release, so they + exist for that release only. `corepack disable` removes them. +- Node.js 25 and later does not ship Corepack (checked on 25.9.0 and 26.12.0, + while 24.21.0, 22.23.3 and 20.20.2 have it). Install it first with `npm + install -g corepack`, or install the package manager itself, for example `npm + install -g pnpm`. +- pnpm can install Node.js too, and a `node` that pnpm puts in front of vfox's + wins over it. See [pnpm.md](pnpm.md). + +## Where the files are + +| What | Where | +| --- | --- | +| The release | `~/.vfox/cache/nodejs/v-/nodejs-/` | +| `node`, `npm`, `npx` and global commands | `bin/` in it (the install directory itself on Windows) | +| Global packages | `lib/node_modules/` in it (`node_modules/` on Windows) | +| What is in use globally | `~/.vfox/sdks/nodejs`, a link to the release | +| npm cache and user config | `~/.npm` and `~/.npmrc` | + +An editor or a build tool that should follow your global choice can point at +`~/.vfox/sdks/nodejs/bin/node`. For a fixed release, use the path in the first +row. See [troubleshooting.md](troubleshooting.md#where-nodejs-is) for the +project and session scopes. diff --git a/docs/nvmrc.md b/docs/nvmrc.md new file mode 100644 index 0000000..fd524d7 --- /dev/null +++ b/docs/nvmrc.md @@ -0,0 +1,164 @@ +# `.nvmrc` and `.node-version` + +vfox can read `.nvmrc`, the project file of [nvm](https://github.com/nvm-sh/nvm), +and `.node-version`, the one that nodenv, fnm and others use, through this +plugin (it lists both in `PLUGIN.legacyFilenames`). A project that already pins +its Node.js version for one of those tools needs no second file for vfox: + +```text +# .nvmrc +22.23.3 +``` + +## Using it + +```bash +eval "$(vfox activate bash)" # once, in ~/.bashrc; other shells: see vfox's quick start + +vfox install nodejs@22.23.3 # these files never install anything +cd my-project # holds the .nvmrc above +node -v # v22.23.3 +``` + +vfox's shell hook reads the file when you enter the directory and selects the +release it names. `vfox use` is not needed. The hook is vfox's own, see its +[quick start](https://vfox.dev/guides/quick-start.html) for zsh, fish, +PowerShell and the others. + +- The file is read from the current directory only. In a subdirectory of the + project it is not found, so after `cd packages/app` you are back to the global + version. +- With both files in a directory, `.node-version` wins. +- A `.vfox.toml` in the same directory that names `nodejs` wins over both, and + so does a `.tool-versions` (asdf's file) that names it. `vfox use --project` + writes a `.vfox.toml`. +- In that directory the file also outranks a `vfox use` without `--project`: the + next prompt switches back, while `vfox current nodejs` still names the choice + of the session. +- vfox creates `.vfox/sdks/nodejs` in that directory and does not add `.vfox/` to + `.gitignore`, so `git status` lists it. Ignore it yourself. +- The release has to be installed already. When nothing installed matches, the + file is ignored and no message is printed. `vfox list nodejs` shows what there + is. +- `vfox config legacyVersionFile.enable false` turns all legacy files off, for + every plugin. The default is `true`. + +## The strategy decides + +What the file's text can name depends on `legacyVersionFile.strategy`, a +setting of vfox: + +```bash +vfox config legacyVersionFile.strategy latest_installed +``` + +| Strategy | What happens with the text | +| --- | --- | +| `specified` (the default) | It is used as the version. Only a full release number (`22.23.3`) selects something, as vfox looks for an installed release of exactly that name. No network | +| `latest_installed` | The newest installed release that starts with the text is selected. No network, until no installed release matches: then the plugin asks the index as `latest_available` does, and the answer counts only if that release is installed | +| `latest_available` | The newest release in the index that starts with the text is the answer, and it selects something only if that release is installed. The plugin downloads `index.json` (about 330 KB) at every prompt in a directory with a legacy file | + +Pick `latest_installed` if the files of your projects say `22`, `lts/jod` or +`lts/*`. Keep `specified` if they say `22.23.3`. `latest_available` needs the +network at every prompt, and a prompt in such a directory waits for it. + +## What the file may hold + +Before it looks at the text, the plugin removes all whitespace, drops a leading +`v`, makes the rest lower case and turns `lts-jod` into `lts/jod`. So +`v22.23.3`, a CRLF file and blanks around the version are fine. A name such as +`lts/jod` becomes the major of its line (22) with the table in +[lts.md](lts.md), except under `latest_available`, where the index is asked for +the newest release with that codename. A release number may end in `x`, `X` or +`*` for the parts it leaves open: `22.x`, `22.x.x` and `22.*` all mean 22. + +Unreleased: release 0.5.2 drops every `v` of the file, so a codename with a `v` +in it does not match, and it does not know `22.x`. + +The results on 2026-10-21 with 20.20.2, 22.23.3, 24.21.0, 25.9.0 and 26.12.0 +installed, when 24.22.0 is the newest 24.x and 22.23.3 the newest 22.x: + +| In the file | `specified` | `latest_installed` | `latest_available` | +| --- | --- | --- | --- | +| `22.23.3`, `v22.23.3` | 22.23.3 | 22.23.3 | 22.23.3 | +| `22`, `v22`, `22.x`, `22.*` | nothing | 22.23.3 | 22.23.3 | +| `22.23` | nothing | 22.23.3 | 22.23.3 | +| `lts/jod`, `lts-jod`, `LTS/Jod` | nothing | 22.23.3 | 22.23.3 | +| `lts/krypton` | nothing | 24.21.0 | nothing: 24.22.0 is the newest and is not installed | +| `lts/*`, `lts` | nothing | 24.21.0 | nothing: 24.22.0 is the newest LTS release and is not installed | +| `18.20.8` or any release that is not installed | nothing | nothing | nothing | + +`latest_available` does not fall back to an older installed release. When the +index names a newer release than the one you have, nothing is selected. Use +`latest_installed` if you want the newest one you have. + +`lts` and `lts/*` mean the newest LTS line. Under `specified` and +`latest_installed` that is the highest major in the codename table (24 at the +time of writing), under `latest_available` the newest release that carries any +codename in the index. See [lts.md](lts.md) for the table and what happens +when Node.js promotes a new line. + +## Not understood + +The plugin understands less than nvm does. These select nothing, under every +strategy: + +- Aliases: `node`, `stable`, `latest`, `unstable`, `system`. +- `lts/-1` and the other relative forms of nvm, and a codename without `lts/` + (`iron`, `jod`). +- Version ranges: `>=20`, `^22.0.0`, `~22.23`, and an `x` before a number + (`22.x.1`). +- Comments. `# pin` on its own line, or `22.23.3 # pin`, becomes part of the + text, as all whitespace is removed before the text is read. +- An empty file. + +## How a prefix is matched + +`latest_installed` and `latest_available` compare the text with the start of +each release, part by part and as numbers, and take the newest release that +starts with it: + +- `22.2` matches 22.2.x, and not 22.23.3. +- vfox hands over the installed releases sorted as text, descending, so `22.9.0` + comes before `22.23.3`. The plugin does not go by that order: with 22.9.0, + 22.3.0 and 22.23.3 installed, `22` selects 22.23.3 and `22.3` selects 22.3.0. + `vfox use nodejs@22` compares numbers as well. + +Unreleased: release 0.5.2 compares text, and takes the first installed release +that starts with it. `22.2` also matches `22.23.3`, and with 22.9.0 and 22.23.3 +installed `22` under `latest_installed` selects 22.9.0 (checked with 22.9.0, +22.3.0 and 22.23.3 installed). Keep one release per line installed, or write the +full release number, to get the same answer there. The text is also used as a +Lua pattern, so `22.*` happens to work and `22.x` does not. + +## Not finding it + +If the release does not switch: + +- The shell hook is not active. `eval "$(vfox activate bash)"` has to run in + the shell, not only once somewhere else. +- The shell is in a subdirectory of the one with the file. +- The release is not installed. `vfox install nodejs@22.23.3`. +- The strategy is `specified` and the file holds `22` or `lts/jod`. +- Under `latest_available`, a newer release than the installed one is out. +- A `.vfox.toml` or `.tool-versions` is in the way, or `legacyVersionFile.enable` + is `false`. +- The text is on the list above, or has a comment in it. + +`vfox --debug env -s bash` prints what the plugin returned and what vfox did +with it: + +```text +Parsing legacy file /home/me/project/.nvmrc +CallFunction: ParseLegacyFile +Found nodejs@22 +SDK nodejs@22 from project scope not installed, trying lower-priority config +``` + +## Differences from nvm + +`nvm install` installs what `.nvmrc` names, and `nvm use` with no argument reads +it. vfox only selects among what is installed, and you install with `vfox +install nodejs@`. A line of an `.nvmrc` is not always a spec that `vfox +install` takes: `22.23.3` is, `22` is, `lts/jod` is not (see +[lts.md](lts.md#installing-an-lts-line)). diff --git a/docs/platforms.md b/docs/platforms.md new file mode 100644 index 0000000..372a698 --- /dev/null +++ b/docs/platforms.md @@ -0,0 +1,147 @@ +# Platform support + +The plugin picks the package for the operating system and the CPU architecture +of the vfox binary (`RUNTIME.osType` and `RUNTIME.archType`, Go's `GOOS` and +`GOARCH`) and, on Linux, for the C library. The file it asks for is named + +```text +node-v-- +``` + +`` is `linux`, `darwin` or `win`. `` is `x64`, `x86`, `armv7l`, +`arm64`, `ppc64le` or `s390x`: `amd64` becomes `x64`, `386` becomes `x86`, `arm` +becomes `armv7l` and every other name is passed through as vfox reports it. +`` is `.zip` on Windows and `.tar.gz` everywhere else. On musl Linux +`-musl` follows the architecture, see [musl.md](musl.md). + +The plugin reads the `SHASUMS256.txt` in the release's directory. When that +file does not list the package, the install stops before anything is +downloaded: + +```text +Node.js package node-v19.8.1-win-arm64.zip is not listed in https://nodejs.org/dist/v19.8.1/SHASUMS256.txt +``` + +## Releases per platform + +The table was taken from nodejs.org on 2026-10-21, from the `SHASUMS256.txt` of +each of the 872 releases in `index.json`. "Releases" is the first and the last +release that has a package under the plugin's file name, and the gaps are +releases in between for which Node.js published none. How to take it again is +in [development.md](development.md#refreshing-the-platform-table). + +| vfox reports | Package | Releases | +| --- | --- | --- | +| Linux amd64 | `linux-x64.tar.gz` | 0.8.6 and later, except 0.9.0, 0.11.4 and 0.11.6 | +| Linux arm64 | `linux-arm64.tar.gz` | 4.0.0 and later | +| Linux ppc64le | `linux-ppc64le.tar.gz` | 4.3.2 and later, except 5.0.0 to 5.6.0 | +| Linux s390x | `linux-s390x.tar.gz` | 6.6.0 and later | +| Linux 386 | `linux-x86.tar.gz` | 0.8.6 to 9.11.2, except 0.9.0, 0.11.4 and 0.11.6 | +| Linux arm | `linux-armv7l.tar.gz` | 4.0.0 to 23.11.1, except 8.1.1. Unreleased: release 0.5.2 asks for `linux-arm`, which Node.js never published, so every install fails | +| Linux amd64, musl | `linux-x64-musl.tar.gz` | see [musl.md](musl.md#which-releases-have-a-musl-build) | +| Linux arm64, musl | `linux-arm64-musl.tar.gz` | see [musl.md](musl.md#which-releases-have-a-musl-build) | +| macOS amd64 | `darwin-x64.tar.gz` | 0.8.6 and later, except 0.9.0, 0.11.4 and 0.11.6 | +| macOS arm64 | `darwin-arm64.tar.gz` | 16.0.0 and later. Earlier releases install the amd64 package, see [macOS](#macos) | +| Windows amd64 | `win-x64.zip` | 4.5.0 and later, except 5.0.0 to 5.12.0 and 6.0.0 to 6.2.0 | +| Windows 386 | `win-x86.zip` | 4.5.0 to 22.23.3, except 5.0.0 to 5.12.0, 6.0.0 to 6.2.0 and 18.0.0 to 18.2.0 | +| Windows arm64 | `win-arm64.zip` | 19.9.0 and later | + +From 19.9.0 on, every release has a package for each row that has no upper +limit. The 32-bit rows stop early: after 22.23.3 on Windows, after 23.11.1 on +Linux arm and after 9.11.2 on Linux x86, so a 32-bit vfox cannot install a +current release. + +`vfox search nodejs` lists all 872 releases whatever the platform is, because +the index does not say which platforms a release has. `vfox install +nodejs@19.8.1` on Windows arm64 therefore starts fine and ends with the message +above, see [troubleshooting.md](troubleshooting.md#messages-from-the-plugin). + +## Not supported + +- **ARMv6, and 32-bit ARM from 24 on.** vfox ships an `armv7` binary that + reports `arm`, and the plugin takes the `linux-armv7l` package for it, which + Node.js published up to 23.11.1. From 24 on there is none, and the install + ends with `Node.js package node-v26.12.0-linux-armv7l.tar.gz is not listed in + ...`. The ARMv6 package (`linux-armv6l`, 4.0.0 to 11.15.0 on nodejs.org) is + never chosen. Unreleased: release 0.5.2 asks for `linux-arm`, which Node.js + never published, so every install on 32-bit ARM fails with `Node.js package + node-v20.19.5-linux-arm.tar.gz is not listed in ...`. +- **Linux riscv64 and loong64.** nodejs.org has no packages for them. The + [Unofficial Builds](https://github.com/nodejs/unofficial-builds) site has + riscv64 (17.7.1 to 26.0.0) and loong64 (18.18.0 and later), but the plugin + uses that site for musl only. + +## Linux + +The plugin takes the glibc package, or the musl one when it detects musl, see +[musl.md](musl.md#detection-and-override). Which glibc and kernel a release +needs is Node.js's business, see its +[supported platforms](https://github.com/nodejs/node/blob/main/BUILDING.md). +System libraries are the user's responsibility, and the plugin installs none of +them. Two to know about: + +- The glibc packages of Node.js 25 and later link `libatomic.so.1`, which stock + Debian and Ubuntu images do not ship. `node` then fails with `error while + loading shared libraries: libatomic.so.1`. Install it with + `sudo apt install --assume-yes libatomic1`. The releases of 24 and earlier do + not link it (checked on 25.9.0 and 26.12.0 against 24.21.0 and 20.20.2), see + [nodejs/node#60790](https://github.com/nodejs/node/issues/60790). +- The musl packages need `libstdc++`, see [musl.md](musl.md#requirements). + +Inside WSL vfox is a Linux binary, so the Linux packages are used. + +## macOS + +The packages are `darwin-x64.tar.gz` and `darwin-arm64.tar.gz`. Node.js +published no arm64 package before 16.0.0, so on Apple silicon the plugin takes +the amd64 one for those releases and says so: + +```text +Note: Node.js versions below 16 do not support Apple Silicon (arm64), using x64 architecture with Rosetta 2 +``` + +Rosetta 2 has to be installed for them to run (`softwareupdate +--install-rosetta`). 16.0.0 and later install natively. + +There is no macOS job in the e2e suite, see [below](#what-ci-covers). The hook +tests check the package name for `darwin/arm64`. + +## Windows + +The package is a zip. `node.exe`, `npm.cmd` and `npx.cmd` end up directly in the +install directory, so the plugin puts that directory itself on `PATH`. There is +no `bin` directory as on Linux and macOS. + +- A home directory with a space in it (`C:\Users\John Doe`) needs vfox 1.0.13 + or later ([vfox#712](https://github.com/version-fox/vfox/pull/712)). The e2e + suite runs every combination against such a home. +- In Git Bash `node`, `npm.cmd` and the extensionless `npm` run once the shell + hook is active ([vfox#293](https://github.com/version-fox/vfox/issues/293)). +- `VFOX_NODEJS_MIRROR` is read from the environment of the process that runs + vfox. In PowerShell set it with `$env:VFOX_NODEJS_MIRROR = '...'` for the + session, see [mirror.md](mirror.md#setting-it). + +## vfox's own platforms + +vfox 1.0.13 ships binaries for Linux x86_64, aarch64, armv7, i386 and loong64, +macOS x86_64 and aarch64, and Windows x86_64, aarch64 and i386. The plugin +takes whatever name the binary reports, so a vfox that you build for Linux +ppc64le or s390x installs the ppc64le or s390x packages. + +## What CI covers + +The container e2e suite, see [e2e.md](e2e.md), installs a pinned release, a +pinned LTS release and `nodejs@latest` on: + +| Platform | Runner | +| --- | --- | +| Linux glibc, amd64 | `ubuntu-latest` | +| Linux glibc, arm64 | `ubuntu-26.04-arm` | +| Linux musl (Alpine), amd64 and arm64 | the same two runners | +| Windows containers, amd64 and arm64 | `windows-latest`, `windows-11-arm` | +| Ubuntu 24.04 on WSL | `windows-2025` | + +macOS, 32-bit systems, ppc64le and s390x are not covered. The hook tests +(`tests/hooks_test.lua`) run offline and check the package name for `windows` +on amd64, 386 and arm64, `linux` on amd64 and arm, musl on amd64 and arm64, and +`darwin` on arm64. diff --git a/docs/source-build.md b/docs/source-build.md index a367e56..7e8c211 100644 --- a/docs/source-build.md +++ b/docs/source-build.md @@ -43,6 +43,11 @@ vfox add nodejs --source /tmp/nodejs.zip vfox install nodejs@26.11.1 ``` +## macOS + +The same commands as on Ubuntu, without the `apt` line: `zip` ships with the +system, and `git` comes with the Xcode command line tools. + ## Alpine 3.24.2 ```sh @@ -55,7 +60,61 @@ vfox add nodejs --source /tmp/nodejs.zip vfox install nodejs@26.11.1 ``` +On Alpine the installed Node.js needs system libraries first, see +[musl.md](musl.md#requirements). + +## Check it + +```bash +vfox info nodejs +vfox install nodejs@24 +vfox use --global nodejs@24 +node -v +``` + +`vfox info nodejs` prints the version of the plugin that is added, the +`PLUGIN.version` in `metadata.lua`. A source build of `main` carries the version +of the last release, so it cannot tell you which commit you have. + +## When nodejs is already added + +`vfox add` refuses with `plugin named nodejs already exists`. You can replace the +plugin or add the source build next to it. + +To replace it, `vfox remove nodejs` comes first, and it removes the Node.js +releases installed through the plugin as well: vfox says `Removing this plugin +will remove the installed sdk along with the plugin.` before it asks (`-y` +skips the question). Install them again afterwards. + +To keep both, give the new one another name: + +```bash +vfox add nodejs --source /tmp/nodejs.zip --alias nodejs-dev +vfox install nodejs-dev@24 +vfox remove -y nodejs-dev # removes only nodejs-dev and what it installed +``` + +The alias has its own list of installed releases, under `~/.vfox/cache/nodejs-dev/`, +but both plugins link the chosen release as `~/.vfox/sdks/nodejs`. Select a +version from only one of them per scope: for the global scope, `vfox unuse +--global nodejs` before you `vfox use --global nodejs-dev@...`, or the other way +round. With both selected, the link points at one of them and `node` follows it, +and you should not count on which. + ## Updating -Re-run the zip + `vfox add` steps after pulling (re-adding replaces the -installed plugin), or `vfox remove nodejs` first for a clean slate. +Pull, package the zip again and add it again. A plugin added under its own name +has to be removed first, and that takes the installed releases with it, so the +alias is the easier route when you update often: + +```bash +vfox remove -y nodejs-dev +vfox add nodejs --source /tmp/nodejs.zip --alias nodejs-dev +``` + +`vfox update nodejs` compares the version in `metadata.lua` with the registry's +and prints `nodejs is already the latest version` while they are equal, so it +leaves a source build alone until a release newer than that version is +published. + +To work on the plugin, see [development.md](development.md). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..9423969 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,238 @@ +# Troubleshooting + +The messages below were produced with vfox 1.0.12 and 1.0.13. vfox prefixes the +plugin's own errors with `plugin [PreInstall] method error: ::` +(`[Available]` for the search) and follows them with a Lua stack trace, which this +page leaves out. + +## Messages from the plugin + +### `version not found for provided version lts` + +What follows `nodejs@` is not a release number, nor the start of one. The plugin +knows `latest`, `24.21.0`, `24.21` and `24`, see [versions.md](versions.md). +`lts`, `lts/iron`, `stable`, `24.21.0.1` and `24.0.0-rc.1` end here, and so do `99` +and `22.99`, which are well formed but start no release. For an LTS line, install +the line by number, see [lts.md](lts.md#installing-an-lts-line). + +### `get checksum failed: HTTP 404 from https://nodejs.org/dist/v24.99.0/SHASUMS256.txt` + +The plugin could not read the `SHASUMS256.txt` of the release, and says why +after the colon. `HTTP 404 from
` means the server has no such file: +the release does not exist (`24.99.0`), or the mirror lacks it. A network error +(`Get "
": tls: failed to verify certificate: x509: certificate signed +by unknown authority`, `... connection refused`, `... EOF`) means the server +could not be reached: a mirror that is down, a proxy, a TLS problem. See +[Network](#network). + +Unreleased: release 0.5.2 prints `get checksum failed` and nothing else, so the +cause is hidden. `vfox search nodejs` asks the same server for the index and +prints the real error. + +### `Node.js package node-v19.8.1-win-arm64.zip is not listed in https://nodejs.org/dist/v19.8.1/SHASUMS256.txt` + +The release exists, and Node.js published no package for your platform under the +name the plugin asks for. `vfox search nodejs` lists releases that have no +package for your platform, as the index does not say which ones have. The file +name in the message is the one the plugin looked for. Compare it with +[platforms.md](platforms.md), which also lists what is not supported (ARMv6, 32-bit +ARM from 24 on, riscv64, loong64). + +On Alpine and other musl systems the file name ends in `-musl.tar.gz`, and the +plugin has tried unofficial-builds.nodejs.org as well. See +[musl.md](musl.md#which-releases-have-a-musl-build). + +### `Failed to fetch Node.js versions: ...` + +The request for `index.json` failed. What follows is the network error +(`Get "https://nodejs.org/dist/index.json": ...`) or the HTTP status +(`HTTP 404`, from a mirror that has no `index.json` at that address). See +[Network](#network). Before 0.5.0 a failed request ended as an empty list, and +vfox cached it. + +### `Cannot determine Node.js package architecture from RUNTIME.archType` + +vfox did not tell the plugin which CPU it runs on. It is not expected. Report it +with the output of `vfox --version`. + +### `Node.js musl builds are only available for x64 and arm64, got x86. See ...` + +The plugin took the system for musl, and Node.js has no musl packages for the +CPU (`x86` and `armv7l` are the ones that end here). If the system is not musl, +set `VFOX_NODEJS_LIBC=glibc`, see +[musl.md](musl.md#detection-and-override). + +### `Node.js (musl build) requires the libatomic system package, which was not found on this system. ...` + +A musl system without a library that the Node.js musl package links: +`libstdc++` for every one of them, `libatomic` for the ones of 26.8.0 and later. +The message names the package, and the command that installs it on Alpine +(`apk add libatomic`) and OpenWrt (`opkg install libatomic`). The install fails +after the unpack and nothing is registered. Install the library and run the +install again. See [musl.md](musl.md#requirements). + +Unreleased: release 0.5.2 looks for `libstdc++` only, and its message has no +command. A missing `libatomic` shows later, as an error of `node` itself, see +below. + +### `Node.js (musl build) cannot run on this system: Error relocating ...` + +The unpacked `node` needs a symbol that no library of the system provides. The +install fails after the unpack and nothing is registered. Unreleased: release +0.5.2 does not run `node` after the unpack, and the error shows when you run it. +See [musl.md](musl.md#requirements). + +### `checksum error` + +vfox prints `Verifying checksum ...`, compares the download with it, and +stops when they differ. It names the file it dropped: `Checksum error, file: +~/.vfox/cache/nodejs/node-v24.21.0-linux-x64.tar.gz`. Typical causes are a +cut-off download and a mirror whose files differ from its `SHASUMS256.txt`. +Nothing is installed. Run it again, or try another [mirror](mirror.md). + +### `nodejs@24.99 is not installed` + +`vfox use` and `vfox uninstall` only know what is installed. `vfox list nodejs` +shows it. `vfox use` takes `24`, `24.21` and `24.21.0` for installed releases, +`vfox uninstall` takes the whole release number. See +[versions.md](versions.md#using). + +### `vfox requires hook support. Please ensure vfox is properly initialized with 'vfox activate'` + +`vfox use` changes the environment of your shell, which only the hook can do. +Add `eval "$(vfox activate bash)"` (or the line for your shell) to the startup +file of the shell, see vfox's +[quick start](https://vfox.dev/guides/quick-start.html). Alpine's own `sh` is +not supported by vfox. Install `bash` there. + +### `plugin named nodejs already exists` + +`vfox add nodejs` found the plugin. For a plugin from source, see +[source-build.md](source-build.md#when-nodejs-is-already-added). + +### `Plugin nodejs is not installed. Use the -y flag to automatically install plugins in non-interactive environments` + +The plugin is not added, or `VFOX_HOME` points at a directory it was not added +to. Run `vfox add nodejs`. + +### Notes the plugin prints + +These are not errors. + +| Note | Meaning | +| --- | --- | +| `Note: Node.js versions below 16 do not support Apple Silicon (arm64), using x64 architecture with Rosetta 2` | macOS arm64 and a release before 16.0.0, see [platforms.md](platforms.md#macos) | +| `Note: musl libc detected, installed node-v22.23.3-linux-x64-musl.tar.gz. See ...` | the plugin took the musl package, see [musl.md](musl.md) | +| `Note: node-v18.20.8-linux-x64-musl.tar.gz is not listed in https://nodejs.org/dist/v18.20.8/, using unofficial musl build` | nodejs.org has no such musl package and the plugin takes it from unofficial-builds.nodejs.org | +| ``Note: /home/me/.local/share/pnpm/bin/node comes before vfox in PATH, so `node` will not run nodejs@24.21.0. See ...`` | another `node` shadows vfox's, see [pnpm.md](pnpm.md) | + +### Errors from Node.js itself + +These come from `node` after a successful install. + +- `node: error while loading shared libraries: libatomic.so.1: cannot open + shared object file`. Node.js 25 and later links `libatomic`, which stock + Debian and Ubuntu do not have: `sudo apt install --assume-yes libatomic1`. See + [platforms.md](platforms.md#linux). +- `node: not found`, though the file exists, on Alpine. That is a glibc package on + a musl system: the plugin did not detect musl, or `VFOX_NODEJS_LIBC` says + `gnu` or `glibc`. See [musl.md](musl.md#detection-and-override). +- `Error loading shared library libatomic.so.1: No such file or directory`, + followed by `Error relocating ...: __atomic_compare_exchange: symbol not + found`, on Alpine. The musl packages of 26.8.0 and later need `libatomic`: + `apk add libatomic`. Unreleased: the install itself stops with the message + above, and release 0.5.2 gets this far. See [musl.md](musl.md#requirements). +- `Error relocating ... symbol not found` on a musl system, without the + `libatomic` line above. A library is missing or too old. Unreleased: the + install stops with `cannot run on this system`, and release 0.5.2 gets this + far. See [musl.md](musl.md#requirements). + +## Network + +The plugin asks the server for two things, and vfox downloads a third: + +| What | Address | +| --- | --- | +| The list of releases (`vfox search`, `latest`, `nodejs@24`) | `/index.json` | +| The sums of one release (`vfox install`) | `/v/SHASUMS256.txt` | +| The package (vfox) | `/v/` | + +`` is `https://nodejs.org/dist` unless `VFOX_NODEJS_MIRROR` says +otherwise, see [mirror.md](mirror.md). Musl systems may also use +`https://unofficial-builds.nodejs.org/download/release`. + +vfox's own proxy setting covers the requests of the plugin and the download: + +```bash +vfox config proxy.enable true +vfox config proxy.url http://127.0.0.1:7890 +``` + +While `proxy.enable` is `false`, the usual `HTTP_PROXY`, `HTTPS_PROXY` and +`NO_PROXY` environment variables apply. A proxy that is down shows in the search +as `proxyconnect tcp: dial tcp 127.0.0.1:7890: connect: connection refused`. + +The usual failures of `vfox search nodejs`. `vfox install` reports the same cause +after `get checksum failed:`. Unreleased: release 0.5.2 leaves it out, and only +the search prints it: + +| After `Failed to fetch Node.js versions:` | Cause | +| --- | --- | +| `Get "https://nodejs.org/dist/index.json": tls: failed to verify certificate: x509: certificate signed by unknown authority` | The system has no CA certificates, as in a bare Debian or Ubuntu Docker image: `apt-get install ca-certificates` | +| `Get "...": ... connection refused`, `i/o timeout` or `no such host` | The server cannot be reached: no route, a dead proxy, a wrong mirror name, DNS. Try a [mirror](mirror.md) or the proxy setting | +| `HTTP 404` | The mirror does not have `index.json` at that address. Point `VFOX_NODEJS_MIRROR` at the directory that holds it | + +## Things that look wrong and are not + +- **A release you expect is missing from `vfox search`.** vfox keeps the result + for 12 hours. `vfox config cache.availableHookDuration 0` turns the cache off. + A mirror can also lag behind nodejs.org. +- **`vfox search nodejs` lists a release that will not install.** The list is the + index for every platform, see [platforms.md](platforms.md). +- **`vfox search nodejs 24` does not filter.** Arguments are ignored, see + [versions.md](versions.md#searching). +- **`vfox install nodejs@24` downloads nothing.** Newest 24.x is already + installed: `nodejs@24.22.0 is already installed`. +- **`vfox use nodejs@24.21.0` says `Now using`, and `node -v` prints another + version.** In a directory with a `.nvmrc` or `.node-version` the file wins + at the next prompt, see [nvmrc.md](nvmrc.md). Another `node` ahead of vfox in + `PATH` wins too, and `vfox use` says so, see [pnpm.md](pnpm.md). +- **`vfox current nodejs` and `node -v` disagree.** The same. The first names + the choice of the session or the scope, the second what `PATH` finds. +- **A global npm package is gone after `vfox use`.** Global packages belong to a + release, see [npm.md](npm.md#global-packages-belong-to-a-release). +- **`corepack` is missing.** Node.js 25 and later does not ship it, see + [npm.md](npm.md#yarn-pnpm-and-corepack). +- **`.nvmrc` selects nothing.** Check the strategy and what the file holds, see + [nvmrc.md](nvmrc.md#not-finding-it). +- **`git status` lists `.vfox/`.** vfox links the release into the project as + `.vfox/sdks/nodejs`. Ignore `.vfox/` in `.gitignore`, and keep `.vfox.toml`. +- **The installed name has no `v`.** Releases are named `24.21.0`, and a `v` in + what you type is dropped. + +## Where Node.js is + +| Scope | Where `node` comes from | +| --- | --- | +| `--global` | `~/.vfox/sdks/nodejs`, a link to the release. `~/.vfox/sdks/nodejs/bin` is on `PATH` | +| `--project` | `.vfox/sdks/nodejs` in the project, a link to the release, and the name in `.vfox.toml` | +| session (the default) | a directory under `~/.vfox/tmp/-/nodejs` | + +The release itself is `~/.vfox/cache/nodejs/v-/nodejs-/`, with +`bin/node`, `bin/npm` and `lib/node_modules`. `vfox info nodejs@24.21.0` prints +the directory above it. `~/.vfox` is the default `$VFOX_HOME`. When `VFOX_HOME` +is set (to a directory that exists), the plugin and the releases (`cache/`) live +there, while the `sdks/nodejs` link and the session directories stay in +`~/.vfox`. `storage.sdkPath` moves the releases to `/nodejs/` for new +installs, see vfox's +[configuration](https://vfox.dev/guides/configuration.html). On Windows `node.exe` +is directly in the release directory. + +## Reporting a problem + +Open an issue at +[version-fox/vfox-nodejs](https://github.com/version-fox/vfox-nodejs/issues) with +the output of `vfox --version` and `vfox info nodejs`, the operating system and +CPU, the command, and everything it printed. `vfox --debug ` prints +more, and so does `vfox --debug env -s bash` for a `.nvmrc`. Problems of vfox +itself belong with [vfox](https://github.com/version-fox/vfox/issues). diff --git a/docs/versions.md b/docs/versions.md new file mode 100644 index 0000000..baa2aad --- /dev/null +++ b/docs/versions.md @@ -0,0 +1,164 @@ +# Versions and names + +The part after `nodejs@` in `vfox install` and `vfox use` is a Node.js release +number, or the start of one: + +```bash +vfox install nodejs@24.21.0 # exactly this release +vfox install nodejs@24.21 # newest 24.21.x +vfox install nodejs@24 # newest 24.x +vfox install nodejs@latest # newest release of any line +``` + +There are no names such as `lts`, `lts/iron` or `stable` here. For a line that +is in long-term support, name the line (`nodejs@24`), see [lts.md](lts.md). +`.nvmrc` files do understand `lts/`, see [nvmrc.md](nvmrc.md). + +## Installing + +`vfox install` asks the plugin which package matches, then vfox downloads it, +checks it and unpacks it. These are the answers on 2026-10-21: + +| You type | Installs | +| --- | --- | +| `24.21.0` | `24.21.0` | +| `v24.21.0` | `24.21.0`, vfox drops the `v` | +| `24.21` | `24.21.0`, the newest 24.21.x | +| `24` | `24.22.0`, the newest 24.x | +| `latest` | `26.12.0`, the newest release in the index. vfox prints `Using latest version: 26.12.0` first | + +- "Newest" is the highest number among the releases that the index lists, LTS + or not. A line that is not in long-term support yet, such as 26 today, can be + newer than the newest LTS line, so `latest` is not "the LTS". +- `24` and `24.21` are looked up in the index, not among what is installed. + `vfox install nodejs@24` installs a newer 24.x than the one you have, and + prints `nodejs@24.22.0 is already installed` once you have the newest. +- The index lists releases with three numbers, and nothing else: no `-rc` + builds, nightlies or four-part numbers. Those end in `version not found for + provided version ...`. +- A release number that does not exist (`24.99.0`) ends in `get checksum + failed: HTTP 404 from ...`, as nodejs.org has no directory for it + (Unreleased: release 0.5.2 leaves out what follows `failed`). A release that + exists but has no package for your platform ends in `Node.js package ... is + not listed in ...`. See + [troubleshooting.md](troubleshooting.md#messages-from-the-plugin) and + [platforms.md](platforms.md). +- Every download is checked against the SHA-256 sum in the release's + `SHASUMS256.txt`. vfox prints `Verifying checksum ...` and, when the sums + differ, stops with `checksum error` and installs nothing. +- An install that finds the name already present downloads nothing. + +A mirror changes where the files come from, not what the names mean, see +[mirror.md](mirror.md). + +## Installed names + +The name of an install is the release number: no `v`, no platform, no flavour. +`vfox list nodejs` shows the names, with the one in use marked. vfox sorts them +as text, highest first, so `22.9.0` comes before `22.23.3`: + +```text +$ vfox list nodejs +-> 26.12.0 +-> 24.21.0 <— current +-> 22.9.0 +-> 22.23.3 +-> 20.20.2 +``` + +`vfox uninstall` needs the whole name: + +```bash +vfox uninstall nodejs@22.23.3 +``` + +`vfox uninstall nodejs@22` and `nodejs@22.23` answer `is not installed`. When you +uninstall the release in use, vfox switches to the first entry that is left in +`vfox list nodejs`, and says so (`Auto switch to the other version.`), which can +be another line. + +The files live in `~/.vfox/cache/nodejs/v-/nodejs-/`, see +[troubleshooting.md](troubleshooting.md#where-nodejs-is). + +## Using + +`vfox use` maps what you type to an installed release and downloads nothing: + +| You type | Selects | +| --- | --- | +| `24.21.0`, `v24.21.0` | that release | +| `24.21` | the newest installed 24.21.x | +| `24` | the newest installed 24.x | +| anything else, such as `lts` or `99` | nothing: `nodejs@lts is not installed` | + +"Newest" compares the numbers, so with 22.3.0, 22.9.0 and 22.23.3 installed +`nodejs@22` selects 22.23.3 and `nodejs@22.3` selects 22.3.0. A shorthand has +to match something installed: with only 22.23.3 installed, `nodejs@22.2` selects +nothing, as there is no 22.2.x. + +Pick the scope with `vfox use`'s flags: `--global`, `--project` or `--session`, +the default. All of them need vfox's shell hook (`eval "$(vfox activate +bash)"` for bash). Without it, `vfox use` stops on Linux with: + +```text +vfox requires hook support. Please ensure vfox is properly initialized with 'vfox activate' +``` + +`--project` writes a `.vfox.toml` that holds the installed name: + +```toml +[tools] +nodejs = "22.23.3" +``` + +A `.nvmrc` or `.node-version` in the same directory is read as well, see +[nvmrc.md](nvmrc.md). `vfox use` prints a note when another `node` comes before +vfox in `PATH`, see [pnpm.md](pnpm.md). + +## Searching + +```bash +vfox search nodejs +``` + +Each line is a name that can follow `nodejs@`, newest first. After the name +come `(LTS)` for releases of a line in long-term support, the npm that comes +with the release, and `(installed)`: + +```text +$ vfox search nodejs +Available versions: + - 26.12.0 [npm 11.20.0] (installed) + - 26.11.1 [npm 11.20.0] + ... + - 24.22.0 (LTS) [npm 11.19.1] + - 24.21.0 (LTS) [npm 11.19.0] (installed) + ... + - 0.6.3 [npm 1.1.0-alpha-2] + - 0.6.2 [npm ] +``` + +- The list is the `index.json` of nodejs.org, or of your + [mirror](mirror.md): 872 releases from 0.1.14 to 26.12.0 on 2026-10-21. npm + is listed from 0.6.3 on and empty before that. +- `(LTS)` is the `lts` field of the index, which Node.js sets from the day a + line enters long-term support. 24.11.0 is the first release of Krypton with + it, 24.10.0 and the releases before have none. See [lts.md](lts.md). +- The plugin ignores arguments: `vfox search nodejs 24` prints the same full + list. To filter, pipe it, for example `vfox search nodejs | grep '^ - 24\.'`. + In a pipe vfox only prints. +- In an interactive terminal vfox shows a picker instead (`Please select a + version of nodejs to install [type to search]`): arrow keys move, typing + filters, Enter confirms. +- The list holds every release, whether or not your platform has a package for + it, see [platforms.md](platforms.md). +- vfox caches the list for 12 hours (`vfox config cache.availableHookDuration`), + so a release published a minute ago can be missing. `vfox config + cache.availableHookDuration 0` turns the cache off. +- When the index cannot be fetched, the search fails with `Failed to fetch + Node.js versions: ...` rather than listing nothing, see + [troubleshooting.md](troubleshooting.md#network). + +`vfox install nodejs 24` does not install 24: arguments separated by spaces +are several SDKs, and it stops with `install requires specifying a version for +nodejs`. Write `nodejs@24`. From 9e19ee54841df32cc70dfcc80dd3cfe079c02223 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Wed, 21 Oct 2026 05:23:27 +0800 Subject: [PATCH 3/3] docs: add a Chinese translation README_CN.md and docs/zh-CN/ translate the README and every page of docs/ under the same names. Each page, in both languages, starts with a link to the other one, and docs/development.md says how the two are kept in step. Code blocks and quoted messages stay in English, only their comments are translated. A link to another page points at its Chinese heading, and a link to the vfox guides at the zh-hans site. The terms follow vfox-java's Chinese docs. --- README.md | 2 + README_CN.md | 110 +++++++++++++++++ docs/changes.md | 3 + docs/development.md | 29 +++-- docs/e2e.md | 2 + docs/lts.md | 2 + docs/mirror.md | 2 + docs/musl.md | 2 + docs/npm.md | 2 + docs/nvmrc.md | 2 + docs/platforms.md | 2 + docs/pnpm.md | 2 + docs/source-build.md | 2 + docs/troubleshooting.md | 2 + docs/versions.md | 2 + docs/zh-CN/changes.md | 58 +++++++++ docs/zh-CN/development.md | 150 +++++++++++++++++++++++ docs/zh-CN/e2e.md | 220 ++++++++++++++++++++++++++++++++++ docs/zh-CN/lts.md | 101 ++++++++++++++++ docs/zh-CN/mirror.md | 90 ++++++++++++++ docs/zh-CN/musl.md | 96 +++++++++++++++ docs/zh-CN/npm.md | 54 +++++++++ docs/zh-CN/nvmrc.md | 112 +++++++++++++++++ docs/zh-CN/platforms.md | 93 ++++++++++++++ docs/zh-CN/pnpm.md | 40 +++++++ docs/zh-CN/source-build.md | 98 +++++++++++++++ docs/zh-CN/troubleshooting.md | 142 ++++++++++++++++++++++ docs/zh-CN/versions.md | 117 ++++++++++++++++++ 28 files changed, 1526 insertions(+), 11 deletions(-) create mode 100644 README_CN.md create mode 100644 docs/zh-CN/changes.md create mode 100644 docs/zh-CN/development.md create mode 100644 docs/zh-CN/e2e.md create mode 100644 docs/zh-CN/lts.md create mode 100644 docs/zh-CN/mirror.md create mode 100644 docs/zh-CN/musl.md create mode 100644 docs/zh-CN/npm.md create mode 100644 docs/zh-CN/nvmrc.md create mode 100644 docs/zh-CN/platforms.md create mode 100644 docs/zh-CN/pnpm.md create mode 100644 docs/zh-CN/source-build.md create mode 100644 docs/zh-CN/troubleshooting.md create mode 100644 docs/zh-CN/versions.md diff --git a/README.md b/README.md index 85a066d..eb8e86e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # vfox-nodejs +[中文](README_CN.md) | [English](README.md) + Node.js plugin for [vfox](https://vfox.dev/). It installs the Node.js release archives published on diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..9f53574 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,110 @@ +# vfox-nodejs + +[中文](README_CN.md) | [English](README.md) + +[vfox](https://vfox.dev/) 的 Node.js 插件。 + +它安装 [nodejs.org](https://nodejs.org/dist/) 或其[镜像](docs/zh-CN/mirror.md)上发布的 Node.js 版本压缩包,并对照一同发布的 SHA-256 校验和检查每次下载。每个版本都自带自己的 npm。在 Alpine、OpenWrt 和其他 musl 系统上,安装的是 musl 构建。vfox 通过它读取 `.nvmrc` 和 `.node-version`。 + +## 安装 + +先安装 [vfox](https://github.com/version-fox/vfox),然后执行下面的命令安装插件: + +```bash +vfox add nodejs +``` + +要让 `node` 出现在 `PATH` 里,需要先把 vfox 挂载到 Shell,见 vfox 的[快速入门](https://vfox.dev/zh-hans/guides/quick-start.html)。 + +### 示例 + +```bash +vfox add nodejs +vfox install nodejs@24 +vfox use --global nodejs@24 +``` + +可以用下面的命令验证: + +```bash +node -v +``` + +`vfox search nodejs` 会列出每个版本及其自带的 npm,属于 LTS 版本线的版本带有标记。在交互式终端里可以从中选一个。 + +只存在于 `main`、还没进入最新发布版的行为,在下面各页里标有“未发布”。[docs/zh-CN/changes.md](docs/zh-CN/changes.md) 里列出了这些行为,想直接用上 `main`,见 [docs/zh-CN/source-build.md](docs/zh-CN/source-build.md)。 + +### 示例:Ubuntu 26.04 + +```bash +vfox add nodejs +vfox install nodejs@26.11.1 +sudo apt install --assume-yes libatomic1 +vfox use --global nodejs@26.11.1 +``` + +注意:`libatomic1` 这一步是为了绕开 Node.js 上游的一个打包缺陷([nodejs/node#60790](https://github.com/nodejs/node/issues/60790),目前仍未关闭):Node 25 及以上版本会链接 `libatomic.so.1`,而原版 Debian/Ubuntu 镜像不自带这个库。系统库由用户自己负责,插件不会安装它们。 + +### 示例:Alpine / musl + +```bash +apk add bash libstdc++ libatomic +vfox add nodejs +vfox install nodejs@24 +vfox use --global nodejs@24 +``` + +Node.js 的 musl 构建需要 `libstdc++`,26.8.0 及以后的构建还需要 `libatomic`。原版 Alpine 镜像两者都没有,而插件不会安装系统库。安装会中止,并给出一条提示,写明缺少哪个包以及安装它的命令(`apk add libatomic`;OpenWrt 把 `libstdc++` 称为 `libstdcpp` 包)。未发布:0.5.2 只检查 `libstdc++`,缺少 `libatomic` 时,要到 `node` 运行失败、报 `Error loading shared library libatomic.so.1` 才能发现。musl 构建只提供 x64 和 arm64。musl 的详细情况(OpenWrt、压缩包对照表、镜像回退、哪些版本有构建)见 [docs/zh-CN/musl.md](docs/zh-CN/musl.md)。 + +## 版本与命名 + +版本就是版本号:`24.21.0`,也可以只写开头的部分,如 `24.21` 和 `24`,它们安装对应版本线的最新版本。`latest` 是所有版本线中最新的版本,不论是否属于 LTS。这里没有 `lts` 别名,请写明版本线(`nodejs@24`)。安装后的版本名就是版本号 `24.21.0`,`vfox uninstall` 需要写完整。`vfox use` 和 `vfox search` 怎样处理这些写法,详见 [docs/zh-CN/versions.md](docs/zh-CN/versions.md)。 + +## 平台 + +Node.js 为 Linux(x64、arm64、ppc64le、s390x)、macOS(x64 和 arm64)和 Windows(x64、arm64,以及到 22.x 为止的 x86)发布包。并不是每个版本都有上面所有的包:Windows on ARM 从 19.9.0 起,Linux arm64 从 4.0.0 起,Apple silicon 从 16.0.0 起(更早的版本在 Rosetta 2 下安装 x64 的包)。32 位 ARM Linux 到 23.11.1 为止有包。未发布:0.5.2 无法安装它们。不支持 riscv64 和 loong64。不论你的平台是什么,`vfox search` 都会列出所有版本。表格见 [docs/zh-CN/platforms.md](docs/zh-CN/platforms.md)。 + +## npm 与全局包 + +用 `npm install -g` 安装的包属于当时正在使用的那个 Node.js 版本,其他版本里没有它们。Node.js 25 及以后的版本不自带 Corepack。见 [docs/zh-CN/npm.md](docs/zh-CN/npm.md)。 + +## 镜像 + +设置 `VFOX_NODEJS_MIRROR` 即可使用 `nodejs.org/dist` 的镜像,例如 `https://npmmirror.com/mirrors/node`。详细说明和检查过的镜像见 [docs/zh-CN/mirror.md](docs/zh-CN/mirror.md)。 + +## `.nvmrc` 和 `.node-version` + +进入目录时,vfox 会读取这些文件,并选中其中指定的版本(前提是已经安装)。文件里能写什么取决于 vfox 的 `legacyVersionFile.strategy`:默认值 `specified` 下,只有完整的版本号才能选中某个版本。详见 [docs/zh-CN/nvmrc.md](docs/zh-CN/nvmrc.md)。 + +## LTS + +LTS 版本线有哪些、怎样安装其中一条,以及 `lts/` 如何解析,见 [docs/zh-CN/lts.md](docs/zh-CN/lts.md)。 + +## pnpm + +pnpm 作为包管理器,可以和这个插件并存。不过 pnpm 自己安装的 `node` 可能遮蔽 vfox 的。见 [docs/zh-CN/pnpm.md](docs/zh-CN/pnpm.md)。 + +## 问题排查 + +插件打印的各种提示、代理设置,以及 Node.js 的位置,见 [docs/zh-CN/troubleshooting.md](docs/zh-CN/troubleshooting.md)。 + +## 从源码安装 + +详见 [docs/zh-CN/source-build.md](docs/zh-CN/source-build.md)。 + +## 开发 + +插件怎样工作、怎样修改,见 [docs/zh-CN/development.md](docs/zh-CN/development.md);容器测试见 [docs/zh-CN/e2e.md](docs/zh-CN/e2e.md);每个版本改了什么,见 [docs/zh-CN/changes.md](docs/zh-CN/changes.md)。 + +## 发布这个插件 + +维护者在默认分支上执行 **Actions → Plugin → Run workflow**,填入不带 `v` 前缀的稳定版插件版本号即可发布。共享工作流会更新 `metadata.lua`,创建版本提交和标签,并把 ZIP 包和 manifest 发布到本仓库;不需要在本地打标签,也不需要额外的发布令牌。PR 只运行检查,PR 标题不再触发发布。 + +仍然支持直接推送版本标签来发布,前提是 `PLUGIN.version` 已经与标签一致。发布失败时,重新运行失败的那个作业即可接着发布。 + +工作流引用的是共享的 `@v1` 发布工具,升级这个工具不会发布本插件。包的约定和首次接入的要求,见[共享工作流的文档](https://github.com/version-fox/plugin-manifest-action)。 + +## 致谢 + +- [Node.js](https://nodejs.org/),插件安装用的版本压缩包、校验和与版本索引都来自它 +- [Node.js Unofficial Builds](https://github.com/nodejs/unofficial-builds),nodejs.org 没有的 musl 构建来自这里 diff --git a/docs/changes.md b/docs/changes.md index b56aa0a..66a12dd 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -1,5 +1,7 @@ # Changes +[中文](zh-CN/changes.md) | [English](changes.md) + What changed in each release, as a user notices it. GitHub's [release pages](https://github.com/version-fox/vfox-nodejs/releases) list the pull requests. The newest release at the time of writing is 0.5.2, and `main` is @@ -41,6 +43,7 @@ ahead of it by everything under "Unreleased". `get checksum failed` and nothing else. See [troubleshooting.md](troubleshooting.md#messages-from-the-plugin). - `VFOX_NODEJS_MIRROR` may end in several slashes. Release 0.5.2 strips one. +- The documentation has a Chinese version, see [README_CN.md](../README_CN.md). ## 0.5.2 (2026-10-17) diff --git a/docs/development.md b/docs/development.md index 4a13157..6f39ffb 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,5 +1,7 @@ # Development +[中文](zh-CN/development.md) | [English](development.md) + ## Layout ```text @@ -251,17 +253,22 @@ Afterwards `plugins/nodejs.json` in [vfox-plugins](https://github.com/version-fox/vfox-plugins) shows the version that `vfox add nodejs` serves. -The documentation marks what is newer than the last release with "Unreleased". -After a release, move those entries in [changes.md](changes.md) under the new -version and take the remarks out: `grep -rn -i unreleased docs README*`. Also -search for the number of the previous release: a few sentences compare against -it without saying "Unreleased". +The documentation marks what is newer than the last release with "Unreleased" +("未发布" in Chinese). After a release, move those entries in +[changes.md](changes.md) under the new version and take the remarks out, in both +languages: `grep -rn -i -e unreleased -e 未发布 docs README*`. Also search for +the number of the previous release: a few sentences compare against it without +saying "Unreleased". ## Writing the docs -English pages are in `docs/`. Prose is wrapped at 80 columns, tables are not. -Pages that quote data give the day it was taken (the platform and musl tables, -the mirror list, the LTS table, the outputs of `vfox search`), and nothing -checks them. CI does not lint Markdown, so run the commands you add, and the -outputs in a page are the ones they printed. When a change in the plugin makes a -page wrong, change the page in the same pull request. +English pages are in `docs/`, the Chinese ones in `docs/zh-CN/` under the same +names (the Chinese README is `README_CN.md`), and each page starts with a link to +the other language. Prose is wrapped at 80 columns in the English pages, tables +are not. Pages that quote data give the day it was taken (the platform and musl +tables, the mirror list, the LTS table, the outputs of `vfox search`), and +nothing checks them. CI does not lint Markdown, so run the commands you add, and +the outputs in a page are the ones they printed. When a change in the plugin +makes a page wrong, change the page and its translation in the same pull request. +A link to `page.md#heading` in a Chinese page uses the heading of the Chinese +page, whose address is not the English one. diff --git a/docs/e2e.md b/docs/e2e.md index 63dd938..89332f8 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -1,5 +1,7 @@ # E2E +[中文](zh-CN/e2e.md) | [English](e2e.md) + The offline Lua hook tests (`tests/hooks_test.lua`) run in the **Test Plugin** workflow. The end-to-end suite runs in containers against real vfox on Linux and Windows; each `vfox` x `mirror` combination gets its own throwaway diff --git a/docs/lts.md b/docs/lts.md index f88cac8..8b9c5c8 100644 --- a/docs/lts.md +++ b/docs/lts.md @@ -1,5 +1,7 @@ # LTS releases +[中文](zh-CN/lts.md) | [English](lts.md) + Node.js majors with even numbers become **LTS** (long-term support) lines: they keep receiving fixes long after odd releases go end-of-life, so most production setups pin an LTS line. Each LTS line has a codename diff --git a/docs/mirror.md b/docs/mirror.md index 96a4f90..24958e6 100644 --- a/docs/mirror.md +++ b/docs/mirror.md @@ -1,5 +1,7 @@ # Mirror +[中文](zh-CN/mirror.md) | [English](mirror.md) + The Node.js packages and the version index (`index.json`, `SHASUMS256.txt`) are downloaded from `https://nodejs.org/dist` by default. Set `VFOX_NODEJS_MIRROR` to use another mirror: diff --git a/docs/musl.md b/docs/musl.md index 7fba115..2c26ca9 100644 --- a/docs/musl.md +++ b/docs/musl.md @@ -1,5 +1,7 @@ # musl libc support (Alpine, OpenWrt/ImmortalWrt) +[中文](zh-CN/musl.md) | [English](musl.md) + Node.js official Linux tarballs link glibc (`ld-linux-*`). On musl systems there is no glibc loader, so the stock download fails with `not found` / `Error loading shared library ld-linux-aarch64.so.1`. diff --git a/docs/npm.md b/docs/npm.md index 3bbcdeb..4fb0552 100644 --- a/docs/npm.md +++ b/docs/npm.md @@ -1,5 +1,7 @@ # npm and global packages +[中文](zh-CN/npm.md) | [English](npm.md) + Every release comes with the npm that Node.js ships it with. `vfox search nodejs` lists that version after the release, and `npm -v` prints it once the release is in use: diff --git a/docs/nvmrc.md b/docs/nvmrc.md index fd524d7..d90d949 100644 --- a/docs/nvmrc.md +++ b/docs/nvmrc.md @@ -1,5 +1,7 @@ # `.nvmrc` and `.node-version` +[中文](zh-CN/nvmrc.md) | [English](nvmrc.md) + vfox can read `.nvmrc`, the project file of [nvm](https://github.com/nvm-sh/nvm), and `.node-version`, the one that nodenv, fnm and others use, through this plugin (it lists both in `PLUGIN.legacyFilenames`). A project that already pins diff --git a/docs/platforms.md b/docs/platforms.md index 372a698..9485b61 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -1,5 +1,7 @@ # Platform support +[中文](zh-CN/platforms.md) | [English](platforms.md) + The plugin picks the package for the operating system and the CPU architecture of the vfox binary (`RUNTIME.osType` and `RUNTIME.archType`, Go's `GOOS` and `GOARCH`) and, on Linux, for the C library. The file it asks for is named diff --git a/docs/pnpm.md b/docs/pnpm.md index 6fb29d3..5000cf0 100644 --- a/docs/pnpm.md +++ b/docs/pnpm.md @@ -1,5 +1,7 @@ # pnpm +[中文](zh-CN/pnpm.md) | [English](pnpm.md) + pnpm as a package manager, for example from `npm install -g pnpm`, works next to this plugin. The conflict starts when pnpm also installs Node.js: `pnpm runtime set node -g` (or the deprecated diff --git a/docs/source-build.md b/docs/source-build.md index 7e8c211..d7bf053 100644 --- a/docs/source-build.md +++ b/docs/source-build.md @@ -1,5 +1,7 @@ # Installing the plugin from source +[中文](zh-CN/source-build.md) | [English](source-build.md) + Normally you install from the registry: ```sh diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 9423969..1c719a0 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,5 +1,7 @@ # Troubleshooting +[中文](zh-CN/troubleshooting.md) | [English](troubleshooting.md) + The messages below were produced with vfox 1.0.12 and 1.0.13. vfox prefixes the plugin's own errors with `plugin [PreInstall] method error: ::` (`[Available]` for the search) and follows them with a Lua stack trace, which this diff --git a/docs/versions.md b/docs/versions.md index baa2aad..c8e23b8 100644 --- a/docs/versions.md +++ b/docs/versions.md @@ -1,5 +1,7 @@ # Versions and names +[中文](zh-CN/versions.md) | [English](versions.md) + The part after `nodejs@` in `vfox install` and `vfox use` is a Node.js release number, or the start of one: diff --git a/docs/zh-CN/changes.md b/docs/zh-CN/changes.md new file mode 100644 index 0000000..823ce4b --- /dev/null +++ b/docs/zh-CN/changes.md @@ -0,0 +1,58 @@ +# 变更记录 + +[中文](changes.md) | [English](../changes.md) + +按发布版本列出用户能感受到的变化。GitHub 的 [Release 页面](https://github.com/version-fox/vfox-nodejs/releases)列出了对应的 PR。写这篇文档时最新的发布版是 0.5.2,`main` 比它多出“未发布”下面的所有内容。想现在就用上这些变化,见 [source-build.md](source-build.md)。 + +## 未发布 + +- 32 位 ARM 的 Linux 会安装 `linux-armv7l` 包,Node.js 到 23.11.1 为止都发布了这个包:`vfox install nodejs@20` 在那里可以用。vfox 的 `armv7` 构建报告的是 `arm`,而 0.5.2 请求 `linux-arm`,这个包并不存在,所以 32 位 ARM 上的每次安装都以 `Node.js package node-v20.19.5-linux-arm.tar.gz is not listed in ...` 告终。从 24 起没有这个包了,ARMv6 的包也永远不会被选中。见 [platforms.md](platforms.md#不支持的情况)。 +- 在 musl 上,解压出来的 `node` 因缺少库而无法运行时,安装会中止。解压之后,插件会运行一次 `node --version`,由动态加载器指出缺少什么。26.8.0 及以后的 musl 包需要 `libatomic`,原版 Alpine 没有它:安装会以 ``Node.js (musl build) requires the libatomic system package, which was not found on this system. Install it with `apk add libatomic` and reinstall.`` 结束,不会登记任何内容。0.5.2 只检查 `libstdc++`,所以安装能成功,而 `node` 会报 `Error loading shared library libatomic.so.1`。关于 `libstdc++` 的提示同样会给出命令,0.5.2 的提示则是 `It is normally installed automatically`。见 [musl.md](musl.md#依赖的系统库)。 +- 在 `legacyVersionFile.strategy` 为 `latest_installed` 和 `latest_available` 时,`.nvmrc` 和 `.node-version` 按版本号的数字匹配版本。已安装 22.9.0 和 22.23.3 时,`22` 选中 22.23.3,而 0.5.2 选中 22.9.0,因为 vfox 是按文本排列已安装版本的。`22.2` 不再匹配 22.23.3。`22.x`、`22.x.x` 和 `22.*` 都表示 22,0.5.2 对 `22.x` 什么也选不中。只会去掉开头的 `v`,所以代号里带 `v` 的 `lts/` 也能用。不是版本号的文本(`node`、`(`、`22[`)不做处理,0.5.2 对它们会报 `unfinished capture at EOS`,而 vfox 把这个错误藏了起来。见 [nvmrc.md](nvmrc.md#前缀怎样匹配)。 +- 获取 `SHASUMS256.txt` 的请求失败时会说明原因:`get checksum failed: HTTP 404 from https://nodejs.org/dist/v24.99.0/SHASUMS256.txt`,或者网络错误,比如 `x509: certificate signed by unknown authority`。0.5.2 只打印 `get checksum failed`,没有别的内容。见 [troubleshooting.md](troubleshooting.md#插件给出的提示)。 +- `VFOX_NODEJS_MIRROR` 的结尾可以有多个斜杠。0.5.2 只去掉一个。 +- 文档有了中文版,见 [README_CN.md](../../README_CN.md)。 + +## 0.5.2(2026-10-17) + +- 当另一个 `node`(比如 `pnpm runtime set node -g` 装的那个)在 `PATH` 里排在 vfox 前面,导致 `node` 不会运行刚选中的版本时,`vfox use` 会打印一条提示。以前 `vfox use` 报告 `Now using`,而 `node -v` 打印的是另一个版本。见 [pnpm.md](pnpm.md)([#38](https://github.com/version-fox/vfox-nodejs/pull/38))。 +- Windows 的 E2E 作业在含有空格的主目录下运行,这需要 vfox 1.0.13([#39](https://github.com/version-fox/vfox-nodejs/pull/39))。 + +## 0.5.1(2026-10-17) + +- Alpine、OpenWrt 等 musl 系统现在装的是 musl 包。以前装的是 glibc 包,`node` 会报 `not found`。nodejs.org 没有 musl 包时,插件改从 unofficial-builds.nodejs.org 获取,这涵盖了全部 arm64 和大部分 x64。`VFOX_NODEJS_LIBC` 可以覆盖检测结果。缺少 `libstdc++` 时,安装会失败,并在提示里指出这个库。见 [musl.md](musl.md)([#31](https://github.com/version-fox/vfox-nodejs/pull/31)、[#29](https://github.com/version-fox/vfox-nodejs/issues/29))。 +- 在 vfox 1.x 下,`legacyVersionFile.strategy` 设为 `latest_installed` 时,`.nvmrc` 和 `.node-version` 可以正常使用。0.3.0 到 0.5.0 调用 `ctx:getInstalledVersions()`,vfox 1.x 会以 `expected 0 arguments for func() []string, got 1` 拒绝它,而且 vfox 把这个错误藏了起来:文件什么也选不中,连 `22.23.3` 也不行。默认策略 `specified` 不受影响([#30](https://github.com/version-fox/vfox-nodejs/pull/30))。 +- 在 `latest_available` 下,`lts` 和 `lts/` 在版本索引里查找,所以比插件还新的代号也仍然有效。其他策略使用的代号表里有 Krypton(24)。见 [lts.md](lts.md)([#31](https://github.com/version-fox/vfox-nodejs/pull/31))。 +- 仓库有了覆盖 Linux、Alpine、Windows 和 WSL 的容器 E2E 套件,以及一个开发容器,见 [e2e.md](e2e.md)([#30](https://github.com/version-fox/vfox-nodejs/pull/30)、[#33](https://github.com/version-fox/vfox-nodejs/pull/33)、[#34](https://github.com/version-fox/vfox-nodejs/pull/34)、[#37](https://github.com/version-fox/vfox-nodejs/pull/37))。 + +## 0.5.0(2026-09-12) + +- 获取不到索引时,`vfox search nodejs` 会以 `Failed to fetch Node.js versions: ...` 报错。以前它什么也不列出,vfox 还会把这个空列表缓存起来([#28](https://github.com/version-fox/vfox-nodejs/pull/28)、[#16](https://github.com/version-fox/vfox-nodejs/issues/16))。 +- 32 位 x86 请求 `x86` 包。以前请求的是 `386`,这个包并不存在。vfox 给出的架构为空时,安装会以 `Cannot determine Node.js package architecture from RUNTIME.archType` 中止([#28](https://github.com/version-fox/vfox-nodejs/pull/28)、[#23](https://github.com/version-fox/vfox-nodejs/issues/23))。 +- `SHASUMS256.txt` 没有列出的包,会让安装以 `Node.js package ... is not listed in ...` 中止。以前 vfox 会在没有校验和的情况下继续下载,结果得到 404([#28](https://github.com/version-fox/vfox-nodejs/pull/28))。 +- `VFOX_NODEJS_MIRROR` 的结尾可以带一个斜杠。以前 URL 里会出现 `//`([#25](https://github.com/version-fox/vfox-nodejs/pull/25))。 + +## 0.4.1(2025-09-29) + +- Node.js 16.0.0 在 Apple silicon 上安装 arm64 的包。以前 16 装的是 amd64 的包,而只有 16 之前的版本才应该这样。这些版本会打印 `Note: Node.js versions below 16 do not support Apple Silicon (arm64), using x64 architecture with Rosetta 2`([#21](https://github.com/version-fox/vfox-nodejs/pull/21))。 + +## 0.4.0(2025-09-28) + +- `.nvmrc` 和 `.node-version` 遵循 `legacyVersionFile.strategy`。以前它们总是使用 `latest_installed`。见 [nvmrc.md](nvmrc.md)([#17](https://github.com/version-fox/vfox-nodejs/pull/17))。 +- `lts` 和 `lts/*` 选中代号表里最新的 LTS 版本线,`lts/jod` 也能识别了([#17](https://github.com/version-fox/vfox-nodejs/pull/17))。 +- 版本未知时会提示 `version not found for provided version `。以前钩子会以 `cannot perform concat operation between string and nil` 失败([#14](https://github.com/version-fox/vfox-nodejs/pull/14))。 + +## 0.3.0(2024-04-22) + +- vfox 能读取 `.nvmrc` 和 `.node-version`:完整的版本号、大版本号和 `lts/`,见 [nvmrc.md](nvmrc.md)([#7](https://github.com/version-fox/vfox-nodejs/pull/7))。 + +## 0.2.x(2024-04) + +- 0.2.2(2024-04-09):`vfox use nodejs@20` 和 `nodejs@20.11` 能找到已安装的版本([#6](https://github.com/version-fox/vfox-nodejs/pull/6))。 +- 0.2.1(2024-04-08):`vfox install nodejs@20` 和 `nodejs@20.11` 会安装这条版本线上最新的版本。它们以前会失败([#5](https://github.com/version-fox/vfox-nodejs/pull/5))。 +- 0.2.0(2024-04-07):`VFOX_NODEJS_MIRROR`,见 [mirror.md](mirror.md)([#3](https://github.com/version-fox/vfox-nodejs/issues/3))。Apple silicon 上安装 Node.js 16 及更早版本时使用 amd64 的包([#4](https://github.com/version-fox/vfox-nodejs/issues/4))。 + +## 0.0.x 和 0.1.x(2024-03 和 2024-04) + +- 0.0.1(2024-03-26)是第一个发布版。`vfox search nodejs` 在其中会失败,从 0.0.5 起可用。安装完整的版本号,并检查它的 SHA-256 校验和,从一开始就可以。 +- 0.1.1(2024-04-01)为 `vfox use` 增加了对版本线的查找,这个查找到 0.2.2 之前什么也找不到。 diff --git a/docs/zh-CN/development.md b/docs/zh-CN/development.md new file mode 100644 index 0000000..eff2d9d --- /dev/null +++ b/docs/zh-CN/development.md @@ -0,0 +1,150 @@ +# 开发 + +[中文](development.md) | [English](../development.md) + +## 目录结构 + +```text +metadata.lua 插件名、版本,以及把 .node-version 和 .nvmrc 注册为兼容版本文件 +hooks/available.lua vfox search:版本索引 +hooks/pre_install.lua vfox install:选择包,以及它的 URL 和校验和 +hooks/post_install.lua musl:node 无法运行时报错,比如缺少 libstdc++ 或 libatomic +hooks/env_keys.lua PATH:Linux 和 macOS 上是 bin,Windows 上是安装目录 +hooks/pre_use.lua vfox use:把 24 或 24.21 映射到已安装的版本,vfox 的 node 被遮蔽时给出提示 +hooks/parse_legacy_file.lua .nvmrc 和 .node-version +lib/util.lua 镜像 URL、musl 检测与缺失的库、运行命令、版本比较与匹配、简写、PATH 探测 +tests/hooks_test.lua 离线的钩子测试 +tests/e2e/ 容器测试,见 e2e.md +Injection.lua vfox 注入对象的类型提示,不会被加载 +.devcontainer/ Linux 开发容器,见 e2e.md +.github/workflows/ test、lint、license、e2e 和 publish +``` + +vfox 加载 `metadata.lua`,命令需要时调用相应的钩子,并向钩子提供 `PLUGIN` 和 `RUNTIME`(`osType`、`archType`、`version`),以及模块 `http` 和 `json`。`osType` 和 `archType` 就是 vfox 二进制的 Go `GOOS` 和 `GOARCH`。如果 vfox 提供了 `RUNTIME.envType`,插件就会读取它(目前还没有哪个 vfox 版本提供),见 [musl.md](musl.md#检测与覆盖)。`lib/util.lua` 用 `require("util")` 加载。 + +## 一条命令会执行什么 + +| 你执行 | 调用的钩子 | +| --- | --- | +| `vfox search nodejs` | `Available` | +| `vfox install nodejs@24` | `PreInstall` 返回版本、URL 和校验和,对 `latest` 和版本前缀还会调用 `Available`。vfox 下载、校验并解压,然后调用 `PostInstall` | +| `vfox use nodejs@24` | `PreUse` 返回要选中的已安装版本。`EnvKeys` 提供 `PATH` | +| 已挂载到 Shell,`cd` 进有 `.nvmrc` 的目录 | `ParseLegacyFile`,然后是 `EnvKeys` | +| `vfox uninstall nodejs@24.21.0` | 无,名字必须精确 | + +几条容易踩坑的约定: + +- `PreInstall` 返回 `version`、`url`、`sha256`,有时还有 `note`。`version` 就是安装后的版本名,所以这里必须把版本前缀解析成完整的版本。没有校验和时 vfox 只会警告(`Checksum is not provided`),然后继续下载,所以从 0.5.0 起,包不在 `SHASUMS256.txt` 里时钩子会报错,而不是不返回校验和。钩子用 `print` 打印的 Note 会出现在 vfox 的输出里。 +- `Available` 遇到网络错误或 200 以外的状态码时会抛出错误。从 0.5.0 起它不再返回空列表,那样的结果会被 vfox 缓存。结果在进程存活期间保存在全局变量 `available_result` 里,测试会把它重置。vfox 自己还会把搜索结果缓存 12 小时。 +- 对于无法映射的输入,`PreUse` 返回 `{ version = nil }`,vfox 就回答 `is not installed`。它拿到的 `ctx.installedSdks` 是以版本为键的表,而不是数组,所以 `calculate_shorthand` 用 `pairs` 遍历。 +- `ParseLegacyFile` 调用 `ctx.getInstalledVersions()` 时不带参数。vfox 1.x 把它绑定成普通的 Go 函数,会拒绝 `ctx:getInstalledVersions()` 多传的那个参数。策略由 `ctx.strategy` 给出。vfox 把已安装的版本按字符串降序传进来,所以 `22.9.0` 排在 `22.23.3` 前面。测试把它当作普通函数传入,并断言调用时没有带参数。 +- `PostInstall` 拿到 `ctx.rootPath` 和 `ctx.sdkInfo`,后者是以 SDK 名(`nodejs`,或者别名)为键的表,表项的 `path` 指向解压后的版本。此时 `bin/node` 已经就位。钩子遍历 `ctx.sdkInfo`,而不是直接读 `ctx.sdkInfo.nodejs`,所以用别名也行。`io.popen` 和 `os.execute` 在 vfox 的 Lua 里可以使用。`io.popen(...):close()` 返回的退出状态是数字,不像标准 Lua 里是 `true`,`util.capture` 两种都能处理。 +- `EnvKeys` 只设置 `PATH`,不设置 `NODE_HOME` 之类的变量。 +- vfox 会先去掉用户输入开头的 `v`,钩子拿到的已经不带它。 + +## 解析版本 + +`PreInstall`: + +1. `latest` 取 `Available` 的第一项,这个列表由 `util.compare_versions` 按从新到旧排序,逐段比较数字。 +2. 任何不是 `x.y.z` 的写法(`^%d+%.%d+%.%d+$`,由 `util.is_semver_simple` 判断)都要到 `util.calculate_shorthand(Available)` 里查。这是一张把 `24`、`24.21` 这样的前缀映射到各自最新版本的表。查不到时以 `version not found for provided version ` 报错。 +3. 不存在的版本会通过前两步,在 `SHASUMS256.txt` 返回 404 时失败:`get checksum failed`。 + +`PreUse` 对已安装的版本执行第 2 步。`ParseLegacyFile` 有自己的规则,见下文。 + +## 安装包 + +`PreInstall` 根据 `RUNTIME` 拼出文件名 `node-v--`(`util.FileName`):`amd64` 对应 `x64`,`386` 对应 `x86`,`arm` 对应 `armv7l`,Windows 对应 `win` 和 `.zip`。在 macOS arm64 上,大版本低于 16 时取 `x64`。在 Linux 上,`util.is_musl()` 为真时,`x64` 和 `arm64` 加上 `-musl`,其他 CPU 则报错。接着它读取 `/v/SHASUMS256.txt`,用 `util.get_checksum` 找到校验和,并返回与校验和文件同一目录下的 URL。请求失败时以 `get checksum failed: ` 或 `get checksum failed: HTTP from
` 报错。如果 musl 的包在那里找不到,就用同样的方式再试 `UnofficialBaseUrl`。哪些包存在,表格见 [platforms.md](platforms.md) 和 [musl.md](musl.md)。 + +`util.getBaseUrl()` 返回镜像地址:去掉末尾斜杠的 `$VFOX_NODEJS_MIRROR`,没有设置时是 `https://nodejs.org/dist`。非官方站点不受它影响。 + +`post_install.lua` 在 musl 以外什么也不做。在 musl 上,如果在常见的库目录里找不到 `libstdc++.so.6`,它就报错(`util.has_libstdcxx`)。然后它用 `util.capture` 把解压出来的 `bin/node --version` 运行一次:动态加载器会指出缺少的库(`Error loading shared library libatomic.so.1`),钩子把它转成一条报错信息,写明需要的包,在 Alpine 和 OpenWrt 上还写明安装命令(`util.MuslLibraries` 存放包名,`util.install_command` 生成命令)。其他任何失败,以及运行环境无法执行命令的情况,都不会干预这次安装,所以一个无法工作的检查绝不会阻止安装。见 [musl.md](musl.md#依赖的系统库)。 + +## 兼容版本文件 + +`ParseLegacyFile` 读取文件,去掉所有空白字符,再把文本交给 `resolve_version(query, strategy)`:它去掉开头的 `v`,转成小写,把 `lts-x` 变成 `lts/x`,把 `lts/` 映射到一个大版本,把 `lts` 和 `lts/*` 映射到 `nodejs_codenames` 里最高的大版本。在 `latest_available` 策略下,代号先从索引里取(`discover_lts`),索引里没找到或请求失败时,回退到代号表。 + +接着 `resolve_legacy_version(strategy, query)` 用 `util.version_prefix` 解析查询:`22` 得到 `{ 22 }`,`22.23` 得到 `{ 22, 23 }`,`22.x`、`22.x.x` 和 `22.*` 都得到 `{ 22 }`。不是这类前缀的文本(`node`、`lts/unknown`、`>=20`)原样返回。对于前缀: + +- `specified`:返回查询本身。 +- `latest_installed`:返回 `ctx.getInstalledVersions()` 里以该前缀开头的最新版本(`util.newest_matching`,逐段按数字比较)。没有这样的版本时,按 `latest_available` 的做法处理。 +- `latest_available`:返回 `Available` 里以该前缀开头的最新版本,没有的话返回查询本身。 + +`vfox` 随后会选中这个结果,前提是它已安装。面向用户的规则和示例见 [nvmrc.md](nvmrc.md) 和 [lts.md](lts.md)。 + +未发布:0.5.2 把查询当作 Lua 模式,去匹配每个版本的开头,按 vfox 列出它们的顺序逐个尝试,取第一个匹配的。见 [nvmrc.md](nvmrc.md#前缀怎样匹配)。 + +## Node.js 把一条版本线升为 LTS 时 + +每年十月,一条偶数版本线会得到代号,见 [lts.md](lts.md#nodejs-升级版本线时)。要让插件认识新的代号: + +1. 在 `hooks/parse_legacy_file.lua` 的 `nodejs_codenames` 里加上 ` = `。 +2. 在 `tests/hooks_test.lua` 里,挨着 `legacy lts krypton maps to its major` 加一个用例。 +3. 在 [lts.md](lts.md#插件认识哪些代号) 的表里加一行,并修改 [lts.md](lts.md) 和 [nvmrc.md](nvmrc.md) 里写着“撰写本文时是 24”的句子。 +4. `tests/e2e/common/common.go` 里的 `DefaultLTSVersion` 可以不动:它只要仍在表内即可。 +5. 发布。 + +## 测试 + +钩子测试在离线状态下用 `gopher-lua` 运行,它就是 vfox 内嵌的 Lua 解释器: + +```bash +go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/hooks_test.lua +``` + +请在仓库根目录运行。`tests/hooks_test.lua` 自己模拟了 `http` 和 `json`(通过 `package.preload`),为每个用例设置 `RUNTIME`,请求的应答来自 `state.response`、`state.decoded` 和 `state.responder`。一个用例写作 `test("name", function() ... end)`,调用它会重置状态和 `available_result`。测试覆盖 `Available` 及其失败情况、各平台的包名和校验和请求失败时的错误、musl 与 `PostInstall`(通过 `with_util` 模拟了 `util.capture`、`util.file_exists` 和 `util.has_libstdcxx`)、各策略下的 `ParseLegacyFile`、有 `node` 遮蔽 vfox 时的 `PreUse`,以及 `lib/util.lua` 里的辅助函数。改动需要的用例就加在那里。 + +gopher-lua 1.1.1 会丢掉闭包对 `local x = nil` 的赋值:外层作用域读到的仍然是 `nil`。要在 `with_util` 或 `pcall` 内部收集值的测试,必须把值放在表的字段里。 + +想对着真实的服务器调用单个钩子,可以用 vfox 1.0.13 及以后版本的 `vfox plugin run`。它不会注册插件,`--os` 和 `--arch` 可以模拟其他平台,`--offline` 会拒绝 HTTP 请求,`VFOX_NODEJS_MIRROR` 同样生效: + +```bash +vfox plugin run . Available --json +vfox plugin run . PreInstall --input '{"version":"24"}' --os windows --arch arm64 --json +vfox plugin run . PreUse --input '{"version":"22","installedSdks":{"22.23.3":{"version":"22.23.3"}}}' --json +vfox plugin run . ParseLegacyFile --input '{"filename":".nvmrc","filepath":"/tmp/p/.nvmrc","strategy":"latest_installed","installedVersions":["24.21.0"]}' --json +vfox plugin run . EnvKeys --input '{"path":"C:\\vfox\\nodejs-24.21.0"}' --os windows --arch amd64 --json +``` + +`vfox --debug ` 会显示 vfox 对钩子做了什么,vfox 吞掉的错误可以靠它找出来,比如 `ParseLegacyFile` 里的错误。 + +Lint、许可证头和容器测试见 [e2e.md](e2e.md#本地检查)。Lua 用 StyLua 格式化(`stylua.toml`:4 个空格缩进,每行 120 列),用 LuaLS 以 `Warning` 级别检查(`.luarc.json` 里声明了 `PLUGIN` 和 `RUNTIME`)。源文件(Lua、Go、YAML、PowerShell 和 Dockerfile)带有许可证头,Markdown 和 JSON 不带。 + +## 更新平台表 + +[platforms.md](platforms.md) 的数据取自 nodejs.org。`index.json` 按从新到旧列出所有版本,以及每个版本有哪些文件。下面的命令对每种包打印出有它的最早版本和最新版本,以及两者之间有多少个版本没有它: + +```bash +curl -s https://nodejs.org/dist/index.json | jq -r ' + def span($key): + [to_entries[] | select(.value.files | index($key))] as $hits + | if ($hits | length) == 0 then "none" + else "\($hits[-1].value.version) to \($hits[0].value.version), \($hits | length) releases, \($hits[-1].key - $hits[0].key + 1 - ($hits | length)) missing in between" + end; + ("linux-x64", "linux-arm64", "linux-ppc64le", "linux-s390x", "linux-x86", "linux-x64-musl", + "osx-x64-tar", "osx-arm64-tar", "win-x64-zip", "win-x86-zip", "win-arm64-zip") as $key + | "\($key): \(span($key))"' +``` + +这里的键是索引对插件所用文件的叫法(`osx-x64-tar` 对应 `darwin-x64.tar.gz`,`win-x64-zip` 对应 `win-x64.zip`)。索引与 `SHASUMS256.txt` 一致(插件读取的是后者),只有两个版本例外:索引在 `linux-x64`、`linux-x86` 和 macOS x64 下列出了 0.11.4 和 0.11.6,但它们没有包。要得到 musl 表,把同一段程序用在 `https://unofficial-builds.nodejs.org/download/release/index.json` 上,键换成 `linux-x64-musl` 和 `linux-arm64-musl`,再用在官方索引上,键换成 `linux-x64-musl`。每次查询只反映一个站点,所以要把两份结果合起来。 + +## 已知限制 + +- 32 位 ARM 取 `linux-armv7l` 的包,最高到 23.11.1。ARMv6 的包(`linux-armv6l`)从不会被选中,而从 24 起 32 位 ARM 已经没有任何包。 +- riscv64 和 loong64 只在 Unofficial Builds 上有包,而插件只把它用于 musl。 +- `PostInstall` 只在 musl 上运行 `node --version`。在 glibc 上,缺少 `libatomic.so.1`(Node.js 25 及以后)要等到 `node` 运行时才会暴露:README 把系统库留给用户自己处理。 +- E2E 的 Alpine 镜像通过依赖 `gcc` 的 `go` 得到 `libatomic`,所以套件没有覆盖不带它的原版 Alpine,钩子测试覆盖了。 +- `.nvmrc` 里的别名(`node`、`stable`、`lts/-1`)、范围和注释都不认识,`lts` 则依据一张手工维护的表,见 [nvmrc.md](nvmrc.md#不认识的写法)。 +- `latest_available` 在带有兼容版本文件的目录里,每次出现提示符都会下载 `index.json`。 +- `VFOX_NODEJS_MIRROR` 对非官方站点不起作用。 +- macOS 没有 E2E 作业。 + +## 发布 + +维护者在默认分支上执行 **Actions → Plugin → Run workflow**,填入不带 `v` 的版本号。共享工作流具体做什么,README 里有说明。发布之后,[vfox-plugins](https://github.com/version-fox/vfox-plugins) 里的 `plugins/nodejs.json` 会显示 `vfox add nodejs` 提供的是哪个版本。 + +文档里用“未发布”(英文是“Unreleased”)标记比最近一次发布更新的内容。发布之后,把 [changes.md](changes.md) 里的这些条目移到新版本下面,并去掉这些标注,两种语言都要处理:`grep -rn -i -e unreleased -e 未发布 docs README*`。上一个版本的版本号也要搜一遍:有几句话是拿它做对比的,没有写“未发布”。 + +## 编写文档 + +英文页面在 `docs/`,中文页面在 `docs/zh-CN/`,文件名相同(中文版 README 是 `README_CN.md`),每页开头有指向另一种语言的链接。英文页面里的正文按 80 列折行,表格不折。引用数据的页面会写明数据取自哪一天(平台表和 musl 表、镜像列表、LTS 表、`vfox search` 的输出),也没有任何检查会核对这些内容。CI 不对 Markdown 做 lint,所以你加的命令请自己跑一遍,页面里的输出就取自它们实际打印的结果。插件的改动让某一页不再正确时,在同一个 PR 里修改这一页和它的译文。中文页面里指向 `page.md#heading` 的链接,用的是中文页面的标题,所以链接地址和英文页面的不一样。 diff --git a/docs/zh-CN/e2e.md b/docs/zh-CN/e2e.md new file mode 100644 index 0000000..8ed8919 --- /dev/null +++ b/docs/zh-CN/e2e.md @@ -0,0 +1,220 @@ +# E2E + +[中文](e2e.md) | [English](../e2e.md) + +离线的 Lua 钩子测试(`tests/hooks_test.lua`)在 **Test Plugin** 工作流里运行。端到端套件在容器里针对真实的 vfox 运行,覆盖 Linux 和 Windows;每个 `vfox` x `mirror` 组合都有自己的一次性容器。在 Linux 上,CI 直接在标准 runner 上运行这套测试(Docker 加 `actions/setup-go` 提供的 Go),仓库的开发容器在本地复现的就是这一环境。在 Windows 上,测试通过 `tests/e2e/windows/run.ps1` 从宿主运行,因为开发容器无法承载 Windows 容器镜像(见下文)。 + +## 开发容器 + +`.devcontainer/` 提供一个 Linux 开发环境,在四种宿主平台上都能用: + +| 宿主 | 开发容器的运行方式 | +| --- | --- | +| Linux amd64 / arm64 | 原生运行 | +| Windows amd64 / arm64 | Docker Desktop 的 Linux 容器模式(WSL 2) | + +它带有 Go 1.27.1(CI 安装的版本)、Docker CLI、stylua、lua-language-server 和许可证工具,版本都与 `.github/workflows/lint.yml` 和 `license.yml` 里固定的一致,并挂载宿主的 Docker daemon socket。套件随后通过 testcontainers 连接这个 daemon(Docker-out-of-Docker),启动的目标容器和原生 CI runner 启动的完全相同;这里刻意不用 Docker-in-Docker,这样镜像缓存和网络都还是宿主的。镜像不需要工作区里的文件,所以构建上下文保持在 `.devcontainer/`;`postCreateCommand` 会预热 `tests/e2e` 的 Go 模块缓存,第一次运行测试时就不必停下来下载。 + +用任何支持 Dev Containers 规范的工具都可以打开它:在 VS Code/Cursor 里执行 **Dev Containers: Reopen in Container**,或者用 CLI: + +```bash +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash +``` + +`TESTCONTAINERS_RYUK_DISABLED=true` 是预先设好的:Ryuk 会在宿主 daemon 上发布一个端口,并等待套件回连,而 Docker-out-of-Docker 的容器做不到这一点。套件改为通过 `t.Cleanup` 显式终止每个容器;被强行中断的运行遗留的容器,名称都带有 `vfox-nodejs-e2e-` 前缀,可以按这个前缀清理。使用 rootless Docker 的用户,请在打开容器之前先在宿主上导出 `DOCKER_SOCK="$XDG_RUNTIME_DIR/docker.sock"`。在 Windows 宿主上,把仓库检出到 WSL 2 里(例如 `\\wsl$` 下)能让文件和 bind mount 的访问保持快速;检出到 Windows 文件系统上也能用。 + +Windows 容器的覆盖(`tests/e2e/windows`)无法在开发容器里运行:Dev Containers 规范不支持 Windows 容器镜像([vscode-remote-release#445](https://github.com/microsoft/vscode-remote-release/issues/445)),而且 Windows 容器只能跑在 Windows daemon 上。这套测试要在 Windows 宿主上通过 `tests/e2e/windows/run.ps1` 运行,见下面的 [Windows 11](#windows-11)。 + +## 套件验证什么 + +每个组合都运行同样的流程(每个组合一个长期存活的容器,由 Go 通过类似 `docker exec` 的调用来驱动): + +1. 从指定的渠道安装 vfox,要么是最新发布版(`latest`),要么用固定版本的 Go 从源码构建(`main`;在 Linux 上于容器内构建,在 Windows 上由 `run.ps1` 在宿主上每次运行构建一次),然后从当前检出的代码注册本地的 `nodejs` 插件。 +2. `$VFOX_NODEJS_MIRROR` 有正反两方面的检查:配置的镜像要能提供版本索引;而配成无效的镜像时,`nodejs@latest` 和固定版本的安装都应失败,而不是悄悄回退到 `nodejs.org`。 +3. 固定的 Node.js 版本安装时会验证校验和,并且能正常运行(`node --version`、`npm --version`、`process.platform`、`process.arch`),因此 `linux-x64` / `linux-arm64` / `win-x64` / `win-arm64` 的包选择是实际检验过的。在 Windows 上,vfox 挂载之后,Git Bash 还会运行 `node`、`npm.cmd` 和不带扩展名的 `npm`,以覆盖 [issue #293](https://github.com/version-fox/vfox/issues/293)。LTS 版本(`LTS_NODE_VERSION`)也会安装,供下面的代号检查使用。 +4. `nodejs@latest` 通过插件的 `Available` 列表解析(取第一项,也就是最新的一项),并在该平台的二进制已经发布时完成安装。Alpine ARM64 只在上游对应的非官方 musl 构建缺失期间才跳过这项检查;固定版本和 LTS 版本仍会验证。 +5. 只写大版本,或只写大版本和小版本的简写,通过 `PreUse` 对照已安装的版本解析。 +6. `.nvmrc` 和 `.node-version` 决定 `vfox env` 选中哪个版本:在默认的 `specified` 策略下,选中文件里固定的那个确切版本;在 `latest_installed` 策略下,LTS 代号(`lts/`,见 [lts](lts.md))会解析为对应的 LTS 版本。每项兼容版本文件的检查都在自己的 Shell 里挂载 vfox,这样选择结果是重新计算出来的,而不是由前一次检查留下的 `vfox env` 状态缓存直接给出。 +7. `vfox search nodejs` 列出已安装的版本,给出的 npm 版本与该二进制自带的 npm 版本相同。 + +默认矩阵是: + +| 平台 | vfox | 镜像 | +| --- | --- | --- | +| amd64 | latest、main | default(`https://nodejs.org/dist`)、CN(`npmmirror.com`) | +| arm64 | latest、main | default | + +默认只有 `latest` 会运行额外的镜像,以控制 CI 的耗时;要扩大或缩小矩阵,请显式指定 `VFOX_VERSION` 和 `MIRROR`。 + +## 环境变量 + +| 变量 | 默认值 | 含义 | +| --- | --- | --- | +| `VFOX_VERSION` | `latest main` | 用空格分隔的 vfox 渠道(`latest`、`main`) | +| `MIRROR` | 见上面的矩阵 | 用空格分隔的镜像;`default` 表示 `nodejs.org` | +| `NODE_VERSION` | `26.11.1` | 套件安装的固定 Node.js 版本 | +| `LTS_NODE_VERSION` | `24.21.0` | 用于 `lts/` 检查的 LTS 版本(见 docs/zh-CN/lts.md) | +| `E2E_MAX_JOBS` | `3`(Linux),`2`(Windows) | 每台宿主并行的容器数 | +| `E2E_RETRY_ATTEMPTS` | `3` | 每个涉及网络的 exec 的尝试次数(线性退避 10 秒) | +| `E2E_LINUX_BASE` | `ubuntu` | Linux 用户空间:`ubuntu`(glibc)或 `alpine`(musl,见 docs/zh-CN/musl.md) | + +## Linux(Ubuntu) + +在开发容器里,从仓库根目录执行: + +```bash +cd tests/e2e +go test -v -timeout 55m -run TestE2E ./linux +``` + +迭代时可以用同样的方式缩小范围: + +```bash +VFOX_VERSION=latest MIRROR=default E2E_MAX_JOBS=1 go test -v -timeout 55m -run TestE2E ./linux +``` + +在原生宿主上,同一条命令也能用,只需要两个前提:Docker Engine(rootful 或 rootless)和 Go 1.27.1,这正是 CI 准备的环境。使用 rootless Docker 的用户要把 Docker 客户端指向自己的 socket(`export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"`);开发容器挂载的 socket 已经替你处理了这种情况。ARM64 只运行默认镜像。 + +## Alpine(musl) + +Alpine 套件会构建 `tests/e2e/linux/Dockerfile-alpine`(带 `libstdc++` 的 musl 用户空间),先断言用户空间确实是 musl,再运行标准流程,以此证明插件选中的是 `*-musl` 压缩包([issue #29](https://github.com/version-fox/vfox-nodejs/issues/29))。压缩包的对应关系见 [docs/zh-CN/musl.md](musl.md)。 + +镜像有意安装 `libstdc++`。`libatomic` 只是经由 `go`(它依赖 `gcc`)装进来的,所以套件没有覆盖原版 Alpine:在原版 Alpine 上,26.8.0 及以后版本的 musl 包需要 `libatomic`,缺了它安装就会中止。钩子测试覆盖了这项检查(`post_install names the library the loader cannot find`),见 [docs/zh-CN/musl.md](musl.md#依赖的系统库)。 + +设置 `E2E_LINUX_BASE`,即可让同一套 Linux 套件切换到 Alpine 镜像运行: + +```bash +cd tests/e2e +E2E_LINUX_BASE=alpine go test -v -timeout 55m -run TestE2E ./linux +``` + +CI 在它的 musl 作业里导出同一个变量;开发容器不需要任何其他设置。 + +## Windows 11 + +Windows 容器无法跨架构模拟,所以 ARM64 套件需要 ARM64 宿主(Copilot+ PC 或 `windows-11-arm` CI runner)。 + +这套测试没有开发容器,因为 Dev Containers 规范拒绝 Windows 容器镜像(见[开发容器](#开发容器)),所以它通过 `tests/e2e/windows/run.ps1` 从 Windows 宿主运行。对宿主的要求是 Windows 容器模式的 Docker;除了 Windows 自带的 `curl.exe` 和 `tar.exe`,脚本不需要别的东西。它会把固定版本的 PowerShell、PortableGit(含 Git Bash 的 Git for Windows)和 Go 下载到按架构区分的缓存里(`$env:E2E_CACHE_DIR`,默认是 `%LOCALAPPDATA%\vfox-nodejs-e2e\`,多次运行之间复用),用这个 Go 构建一次 `vfox@main`,编译套件,然后运行。这些工具都不会被安装,也不会放进用户的 `PATH`,所以不会与 vfox 管理的工具链冲突。 + +缓存的工具以只读方式 bind mount 到每个组合容器的 `C:\tools`,所以 `tests/e2e/windows/Dockerfile` 只需要在原版 servercore 上加 `PATH` 条目和插件文件。如果改为把工具构建进镜像层,托管的 runner 上仅提交镜像层就要花几分钟(这些工具共有几万个文件)。拉取 servercore 在全新的 CI runner 上要五到七分钟,它在后台进行,同时获取工具、编译 Go 代码。 + +`windows-11-arm` runner 没有 Windows Hypervisor Platform 功能,所以无法使用 Hyper-V 隔离;进程隔离只需要 Containers 功能。 + +每个组合运行时,`VFOX_HOME` 都指向一个带空格的路径(`C:\Users\John Doe\.vfox`),以复现 [issue #33](https://github.com/version-fox/vfox-flutter/issues/33)。vfox 从 1.0.13 起支持这样的路径([vfox#712](https://github.com/version-fox/vfox/pull/712)),所以 `latest` 发布版和 `main` 都在这个路径下运行。 + +### x64 + +下面所有命令都在 PowerShell 7 中运行。如果还没有安装,请先装上(这一条引导命令可以在自带的 Windows PowerShell 里运行): + +```powershell +winget install --id Microsoft.PowerShell --source winget --exact +``` + +1. 在 PowerShell 7 中执行: + +```powershell +winget install --id Git.Git --source winget --exact +# 按需配置 Git。 + +curl.exe -fsSL ` + "https://raw.githubusercontent.com/microsoft/Windows-Containers/Main/helpful_tools/Install-DockerCE/install-docker-ce.ps1" ` + -o ./install-docker-ce.ps1 +``` + +2. 以管理员身份在 PowerShell 7 中执行: + +```powershell +./install-docker-ce.ps1 +``` + +3. 在 PowerShell 7 中执行: + +```powershell +git clone git@github.com:version-fox/vfox-nodejs.git +cd ./vfox-nodejs/ +pwsh -NoProfile -File tests/e2e/windows/run.ps1 +``` + +### arm64 + +下面所有命令都在 PowerShell 7 中运行。如果还没有安装,请先装上(这一条引导命令可以在自带的 Windows PowerShell 里运行): + +```powershell +winget install --id Microsoft.PowerShell --source winget --exact +``` + +1. 在 PowerShell 7 中执行: + +```powershell +winget install --id Git.Git --source winget --exact +# 按需配置 Git。 +winget install --id GoLang.Go --source winget --exact +git clone git@github.com:version-fox/vfox-nodejs.git +cd ./vfox-nodejs/ +pwsh -NoProfile -File .\tests\e2e\windows\install-docker-arm64.ps1 +pwsh -NoProfile -File .\tests\e2e\windows\run.ps1 +``` + +在 arm64 上,只有构建 Docker 引擎(`install-docker-arm64.ps1`)时才需要宿主上的 Go;`run.ps1` 跑套件时用的是它自己缓存的 Go。脚本会复用之前留在 `C:\vfox-docker\bin` 里的构建产物;CI 用 `actions/cache` 恢复这个目录,缓存以这个脚本为键,所以命中缓存时会同时跳过 `actions/setup-go` 和构建。 + +`$env:E2E_RUN` 用来收窄 `-test.run` 的匹配模式(默认是 `TestE2E`),例如 `TestE2E/latest-default`。想缩小运行范围,在运行命令之前加上 `$env:VFOX_VERSION = 'latest'; $env:MIRROR = 'default'`,和其他平台一样;`VFOX_VERSION` 里没有 `main` 时,脚本会跳过构建 `vfox@main`。CI 在两个 Windows 作业上都设置了 `E2E_MAX_JOBS=3`,让每个组合同时运行;本地默认值仍是 2。 + +## WSL + +WSL 套件(`tests/e2e/wsl/`)在 Windows 宿主上通过 `wsl.exe` 直接驱动 Ubuntu 发行版,不使用容器。它是为了复现 [issue #32](https://github.com/version-fox/vfox-nodejs/issues/32) 而存在的:在 WSL 里,`vfox install` 报告成功,`node` 却不在 `PATH` 上。在共用的流程(带校验和验证的安装、工具链、简写、搜索、兼容版本文件)之外,它还断言 `command -v node` 解析到 `VFOX_HOME` 之下,并且 `vfox env -s bash` 导出了 SDK 的 bin 目录。 + +固定的版本默认是 `20.16.0`(issue 里的版本);`NODE_VERSION` 可以覆盖它。LTS 版本(`LTS_NODE_VERSION`,默认值以 `tests/e2e/common` 为准)和其他套件一样,用于 `lts/` 检查。各组合共用一个发行版并依次运行,每个组合使用各自独立的 `HOME`/`VFOX_HOME`,彼此隔离。`MIRROR` 的默认值照常适用;CI 固定 `MIRROR=default`,使运行只覆盖 `latest`/`main` 这两个 vfox 渠道。 + +它在 Windows 宿主上运行(需要 `wsl.exe` 和已安装的发行版),而不是在开发容器里。 + +### CI + +`WSL (windows-2025)` 作业用 `wsl --install --no-launch --web-download` 安装 `Ubuntu-24.04`(镜像自带 WSL,但没有发行版;`--web-download` 绕过无法访问的 Store),以 root 身份启动一次来跳过创建用户,然后在 `tests/e2e` 下运行 `go test -run TestE2E ./wsl`。日志里记录了 `wsl --version`、`wsl -l -v`(WSL 1 还是 2)和 `uname -a`,便于解读结果。 + +### 本地运行 + +在装有 WSL 和 Go 的 Windows 11 上,用 PowerShell 执行: + +```powershell +wsl --install -d Ubuntu-24.04 +$env:WSL_DISTRO = 'Ubuntu-24.04' +$env:VFOX_VERSION = 'latest'; $env:MIRROR = 'default'; $env:NODE_VERSION = '20.16.0' +cd ./vfox-nodejs/tests/e2e/ +go test -v -timeout 55m -run TestE2E ./wsl +``` + +## 本地检查 + +在开发容器里执行,下面所有工具都已预装,版本和工作流使用的一致;也可以在宿主上装好同样的工具。 + +```bash +# Lua 和 Go 检查(.github/workflows/lint.yml)。 +cd tests/e2e +go test -short ./... +test -z "$(gofmt -l .)" +go vet ./... +cd ../.. + +go run github.com/yuin/gopher-lua/cmd/glua@v1.1.1 tests/hooks_test.lua + +stylua --check . +lua-language-server --check . --checklevel=Warning +``` + +```bash +# 许可证头(.github/workflows/license.yml)。 +license-eye header check +license-eye header fix +addlicense -check -f .license.tpl tests/e2e/windows/*.ps1 +addlicense -f .license.tpl tests/e2e/windows/*.ps1 +``` + +在只有 Docker 的宿主上,许可证检查改用容器运行(和 CI 用的镜像相同): + +```bash +docker run --rm -v "$PWD:/work" -w /work apache/skywalking-eyes:0.9.0 header check +docker run --rm -v "$PWD:/work" -w /work apache/skywalking-eyes:0.9.0 header fix +docker run --rm -v "$PWD:/src" ghcr.io/google/addlicense:v1.2.0 -check -f .license.tpl tests/e2e/windows/*.ps1 +docker run --rm -v "$PWD:/src" ghcr.io/google/addlicense:v1.2.0 -f .license.tpl tests/e2e/windows/*.ps1 +``` diff --git a/docs/zh-CN/lts.md b/docs/zh-CN/lts.md new file mode 100644 index 0000000..cfcd48d --- /dev/null +++ b/docs/zh-CN/lts.md @@ -0,0 +1,101 @@ +# LTS 版本 + +[中文](lts.md) | [English](../lts.md) + +Node.js 中编号为偶数的大版本会成为 **LTS**(长期支持)版本线:奇数版本结束生命周期很久之后,它们仍会继续收到修复,所以大多数生产环境都会固定使用某条 LTS 版本线。每条 LTS 版本线都有一个代号(4 是 `argon`,6 是 `boron`,……,24 是 `krypton`)。 + +## 安装一条 LTS 版本线 + +插件没有给 `vfox install` 提供 `lts` 别名:`vfox install nodejs@lts` 会失败,报 `version not found for provided version lts`。请改为写版本线,这样会安装这条版本线的最新版本: + +```bash +vfox install nodejs@24 +``` + +`vfox search nodejs` 会用 `(LTS)` 标出 LTS 版本线的版本,所以最新的 LTS 版本就是第一个带这个标记的行: + +```text +$ vfox search nodejs | grep -m1 '(LTS)' + - 24.22.0 (LTS) [npm 11.19.1] +``` + +用 `vfox install nodejs@24.22.0` 安装它,或者用 `nodejs@24` 跟随这条版本线。这个标记来自版本索引的 `lts` 字段,Node.js 从一条版本线进入长期支持的那天起设置它:24.11.0 是 Krypton 中第一个带它的版本,24.10.0 及更早的版本没有。`lts/` 只在 `.nvmrc` 和 `.node-version` 文件里才会被识别,见 [nvmrc.md](nvmrc.md)。 + +## 插件认识哪些代号 + +在 `latest_available` 策略下,插件从版本索引里动态获取 LTS 代号(`index.json` 在每个版本的 `lts` 字段里写明了代号),所以插件发布之后才出现的代号,在这个策略下仍然有效。其他情况下,以及离线时的回退,用的是 `hooks/parse_legacy_file.lua` 里硬编码的表,因为 `specified` 和 `latest_installed` 必须在没有网络的情况下完成解析。“首个 LTS 版本”和“最后一个版本”取自 2026-10-21 的索引,Krypton 仍在发布新版本: + +| 代号 | 大版本 | 首个 LTS 版本 | 最后一个版本 | +| --- | --- | --- | --- | +| argon | 4 | 4.2.0 | 4.9.1 | +| boron | 6 | 6.9.0 | 6.17.1 | +| carbon | 8 | 8.9.0 | 8.17.0 | +| dubnium | 10 | 10.13.0 | 10.24.1 | +| erbium | 12 | 12.13.0 | 12.22.12 | +| fermium | 14 | 14.15.0 | 14.21.3 | +| gallium | 16 | 16.13.0 | 16.20.2 | +| hydrogen | 18 | 18.12.0 | 18.20.8 | +| iron | 20 | 20.9.0 | 20.20.2 | +| jod | 22 | 22.11.0 | 22.23.3 | +| krypton | 24 | 24.11.0 | 24.22.0 | + +## Node.js 升级版本线时 + +编号为偶数的版本线每年十月进入长期支持,并在那时得到代号。根据[发布计划](https://github.com/nodejs/Release),Node.js 26 定于 2026-10-28 进入长期支持,目前还没有公布代号。 + +- 在 `latest_available` 下,新代号一出现在索引里,插件当天就能找到。这时 `lts` 和 `lts/*` 指的是新版本线的最新版本。 +- 在 `specified` 和 `latest_installed` 下由上面的表决定,而这张表只有随着插件发布新版本才会改变。在那之前,`lts/` 什么都选不中,`lts` 和 `lts/*` 仍然指表里最高的版本线,撰写本文时为 24。 +- `vfox search` 里的 `(LTS)` 跟随索引,所以会立刻变化。 + +这张表在 `hooks/parse_legacy_file.lua` 里,见 [development.md](development.md#nodejs-把一条版本线升为-lts-时)。 + +## 示例 + +假设有一个 `.nvmrc`(`.node-version` 同理),并且已安装 `22.23.3` 和 `24.21.0`。示例 1 到 4 都在 `latest_installed` 策略下运行,这个策略由示例 1 设置,各个策略见 [nvmrc.md](nvmrc.md#由策略决定): + +```sh +# 1. LTS 代号 -> 大版本,再由策略选出具体的版本。 +$ cat .nvmrc +lts/jod +$ vfox config legacyVersionFile.strategy latest_installed +$ cd && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version +v22.23.3 +``` + +```sh +# 2. 单独的 `lts`(或 `lts/*`)表示“映射表里最高的大版本”, +# 撰写本文时为 24。 +$ printf 'lts\n' > .nvmrc +$ cd && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version +v24.21.0 +``` + +```sh +# 3. 写全版本号时,完全不经过代号表。 +$ printf '22.23.3\n' > .nvmrc +$ cd && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version +v22.23.3 +``` + +```sh +# 4. 当前的 LTS 版本线也是同样的做法。 +$ printf 'lts/krypton\n' > .nvmrc +$ cd && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version +v24.21.0 +``` + +```sh +# 5. 在 latest_available 下会查询索引,所以连比上面的表更新的代号 +# 也能解析(离线时回退到这张表)。 +$ vfox config legacyVersionFile.strategy latest_available +$ printf 'lts/\n' > .nvmrc +$ cd && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version +v +$ vfox config legacyVersionFile.strategy specified +``` + +示例 1 是怎样一步步解析出来的(`latest_installed`): + +``` +.nvmrc "lts/jod" --resolve_version--> "22" --prefix-match installed--> "22.23.3" +``` diff --git a/docs/zh-CN/mirror.md b/docs/zh-CN/mirror.md new file mode 100644 index 0000000..641df8c --- /dev/null +++ b/docs/zh-CN/mirror.md @@ -0,0 +1,90 @@ +# 镜像 + +[中文](mirror.md) | [English](../mirror.md) + +Node.js 的包和版本索引(`index.json`、`SHASUMS256.txt`)默认从 `https://nodejs.org/dist` 下载。想用别的镜像,设置 `VFOX_NODEJS_MIRROR`: + +```bash +export VFOX_NODEJS_MIRROR="https://npmmirror.com/mirrors/node" +vfox install nodejs@26.11.1 +``` + +请把它指向镜像站上与 `nodejs.org/dist` 对应的根地址;插件会把自己用到的路径(`/index.json`、`/v/...`)接在这个基础 URL 后面。末尾的斜杠会被处理,所以 `https://nodejs.org/dist` 和 `https://nodejs.org/dist/` 都可以用。 + +## 怎样设置 + +这个变量是从运行 vfox 的进程的环境里读取的,所以要按你所用 Shell 设置环境变量的方式来设置它: + +```bash +# bash、zsh:写在 ~/.bashrc 或 ~/.zshrc 里 +export VFOX_NODEJS_MIRROR="https://npmmirror.com/mirrors/node" + +# fish +set -Ux VFOX_NODEJS_MIRROR https://npmmirror.com/mirrors/node + +# 只对一条命令生效 +VFOX_NODEJS_MIRROR=https://npmmirror.com/mirrors/node vfox search nodejs +``` + +```powershell +# PowerShell:只对当前会话有效 +$env:VFOX_NODEJS_MIRROR = "https://npmmirror.com/mirrors/node" + +# PowerShell:对当前用户生效,之后打开的 Shell 里都有 +[Environment]::SetEnvironmentVariable("VFOX_NODEJS_MIRROR", "https://npmmirror.com/mirrors/node", "User") +``` + +值为空等同于没有设置,使用的是 `https://nodejs.org/dist`。 + +## 镜像要提供什么 + +和 `nodejs.org/dist` 相同的目录结构: + +```text +/index.json +/v24.21.0/SHASUMS256.txt +/v24.21.0/node-v24.21.0-linux-x64.tar.gz +``` + +- `index.json` 决定了 `vfox search nodejs`、`nodejs@latest`、`nodejs@24` 和 `nodejs@24.21` 能看到哪些版本。更新滞后的镜像列出的版本更少;列出了却没有提供的版本,会在下载时失败。 +- 下载的文件会与镜像自己的 `SHASUMS256.txt` 里的校验和比对。文件与校验和对不上的镜像,最终会报 `checksum error`。 +- 会跟随重定向。`https://npmmirror.com/mirrors/node` 对 `index.json` 的请求回应的是重定向,也能正常使用。 + +## 可用的镜像 + +这些镜像在 2026-10-21 检查过:每个镜像提供的 `index.json` 都和 nodejs.org 一样列有 872 个版本,`node-v24.21.0-linux-x64.tar.gz` 的 SHA-256 校验和以及包本身都与 nodejs.org 一致,也都提供了 26.12.0 的 musl 包。这份列表只是记录检查结果,不为任何镜像背书。镜像随时可能变化或消失,插件无从得知。 + +| 镜像 | `VFOX_NODEJS_MIRROR` | +| --- | --- | +| npmmirror | `https://npmmirror.com/mirrors/node`(`https://cdn.npmmirror.com/binaries/node` 是同一份数据) | +| 华为云 | `https://mirrors.huaweicloud.com/nodejs` | +| 清华大学(TUNA) | `https://mirrors.tuna.tsinghua.edu.cn/nodejs-release` | +| 中国科学技术大学(USTC) | `https://mirrors.ustc.edu.cn/node` | +| 阿里云 | `https://mirrors.aliyun.com/nodejs-release` | +| 腾讯云 | `https://mirrors.cloud.tencent.com/nodejs-release` | +| 南京大学 | `https://mirror.nju.edu.cn/nodejs-release` | +| 北京外国语大学 | `https://mirrors.bfsu.edu.cn/nodejs-release` | + +Node.js 的镜像不是 npm 的 registry。`npm install` 获取的包来自 npm 配置里的 registry,例如 `npm config set registry https://registry.npmmirror.com`,见 [npm.md](npm.md)。 + +## 确认设置生效 + +没有响应的镜像会直接失败,不会回退到 nodejs.org,据此可以确认变量确实被读取了: + +```bash +VFOX_NODEJS_MIRROR=https://example.invalid/dist vfox search nodejs +# Failed to fetch Node.js versions: Get "https://example.invalid/dist/index.json": ... +``` + +在同一个镜像上,`vfox install nodejs@24.21.0` 会失败,报 `get checksum failed: Get "https://example.invalid/dist/v24.21.0/SHASUMS256.txt": ...`(未发布:0.5.2 会省略 `failed` 后面的内容)。镜像在那个地址上没有提供 `index.json` 时,搜索会失败,报 `Failed to fetch Node.js versions: HTTP 404`,见 [troubleshooting.md](troubleshooting.md#网络)。vfox 会把搜索结果缓存 12 小时,所以改动之后请执行 `vfox config cache.availableHookDuration 0`,否则在一段时间内看到的还是旧列表。 + +代理是另一项设置,对镜像同样有效,见 [troubleshooting.md](troubleshooting.md#网络)。 + +## musl 与非官方构建 + +`VFOX_NODEJS_MIRROR` 只管 `nodejs.org/dist`,**不**适用于 `https://unofficial-builds.nodejs.org/download/release`,插件在 musl Linux 上把它当作回退来源(见 [musl](musl.md)): + +- Linux musl x64,并且 nodejs.org 提供该版本的 musl 包(22.23.3、24.20.0 到 24.22.0、26.8.0 及之后):使用官方的 `*-musl` 压缩包,所以镜像照常生效(前提是镜像提供它)。 +- Linux musl arm64(所有版本),或者其他任何版本上的 musl x64:官方索引里没有这样的文件,所以插件直接从 `unofficial-builds.nodejs.org` 下载,忽略镜像。 + +连 `SHASUMS256.txt` 都提供不了的镜像,安装仍然会失败,不会回退;只有缺少 musl 文件名时,才会转到非官方构建。 diff --git a/docs/zh-CN/musl.md b/docs/zh-CN/musl.md new file mode 100644 index 0000000..5ca5978 --- /dev/null +++ b/docs/zh-CN/musl.md @@ -0,0 +1,96 @@ +# musl libc 支持(Alpine、OpenWrt/ImmortalWrt) + +[中文](musl.md) | [English](../musl.md) + +Node.js 官方的 Linux 压缩包链接的是 glibc(`ld-linux-*`)。musl 系统上没有 glibc 的加载器,所以下载官方原版的包后无法运行,会报 `not found` 或 `Error loading shared library ld-linux-aarch64.so.1`。见 [issue #29](https://github.com/version-fox/vfox-nodejs/issues/29)。 + +这个插件会检测 musl,改为安装 musl 压缩包。 + +## Alpine 上的最短流程 + +```sh +apk add bash libstdc++ libatomic +vfox add nodejs +vfox install nodejs@24 +vfox use --global nodejs@24 +``` + +`vfox use` 要求 vfox 已经挂载到 Shell,而 vfox 不支持 BusyBox 的 `ash`(`vfox activate sh` 会报 `unknown target shell sh`),所以要在 bash 里运行,并且先在那里执行 `eval "$(vfox activate bash)"`。这两个库的作用见下文。 + +## 依赖的系统库 + +Node.js 的 musl 包会链接一些系统库,插件不负责安装它们。原版 Alpine 镜像里一个都没有,常见的工具链也不一定会把它们装进来: + +| 库 | 需要它的包 | Alpine | OpenWrt、ImmortalWrt | +| --- | --- | --- | --- | +| `libstdc++` | 所有 musl 包 | `apk add libstdc++` | `libstdcpp` 包 | +| `libatomic` | 26.8.0 及以后的 musl 包 | `apk add libatomic` | `libatomic`,未验证 | + +安装时会检查这些库,避免安装看似成功、之后 `node --version` 却失败。刚解压完,`PostInstall` 钩子先在常见的库目录里查找 `libstdc++`,再运行一次 `node --version`:还缺别的库的话,动态加载器会指出是哪一个。报错信息会写出缺的是哪个包以及安装它的命令,这次安装不会被登记(只装过这一个版本的话,`vfox list nodejs` 会显示 `no available version`),装好库之后,再执行一次 `vfox install`。在无法运行命令的环境里,或者 `node` 因别的原因失败时,插件不会干预这次安装。 + +```text +plugin [PostInstall] method error: :: Node.js (musl build) requires the libstdc++ system package, which was not found on this system. Install it with `apk add libstdc++` and reinstall. See https://github.com/version-fox/vfox-nodejs/blob/main/README.md +failed to install nodejs +``` + +```text +plugin [PostInstall] method error: :: Node.js (musl build) requires the libatomic system package, which was not found on this system. Install it with `apk add libatomic` and reinstall. See https://github.com/version-fox/vfox-nodejs/blob/main/README.md +failed to install nodejs +``` + +在 OpenWrt 上,命令是 `opkg install libstdcpp`。既不是 Alpine 也不是 OpenWrt 的系统,得到的信息里不带命令。库里缺少某个符号时,加载器会报 `Error relocating ...: symbol not found`,安装最终则报 `Node.js (musl build) cannot run on this system: Error relocating ...`。 + +未发布:0.5.2 只查找 `libstdc++`,它的报错信息写的是 `It is normally installed automatically; install it and reinstall.`,没有给出命令。它不查找 `libatomic`:安装会成功,之后 `node` 运行失败。 + +```text +Error loading shared library libatomic.so.1: No such file or directory (needed by /root/.vfox/cache/nodejs/v-26.12.0/nodejs-26.12.0/bin/node) +Error relocating /root/.vfox/cache/nodejs/v-26.12.0/nodejs-26.12.0/bin/node: __atomic_compare_exchange: symbol not found +``` + +哪些版本需要 `libatomic`,是在 Alpine 3.24.1(x86_64)上、只装了 `libstdc++` 的情况下检查的:18.20.8、22.23.3、24.20.0、24.22.0、25.2.0、25.6.0、25.9.0、26.0.0 和 26.5.1 能运行,26.8.0、26.11.1 和 26.12.0 不能。后面这三个的共同点是都由 nodejs.org 提供:它的 26 的 musl 包从 26.8.0 开始,更早的 26.x 来自 Unofficial Builds,见下文。nodejs.org 提供的 22.23.3、24.20.0 和 24.22.0 的包,没有 `libatomic` 也能运行。已经装了 `gcc` 的系统自带 `libatomic`,所以 E2E 镜像(`go` 依赖 `gcc`)不会暴露这个问题。 + +`libatomic1` 是同一个库在 Debian 和 Ubuntu 上的包名,Node.js 25 及以后的 glibc 包会链接它,见 [platforms.md](platforms.md#linux)。 + +## 安装哪个压缩包 + +| 系统 | 压缩包 | 来源 | +| --- | --- | --- | +| Linux glibc,x64/arm64 | `node-v-linux-x64.tar.gz` 等 | `nodejs.org/dist` | +| Linux musl,x64 | `node-v-linux-x64-musl.tar.gz` | nodejs.org 有这个版本时用 `nodejs.org/dist`,否则用 `unofficial-builds.nodejs.org` | +| Linux musl,arm64 | `node-v-linux-arm64-musl.tar.gz` | `unofficial-builds.nodejs.org`(nodejs.org 从不提供) | + +说明: + +- nodejs.org 上没有的版本会自动回退到 `https://unofficial-builds.nodejs.org/download/release`,插件会给出提示:`Note: node-v25.9.0-linux-x64-musl.tar.gz is not listed in https://nodejs.org/dist/v25.9.0/, using unofficial musl build`。 +- musl 上镜像的行为(镜像何时生效,何时改用非官方构建),见[镜像](mirror.md)。 +- musl 构建只有 `x64` 和 `arm64`。其他 Linux 架构(比如 `x86` 或 `armv7l`)会报错,信息里指向 README。 +- 每次 musl 安装一开始都会打印一条提示,写明装的是哪个包,并指向 README:`Note: musl libc detected, installed node-v22.23.3-linux-x64-musl.tar.gz. See ...`。 + +## 哪些版本有 musl 构建 + +截至 2026-10-21,数据来自 nodejs.org 和 Unofficial Builds 各自的 `index.json` 与 `SHASUMS256.txt`: + +| | nodejs.org | Unofficial Builds | +| --- | --- | --- | +| x64 | 22.23.3、24.20.0 到 24.22.0,以及 26.8.0 及以后 | 8.16.0 及以后,但不是每个版本都有 | +| arm64 | 无 | 20.20.1 和 20.20.2,22.21.1 及以后的 22.x,24.9.0 及以后,25.2.0 及以后,以及 26 的所有版本 | + +- x64 上,12.0.0 起的每个版本都有,12.11.0、13.9.0、13.10.0 和 19.8.0 除外。8.x、10.x 和 11.x 只有一部分有构建,9.x 和 8.16.0 之前的版本都没有。 +- arm64 上,18、19、21 和 23 都没有,20.20.1 之前的版本线也没有。 +- 没有对应包的版本,安装最终会报 `Node.js package node-v9.11.2-linux-x64-musl.tar.gz is not listed in https://nodejs.org/dist/v9.11.2/SHASUMS256.txt`。换一个版本,或者见 [platforms.md](platforms.md)。 +- 两个站点的内容都会变。[development.md](development.md#更新平台表) 说明了怎样重新查一遍。 + +## 检测与覆盖 + +`lib/util.lua` 里的检测顺序: + +1. `$VFOX_NODEJS_LIBC`:`musl` 强制使用 musl,`gnu`/`glibc` 强制使用 glibc。适合非常规的用户空间环境和测试。 +2. vfox 核心将来如果提供 `RUNTIME.envType`,就用它(`musl`/`gnu`)。 +3. 能从 Lua 里读取的文件探测:`/etc/alpine-release`,以及 `/lib`、`/usr/lib`、`/lib64` 下的 `ld-musl-*.so.1`。两种链接器同时存在时(比如 `gcompat` 这类 shim),以 musl 为准。非 Linux 系统一律不算 musl。 + +```sh +# 强制选择 musl(仅用于调试) +VFOX_NODEJS_LIBC=musl vfox install nodejs@26.11.1 +``` + +反过来强制使用 glibc,装上的是无法在 musl 上运行的 glibc 包。在 Alpine 上,`VFOX_NODEJS_LIBC=gnu vfox install nodejs@20.20.2` 装的是 `node-v20.20.2-linux-x64.tar.gz`,运行其中的 `node` 会报 `not found`。 diff --git a/docs/zh-CN/npm.md b/docs/zh-CN/npm.md new file mode 100644 index 0000000..7204680 --- /dev/null +++ b/docs/zh-CN/npm.md @@ -0,0 +1,54 @@ +# npm 与全局包 + +[中文](npm.md) | [English](../npm.md) + +每个版本都自带 Node.js 随它一起发布的那个 npm。`vfox search nodejs` 会在每个版本后面列出它自带的 npm 版本;使用某个版本之后,`npm -v` 就会打印这个 npm 版本: + +```bash +vfox install nodejs@24.21.0 +vfox use --global nodejs@24.21.0 +npm -v # 11.19.0 +``` + +## 全局包属于某个版本 + +npm 把 `-g` 安装的包放在运行它的那个 Node.js 旁边。在 vfox 下,那就是已安装版本的目录,所以每个版本都有自己的全局包和自己的命令。版本正在使用时,这些命令位于 `PATH` 里的 `bin` 目录中: + +```text +$ vfox use --global nodejs@24.21.0 +$ npm install -g semver +$ npm root -g +/home/me/.vfox/cache/nodejs/v-24.21.0/nodejs-24.21.0/lib/node_modules +$ command -v semver +/home/me/.vfox/sdks/nodejs/bin/semver +$ vfox use --global nodejs@22.23.3 +$ command -v semver +$ # 没有输出:22.23.3 里没有 semver +``` + +- 切换到另一个版本后,需要的包要重新安装。vfox 不会把全局包从一个版本复制到下一个版本。 +- 这个目录是你自己的,所以 `npm install -g` 不需要 `sudo`,也不要对它使用 `sudo`。 +- `vfox uninstall nodejs@24.21.0` 会把这个版本连同它的全局包一起删除。 +- 设置有一部分是共享的。用户配置(`~/.npmrc`)和缓存(`~/.npm`)是所有版本共用的,npm 的 `registry` 设置应该放在这里。全局配置(`npm config get globalconfig`)是 `/etc/npmrc`,属于各自的版本。 + +## 更新 npm + +`npm install -g npm@latest` 会替换正在使用的那个版本里的 npm,并且只替换这一个。24.21.0 自带 npm 11.19.0,在它上面执行 `npm install -g npm@10` 之后,`npm -v` 是 10.9.9,而其他版本的 npm 都不变。`vfox search` 列出的仍然是该版本自带的 npm。 + +## Yarn、pnpm 与 Corepack + +- 24 及更早的 Node.js 版本自带 Corepack。`corepack enable` 会把 `yarn`、`yarnpkg`、`pnpm` 和 `pnpx` 加到该版本的 `bin` 目录里,所以它们只对这个版本存在。`corepack disable` 会把它们删掉。 +- Node.js 25 及以后的版本不自带 Corepack(在 25.9.0 和 26.12.0 上检查过,而 24.21.0、22.23.3 和 20.20.2 都有)。需要先用 `npm install -g corepack` 安装它,或者直接安装包管理器本身,例如 `npm install -g pnpm`。 +- pnpm 也可以安装 Node.js,而 pnpm 放到 vfox 前面的 `node` 会优先于 vfox 的。见 [pnpm.md](pnpm.md)。 + +## 文件在哪里 + +| 内容 | 位置 | +| --- | --- | +| 版本本身 | `~/.vfox/cache/nodejs/v-/nodejs-/` | +| `node`、`npm`、`npx` 和全局命令 | 其中的 `bin/`(Windows 上就是安装目录本身) | +| 全局包 | 其中的 `lib/node_modules/`(Windows 上是 `node_modules/`) | +| 全局正在使用的版本 | `~/.vfox/sdks/nodejs`,指向该版本的链接 | +| npm 缓存和用户配置 | `~/.npm` 和 `~/.npmrc` | + +想让编辑器或构建工具跟随你的全局选择,可以让它指向 `~/.vfox/sdks/nodejs/bin/node`。要固定某个版本,就用第一行里的路径。项目和会话作用域的情况见 [troubleshooting.md](troubleshooting.md#nodejs-在哪里)。 diff --git a/docs/zh-CN/nvmrc.md b/docs/zh-CN/nvmrc.md new file mode 100644 index 0000000..22b88c8 --- /dev/null +++ b/docs/zh-CN/nvmrc.md @@ -0,0 +1,112 @@ +# `.nvmrc` 和 `.node-version` + +[中文](nvmrc.md) | [English](../nvmrc.md) + +vfox 可以通过这个插件,读取 [nvm](https://github.com/nvm-sh/nvm) 的项目文件 `.nvmrc`,以及 nodenv、fnm 等工具使用的 `.node-version`(插件在 `PLUGIN.legacyFilenames` 里声明了这两个文件)。已经为其中某个工具固定了 Node.js 版本的项目,不需要再为 vfox 另写一个文件: + +```text +# .nvmrc +22.23.3 +``` + +## 使用方法 + +```bash +eval "$(vfox activate bash)" # 只需配置一次,写进 ~/.bashrc;其他 Shell 见 vfox 的快速入门 + +vfox install nodejs@22.23.3 # 这些文件不会安装任何东西 +cd my-project # 里面有上面那个 .nvmrc +node -v # v22.23.3 +``` + +vfox 挂载到 Shell 之后,进入目录时会读取这个文件,并选中其中指定的版本,不需要 `vfox use`。挂载由 vfox 自己完成,zsh、fish、PowerShell 等其他 Shell 的挂载方式见它的[快速入门](https://vfox.dev/zh-hans/guides/quick-start.html)。 + +- 只读取当前目录下的文件。进入项目的子目录就找不到它了,所以 `cd packages/app` 之后会回到全局版本。 +- 同一目录里两个文件都有时,`.node-version` 优先。 +- 同一目录里的 `.vfox.toml` 或 `.tool-versions`(asdf 的文件)只要写了 `nodejs`,就优先于这两个文件。`vfox use --project` 会写出 `.vfox.toml`。 +- 在这个目录里,这个文件还优先于不带 `--project` 的 `vfox use`:下一个提示符就会切回来,而 `vfox current nodejs` 仍然显示会话里选的那个。 +- vfox 会在这个目录里创建 `.vfox/sdks/nodejs`,并且不会把 `.vfox/` 加进 `.gitignore`,所以 `git status` 会列出它,需要你自己忽略。 +- 版本必须已经安装。没有任何已安装的版本匹配时,这个文件会被忽略,也不会有任何提示。`vfox list nodejs` 可以看到已有哪些。 +- `vfox config legacyVersionFile.enable false` 会关闭所有兼容版本文件,对每个插件都有效。默认值是 `true`。 + +## 由策略决定 + +文件里的文本能指明什么,取决于 vfox 的设置 `legacyVersionFile.strategy`: + +```bash +vfox config legacyVersionFile.strategy latest_installed +``` + +| 策略 | 文本会怎样处理 | +| --- | --- | +| `specified`(默认) | 文本直接当作版本使用。只有写全的版本号(`22.23.3`)才会选中版本,因为 vfox 查找的是名字与之完全一致的已安装版本。不联网 | +| `latest_installed` | 选中最新的以该文本开头的已安装版本。不联网,只有在没有任何已安装版本匹配时才例外:这时插件会像 `latest_available` 那样去查索引,查到的版本只有已经安装才算数 | +| `latest_available` | 索引里最新的以该文本开头的版本就是结果,只有这个版本已安装时才会选中。在带有兼容版本文件的目录里,每个提示符出现时插件都会下载 `index.json`(约 330 KB) | + +项目里的文件写的是 `22`、`lts/jod` 或 `lts/*` 时,选 `latest_installed`;写的是 `22.23.3` 时,保持 `specified`。`latest_available` 每次出现提示符都要联网,在带有兼容版本文件的目录里,提示符要等联网完成才会出现。 + +## 文件里可以写什么 + +插件在处理文本之前,会先去掉所有空白,去掉开头的 `v`,把其余部分转成小写,并把 `lts-jod` 改成 `lts/jod`。所以 `v22.23.3`、CRLF 换行的文件,以及版本前后的空白,都没有问题。`lts/jod` 这样的名字,会按 [lts.md](lts.md) 里的表变成它所在版本线的大版本(22);`latest_available` 除外,它会向索引查询带有这个代号的最新版本。版本号末尾可以用 `x`、`X` 或 `*` 代替不作限定的部分:`22.x`、`22.x.x` 和 `22.*` 都表示 22。 + +未发布:0.5.2 会去掉文件里所有的 `v`,所以带 `v` 的代号匹配不上,也不认识 `22.x`。 + +下面是 2026-10-21 在已安装 20.20.2、22.23.3、24.21.0、25.9.0 和 26.12.0 的情况下得到的结果,此时 24.22.0 是最新的 24.x,22.23.3 是最新的 22.x: + +| 文件里写 | `specified` | `latest_installed` | `latest_available` | +| --- | --- | --- | --- | +| `22.23.3`、`v22.23.3` | 22.23.3 | 22.23.3 | 22.23.3 | +| `22`、`v22`、`22.x`、`22.*` | 无 | 22.23.3 | 22.23.3 | +| `22.23` | 无 | 22.23.3 | 22.23.3 | +| `lts/jod`、`lts-jod`、`LTS/Jod` | 无 | 22.23.3 | 22.23.3 | +| `lts/krypton` | 无 | 24.21.0 | 无:最新的是 24.22.0,而它没有安装 | +| `lts/*`、`lts` | 无 | 24.21.0 | 无:最新的 LTS 版本是 24.22.0,而它没有安装 | +| `18.20.8` 或任何没有安装的版本 | 无 | 无 | 无 | + +`latest_available` 不会回退到较旧的已安装版本。索引里列出的版本比你已安装的更新时,什么都不会选中。想选中已安装的最新版本,请用 `latest_installed`。 + +`lts` 和 `lts/*` 指最新的 LTS 版本线。在 `specified` 和 `latest_installed` 下,指的是代号表里最高的大版本(撰写本文时为 24);在 `latest_available` 下,指的是索引里带有任意代号的最新版本。代号表,以及 Node.js 把一条新版本线升为 LTS 时会发生什么,见 [lts.md](lts.md)。 + +## 不认识的写法 + +插件认识的写法比 nvm 少。下面这些在任何策略下都什么也选不中: + +- 别名:`node`、`stable`、`latest`、`unstable`、`system`。 +- `lts/-1` 和 nvm 的其他相对写法,以及不带 `lts/` 的代号(`iron`、`jod`)。 +- 版本范围:`>=20`、`^22.0.0`、`~22.23`,以及 `x` 后面还有数字的写法(`22.x.1`)。 +- 注释。单独一行的 `# pin`,或者 `22.23.3 # pin`,会成为文本的一部分,因为读取文本之前已经去掉了所有空白。 +- 空文件。 + +## 前缀怎样匹配 + +`latest_installed` 和 `latest_available` 把文本与每个版本的开头逐段、按数字比较,取最新的以它开头的版本: + +- `22.2` 匹配 22.2.x,不匹配 22.23.3。 +- vfox 交给插件的已安装版本是按文本降序排列的,所以 `22.9.0` 排在 `22.23.3` 前面。插件不按这个顺序取:已安装 22.9.0、22.3.0 和 22.23.3 时,`22` 选中 22.23.3,`22.3` 选中 22.3.0。`vfox use nodejs@22` 同样按数字比较。 + +未发布:0.5.2 比较的是文本,取第一个以它开头的已安装版本。`22.2` 也会匹配 `22.23.3`;已安装 22.9.0 和 22.23.3 时,`latest_installed` 下的 `22` 会选中 22.9.0(这是在已安装 22.9.0、22.3.0 和 22.23.3 的情况下验证过的)。想在它上面得到上面的结果,请每条版本线只装一个版本,或者写全版本号。这段文本还会被当作 Lua 模式使用,所以 `22.*` 碰巧能用,`22.x` 则不行。 + +## 没有生效 + +版本没有切换时: + +- vfox 没有挂载到当前的 Shell。`eval "$(vfox activate bash)"` 要在当前 Shell 里执行过,只在别处执行过不算。 +- 当前目录是文件所在目录的子目录。 +- 版本没有安装,用 `vfox install nodejs@22.23.3` 安装。 +- 策略是 `specified`,而文件里写的是 `22` 或 `lts/jod`。 +- 在 `latest_available` 下,已经发布了比已安装版本更新的版本。 +- 被 `.vfox.toml` 或 `.tool-versions` 挡住了,或者 `legacyVersionFile.enable` 是 `false`。 +- 文本属于上面列出的不认识的写法,或者里面带有注释。 + +`vfox --debug env -s bash` 会打印插件返回了什么,以及 vfox 怎样处理它: + +```text +Parsing legacy file /home/me/project/.nvmrc +CallFunction: ParseLegacyFile +Found nodejs@22 +SDK nodejs@22 from project scope not installed, trying lower-priority config +``` + +## 与 nvm 的区别 + +`nvm install` 会安装 `.nvmrc` 里指明的版本,不带参数的 `nvm use` 会读取它。vfox 只在已安装的版本里选择,安装要用 `vfox install nodejs@`。`.nvmrc` 里的一行不一定是 `vfox install` 接受的写法:`22.23.3` 可以,`22` 可以,`lts/jod` 不行(见 [lts.md](lts.md#安装一条-lts-版本线))。 diff --git a/docs/zh-CN/platforms.md b/docs/zh-CN/platforms.md new file mode 100644 index 0000000..8f09d44 --- /dev/null +++ b/docs/zh-CN/platforms.md @@ -0,0 +1,93 @@ +# 平台支持 + +[中文](platforms.md) | [English](../platforms.md) + +插件会按操作系统和 vfox 二进制所属的 CPU 架构(`RUNTIME.osType` 和 `RUNTIME.archType`,即 Go 的 `GOOS` 和 `GOARCH`)选择包,在 Linux 上还要按 C 库选择。它请求的文件名是这样的: + +```text +node-v-- +``` + +`` 是 `linux`、`darwin` 或 `win`。`` 是 `x64`、`x86`、`armv7l`、`arm64`、`ppc64le` 或 `s390x`:`amd64` 变成 `x64`,`386` 变成 `x86`,`arm` 变成 `armv7l`,其余的名字按 vfox 报告的原样传递。`` 在 Windows 上是 `.zip`,其他系统上是 `.tar.gz`。musl Linux 上,架构后面还有 `-musl`,见 [musl.md](musl.md)。 + +插件会读取该版本目录下的 `SHASUMS256.txt`。这个文件里没有列出该包时,安装会在下载任何内容之前停止: + +```text +Node.js package node-v19.8.1-win-arm64.zip is not listed in https://nodejs.org/dist/v19.8.1/SHASUMS256.txt +``` + +## 各平台有哪些版本 + +这张表取自 2026-10-21 的 nodejs.org,数据来自 `index.json` 里 872 个版本各自的 `SHASUMS256.txt`。“版本”一列写的是:按插件使用的文件名,有包的第一个版本和最后一个版本;中间的空缺,是 Node.js 没有发布这个包的版本。重新获取这张表的方法见 [development.md](development.md#更新平台表)。 + +| vfox 报告的平台 | 包 | 版本 | +| --- | --- | --- | +| Linux amd64 | `linux-x64.tar.gz` | 0.8.6 及以后,0.9.0、0.11.4 和 0.11.6 除外 | +| Linux arm64 | `linux-arm64.tar.gz` | 4.0.0 及以后 | +| Linux ppc64le | `linux-ppc64le.tar.gz` | 4.3.2 及以后,5.0.0 到 5.6.0 除外 | +| Linux s390x | `linux-s390x.tar.gz` | 6.6.0 及以后 | +| Linux 386 | `linux-x86.tar.gz` | 0.8.6 到 9.11.2,0.9.0、0.11.4 和 0.11.6 除外 | +| Linux arm | `linux-armv7l.tar.gz` | 4.0.0 到 23.11.1,8.1.1 除外。未发布:0.5.2 请求的是 `linux-arm`,Node.js 从未发布过,所以每次安装都会失败 | +| Linux amd64,musl | `linux-x64-musl.tar.gz` | 见 [musl.md](musl.md#哪些版本有-musl-构建) | +| Linux arm64,musl | `linux-arm64-musl.tar.gz` | 见 [musl.md](musl.md#哪些版本有-musl-构建) | +| macOS amd64 | `darwin-x64.tar.gz` | 0.8.6 及以后,0.9.0、0.11.4 和 0.11.6 除外 | +| macOS arm64 | `darwin-arm64.tar.gz` | 16.0.0 及以后。更早的版本装的是 amd64 的包,见 [macOS](#macos) | +| Windows amd64 | `win-x64.zip` | 4.5.0 及以后,5.0.0 到 5.12.0 和 6.0.0 到 6.2.0 除外 | +| Windows 386 | `win-x86.zip` | 4.5.0 到 22.23.3,5.0.0 到 5.12.0、6.0.0 到 6.2.0 和 18.0.0 到 18.2.0 除外 | +| Windows arm64 | `win-arm64.zip` | 19.9.0 及以后 | + +从 19.9.0 起,表里没有上限的每一行,每个版本都有包。32 位的几行结束得早:Windows 上到 22.23.3 为止,Linux arm 上到 23.11.1 为止,Linux x86 上到 9.11.2 为止,所以 32 位的 vfox 装不了较新的版本。 + +不论在哪个平台,`vfox search nodejs` 都会列出全部 872 个版本,因为索引里没有写明一个版本有哪些平台的包。所以在 Windows arm64 上,`vfox install nodejs@19.8.1` 一开始一切正常,最后会报上面那条信息,见 [troubleshooting.md](troubleshooting.md#插件给出的提示)。 + +## 不支持的情况 + +- **ARMv6,以及 24 起的 32 位 ARM。** vfox 提供一个 `armv7` 的二进制,它报告的是 `arm`,插件为它选 `linux-armv7l` 包,而这个包 Node.js 只发布到 23.11.1。24 起就没有了,安装最终会报 `Node.js package node-v26.12.0-linux-armv7l.tar.gz is not listed in ...`。ARMv6 的包(nodejs.org 上的 `linux-armv6l`,4.0.0 到 11.15.0)从不会被选中。未发布:0.5.2 请求的是 `linux-arm`,Node.js 从未发布过,所以 32 位 ARM 上每次安装都会失败,报 `Node.js package node-v20.19.5-linux-arm.tar.gz is not listed in ...`。 +- **Linux riscv64 和 loong64。** nodejs.org 没有它们的包。[Unofficial Builds](https://github.com/nodejs/unofficial-builds) 站点有 riscv64(17.7.1 到 26.0.0)和 loong64(18.18.0 及以后),但插件只在 musl 上使用那个站点。 + +## Linux + +插件使用 glibc 的包;检测到 musl 时用 musl 的包,见 [musl.md](musl.md#检测与覆盖)。某个版本需要什么样的 glibc 和内核,是 Node.js 自己的事,见[Node.js 支持的平台](https://github.com/nodejs/node/blob/main/BUILDING.md)。系统库由用户自己负责,插件一个也不会安装。需要留意的有两个: + +- Node.js 25 及以后的 glibc 包会链接 `libatomic.so.1`,而原版的 Debian 和 Ubuntu 镜像里没有它。这时 `node` 会失败,报 `error while loading shared libraries: libatomic.so.1`。用 `sudo apt install --assume-yes libatomic1` 安装它。24 及更早的版本不链接它(检查过 25.9.0 和 26.12.0,对照的是 24.21.0 和 20.20.2),见 [nodejs/node#60790](https://github.com/nodejs/node/issues/60790)。 +- musl 包需要 `libstdc++`,见 [musl.md](musl.md#依赖的系统库)。 + +在 WSL 里,vfox 是 Linux 二进制,所以使用 Linux 的包。 + +## macOS + +包是 `darwin-x64.tar.gz` 和 `darwin-arm64.tar.gz`。Node.js 在 16.0.0 之前没有发布 arm64 的包,所以在 Apple silicon 上,这些版本会改用 amd64 的包,插件会给出提示: + +```text +Note: Node.js versions below 16 do not support Apple Silicon (arm64), using x64 architecture with Rosetta 2 +``` + +要运行这些版本,必须装有 Rosetta 2(`softwareupdate --install-rosetta`)。16.0.0 及以后的版本以原生方式安装。 + +E2E 套件里没有 macOS 作业,见[下文](#ci-覆盖什么)。钩子测试检查了 `darwin/arm64` 的包名。 + +## Windows + +包是 zip 格式。`node.exe`、`npm.cmd` 和 `npx.cmd` 直接放在安装目录里,所以插件把这个目录本身加进 `PATH`。不像 Linux 和 macOS 那样有 `bin` 目录。 + +- 用户主目录里带空格(`C:\Users\John Doe`)时,需要 vfox 1.0.13 及以后的版本([vfox#712](https://github.com/version-fox/vfox/pull/712))。E2E 套件对每种组合都用这样的主目录跑一遍。 +- 在 Git Bash 里,vfox 挂载到 Shell 之后,`node`、`npm.cmd` 和不带扩展名的 `npm` 都能运行([vfox#293](https://github.com/version-fox/vfox/issues/293))。 +- `VFOX_NODEJS_MIRROR` 从运行 vfox 的进程的环境里读取。在 PowerShell 里用 `$env:VFOX_NODEJS_MIRROR = '...'` 为当前会话设置,见 [mirror.md](mirror.md#怎样设置)。 + +## vfox 自己支持的平台 + +vfox 1.0.13 提供这些平台的二进制:Linux 的 x86_64、aarch64、armv7、i386 和 loong64,macOS 的 x86_64 和 aarch64,Windows 的 x86_64、aarch64 和 i386。插件直接采用二进制报告的名字,所以你自己为 Linux ppc64le 或 s390x 编译的 vfox,装的是 ppc64le 或 s390x 的包。 + +## CI 覆盖什么 + +容器 E2E 套件(见 [e2e.md](e2e.md))会在下面这些平台上安装一个固定的版本、一个固定的 LTS 版本和 `nodejs@latest`: + +| 平台 | Runner | +| --- | --- | +| Linux glibc,amd64 | `ubuntu-latest` | +| Linux glibc,arm64 | `ubuntu-26.04-arm` | +| Linux musl(Alpine),amd64 和 arm64 | 同样两个 runner | +| Windows 容器,amd64 和 arm64 | `windows-latest`、`windows-11-arm` | +| WSL 上的 Ubuntu 24.04 | `windows-2025` | + +macOS、32 位系统、ppc64le 和 s390x 没有覆盖。钩子测试(`tests/hooks_test.lua`)离线运行,检查以下平台的包名:`windows` 的 amd64、386 和 arm64,`linux` 的 amd64 和 arm,musl 的 amd64 和 arm64,以及 `darwin` 的 arm64。 diff --git a/docs/zh-CN/pnpm.md b/docs/zh-CN/pnpm.md new file mode 100644 index 0000000..69920f3 --- /dev/null +++ b/docs/zh-CN/pnpm.md @@ -0,0 +1,40 @@ +# pnpm + +[中文](pnpm.md) | [English](../pnpm.md) + +pnpm 作为包管理器使用(比如通过 `npm install -g pnpm` 安装)时,可以和这个插件并存。冲突出在 pnpm 也去安装 Node.js 的时候:`pnpm runtime set node -g`(或者已弃用的 `pnpm env use --global `)会在 `$PNPM_HOME/bin` 里放进第二个 `node`。真正运行的只有 `PATH` 里排在最前面的那个 `node`,而 pnpm 写入的 Shell 配置恰好让它排在最前,所以不管 `vfox use` 选了什么,运行的都是 pnpm 的。 + +以下现象是在 Ubuntu 的 bash 里,用 pnpm 12.14.0 和 vfox 1.0.12 观察到的。 + +## 现象 + +- `vfox use nodejs@24.21.0` 打印 `Now using nodejs@24.21.0`,`vfox current nodejs` 也与之一致,但 `node --version` 打印的却是 pnpm 管理的版本。全局、会话和项目作用域都受影响。 +- `type -a node` 把 `$PNPM_HOME/bin/node` 列在 vfox 的路径之前。 +- 在带有 `.nvmrc` 或 `.node-version` 的目录里,`node` 运行的是 pnpm 首次使用时下载的那份副本,即使 vfox 已经装了这个版本。 +- `npm install -g ` 显示成功,但找不到装好的命令。pnpm 的运行时不带 npm,所以 vfox 的 `npm` 跑在 pnpm 的 `node` 上,装进了 `$PNPM_HOME` 下 pnpm 那份 Node.js 里。 + +## 原因 + +1. 在 `$PNPM_HOME/bin` 进入 `PATH` 之前,`pnpm runtime set node -g` 拒绝执行(`ERR_PNPM_GLOBAL_BIN_DIR_NOT_IN_PATH`)。它建议的修复办法 `pnpm setup`,会在 Shell 的 rc 文件末尾,也就是 `eval "$(vfox activate bash)"` 的下面,加一段把 `$PNPM_HOME/bin` 放到 `PATH` 最前面的配置。 +2. 每次出现提示符时,`vfox env` 都会把 `PATH` 里排在它自己的第一个路径之前的条目,保留在所有 SDK 路径的前面,这样已经激活的 Python 虚拟环境才能保持优先。`$PNPM_HOME/bin` 正是这样一个条目。 +3. 那个 `node` 是 pnpm 的 shim:它运行 pnpm 的全局 Node.js,或者当前项目通过 `devEngines.runtime`、`engines.runtime`、`.node-version` 或 `.nvmrc` 固定的版本(见[项目感知的全局 bin](https://pnpm.io/global-packages#project-aware-global-bins))。pnpm 的 `globalShims` 设置和 `PNPM_SHIM_BYPASS=1` 只能关掉按项目查找;`node` 运行的仍然是 pnpm 的全局 Node.js,而不是 vfox 的。 + +不管是哪个工具放的,只要有 `node` 排在 vfox 前面,`vfox use nodejs@` 就会打印一条提示: + +```text +Note: /home/user/.local/share/pnpm/bin/node comes before vfox in PATH, so `node` will not run nodejs@24.21.0. See https://github.com/version-fox/vfox-nodejs/blob/main/docs/pnpm.md +``` + +## 让 vfox 继续管理 `node` + +卸掉 pnpm 全局安装的 Node.js。pnpm 照常可用,因为 `pnpm setup` 已经把它复制到了 `$PNPM_HOME`: + +```sh +pnpm remove -g node +``` + +把 vfox 的挂载语句挪到 rc 文件里 pnpm 那段配置的下面是不够的:除非 `$PNPM_HOME/bin` 已经排在最前,pnpm 的配置每次都会把它再放回最前面,所以嵌套的 Shell 里 pnpm 的 `node` 又会回到前面。 + +用 `devEngines.runtime` 固定了运行时的项目,脚本里仍然用固定的那个:`pnpm install` 之后,`pnpm run` 和 `pnpm exec` 使用 `node_modules/.bin` 里固定的 Node.js,而直接敲的 `node` 跟随 vfox。 + +想让 pnpm 来管理 Node.js 的话,就不要在同一个 Shell 里用 vfox 选 `nodejs`。 diff --git a/docs/zh-CN/source-build.md b/docs/zh-CN/source-build.md new file mode 100644 index 0000000..d7f0bfe --- /dev/null +++ b/docs/zh-CN/source-build.md @@ -0,0 +1,98 @@ +# 从源码安装插件 + +[中文](source-build.md) | [English](../source-build.md) + +通常从插件注册表安装: + +```sh +vfox add nodejs +``` + +需要尚未发布的内容(`main` 分支,或者检出的某个 PR)时,就从源码安装:把检出的代码打包成 zip,交给 `vfox add --source`。zip 的顶层必须有 `metadata.lua`、`hooks/` 和 `lib/`,这是插件包的约定(见[共享工作流文档](https://github.com/version-fox/plugin-manifest-action))。 + +已发布的插件 zip 的 URL 也可以用作 `--source`,例如 `vfox add --source https://github.com/version-fox/vfox-nodejs/releases/download/v0.0.5/vfox-nodejs_0.0.5.zip`。 + +所有系统都需要事先装好 [vfox](https://github.com/version-fox/vfox) 本身和 `git`。 + +## Windows 11 + +在 PowerShell 7 里执行(`tar` 是系统自带的,不需要另装 zip 工具): + +```powershell +git clone https://github.com/version-fox/vfox-nodejs.git +cd vfox-nodejs +# 如果是 PR:git fetch origin pull//head:pr; git checkout pr +tar -a -c -f $env:TEMP\nodejs.zip metadata.lua hooks lib +vfox add nodejs --source "$env:TEMP\nodejs.zip" +vfox install nodejs@26.11.1 +``` + +## Ubuntu 26.04 + +```bash +sudo apt install --assume-yes git zip +git clone https://github.com/version-fox/vfox-nodejs.git +cd vfox-nodejs +# 如果是 PR:git fetch origin pull//head:pr && git checkout pr +zip -qr /tmp/nodejs.zip metadata.lua hooks lib +vfox add nodejs --source /tmp/nodejs.zip +vfox install nodejs@26.11.1 +``` + +## macOS + +命令与 Ubuntu 上相同,去掉 `apt` 那一行:`zip` 是系统自带的,`git` 随 Xcode 命令行工具提供。 + +## Alpine 3.24.2 + +```sh +apk add git zip +git clone https://github.com/version-fox/vfox-nodejs.git +cd vfox-nodejs +# 如果是 PR:git fetch origin pull//head:pr && git checkout pr +zip -qr /tmp/nodejs.zip metadata.lua hooks lib +vfox add nodejs --source /tmp/nodejs.zip +vfox install nodejs@26.11.1 +``` + +在 Alpine 上,安装的 Node.js 需要先装好系统库,见 [musl.md](musl.md#依赖的系统库)。 + +## 验证 + +```bash +vfox info nodejs +vfox install nodejs@24 +vfox use --global nodejs@24 +node -v +``` + +`vfox info nodejs` 打印的是已添加的插件的版本,也就是 `metadata.lua` 里的 `PLUGIN.version`。`main` 的源码构建带的是上一个发布版的版本号,所以靠它看不出你用的是哪个提交。 + +## 已经添加过 nodejs 时 + +`vfox add` 会拒绝,报 `plugin named nodejs already exists`。可以替换原有的插件,也可以让源码构建的版本与它并存。 + +替换要先执行 `vfox remove nodejs`,它会连同通过这个插件装的 Node.js 版本一起删掉:vfox 在询问之前会先说 `Removing this plugin will remove the installed sdk along with the plugin.`(`-y` 可以跳过询问)。之后需要重新安装这些版本。 + +想两个都保留,就给新的另起一个名字: + +```bash +vfox add nodejs --source /tmp/nodejs.zip --alias nodejs-dev +vfox install nodejs-dev@24 +vfox remove -y nodejs-dev # 只删除 nodejs-dev 和它装的东西 +``` + +别名有它自己的已安装版本列表,放在 `~/.vfox/cache/nodejs-dev/` 下,但两个插件都把选中的版本链接成 `~/.vfox/sdks/nodejs`。每个作用域里只从其中一个选版本:全局作用域下,先执行 `vfox unuse --global nodejs`,再执行 `vfox use --global nodejs-dev@...`,反过来也一样。两个都选了的话,这个链接会指向其中一个,`node` 跟着它走,指向哪一个说不准。 + +## 更新 + +拉取代码,重新打包 zip,再添加一次。以原名添加的插件必须先删除,而这会连已安装的版本一起删掉,所以经常更新时用别名更省事: + +```bash +vfox remove -y nodejs-dev +vfox add nodejs --source /tmp/nodejs.zip --alias nodejs-dev +``` + +`vfox update nodejs` 会拿 `metadata.lua` 里的版本号和注册表里的比较,两者相同时只会打印 `nodejs is already the latest version`,所以在发布出比它更新的版本之前,不会动源码构建的版本。 + +开发插件,见 [development.md](development.md)。 diff --git a/docs/zh-CN/troubleshooting.md b/docs/zh-CN/troubleshooting.md new file mode 100644 index 0000000..84a0ecc --- /dev/null +++ b/docs/zh-CN/troubleshooting.md @@ -0,0 +1,142 @@ +# 问题排查 + +[中文](troubleshooting.md) | [English](../troubleshooting.md) + +下面的提示是用 vfox 1.0.12 和 1.0.13 得到的。插件自己的错误,vfox 会在前面加上 `plugin [PreInstall] method error: ::`(搜索时是 `[Available]`),后面跟一段 Lua 堆栈跟踪,本页省略了这一段。 + +## 插件给出的提示 + +### `version not found for provided version lts` + +`nodejs@` 后面既不是版本号,也不是某个版本号的开头。插件认识 `latest`、`24.21.0`、`24.21` 和 `24`,见 [versions.md](versions.md)。`lts`、`lts/iron`、`stable`、`24.21.0.1` 和 `24.0.0-rc.1` 都会报这个错,`99` 和 `22.99` 也一样:它们的格式没有问题,但没有任何版本以它们开头。要安装某条 LTS 版本线,请按编号安装,见 [lts.md](lts.md#安装一条-lts-版本线)。 + +### `get checksum failed: HTTP 404 from https://nodejs.org/dist/v24.99.0/SHASUMS256.txt` + +插件读不到这个版本的 `SHASUMS256.txt`,原因写在冒号后面。`HTTP 404 from
` 表示服务器上没有这个文件:要么这个版本不存在(`24.99.0`),要么镜像里没有它。网络错误(`Get "
": tls: failed to verify certificate: x509: certificate signed by unknown authority`、`... connection refused`、`... EOF`)表示连不上服务器:镜像不可用、代理有问题,或者 TLS 出了问题。见[网络](#网络)。 + +未发布:0.5.2 只打印 `get checksum failed`,没有别的内容,所以看不出原因。`vfox search nodejs` 向同一台服务器请求索引,会打印真正的错误。 + +### `Node.js package node-v19.8.1-win-arm64.zip is not listed in https://nodejs.org/dist/v19.8.1/SHASUMS256.txt` + +这个版本是存在的,但 Node.js 没有以插件请求的名字为你的平台发布包。`vfox search nodejs` 列出的版本里,有的并没有你所在平台的包,因为索引里没有写明哪些版本有。提示里的文件名就是插件要找的那个。请对照 [platforms.md](platforms.md),它也列出了不支持的情况(ARMv6、24 起的 32 位 ARM、riscv64、loong64)。 + +在 Alpine 等 musl 系统上,文件名以 `-musl.tar.gz` 结尾,插件还去 unofficial-builds.nodejs.org 找过。见 [musl.md](musl.md#哪些版本有-musl-构建)。 + +### `Failed to fetch Node.js versions: ...` + +获取 `index.json` 的请求失败了。冒号后面是网络错误(`Get "https://nodejs.org/dist/index.json": ...`),或者 HTTP 状态码(`HTTP 404`,镜像在该地址没有 `index.json`)。见[网络](#网络)。0.5.0 之前,请求失败只会得到一个空列表,vfox 还会把它缓存起来。 + +### `Cannot determine Node.js package architecture from RUNTIME.archType` + +vfox 没有告诉插件当前运行在哪种 CPU 上。这是不应该出现的情况,请连同 `vfox --version` 的输出一起报告。 + +### `Node.js musl builds are only available for x64 and arm64, got x86. See ...` + +插件把这个系统当成了 musl 系统,而 Node.js 没有适用于这种 CPU 的 musl 包(会停在这里的是 `x86` 和 `armv7l`)。如果系统其实不是 musl,请设置 `VFOX_NODEJS_LIBC=glibc`,见 [musl.md](musl.md#检测与覆盖)。 + +### `Node.js (musl build) requires the libatomic system package, which was not found on this system. ...` + +这个 musl 系统缺少 Node.js 的 musl 包所链接的库:所有 musl 包都链接 `libstdc++`,26.8.0 及以后的 musl 包还链接 `libatomic`。提示里写明了缺的是哪个包,以及在 Alpine(`apk add libatomic`)和 OpenWrt(`opkg install libatomic`)上安装它的命令。安装在解压之后失败,不会登记任何内容。请先安装这个库,再重新执行安装。见 [musl.md](musl.md#依赖的系统库)。 + +未发布:0.5.2 只查找 `libstdc++`,提示里也没有命令。缺少 `libatomic` 要到后面才会暴露,表现为 `node` 自己报的错,见下文。 + +### `Node.js (musl build) cannot run on this system: Error relocating ...` + +解压出来的 `node` 需要一个符号,而系统里没有任何库提供它。安装在解压之后失败,不会登记任何内容。未发布:0.5.2 在解压之后不会运行 `node`,这个错误要到你自己运行它的时候才会出现。见 [musl.md](musl.md#依赖的系统库)。 + +### `checksum error` + +vfox 会打印 `Verifying checksum ...`,拿下载的文件和这个校验和比对,不一致就停下来,并指出它丢弃的文件:`Checksum error, file: ~/.vfox/cache/nodejs/node-v24.21.0-linux-x64.tar.gz`。常见的原因是下载被截断,或者镜像上的文件与它自己的 `SHASUMS256.txt` 对不上。不会安装任何东西。请重试,或者换一个[镜像](mirror.md)。 + +### `nodejs@24.99 is not installed` + +`vfox use` 和 `vfox uninstall` 只认已安装的版本,`vfox list nodejs` 可以看到它们。对已安装的版本,`vfox use` 接受 `24`、`24.21` 和 `24.21.0`,`vfox uninstall` 需要写完整的版本号。见 [versions.md](versions.md#使用)。 + +### `vfox requires hook support. Please ensure vfox is properly initialized with 'vfox activate'` + +`vfox use` 要修改当前 Shell 的环境,只有把 vfox 挂载到 Shell 之后才能做到。请把 `eval "$(vfox activate bash)"`(或者你所用 Shell 对应的那一行)加到该 Shell 的启动文件里,见 vfox 的[快速入门](https://vfox.dev/zh-hans/guides/quick-start.html)。vfox 不支持 Alpine 自带的 `sh`,请在那里安装 `bash`。 + +### `plugin named nodejs already exists` + +`vfox add nodejs` 发现已经添加过这个插件了。如果添加的是源码构建的插件,见 [source-build.md](source-build.md#已经添加过-nodejs-时)。 + +### `Plugin nodejs is not installed. Use the -y flag to automatically install plugins in non-interactive environments` + +插件没有添加,或者 `VFOX_HOME` 指向的目录不是添加它的那个目录。请运行 `vfox add nodejs`。 + +### 插件打印的 Note + +这些都不是错误。 + +| Note | 含义 | +| --- | --- | +| `Note: Node.js versions below 16 do not support Apple Silicon (arm64), using x64 architecture with Rosetta 2` | 在 macOS arm64 上安装早于 16.0.0 的版本,见 [platforms.md](platforms.md#macos) | +| `Note: musl libc detected, installed node-v22.23.3-linux-x64-musl.tar.gz. See ...` | 插件安装的是 musl 包,见 [musl.md](musl.md) | +| `Note: node-v18.20.8-linux-x64-musl.tar.gz is not listed in https://nodejs.org/dist/v18.20.8/, using unofficial musl build` | nodejs.org 没有这个 musl 包,插件改从 unofficial-builds.nodejs.org 获取 | +| ``Note: /home/me/.local/share/pnpm/bin/node comes before vfox in PATH, so `node` will not run nodejs@24.21.0. See ...`` | 另一个 `node` 遮蔽了 vfox 的,见 [pnpm.md](pnpm.md) | + +### Node.js 自己报的错 + +这些是安装成功之后 `node` 报的错。 + +- `node: error while loading shared libraries: libatomic.so.1: cannot open shared object file`。Node.js 25 及以后的版本要链接 `libatomic`,而原版 Debian 和 Ubuntu 没有它:`sudo apt install --assume-yes libatomic1`。见 [platforms.md](platforms.md#linux)。 +- 在 Alpine 上报 `node: not found`,尽管文件是存在的。这是把 glibc 的包安装到了 musl 系统上:要么插件没有检测到 musl,要么 `VFOX_NODEJS_LIBC` 的值是 `gnu` 或 `glibc`。见 [musl.md](musl.md#检测与覆盖)。 +- 在 Alpine 上先报 `Error loading shared library libatomic.so.1: No such file or directory`,接着报 `Error relocating ...: __atomic_compare_exchange: symbol not found`。26.8.0 及以后的 musl 包需要 `libatomic`:`apk add libatomic`。未发布:安装本身会以上面那条提示停下来,0.5.2 则要到这里才会报错。见 [musl.md](musl.md#依赖的系统库)。 +- 在 musl 系统上报 `Error relocating ... symbol not found`,而且前面没有上一条里关于 `libatomic` 的那一行。说明缺少某个库,或者库的版本太旧。未发布:安装会以 `cannot run on this system` 停下来,0.5.2 则要到这里才会报错。见 [musl.md](musl.md#依赖的系统库)。 + +## 网络 + +插件向服务器请求两样东西,vfox 再下载第三样: + +| 内容 | 地址 | +| --- | --- | +| 版本列表(`vfox search`、`latest`、`nodejs@24`) | `/index.json` | +| 某个版本的校验和(`vfox install`) | `/v/SHASUMS256.txt` | +| 包(由 vfox 下载) | `/v/` | + +`` 是 `https://nodejs.org/dist`,除非 `VFOX_NODEJS_MIRROR` 另有设置,见 [mirror.md](mirror.md)。musl 系统还可能用到 `https://unofficial-builds.nodejs.org/download/release`。 + +vfox 自己的代理设置对插件发出的请求和下载都有效: + +```bash +vfox config proxy.enable true +vfox config proxy.url http://127.0.0.1:7890 +``` + +`proxy.enable` 为 `false` 时,通常的 `HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY` 环境变量依然有效。代理不可用时,搜索会显示 `proxyconnect tcp: dial tcp 127.0.0.1:7890: connect: connection refused`。 + +`vfox search nodejs` 的常见失败原因如下。`vfox install` 会在 `get checksum failed:` 之后报告同样的原因。未发布:0.5.2 会省略它,只有搜索会打印: + +| `Failed to fetch Node.js versions:` 之后 | 原因 | +| --- | --- | +| `Get "https://nodejs.org/dist/index.json": tls: failed to verify certificate: x509: certificate signed by unknown authority` | 系统里没有 CA 证书,比如最基础的 Debian 或 Ubuntu Docker 镜像:`apt-get install ca-certificates` | +| `Get "...": ... connection refused`、`i/o timeout` 或 `no such host` | 连不上服务器:没有路由、代理不可用、镜像地址写错,或者 DNS 有问题。可以试试[镜像](mirror.md)或者代理设置 | +| `HTTP 404` | 镜像在该地址没有 `index.json`。请把 `VFOX_NODEJS_MIRROR` 指向存放它的目录 | + +## 看起来不对,其实没问题 + +- **`vfox search` 里没有你预期的版本。** vfox 会缓存搜索结果 12 小时。`vfox config cache.availableHookDuration 0` 可以关闭缓存。镜像也可能落后于 nodejs.org。 +- **`vfox search nodejs` 列出了一个装不上的版本。** 这个列表就是所有平台共用的索引,见 [platforms.md](platforms.md)。 +- **`vfox search nodejs 24` 不会过滤。** 参数会被忽略,见 [versions.md](versions.md#搜索)。 +- **`vfox install nodejs@24` 什么也没有下载。** 最新的 24.x 已经装好了:`nodejs@24.22.0 is already installed`。 +- **`vfox use nodejs@24.21.0` 显示了 `Now using`,而 `node -v` 打印的是另一个版本。** 在带有 `.nvmrc` 或 `.node-version` 的目录里,下一次出现提示符时以文件为准,见 [nvmrc.md](nvmrc.md)。`PATH` 里排在 vfox 前面的另一个 `node` 也会胜出,`vfox use` 会提示这一点,见 [pnpm.md](pnpm.md)。 +- **`vfox current nodejs` 和 `node -v` 的结果不一致。** 原因同上。前者给出的是会话或作用域里的选择,后者是 `PATH` 实际找到的。 +- **`vfox use` 之后,全局安装的 npm 包不见了。** 全局包属于某个版本,见 [npm.md](npm.md#全局包属于某个版本)。 +- **找不到 `corepack`。** Node.js 25 及以后的版本不自带它,见 [npm.md](npm.md#yarnpnpm-与-corepack)。 +- **`.nvmrc` 什么也没选中。** 请检查策略和文件里的内容,见 [nvmrc.md](nvmrc.md#没有生效)。 +- **`git status` 列出了 `.vfox/`。** vfox 会把这个版本以 `.vfox/sdks/nodejs` 的形式链接到项目里。请在 `.gitignore` 里忽略 `.vfox/`,并保留 `.vfox.toml`。 +- **安装后的版本名里没有 `v`。** 版本的名字形如 `24.21.0`,你输入时带的 `v` 会被去掉。 + +## Node.js 在哪里 + +| 作用域 | `node` 来自哪里 | +| --- | --- | +| `--global` | `~/.vfox/sdks/nodejs`,指向该版本的链接。`~/.vfox/sdks/nodejs/bin` 在 `PATH` 里 | +| `--project` | 项目里的 `.vfox/sdks/nodejs`,指向该版本的链接,版本名记录在 `.vfox.toml` 里 | +| 会话(默认) | `~/.vfox/tmp/-/nodejs` 下的一个目录 | + +版本本身在 `~/.vfox/cache/nodejs/v-/nodejs-/`,里面有 `bin/node`、`bin/npm` 和 `lib/node_modules`。`vfox info nodejs@24.21.0` 打印的是它的上一级目录。`~/.vfox` 是 `$VFOX_HOME` 的默认值。设置了 `VFOX_HOME`(指向一个已存在的目录)之后,插件和各个版本(`cache/`)放在那里,`sdks/nodejs` 链接和会话目录仍留在 `~/.vfox`。`storage.sdkPath` 会让新安装的版本放到 `/nodejs/` 下,见 vfox 的[配置文档](https://vfox.dev/zh-hans/guides/configuration.html)。在 Windows 上,`node.exe` 直接放在版本目录里。 + +## 报告问题 + +请到 [version-fox/vfox-nodejs](https://github.com/version-fox/vfox-nodejs/issues) 提 issue,附上 `vfox --version` 和 `vfox info nodejs` 的输出、操作系统和 CPU、你执行的命令以及它打印的全部内容。`vfox --debug ` 会打印更多信息;排查 `.nvmrc` 时,`vfox --debug env -s bash` 也一样。vfox 本身的问题请向 [vfox](https://github.com/version-fox/vfox/issues) 反馈。 diff --git a/docs/zh-CN/versions.md b/docs/zh-CN/versions.md new file mode 100644 index 0000000..a69b2ff --- /dev/null +++ b/docs/zh-CN/versions.md @@ -0,0 +1,117 @@ +# 版本与命名 + +[中文](versions.md) | [English](../versions.md) + +`vfox install` 和 `vfox use` 里 `nodejs@` 后面的部分,是 Node.js 的版本号,或者版本号的开头部分: + +```bash +vfox install nodejs@24.21.0 # 恰好是这个版本 +vfox install nodejs@24.21 # 最新的 24.21.x +vfox install nodejs@24 # 最新的 24.x +vfox install nodejs@latest # 所有版本线中最新的版本 +``` + +这里没有 `lts`、`lts/iron` 或 `stable` 这样的名字。要装处于长期支持的版本线,请写明版本线(`nodejs@24`),见 [lts.md](lts.md)。`.nvmrc` 文件则能识别 `lts/`,见 [nvmrc.md](nvmrc.md)。 + +## 安装 + +`vfox install` 先问插件哪个包符合要求,再由 vfox 下载、校验并解压。2026-10-21 当天的结果如下: + +| 输入 | 安装的版本 | +| --- | --- | +| `24.21.0` | `24.21.0` | +| `v24.21.0` | `24.21.0`,vfox 会去掉 `v` | +| `24.21` | `24.21.0`,即最新的 24.21.x | +| `24` | `24.22.0`,即最新的 24.x | +| `latest` | `26.12.0`,即索引里最新的版本。vfox 会先打印 `Using latest version: 26.12.0` | + +- “最新”指索引里列出的所有版本中数字最大的那个,不论是否属于 LTS。尚未进入长期支持的版本线(比如现在的 26)可以比最新的 LTS 版本线更新,所以 `latest` 并不等于 LTS 版本。 +- `24` 和 `24.21` 是在索引里查找的,不是在已安装的版本里查找。`vfox install nodejs@24` 会装上比你已有的更新的 24.x;已经是最新版本时,则打印 `nodejs@24.22.0 is already installed`。 +- 索引里只列出由三段数字组成的版本,别的一概没有:没有 `-rc` 构建、每日构建,也没有四段数字的版本号。这些写法最终都会报 `version not found for provided version ...`。 +- 不存在的版本号(`24.99.0`)最终会报 `get checksum failed: HTTP 404 from ...`,因为 nodejs.org 上没有对应的目录(未发布:0.5.2 省略了 `failed` 后面的部分)。版本存在但没有你所在平台的包时,最终会报 `Node.js package ... is not listed in ...`。见 [troubleshooting.md](troubleshooting.md#插件给出的提示) 和 [platforms.md](platforms.md)。 +- 每次下载都会对照该版本 `SHASUMS256.txt` 里的 SHA-256 校验和进行检查。vfox 会打印 `Verifying checksum ...`,校验和不一致时会报 `checksum error` 并停止,什么也不会安装。 +- 安装时如果发现同名的版本已经存在,就不会下载任何东西。 + +镜像改变的是文件的来源,不改变名字的含义,见 [mirror.md](mirror.md)。 + +## 安装后的版本名 + +安装后的版本名就是版本号:不带 `v`,不带平台,也不带其他变体。`vfox list nodejs` 显示这些名字,正在使用的那个带有标记。vfox 按字符串排序,大的在前,所以 `22.9.0` 排在 `22.23.3` 前面: + +```text +$ vfox list nodejs +-> 26.12.0 +-> 24.21.0 <— current +-> 22.9.0 +-> 22.23.3 +-> 20.20.2 +``` + +`vfox uninstall` 需要写完整的名字: + +```bash +vfox uninstall nodejs@22.23.3 +``` + +`vfox uninstall nodejs@22` 和 `nodejs@22.23` 会提示 `is not installed`。卸载正在使用的版本时,vfox 会切换到 `vfox list nodejs` 里剩下的第一项,并给出提示(`Auto switch to the other version.`),那可能是另一条版本线。 + +文件位于 `~/.vfox/cache/nodejs/v-/nodejs-/`,见 [troubleshooting.md](troubleshooting.md#nodejs-在哪里)。 + +## 使用 + +`vfox use` 把你输入的内容映射到已安装的版本,不会下载任何东西: + +| 输入 | 选中 | +| --- | --- | +| `24.21.0`、`v24.21.0` | 这个版本 | +| `24.21` | 已安装的最新的 24.21.x | +| `24` | 已安装的最新的 24.x | +| 其他任何写法,比如 `lts` 或 `99` | 什么也选不中:`nodejs@lts is not installed` | + +“最新”按数字比较,所以装了 22.3.0、22.9.0 和 22.23.3 时,`nodejs@22` 选中 22.23.3,`nodejs@22.3` 选中 22.3.0。简写必须能匹配到某个已安装的版本:只装了 22.23.3 时,`nodejs@22.2` 什么也选不中,因为没有 22.2.x。 + +作用域用 `vfox use` 的参数选择:`--global`、`--project`,或者默认的 `--session`。这几种都需要先把 vfox 挂载到 Shell(bash 里是 `eval "$(vfox activate bash)"`)。没有挂载时,`vfox use` 在 Linux 上会报下面的错并停止: + +```text +vfox requires hook support. Please ensure vfox is properly initialized with 'vfox activate' +``` + +`--project` 会写出一个 `.vfox.toml`,里面记录的是安装后的版本名: + +```toml +[tools] +nodejs = "22.23.3" +``` + +同一目录里的 `.nvmrc` 或 `.node-version` 也会被读取,见 [nvmrc.md](nvmrc.md)。`PATH` 里有另一个 `node` 排在 vfox 前面时,`vfox use` 会打印一条提示,见 [pnpm.md](pnpm.md)。 + +## 搜索 + +```bash +vfox search nodejs +``` + +每一行都是可以接在 `nodejs@` 后面的名字,新的在前。名字后面依次是:`(LTS)`(属于长期支持版本线的版本才有)、这个版本自带的 npm,以及 `(installed)`: + +```text +$ vfox search nodejs +Available versions: + - 26.12.0 [npm 11.20.0] (installed) + - 26.11.1 [npm 11.20.0] + ... + - 24.22.0 (LTS) [npm 11.19.1] + - 24.21.0 (LTS) [npm 11.19.0] (installed) + ... + - 0.6.3 [npm 1.1.0-alpha-2] + - 0.6.2 [npm ] +``` + +- 列表就是 nodejs.org(或你的[镜像](mirror.md))的 `index.json`:2026-10-21 共有 872 个版本,从 0.1.14 到 26.12.0。npm 从 0.6.3 起才有,更早的版本是空的。 +- `(LTS)` 对应索引里的 `lts` 字段,Node.js 从一条版本线进入长期支持的那天起才会设置它。Krypton 版本线里第一个带有该字段的版本是 24.11.0,24.10.0 及更早的版本没有。见 [lts.md](lts.md)。 +- 插件会忽略参数:`vfox search nodejs 24` 打印的仍是同一份完整的列表。要过滤的话,用管道,比如 `vfox search nodejs | grep '^ - 24\.'`。在管道里,vfox 只打印输出。 +- 在交互式终端里,vfox 改为显示一个选择列表(`Please select a version of nodejs to install [type to search]`):方向键移动,输入内容过滤,回车确认。 +- 列表包含所有版本,不论你的平台有没有对应的包,见 [platforms.md](platforms.md)。 +- vfox 会把列表缓存 12 小时(`vfox config cache.availableHookDuration`),所以一分钟前刚发布的版本可能还看不到。`vfox config cache.availableHookDuration 0` 会关闭缓存。 +- 取不到索引时,搜索会失败并报 `Failed to fetch Node.js versions: ...`,而不是什么也不列出,见 [troubleshooting.md](troubleshooting.md#网络)。 + +`vfox install nodejs 24` 装的并不是 24:用空格分隔的参数表示多个 SDK,它会停下来并报 `install requires specifying a version for nodejs`。请写 `nodejs@24`。