diff --git a/README.md b/README.md index 068ad14..eb8e86e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ # 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 +[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 +18,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 +60,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 +154,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/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 new file mode 100644 index 0000000..66a12dd --- /dev/null +++ b/docs/changes.md @@ -0,0 +1,145 @@ +# 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 +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. +- The documentation has a Chinese version, see [README_CN.md](../README_CN.md). + +## 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..6f39ffb --- /dev/null +++ b/docs/development.md @@ -0,0 +1,274 @@ +# Development + +[中文](zh-CN/development.md) | [English](development.md) + +## 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" +("未发布" 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/`, 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 7118928..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 @@ -50,8 +52,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 +144,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..8b9c5c8 100644 --- a/docs/lts.md +++ b/docs/lts.md @@ -1,10 +1,37 @@ # 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 (`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 +39,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 +92,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..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: @@ -14,16 +16,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..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`. @@ -7,43 +9,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 +136,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..4fb0552 --- /dev/null +++ b/docs/npm.md @@ -0,0 +1,77 @@ +# 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: + +```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..d90d949 --- /dev/null +++ b/docs/nvmrc.md @@ -0,0 +1,166 @@ +# `.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 +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..9485b61 --- /dev/null +++ b/docs/platforms.md @@ -0,0 +1,149 @@ +# 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 + +```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/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 a367e56..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 @@ -43,6 +45,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 +62,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..1c719a0 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,240 @@ +# 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 +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..c8e23b8 --- /dev/null +++ b/docs/versions.md @@ -0,0 +1,166 @@ +# 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: + +```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`. 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`。 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)