Link the first mention of each named person to their GitHub and each named project, tool, or OS to its official homepage or repo.
Mentions that appear only inside a heading (Ansible),
an HTML comment (doctoc),
or a multi-word code span (grunt) are left unlinked,
since a link there would
break the generated table-of-contents anchors or fail to render.
Two `nvm install <same version>` runs could race on the version directory
- one removing or replacing it while the other reads or writes it.
Take a per-version advisory lock
(an atomically-created directory under $NVM_DIR/.cache locks)
around the binary/source install,
so a second run of the same version waits for the first;
installs of different versions never contend.
NVM_INSTALL_LOCK_TIMEOUT (seconds, default 600) bounds the wait,
after which nvm reports the lock path so a lock left by a killed install can be removed.
NVM_INSTALL_LOCK_STALE (minutes, default 0 / off)
opts into automatically stealing a lock older than that,
for unattended or CI use.
By default a failed binary download falls back to compiling node from source,
which is slow and pointless on platforms that always have prebuilt binaries.
`-b` already disables the fallback per-invocation;
`NVM_NO_SOURCE_FALLBACK=1` makes it the default for every `nvm install`,
so CI images and dev setups need not thread `-b` through every callsite.
It behaves exactly like `-b`
- aborting with a non-zero exit on a failed binary download
- and is mutually exclusive with `-s`, for the same reason `-b` is.
`nvm_is_version_installed` only checks that `bin/node` has the execute bit,
which a zero-byte binary and a dangling `npm` symlink both pass, so a partial
install could be reported as a success — and a broken existing version could
short-circuit `nvm install` as "already installed".
Add `nvm_validate_install`, which requires a non-empty `bin/node` and an `npm`
entry that resolves, and use it in two places: gate the "already installed"
shortcut on it, so a broken version is reinstalled rather than reused, and
re-check it after an install reports success, so a broken result fails loudly
instead of being activated.
It checks layout, not execution: a correctly installed binary can still fail
to run on an incompatible host (e.g. a newer node on an older glibc), which is
not a broken install, and a corrupt download is already rejected by the
checksum check before extraction.
A version directory left without a working `bin/node`
- e.g. a partial or interrupted earlier install
- wedged reinstallation:
`nvm install` saw the version as not installed and re-extracted,
but the per-entry `mv` refused to overwrite the leftover non-empty `bin/`,
`lib/`, … subdirectories and left a half-updated tree behind
(and, without `-b`, fell back to a from-source compile)
Remove any pre-existing version directory and move the freshly extracted tree into place with a single rename,
so a version is either fully installed or not present at all.
The removal is safe: it runs only after the tarball has downloaded and extracted successfully into the cache.
Fall back to the previous per-entry move when a single rename cannot cross filesystems.
`install.sh` has its own `nvm_download`,
which joined `"$@"` into a single string with `nvm_echo` and rewrote the curl-style flags with `sed`,
then ran the result through `eval command wget $ARGS`.
Flattening argv into a string is the root cause: every argument was re-parsed by the shell,
so any shell metacharacter in a legitimate value was interpreted rather than passed through.
An `$NVM_DIR` containing a space silently word-split and wrote to the wrong path,
an apostrophe or parentheses were a syntax error,
a glob character expanded against the cwd,
and an `$NVM_SOURCE` mirror URL with an `&` in its query string backgrounded `wget` and ran the remainder as a bogus command.
The `sed` rewrites were also unanchored substring replacements that would mangle a URL legitimately containing `-o `,
and `ARGS` was an undeclared global that `nvm_reset` never unset.
Translate the flags per-argument with a POSIX `set --` loop and invoke `command wget "$@"` instead,
so each argument stays a literal argv element.
This is the same shape `nvm.sh`'s `nvm_download` has used since 6d870d18,
and it stops the two implementations from diverging.
Unlike `nvm.sh`, no mirror-supplied data reaches this function:
its arguments come only from `$NVM_SOURCE`, `$NVM_INSTALL_GITHUB_REPO`, `$NVM_INSTALL_VERSION`, `$NVM_DIR`, `$XDG_CONFIG_HOME`, and `$HOME`, and `install.sh`'s `nvm_latest_version` is a hardcoded string rather than `nvm.sh`'s `nvm_get_latest`.
This is therefore a robustness fix and not a security one,
since the only party who can influence these arguments is the one already running the installer.
The avoid_ref check read HEAD's tags via `git for-each-ref` with no --git-dir,
so it inspected the caller's checkout instead of the installed clone.
Whether it passed then depended on which ref triggered the workflow
(the same commit passed when built as the v0.40.6 tag but failed when built as master, since only the former had the tag present),
not on the install result.
Query the installed clone and compare commits,
so a HEAD that is legitimately the latest release commit
(eg master right after a release)
is exempted deterministically.
A mirror-supplied LTS codename (field 10 of index.tab) was used verbatim as an alias filename,
so a hostile codename such as `../../../.bashrc` could make nvm_make_alias write outside $NVM_DIR/alias
- with the default layout, into shell startup files.
Constrain remote codenames to safe filename characters at ingestion in nvm_ls_remote_index_tab,
and reject any `..` path component in nvm_make_alias as a backstop for every caller.
Payloads
(binaries, plus source tarballs that nvm compiles and runs at install time) are trusted by construction,
since nvm exists to build and run them;
parsed metadata (index.tab, SHASUMS, LTS codenames) is not,
and must never reach a command evaluator,
an awk/sed program body,
or an unvalidated filesystem path.
This records why the existing version-string and checksum hardening exists,
and scopes a malicious payload from a configured mirror as out of scope:
no privilege boundary is crossed.
The full suite cannot run on Alpine: the install-based suites pin ancient Node (0.10.x, io.js) that has no musl binary and cannot source-compile on musl.
Run the fast unit suite instead (mirroring the ubuntu runner: non-root via su-exec, passwordless sudo, a PTY, no system node), plus a binary-only regression matrix that installs every (Alpine, Node) pair with a real unofficial musl binary via `nvm install -b`: x64 back to node 8.17.0 on old Alpine, arm64 at the v20.20.1/v22.21.1/v24.9.0 floors on modern Alpine.
`nvm_get_arch`: skip the smartos arm64/armv8l cases, which have no uname mock and fall through to the real host (adding a musl suffix on Alpine).
`nvm_get_arch_unofficial`: skip the glibc chroot (fixed /lib64 loader, sudo); the musl mapping is already covered by the `nvm_get_arch alpine` test.
`nvm_install_no_progress_bar`: skip below the musl floor, since v0.12.18 has no musl binary and hardcodes the glibc tarball URL.
Sourcing nvm.sh under `set -u` on a POSIX shell that leaves `$_` unset (e.g. busybox ash or dash on Alpine, where the parent shell does not export it) aborted at `NVM_SCRIPT_SOURCE="$_"`.
Default to empty; the only consumer already falls back to `$0`.
The `/etc/alpine-release` check applied the -musl suffix regardless of the resolved OS.
In practice Alpine is always linux, so this is behavior-neutral, but it also makes the `nvm_get_arch` unit test hermetic: its mocked smartos/osx cases no longer pick up a real host's Alpine marker.
Node.js unofficial-builds publishes linux-arm64-musl binaries since v20.20.1 / v22.21.1 / v24+.
Map NVM_ARCH=arm64 to arm64-musl on Alpine so `nvm install`
resolves to the correct tarball instead of the glibc-linked arm64 build that won't run against musl.
[Tests] `nvm_get_arch`: expect arm64-musl on Alpine
Co-authored-by: Jesse Zhu <jesse@itjesse.com>
Co-authored-by: Jordan Harband <ljharb@gmail.com>
urchin only executes test files that have the executable bit, silently skipping the rest,
and `npm run test:check-exec` would catch non-executable ones, but no workflow ran it -
so several test files have never actually run.
Make all of them executable, and fix the ones that were broken:
- the io.js source-install tests, skipped since 2016: io.js requires python 2 and gcc <= 5,
so they now skip on toolchains that can not compile it,
and a new gcc:4.9-container CI job runs them unmodified -
resolving, downloading, checksumming, and compiling io.js for real.
New `fake source` variants exercise nvm's source pipeline everywhere -
cache lookup, offline resolution, extraction, configure, make, make install, activation -
via a tiny fake source tree seeded into the download cache and installed with `--offline`.
Also strengthen the `nvm run | grep` assertions with `--silent`,
since the version banner used to satisfy the grep even when the built binary could not run,
and give the final assertions the `die` they always meant to call.
- `nvm uninstall ... incorrect file permissions fails nicely`, parked in 2025 as failing:
`sudo npm` resets PATH, so node 5's npm crashed under the system node.
The permissions check only trips on files that are neither writable nor self-owned,
and that precondition only needs `sudo touch` - not npm, jspm, or the network -
so use that, against a fake installed version,
and skip only where passwordless sudo does not exist.
- `nvm install --offline`: `nvm ls | tail -1` always grabbed an alias line, never a version;
use `nvm_ls` and skip its trailing `system <version>` line.
- `nvm exec ... help should not parse`: in a BRE, `[options]` is a bracket expression,
so the grep could never match node's literal `Usage: node [options]` output.
- `nvm use ... nvmrc containing not installed version`: current output once again matches its expectations verbatim; no changes needed.
Rewrite `test:check-exec` in POSIX sh - npm runs scripts with /bin/sh, which is dash on Ubuntu,
where the previous `[[`/`$'\n'` bashisms would break;
match by extension, so that filenames containing dots can not dodge the check;
and disable git's `core.quotePath` mangling of the non-ASCII test filenames.
Then wire it into the lint workflow.
`--offline` version resolution could only see cached node binaries:
`nvm_ls_cached` listed `.cache/bin` with a `node-` prefix filter,
so cached source tarballs, and everything io.js, could never resolve offline.
List both cache kinds, for both flavors,
and update the pinned test expectations to match.
The test was born without its executable bit,
and urchin silently skips non-executable files, so it has never run.
Also pin what `nvm_ls_cached` can currently see:
only bin-cached node artifacts - bin-cached io.js, and src-cached anything,
are invisible to it, and thus to `--offline` version resolution.
The cycle scenarios all pass with the old grep-based detection too;
the bug the `case`-based detection actually fixes is resolved names interpolated into the grep pattern as regexes,
where resolving axb -> a.b falsely reported ∞ because the pattern `a.b` matches the seen name `axb`.
Pin both directions: the fixed false positive, and a genuine cycle through a metachar name.
Also correct the space-name scenario comment
(the old anchored grep passed that scenario; only token-delimited seen-storage would not),
and register every new fixture name in the suite teardown,
so a mid-test failure can not leak aliases into later tests.
[Tests] `nvm_alias`, `nvm_resolve_alias`: add edge-case tests
nvm_alias() used a sed/awk pipeline to strip comments and blank lines from alias files that almost always contain a single word.
A while-read loop with parameter expansion does the same filtering more directly.
nvm_resolve_alias() piped nvm_alias through head and tail to extract one line, and used printf/grep for cycle detection.
Parameter expansion and a case statement replace both without the extra plumbing.
All replacements are POSIX (read -r, case, IFS=, parameter expansion).
As a side effect, this also removes 4 external process invocations during shell init.
[Fix] `nvm_resolve_alias`: detect cycles via newline-anchored `case`
The original commit referenced above changed SEEN_ALIASES from `\n`-delimited
storage (interpreted by `printf '%b' | nvm_grep -e "^${name}$"`) to space-
delimited but left the line-anchored grep in place — without newlines in
the haystack the anchored pattern can never match, so cycles never break.
Switch to literal-newline storage and a `case` pattern anchored on those
newlines. Newline anchoring also handles alias names containing spaces,
which token-based patterns false-positive on (e.g. lookup of `bar` matches
substring " bar " inside " foo bar midway " when the chain visits the
multi-token alias `foo bar`).
New test file covers self-loop, multi-hop loop, cycle through a
space-bearing alias name, and a non-cycle through a space-bearing
alias name. Existing `test/fast/Aliases/circular/` fixtures continue
to pass.
The sed/awk pipeline exits with awk's status,
so an existing-but-unreadable alias file produces empty output with status 0 - but only by accident,
alongside sed's read error on stderr.
Make that contract explicit:
a nonzero status here would flip `nvm_ensure_default_set` from "default is already set" to recreating it,
silently overwriting a write-only default alias.
The two `v0.40.0` + `nvmrc` jobs fail by design (https://github.com/nvm-sh/nvm/issues/3405),
but job-level `continue-on-error` still displays them as failures on every run,
which reads like the contributor broke something.
Tolerate failure only in the steps that exercise the bug, scoped to that matrix combination,
and add a final step that fails the job when the expected failure did not happen -
so those jobs are green exactly when v0.40.0 misbehaves as documented,
and turn red as a signal to remove this handling if v0.40.0 ever starts passing.
This also restores strict failure semantics to the passing `v0.40.0` + `no nvmrc` jobs,
which the job-level `continue-on-error` was needlessly masking.
- `cleanup` unset TEST_DIR before `rm -rf "${TEST_DIR-}"`, so the temp dir (with an executable fake `node`) was never removed,
and urchin executes any executable file it finds under the test dir on the next local run.
- the preexisting `foo#bar` assertions ran against the ambient NVM_DIR;
now that `#` patterns comment-strip to `foo`, a real local alias named `foo` would resolve and break them,
so all assertions now run against an isolated NVM_DIR.
- the multiline content used a full x.y.z version, which takes nvm_ls's explicit-version fast path and never reaches the find/sed pipeline where the newline actually broke sed;
a partial version exercises that path, and matching on "unterminated" covers both the BSD and GNU sed error wordings.
`nvm_has` matches shell functions and aliases,
but downloads now run via `command`, which skips them
- a `curl` shell function with no curl binary on the PATH would select the curl path and fail with exit 127,
instead of falling back to an available wget executable.
The new `nvm_has_executable` helper resolves names the same way `command` does, so downloader selection and execution agree.
zsh (and interactive bash with `expand_aliases`) bakes a preexisting `curl` alias into nvm's function bodies at source time,
and shell functions named `curl`/`wget` shadow the binaries at call time - either one breaks downloads.
Prefixing invocations with `command` bypasses both: here, `nvm_download`'s dispatch, `nvm_curl_version`, `nvm_curl_libz_support`, and the wget branch of `nvm_get_latest`;
the remaining bare `curl` invocations in `nvm_get_latest` and the install script are prefixed in a followup commit.
The tests that previously mocked curl/wget as shell functions now install fake executables on PATH instead,
via a shared `make_fake_curl` helper in `test/common.sh`,
and a new test asserts the bypass.
Refs #2923
On my system, even without using local mirrors, the build is only two
minutes, even though I have only a 1 Gbps Internet connection and I'm
half-way across the world in Tokyo.
There probably are still users with much slower Internet connections
where the build could take eight minutes or more, but "several" still
covers that.
The base image is updated regularly (it's currently 22.04), and it's
easy to forget to update this readme, so best we simply not repeat
ourselves and instead let people look at `Dockerfile` to see exactly
which version of Ubuntu it's using.
`is-nan@1.0.0` was published on 2014-07-05 and unpublished minutes later
(the registry's `time` map still lists it, but `versions` jumps from `0.0.0` to `1.0.1`),
so `npm install -g is-nan@1.0.0` fails with `ETARGET`,
and the regression test added in ce157343 fails deterministically in every shell.
See https://github.com/nvm-sh/nvm/actions/runs/28407118533
When `nvm install <target>` found that `<target>` was already installed,
the post-`nvm use` steps
(`--reinstall-packages-from`, default packages, and `--alias`/`--default`)
were gated on `[ $EXIT_CODE -ne 0 ]`.
Since that branch is only entered when `nvm use` succeeded (EXIT_CODE == 0),
those conditions were always false, so the steps were silently skipped.
The fresh-install branch correctly uses `-eq 0`;
mirror that here so `--reinstall-packages-from` actually migrates global packages
(and the alias/default get set) when the target version already exists.
Includes a regression test covering the already-installed path.
Fixes#3858
The standalone `nvm reinstall-packages <version>`
command can migrate global npm packages between already-installed versions,
but it was only mentioned in `nvm --help`, not in the README.
Add a section covering it.
Refs #3858
- Help text for `nvm install`/`use`/`exec`/`run`/`which` now ends with
"Uses .nvmrc if version is omitted; otherwise errors."
so it's clear that omitting the version is not a free fallback.
- Help signatures for `use`/`exec`/`run` now show `[current | <version>]`
to mirror `nvm which` and document that `current` is accepted.
- `nvm current` description now spells out that it resolves via `$PATH`
and is not affected by `.nvmrc`.
- README: add `current` to the list of special aliases, with the same caveat.
The `.nvmrc` section now states that `nvm use`/`install`/`which`
exit with status 127 when neither a version nor an `.nvmrc` resolves,
notes the (current) `exec`/`run` fallback as undefined behavior,
and points readers at `current` for the explicit "active node" use case.
Refs #3755
Previously a number of subcommands dumped the entire `nvm --help`
output (~100 lines) when arguments were missing or invalid,
drowning the real error.
Replace each dump with a short,
command-specific usage block that names the expected syntax and points to `nvm --help` for full help.
The exit code (127) is unchanged.
Affected subcommands:
- `nvm install` (no version + no .nvmrc)
- `nvm use` (version unresolvable)
- `nvm run` (no version + no .nvmrc)
- `nvm which` (no version + no .nvmrc)
- `nvm cache` (unknown subcommand)
- `nvm uninstall` (wrong arg count)
- `nvm unalias` (wrong arg count)
- `nvm install-latest-npm` (wrong arg count)
- `nvm reinstall-packages` / `nvm copy-packages` (wrong arg count)
The catch-all unknown-subcommand handler still dumps full help, since
in that case the user has no narrower context to be reminded about.
Refs #3755
Previously the message read "No .nvmrc file found",
which obscured the fact that the user also did not pass a version.
The new wording names both halves of the actual problem.
Refs #3755
Currently these commands silently fall back to the active node version when neither a version argument nor an `.nvmrc` resolves,
making them invisibly dependent on shell state and impossible to script predictably (see #3755).
Print a stderr deprecation warning in this case (suppressed by `--silent`) and continue with the active node version,
so existing callers keep working.
The follow-up change will turn this into a hard error;
pass `current` explicitly (e.g. `nvm exec current node ...`) to silence the warning and lock in the new behavior now.
Refs #3755
The branch-reset step sent `force` as a string via `gh api -f "force=true"`,
which the Git refs API rejects ("not a boolean", HTTP 422), failing the release run.
Use `-F` so it is sent as a boolean.
Also resolve the version from the latest `vX.Y.Z` tag
(via the tags API) rather than `releases/latest`,
so a manual `workflow_dispatch` with no input works even before the GitHub release object for a freshly pushed tag is published.
Upstream's `.gitmodules` uses a relative submodule url (`../nvmrc.git`),
which resolves against the superproject's origin:
on a fork without its own `nvmrc` fork that is `<owner>/nvmrc`,
which 404s and fails `actions/checkout` with `submodules: true`.
Keep `submodules: true` (so a fork's own `nvmrc` is used when present),
but mark checkout `continue-on-error` and, only when it failed,
re-point the submodule at `nvm-sh/nvmrc` and update.
The mirror-supplied (untrusted) version flows into download URLs,
filesystem paths, and the checksum awk match.
Reject any version outside the node/io.js grammar
(`[0-9A-Za-z._+-]`) before it is used.
A blocklist of metacharacters is used rather than a strict semver allowlist so RCs, nightlies, v8-canary, and io.js versions still install.
Completes the remediation of GHSA-3c52-35h2-gfmm.