Skip to content

fix(nodeenv): stop -p from reinstalling node that is already there - #403

Merged
ekalinin merged 3 commits into
masterfrom
fix/reinstall-node-check
Sep 19, 2026
Merged

ekalinin merged 3 commits into
masterfrom
fix/reinstall-node-check

Conversation

@ekalinin

Copy link
Copy Markdown
Owner

Closes #159.

The problem

nodeenv -p reinstalls node on every run. create_environment() skips the
"environment already exists" guard whenever -p is given, because the
virtualenv directory always exists:

if os.path.exists(env_dir) and not args.python_virtualenv:
    logger.info(' * Environment already exists: %s', env_dir)
    if not args.force:
        sys.exit(2)

So the -p path has no protection at all: each call downloads the tarball
again (there is no download cache) and overwrites bin/node. Reproduced on
master with a fresh python3 -m venv /tmp/nv159:

--- RUN 1 ---
 * Install prebuilt node (26.9.0) ..... done.
 * Appending data to /tmp/nv159/bin/activate
 * Appending data to /tmp/nv159/bin/activate.fish
10.6s

--- RUN 2 ---
 * Install prebuilt node (26.9.0) ..... done.
8.4s

The other half of the issue - the repeated * Appending data to .../bin/activate - no longer reproduces: writefile() returns early on
if content in c. But set_predeactivate_hook() appends through
open(..., 'a') with no such check, so bin/predeactivate grew from 65 to
130 bytes across those two runs.

What this changes

get_installed_node_version() reads the version out of the environment's own
bin/node, and create_environment() skips install_node() when it matches
the requested version:

    elif not args.force and \
            get_installed_node_version(env_dir) == parse_version(args.node):
        logger.info(' * Node.js %s is already installed, skipping '
                    '(use --force to reinstall)', args.node)

By that point main() has resolved args.node to an exact version, so the
comparison also holds for latest, lts and ranges such as ^24. Everything
after the block - install_activate(), npm, requirements, the predeactivate
hook - keeps running, so a skipped install still refreshes the activation
scripts.

Rejecting the run outright, as the issue suggests, would have broken two
supported workflows: changing the version in place (nodeenv -p --node=24)
and the nodeenv -p --prebuilt then nodeenv -p --node=system sequence that
install_activate() documents in a comment. Comparing versions keeps both
working without flags.

A shim counts as "not installed". After nodeenv -p --node=system the file at
bin/node is the SHIM script and it reports the system node's version;
without the distinction, nodeenv -p --node=26.8.1 would be skipped whenever
the system node happens to match, leaving the shim in place. The shim starts
with #! and a real node binary does not.

set_predeactivate_hook() now goes through the existing
writefile(..., append=True) instead of carrying its own open(..., 'a'),
which gets the deduplication for free.

Behaviour

Command, run twice in the same virtualenv Second run
nodeenv -p --prebuilt skips, logs that the version is already installed
nodeenv -p --node=24.0.0 over 26.9.0 installs 24.0.0
nodeenv -p --node=system unchanged
nodeenv -p --prefer-system with system node present unchanged
nodeenv -p --prebuilt --force installs
nodeenv -p --prebuilt over a shim from --node=system installs

Verified

Every row of that table was run against a real virtualenv. The second run
drops from 5.0s to 0.49s with no download:

=== RUN 2 (same version) ===
 * Node.js 26.9.0 is already installed, skipping (use --force to reinstall)
0.49s

bin/predeactivate stayed 65 bytes across all runs. Requesting the system
version (26.8.1) while a shim was in place installed a real Mach-O binary
rather than skipping.

Seven new unit tests cover the helper, the three install/skip branches and
hook idempotency. Full suite: 274 passed, plus both integration tests; flake8
clean.

@ekalinin
ekalinin merged commit 5383416 into master Sep 19, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nodeenv -p should check if node exists in the venv before installing it

1 participant