fix(pm): keep npm's self-install cache out of the download entry
npm caches the tarball it installs. The cache was written next to the archive, inside the store's fetch-<sha> entry. Removing that entry after publication then had to delete a tree, and on Windows it failed while Defender still held the fresh tarball copy: [WinError 145] The directory is not empty: ...\fetch-<sha>\.npm-cache\... Give npm a throwaway temp cache whose cleanup cannot fail the install, so the download entry holds only the archive.
This commit is contained in:
@@ -8,6 +8,7 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -567,20 +568,24 @@ class Npm(BinaryPackage):
|
||||
if not bundled_cli.is_file():
|
||||
raise InstallError(self.name, "node's entry is missing its bundled npm-cli.js")
|
||||
|
||||
env = npm_env(archive.parent / ".npm-cache")
|
||||
|
||||
staged.mkdir(parents=True, exist_ok=True)
|
||||
proc = subprocess.run(
|
||||
[
|
||||
str(node_bin), str(bundled_cli), "install", "--global",
|
||||
"--prefix", str(staged), "--offline", "--ignore-scripts",
|
||||
"--no-audit", "--no-fund", str(archive),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=900,
|
||||
env=env,
|
||||
)
|
||||
# npm caches the tarball it installs. The archive's directory is the
|
||||
# store's download entry, which must hold only the archive: a cache
|
||||
# there turns its removal into a tree delete that fails on Windows
|
||||
# while Defender still holds the fresh copy (WinError 145). Cleanup
|
||||
# of this throwaway cache must never fail the install.
|
||||
with tempfile.TemporaryDirectory(prefix="hermes-npm-cache-", ignore_cleanup_errors=True) as cache:
|
||||
proc = subprocess.run(
|
||||
[
|
||||
str(node_bin), str(bundled_cli), "install", "--global",
|
||||
"--prefix", str(staged), "--offline", "--ignore-scripts",
|
||||
"--no-audit", "--no-fund", str(archive),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=900,
|
||||
env=npm_env(Path(cache)),
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise InstallError(
|
||||
self.name, f"self-install exited {proc.returncode}: {proc.stderr[-400:]}"
|
||||
|
||||
59
tests/pm/test_npm_self_install.py
Normal file
59
tests/pm/test_npm_self_install.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""npm's self-install keeps its cache out of the store's download entry.
|
||||
|
||||
A cache beside the archive turned the entry's removal into a tree delete
|
||||
that failed on Windows while Defender held the fresh tarball copy
|
||||
(``[WinError 145] The directory is not empty: ...fetch-<sha>\\.npm-cache``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pm.install
|
||||
import pm.packages
|
||||
from pm.registry import get_package
|
||||
from pm.store import Store, current_target
|
||||
|
||||
|
||||
def _fake_node(store: Store, target: str) -> None:
|
||||
node_bin = get_package("node").binary(store.entry("node-1"), target)
|
||||
node_bin.parent.mkdir(parents=True, exist_ok=True)
|
||||
node_bin.write_bytes(b"node")
|
||||
cli_root = node_bin.parent if target.startswith("win32") else node_bin.parent.parent / "lib"
|
||||
cli = cli_root / "node_modules" / "npm" / "bin" / "npm-cli.js"
|
||||
cli.parent.mkdir(parents=True)
|
||||
cli.write_text("", encoding="utf-8")
|
||||
|
||||
|
||||
def test_npm_self_install_leaves_download_entry_holding_only_the_archive(tmp_path, monkeypatch):
|
||||
target = current_target()
|
||||
store = Store(tmp_path / "store")
|
||||
_fake_node(store, target)
|
||||
monkeypatch.setattr(pm.install, "_lockfile", lambda: None)
|
||||
monkeypatch.setattr(pm.install, "_installed_location",
|
||||
lambda package, lockfile, target: ({"node": {"entry": "node-1"}}, store))
|
||||
|
||||
caches: list[Path] = []
|
||||
|
||||
def fake_npm(cmd, **kwargs):
|
||||
# Stand in for npm: cacache writes the installed tarball into the cache.
|
||||
cache = Path(kwargs["env"]["npm_config_cache"])
|
||||
content = cache / "_cacache" / "content-v2" / "sha512" / "b8" / "85"
|
||||
content.mkdir(parents=True)
|
||||
(content / "blob").write_bytes(b"tarball")
|
||||
caches.append(cache)
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
monkeypatch.setattr(pm.packages.subprocess, "run", fake_npm)
|
||||
|
||||
entry = store.entry("fetch-" + "a" * 64)
|
||||
entry.mkdir(parents=True)
|
||||
archive = entry / "npm-1.0.0.tgz"
|
||||
archive.write_bytes(b"archive")
|
||||
staged = tmp_path / "scratch" / "tree"
|
||||
|
||||
get_package("npm").unpack(archive, staged, target)
|
||||
|
||||
assert [p.name for p in entry.iterdir()] == [archive.name]
|
||||
assert caches and not caches[0].is_relative_to(staged)
|
||||
Reference in New Issue
Block a user