Skip to content

Commit 9495aa8

Browse files
authored
Add musl libc support (Alpine/OpenWrt) with Alpine E2E (#31)
1 parent 1344750 commit 9495aa8

24 files changed

Lines changed: 889 additions & 58 deletions

‎.github/workflows/e2e.yml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ jobs:
4242
persist-credentials: false
4343
- run: docker compose -f tests/e2e/linux/compose.yaml run --rm e2e
4444

45+
e2e-linux-musl:
46+
if: github.repository == 'version-fox/vfox-nodejs'
47+
name: Linux musl container (${{ matrix.runs-on }})
48+
strategy:
49+
fail-fast: false
50+
matrix:
51+
runs-on: [ubuntu-latest, ubuntu-26.04-arm]
52+
runs-on: ${{ matrix.runs-on }}
53+
timeout-minutes: 60
54+
steps:
55+
- uses: actions/checkout@v7
56+
with:
57+
persist-credentials: false
58+
- run: docker compose -f tests/e2e/linux/compose.alpine.yaml run --rm e2e
59+
4560
e2e-windows:
4661
if: github.repository == 'version-fox/vfox-nodejs'
4762
name: Windows container (${{ matrix.runs-on }})

‎README.md‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,23 @@ vfox add nodejs
1414

1515
```bash
1616
vfox add nodejs
17-
vfox install nodejs@26.10.0
17+
vfox install nodejs@26.11.1
1818
sudo apt install --assume-yes libatomic1
19-
vfox use --global nodejs@26.10.0
19+
vfox use --global nodejs@26.11.1
20+
```
21+
22+
Note: the `libatomic1` step works around an upstream Node.js packaging bug
23+
([nodejs/node#60790](https://github.com/nodejs/node/issues/60790), still
24+
open): Node 25+ links `libatomic.so.1`, which stock Debian/Ubuntu images do
25+
not ship. System libraries are the user's responsibility; the plugin does
26+
not install them.
27+
28+
### Example: Alpine / musl
29+
30+
```bash
31+
vfox add nodejs
32+
vfox install nodejs@26.11.1
33+
vfox use --global nodejs@26.11.1
2034
```
2135

2236
You can verify this using the following command:
@@ -25,11 +39,23 @@ You can verify this using the following command:
2539
node -v
2640
```
2741

42+
musl details (OpenWrt, tarball matrix, mirror fallback): see [docs/musl.md](docs/musl.md).
43+
2844
## Mirror
2945

3046
See [docs/mirror.md](docs/mirror.md) for details.
3147

32-
## Testing
48+
## FAQ
49+
50+
### Install from source
51+
52+
See [docs/source-build.md](docs/source-build.md) for details.
53+
54+
### LTS
55+
56+
See [docs/lts.md](docs/lts.md) for details.
57+
58+
### Testing
3359

3460
See [docs/e2e.md](docs/e2e.md) for details.
3561

‎docs/e2e.md‎

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ driven from Go through `docker exec`-style calls):
2020
3. A pinned Node.js release installs with checksum verification and runs
2121
(`node --version`, `npm --version`, `process.platform`, `process.arch`),
2222
so the `linux-x64` / `linux-arm64` / `win-x64` / `win-arm64` package
23-
selection is checked for real.
23+
selection is checked for real. The LTS release (`LTS_NODE_VERSION`)
24+
installs as well, backing the codename probe below.
2425
4. `nodejs@latest` resolves through the plugin's `Available` list (the
2526
first, newest entry) and installs.
2627
5. The major and major.minor shorthands resolve through `PreUse` against the
2728
installed versions.
2829
6. `.nvmrc` and `.node-version` drive `vfox env` selection: the exact
29-
version under the default `specified` strategy, and an LTS codename
30-
(`lts/<codename>`) under `latest_installed`. Each legacy probe activates
30+
pinned version under the default `specified` strategy, and an LTS
31+
codename (`lts/<codename>`, see [lts](lts.md)) resolving to the LTS
32+
release under `latest_installed`. Each legacy probe activates
3133
inside its own shell so the selection is recomputed rather than answered
3234
from the `vfox env` state cache of an earlier probe.
3335
7. `vfox search nodejs` lists the installed release with the same npm
@@ -49,9 +51,11 @@ pin `VFOX_VERSION` and `MIRROR` explicitly to widen or narrow the matrix.
4951
| ------------------- | ------------------ | ----------------------------------------------------------------------- |
5052
| `VFOX_VERSION` | `latest main` | Space-separated vfox channels (`latest`, `main`) |
5153
| `MIRROR` | see matrix above | Space-separated mirrors; `default` means `nodejs.org` |
52-
| `NODE_VERSION` | `22.23.3` | Pinned Node.js release installed by the suite |
54+
| `NODE_VERSION` | `26.11.1` | Pinned Node.js release installed by the suite |
55+
| `LTS_NODE_VERSION` | `24.21.0` | LTS release backing the `lts/<codename>` probe (see docs/lts.md) |
5356
| `E2E_MAX_JOBS` | `3` (Linux), `2` (Windows) | Parallel containers per host |
5457
| `E2E_RETRY_ATTEMPTS`| `3` | Attempts per network-touching exec (linear 10s backoff) |
58+
| `E2E_LINUX_BASE` | `ubuntu` | Linux userland: `ubuntu` (glibc) or `alpine` (musl, see docs/musl.md) |
5559

5660
## Ubuntu
5761

@@ -81,6 +85,22 @@ cd ./vfox-nodejs/
8185
DOCKER_SOCK="$XDG_RUNTIME_DIR/docker.sock" docker compose -f tests/e2e/linux/compose.yaml run --rm e2e
8286
```
8387

88+
## Alpine (musl)
89+
90+
Same host requirement. The Alpine suite builds
91+
`tests/e2e/linux/Dockerfile-alpine` (musl userland with `libstdc++`) and
92+
asserts the musl userland before running the standard flow, proving the
93+
plugin selects the `*-musl` tarballs
94+
([issue #29](https://github.com/version-fox/vfox-nodejs/issues/29)).
95+
See [docs/musl.md](musl.md) for the tarball matrix.
96+
97+
```bash
98+
docker compose -f tests/e2e/linux/compose.alpine.yaml run --rm e2e
99+
```
100+
101+
Scope it down locally the same way, e.g.
102+
`VFOX_VERSION=latest MIRROR=default E2E_MAX_JOBS=1` in front of the command.
103+
84104
## Windows 11 Pro
85105

86106
Windows containers cannot be emulated across architectures, so the ARM64
@@ -99,13 +119,15 @@ combinations still run with an unspaced home.
99119

100120
### x64
101121

102-
1. Execute in Windows PowerShell 5.1,
122+
All commands below run in PowerShell 7. If it is not installed yet, install
123+
it first (this single bootstrap command may run in the built-in Windows
124+
PowerShell):
103125

104126
```powershell
105127
winget install --id Microsoft.PowerShell --source winget --exact
106128
```
107129

108-
2. Execute in PowerShell 7,
130+
1. Execute in PowerShell 7,
109131

110132
```powershell
111133
winget install --id Git.Git --source winget --exact
@@ -116,13 +138,13 @@ curl.exe -fsSL `
116138
-o ./install-docker-ce.ps1
117139
```
118140

119-
3. Execute in PowerShell 7 with administrator privileges,
141+
2. Execute in PowerShell 7 with administrator privileges,
120142

121143
```powershell
122144
./install-docker-ce.ps1
123145
```
124146

125-
4. Execute in PowerShell 7,
147+
3. Execute in PowerShell 7,
126148

127149
```powershell
128150
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.docker\cli-plugins" | Out-Null
@@ -138,13 +160,15 @@ docker compose -f tests/e2e/windows/compose.amd64.yaml run --rm --build e2e
138160

139161
### arm64
140162

141-
1. Execute in Windows PowerShell 5.1,
163+
All commands below run in PowerShell 7. If it is not installed yet, install
164+
it first (this single bootstrap command may run in the built-in Windows
165+
PowerShell):
142166

143167
```powershell
144168
winget install --id Microsoft.PowerShell --source winget --exact
145169
```
146170

147-
2. Execute in PowerShell 7,
171+
1. Execute in PowerShell 7,
148172

149173
```powershell
150174
winget install --id Git.Git --source winget --exact

‎docs/lts.md‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# LTS releases
2+
3+
Node.js majors with even numbers become **LTS** (long-term support) lines:
4+
they keep receiving fixes long after odd releases go end-of-life, so most
5+
production setups pin an LTS line. Each LTS line has a codename
6+
(`argon` for 4, `boron` for 6, …, `krypton` for 24).
7+
8+
## What this plugin understands
9+
10+
Under the `latest_available` strategy the plugin discovers LTS codenames
11+
dynamically from the version index (`index.json` carries the codename in
12+
each release's `lts` field), so codenames that postdate this plugin keep
13+
working there. Everywhere else — and as the offline fallback — a hardcoded
14+
table in `hooks/parse_legacy_file.lua` is used, because `specified` and
15+
`latest_installed` must resolve without network access:
16+
17+
| Codename | Major |
18+
| -------- | ----- |
19+
| argon | 4 |
20+
| boron | 6 |
21+
| carbon | 8 |
22+
| dubnium | 10 |
23+
| erbium | 12 |
24+
| fermium | 14 |
25+
| gallium | 16 |
26+
| hydrogen | 18 |
27+
| iron | 20 |
28+
| jod | 22 |
29+
| krypton | 24 |
30+
31+
## Examples
32+
33+
Given an `.nvmrc` (the same applies to `.node-version`) and
34+
`22.23.3` plus `24.21.0` installed:
35+
36+
```sh
37+
# 1. LTS codename -> major, then strategy picks the release.
38+
$ cat .nvmrc
39+
lts/jod
40+
$ vfox config legacyVersionFile.strategy latest_installed
41+
$ cd <dir-with-.nvmrc> && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version
42+
v22.23.3
43+
```
44+
45+
```sh
46+
# 2. Bare `lts` (or `lts/*`) means "the highest mapped major", currently 24.
47+
$ printf 'lts\n' > .nvmrc
48+
$ cd <dir-with-.nvmrc> && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version
49+
v24.21.0
50+
```
51+
52+
```sh
53+
# 3. An exact version skips the codename table entirely.
54+
$ printf '22.23.3\n' > .nvmrc
55+
$ cd <dir-with-.nvmrc> && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version
56+
v22.23.3
57+
```
58+
59+
```sh
60+
# 4. The current LTS line works the same way.
61+
$ printf 'lts/krypton\n' > .nvmrc
62+
$ cd <dir-with-.nvmrc> && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version
63+
v24.21.0
64+
```
65+
66+
```sh
67+
# 5. Under latest_available the index is consulted, so even a codename
68+
# newer than the table above resolves (offline it falls back to the table).
69+
$ vfox config legacyVersionFile.strategy latest_available
70+
$ printf 'lts/<future-codename>\n' > .nvmrc
71+
$ cd <dir-with-.nvmrc> && eval "$(vfox activate bash)" && eval "$(vfox env -s bash)" && node --version
72+
v<newest release carrying that codename>
73+
$ vfox config legacyVersionFile.strategy specified
74+
```
75+
76+
How example 1 resolves, step by step (`latest_installed`):
77+
78+
```
79+
.nvmrc "lts/jod" --resolve_version--> "22" --prefix-match installed--> "22.23.3"
80+
```
81+
82+
## E2E coverage
83+
84+
The legacy-file suite needs an installed release whose major is inside the
85+
table, independently of the pinned release under test. `LTS_NODE_VERSION`
86+
(default `24.21.0`) provides it: the exact-version probes target
87+
`NODE_VERSION` while the `lts/<codename>` probe targets the LTS release.
88+
89+
```sh
90+
# Narrow the matrix and watch the codename probe pick the LTS release:
91+
VFOX_VERSION=latest MIRROR=default E2E_MAX_JOBS=1 LTS_NODE_VERSION=24.21.0 \
92+
docker compose -f tests/e2e/linux/compose.yaml run --rm e2e
93+
# PASS legacy files select 26.11.1 (.nvmrc, .node-version) and 24.21.0 (lts/krypton)
94+
```
95+
96+
The E2E `LTSCodename` helper (`tests/e2e/common/common.go`) mirrors the
97+
plugin's table, so both sides stay in sync. See [e2e](e2e.md).

‎docs/mirror.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,27 @@ are downloaded from `https://nodejs.org/dist` by default. Set
66

77
```bash
88
export VFOX_NODEJS_MIRROR="https://npmmirror.com/mirrors/node"
9-
vfox install nodejs@22.23.3
9+
vfox install nodejs@26.11.1
1010
```
1111

1212
Point it at the root that mirrors `nodejs.org/dist`; the plugin appends its
1313
paths (`/index.json`, `/v<version>/...`) to that base URL. Trailing slashes
1414
are handled, so both `https://nodejs.org/dist` and `https://nodejs.org/dist/`
1515
work.
16+
17+
## musl and unofficial builds
18+
19+
`VFOX_NODEJS_MIRROR` only covers `nodejs.org/dist`. It does **not** apply to
20+
`https://unofficial-builds.nodejs.org/download/release`, which the plugin
21+
uses as a fallback on musl Linux (see [musl](musl.md)):
22+
23+
- Linux musl x64 on a recent release (`26.8+`, `24.20+`, `22.23.3+`): the
24+
official `*-musl` tarball is used, so the mirror applies normally (as long
25+
as the mirror carries it).
26+
- Linux musl arm64 (all releases), or musl x64 on an older release: the
27+
official index has no such file, so the plugin downloads from
28+
`unofficial-builds.nodejs.org` directly, ignoring the mirror.
29+
30+
A mirror that cannot even serve `SHASUMS256.txt` still fails the install
31+
instead of falling back (this is covered by the bogus-mirror negative probes
32+
in [e2e](e2e.md)); only a missing musl filename falls through to unofficial.

‎docs/musl.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# musl libc support (Alpine, OpenWrt/ImmortalWrt)
2+
3+
Node.js official Linux tarballs link glibc (`ld-linux-*`). On musl systems
4+
there is no glibc loader, so the stock download fails with `not found` /
5+
`Error loading shared library ld-linux-aarch64.so.1`.
6+
See [issue #29](https://github.com/version-fox/vfox-nodejs/issues/29).
7+
8+
This plugin detects musl and installs a musl tarball instead.
9+
10+
## Requirements
11+
12+
The musl builds link `libstdc++`, which musl distributions normally install
13+
automatically alongside the rest of the system toolchain, so there is
14+
usually nothing extra to do. If `node --version` ever fails with
15+
`Error relocating ... symbol not found`, the system is missing its
16+
`libstdc++` (`libstdcpp` on OpenWrt) runtime. The plugin's `PostInstall`
17+
hook detects this case and fails the install with a pointer at the README
18+
instead of leaving the cryptic relocation error.
19+
20+
## Which tarball is installed
21+
22+
| Host | Tarball | Source |
23+
| ---- | ------- | ------ |
24+
| Linux glibc, x64/arm64 | `node-v<V>-linux-x64.tar.gz` etc. | `nodejs.org/dist` |
25+
| Linux musl, x64 | `node-v<V>-linux-x64-musl.tar.gz` | official index when present, else unofficial |
26+
| Linux musl, arm64 | `node-v<V>-linux-arm64-musl.tar.gz` | `unofficial-builds.nodejs.org` (official never ships it) |
27+
28+
Notes:
29+
30+
- Official `nodejs.org/dist` only carries `linux-x64-musl` for recent
31+
releases (roughly `26.8+`, `24.20+`, `22.23.3+`) and never carries
32+
`linux-arm64-musl`. Older releases and all `arm64-musl` installs fall back
33+
to `https://unofficial-builds.nodejs.org/download/release` automatically.
34+
- Mirror behavior on musl (when the mirror applies, when unofficial is
35+
used): see [mirror](mirror.md).
36+
- musl builds exist only for `x64` and `arm64`. Other Linux architectures
37+
(e.g. `x86`) error out with a pointer to the README.
38+
- After install the plugin returns a note pointing at the README so the
39+
`libstdc++` requirement is visible in normal `vfox install` output.
40+
41+
## Detection and override
42+
43+
Detection order in `lib/util.lua`:
44+
45+
1. `$VFOX_NODEJS_LIBC`: `musl` forces musl, `gnu`/`glibc` forces glibc.
46+
Useful on exotic userlands and in tests.
47+
2. `RUNTIME.envType` when a future vfox core provides it (`musl`/`gnu`).
48+
3. File probes readable from Lua: `/etc/alpine-release`,
49+
`/lib`/`/usr/lib`/`/lib64` `ld-musl-*.so.1`. musl wins when both linkers
50+
exist (e.g. `gcompat` shims). Non-Linux is never musl.
51+
52+
```sh
53+
# Force musl selection (debugging only)
54+
VFOX_NODEJS_LIBC=musl vfox install nodejs@26.11.1
55+
```

0 commit comments

Comments
 (0)