fix(nodeenv): stop -p from reinstalling node that is already there - #403
Merged
Merged
Conversation
This was referenced Sep 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #159.
The problem
nodeenv -preinstalls node on every run.create_environment()skips the"environment already exists" guard whenever
-pis given, because thevirtualenv directory always exists:
So the
-ppath has no protection at all: each call downloads the tarballagain (there is no download cache) and overwrites
bin/node. Reproduced onmasterwith a freshpython3 -m venv /tmp/nv159:The other half of the issue - the repeated
* Appending data to .../bin/activate- no longer reproduces:writefile()returns early onif content in c. Butset_predeactivate_hook()appends throughopen(..., 'a')with no such check, sobin/predeactivategrew from 65 to130 bytes across those two runs.
What this changes
get_installed_node_version()reads the version out of the environment's ownbin/node, andcreate_environment()skipsinstall_node()when it matchesthe requested version:
By that point
main()has resolvedargs.nodeto an exact version, so thecomparison also holds for
latest,ltsand ranges such as^24. Everythingafter the block -
install_activate(), npm, requirements, the predeactivatehook - 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 --prebuiltthennodeenv -p --node=systemsequence thatinstall_activate()documents in a comment. Comparing versions keeps bothworking without flags.
A shim counts as "not installed". After
nodeenv -p --node=systemthe file atbin/nodeis theSHIMscript and it reports the system node's version;without the distinction,
nodeenv -p --node=26.8.1would be skipped wheneverthe 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 existingwritefile(..., append=True)instead of carrying its ownopen(..., 'a'),which gets the deduplication for free.
Behaviour
nodeenv -p --prebuiltnodeenv -p --node=24.0.0over 26.9.0nodeenv -p --node=systemnodeenv -p --prefer-systemwith system node presentnodeenv -p --prebuilt --forcenodeenv -p --prebuiltover a shim from--node=systemVerified
Every row of that table was run against a real virtualenv. The second run
drops from 5.0s to 0.49s with no download:
bin/predeactivatestayed 65 bytes across all runs. Requesting the systemversion (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.