diff --git a/CHANGES b/CHANGES index 369fc68..d96adbf 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,9 @@ Nodeenv changelog Version [unreleased] -------------------- +- `--prebuilt` installs no longer fail when the environment path or the + node.js version contains `[`, `*` or `?` + `#441 `_ - On Windows, Cygwin and MSYS the node.js zip archive is extracted 32 files at a time instead of one by one, so a security scanner that checks every new file no longer makes the install wait for each file in turn. diff --git a/nodeenv.py b/nodeenv.py index 0a2d741..3154e8b 100644 --- a/nodeenv.py +++ b/nodeenv.py @@ -1070,9 +1070,9 @@ def copy_node_from_prebuilt(env_dir, src_dir, node_version): else: dest = env_dir - src_folder_tpl = src_dir + '/node-v%s-*' % node_version - src_folder, = glob.glob(src_folder_tpl) - copytree(src_folder, dest, True) + src_folder_tpl = glob.escape('node-v%s-' % node_version) + '*' + src_folder, = glob.glob(src_folder_tpl, root_dir=src_dir) + copytree(join(src_dir, src_folder), dest, True) if is_CYGWIN: for filename in ('npm', 'npx', 'node.exe'): diff --git a/tests/nodeenv_test.py b/tests/nodeenv_test.py index 6a99142..cb5a45e 100644 --- a/tests/nodeenv_test.py +++ b/tests/nodeenv_test.py @@ -1172,6 +1172,58 @@ def test_copytree_keeps_existing_symlink(tmpdir): assert os.readlink(tmpdir.join('dst', 'bin', 'npm').strpath) == target +@pytest.mark.parametrize( + 'dirname', + ( + 'proj[1]', + pytest.param('proj*', marks=pytest.mark.skipif( + nodeenv.is_WIN, reason='* is not a valid Windows filename')), + pytest.param('proj?', marks=pytest.mark.skipif( + nodeenv.is_WIN, reason='? is not a valid Windows filename')), + ), +) +def test_copy_node_from_prebuilt_literal_path(tmpdir, monkeypatch, dirname): + monkeypatch.setattr(nodeenv, 'is_WIN', False) + monkeypatch.setattr(nodeenv, 'is_CYGWIN', False) + env_dir = tmpdir.join(dirname, 'env').ensure(dir=True) + src_dir = env_dir.join('src').ensure(dir=True) + archive = 'node-v18.0.0-test-platform' + source = src_dir.join(archive, 'bin', 'node').ensure() + source.write('literal source') + env_dir.join('keep.txt').write('keep this file') + decoy = tmpdir.join('proj1', 'env', 'src', archive, 'bin', 'node').ensure() + decoy.write('neighbor source') + + nodeenv.copy_node_from_prebuilt(env_dir.strpath, src_dir.strpath, '18.0.0') + + assert env_dir.join('bin', 'node').read() == 'literal source' + assert env_dir.join('keep.txt').read() == 'keep this file' + assert source.read() == 'literal source' + assert decoy.read() == 'neighbor source' + + +def test_copy_node_from_prebuilt_literal_version(tmp_path, monkeypatch): + monkeypatch.setattr(nodeenv, 'is_WIN', False) + monkeypatch.setattr(nodeenv, 'is_CYGWIN', False) + version = '18.0.0+build[1]' + env_dir = tmp_path / 'env' + env_dir.mkdir() + src_dir = tmp_path / 'src' + archive = src_dir / ('node-v%s-linux-x64' % version) + neighbor = src_dir / 'node-v18.0.0+build1-linux-x64' + for directory, contents in ( + (archive, 'literal version'), + (neighbor, 'neighbor version')): + directory.mkdir(parents=True) + (directory / 'payload').write_text(contents, encoding='utf-8') + + nodeenv.copy_node_from_prebuilt(str(env_dir), str(src_dir), version) + + assert (env_dir / 'payload').read_text() == 'literal version' + assert (archive / 'payload').read_text() == 'literal version' + assert (neighbor / 'payload').read_text() == 'neighbor version' + + def test_parse_version(): assert nodeenv.parse_version("v21.7") == (21, 7) assert nodeenv.parse_version("v21.7.3") == (21, 7, 3)