fix(cli): inline the $HERMES_HOME/npmrc lookup, trim tests to two, document it
Fold the helper into _npm_lifecycle_env itself: the whole fix is one is_file() check plus a setdefault, so a separate function, the try/except around get_hermes_home() (it never raises) and the dict-returning indirection were shape-gate violations. Build the path with os.fspath so it is correct on Windows too. Tests: keep the two invariants (file present -> NPM_CONFIG_USERCONFIG points at it; explicit process/caller value wins and a missing file sets nothing), fold the other two into the negative test. Docs: one paragraph in the desktop troubleshooting page next to ELECTRON_MIRROR describing $HERMES_HOME/npmrc. Refs #106373
This commit is contained in:
@@ -420,37 +420,22 @@ def _ensure_tui_workspace(tui_dir: Path) -> None:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def _persistent_npm_userconfig() -> Optional[dict[str, str]]:
|
|
||||||
"""``NPM_CONFIG_USERCONFIG`` for ``$HERMES_HOME/npmrc`` when it exists.
|
|
||||||
|
|
||||||
The repo-root ``.npmrc`` is git-tracked, so the updater's autostash parks
|
|
||||||
any mirror/proxy line added there and every update reinstalls without it
|
|
||||||
(restricted networks then prune optional native deps like get-windows and
|
|
||||||
the rebuild fails). ``$HERMES_HOME`` lives outside the git tree, survives
|
|
||||||
autostash/reset, and the update hand-off already carries ``HERMES_HOME``
|
|
||||||
down to every npm child. An explicit ``NPM_CONFIG_USERCONFIG`` wins.
|
|
||||||
"""
|
|
||||||
from hermes_constants import get_hermes_home
|
|
||||||
|
|
||||||
if os.environ.get("NPM_CONFIG_USERCONFIG", "").strip():
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
candidate = get_hermes_home() / "npmrc"
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
if not candidate.is_file():
|
|
||||||
return None
|
|
||||||
return {"NPM_CONFIG_USERCONFIG": str(candidate)}
|
|
||||||
|
|
||||||
|
|
||||||
def _npm_lifecycle_env(env: dict[str, str] | None = None) -> dict[str, str]:
|
def _npm_lifecycle_env(env: dict[str, str] | None = None) -> dict[str, str]:
|
||||||
"""Build a clean environment for the pinned UI toolchain lifecycle."""
|
"""Build a clean environment for the pinned UI toolchain lifecycle."""
|
||||||
run_env = {**os.environ, **(env or {}), "CI": "1"}
|
run_env = {**os.environ, **(env or {}), "CI": "1"}
|
||||||
# esbuild treats this as an executable override. If a shell points it at a
|
# esbuild treats this as an executable override. If a shell points it at a
|
||||||
# different release, the pinned package's postinstall rejects that binary.
|
# different release, the pinned package's postinstall rejects that binary.
|
||||||
run_env.pop("ESBUILD_BINARY_PATH", None)
|
run_env.pop("ESBUILD_BINARY_PATH", None)
|
||||||
for key, value in (_persistent_npm_userconfig() or {}).items():
|
# The repo-root ``.npmrc`` is git-tracked, so the updater's autostash parks
|
||||||
run_env.setdefault(key, value)
|
# any mirror/proxy line added there and every update reinstalls without it
|
||||||
|
# (restricted networks then prune optional native deps like get-windows and
|
||||||
|
# the rebuild fails). ``$HERMES_HOME`` lives outside the git tree and the
|
||||||
|
# update hand-off already carries ``HERMES_HOME`` down to every npm child.
|
||||||
|
# An explicit ``NPM_CONFIG_USERCONFIG`` wins (#106373).
|
||||||
|
from hermes_constants import get_hermes_home
|
||||||
|
npmrc = get_hermes_home() / "npmrc"
|
||||||
|
if npmrc.is_file():
|
||||||
|
run_env.setdefault("NPM_CONFIG_USERCONFIG", os.fspath(npmrc))
|
||||||
return run_env
|
return run_env
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -781,56 +781,34 @@ def test_make_tui_argv_omits_workspace_and_scrubs_esbuild_override(
|
|||||||
|
|
||||||
|
|
||||||
class TestPersistentNpmUserconfig:
|
class TestPersistentNpmUserconfig:
|
||||||
"""$HERMES_HOME/npmrc must reach every npm lifecycle child process."""
|
"""$HERMES_HOME/npmrc must reach every npm lifecycle child process (#106373)."""
|
||||||
|
|
||||||
def test_hermes_home_npmrc_sets_userconfig(self, tmp_path, monkeypatch):
|
def test_hermes_home_npmrc_sets_userconfig(self, tmp_path, monkeypatch):
|
||||||
home = tmp_path / "home"
|
home = tmp_path / "home"
|
||||||
home.mkdir()
|
home.mkdir()
|
||||||
(home / "npmrc").write_text(
|
(home / "npmrc").write_text(
|
||||||
"node_get_windows_binary_host_mirror=https://ghproxy.net/https://github.com/sindresorhus/get-windows/releases/download/\n",
|
"node_get_windows_binary_host_mirror=https://mirror.example/get-windows/\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||||
monkeypatch.delenv("NPM_CONFIG_USERCONFIG", raising=False)
|
monkeypatch.delenv("NPM_CONFIG_USERCONFIG", raising=False)
|
||||||
monkeypatch.delenv("ESBUILD_BINARY_PATH", raising=False)
|
|
||||||
|
|
||||||
env = main_tui_launch._npm_lifecycle_env()
|
env = main_tui_launch._npm_lifecycle_env()
|
||||||
|
|
||||||
assert env["NPM_CONFIG_USERCONFIG"] == str(home / "npmrc")
|
assert env["NPM_CONFIG_USERCONFIG"] == os.fspath(home / "npmrc")
|
||||||
|
|
||||||
def test_missing_npmrc_leaves_env_untouched(self, tmp_path, monkeypatch):
|
def test_explicit_userconfig_wins_and_missing_file_sets_nothing(self, tmp_path, monkeypatch):
|
||||||
home = tmp_path / "home"
|
home = tmp_path / "home"
|
||||||
home.mkdir()
|
home.mkdir()
|
||||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
||||||
monkeypatch.delenv("NPM_CONFIG_USERCONFIG", raising=False)
|
monkeypatch.delenv("NPM_CONFIG_USERCONFIG", raising=False)
|
||||||
|
|
||||||
env = main_tui_launch._npm_lifecycle_env()
|
assert "NPM_CONFIG_USERCONFIG" not in main_tui_launch._npm_lifecycle_env()
|
||||||
|
|
||||||
assert "NPM_CONFIG_USERCONFIG" not in env
|
(home / "npmrc").write_text("registry=https://example.invalid\n", encoding="utf-8")
|
||||||
|
|
||||||
def test_explicit_userconfig_wins(self, tmp_path, monkeypatch):
|
|
||||||
home = tmp_path / "home"
|
|
||||||
home.mkdir()
|
|
||||||
(home / "npmrc").write_text(
|
|
||||||
"registry=https://example.invalid\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
||||||
monkeypatch.setenv("NPM_CONFIG_USERCONFIG", str(tmp_path / "custom-npmrc"))
|
monkeypatch.setenv("NPM_CONFIG_USERCONFIG", str(tmp_path / "custom-npmrc"))
|
||||||
|
assert main_tui_launch._npm_lifecycle_env()["NPM_CONFIG_USERCONFIG"] == str(tmp_path / "custom-npmrc")
|
||||||
|
|
||||||
env = main_tui_launch._npm_lifecycle_env()
|
monkeypatch.delenv("NPM_CONFIG_USERCONFIG")
|
||||||
|
env = main_tui_launch._npm_lifecycle_env({"NPM_CONFIG_USERCONFIG": "/from-caller/npmrc"})
|
||||||
assert env["NPM_CONFIG_USERCONFIG"] == str(tmp_path / "custom-npmrc")
|
|
||||||
|
|
||||||
def test_caller_env_does_not_override_userconfig(self, tmp_path, monkeypatch):
|
|
||||||
env_in = {"NPM_CONFIG_USERCONFIG": "/from-caller/npmrc"}
|
|
||||||
home = tmp_path / "home"
|
|
||||||
home.mkdir()
|
|
||||||
(home / "npmrc").write_text(
|
|
||||||
"registry=https://example.invalid\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
||||||
monkeypatch.delenv("NPM_CONFIG_USERCONFIG", raising=False)
|
|
||||||
|
|
||||||
env = main_tui_launch._npm_lifecycle_env(env_in)
|
|
||||||
|
|
||||||
assert env["NPM_CONFIG_USERCONFIG"] == "/from-caller/npmrc"
|
assert env["NPM_CONFIG_USERCONFIG"] == "/from-caller/npmrc"
|
||||||
|
|||||||
@@ -539,6 +539,8 @@ ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ \
|
|||||||
bash -c 'cd "$HOME/.hermes/hermes-agent/apps/desktop" && CSC_IDENTITY_AUTO_DISCOVERY=false npm run pack'
|
bash -c 'cd "$HOME/.hermes/hermes-agent/apps/desktop" && CSC_IDENTITY_AUTO_DISCOVERY=false npm run pack'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Other native downloads (e.g. the `get-windows` prebuilt on Windows) that need a mirror:** put the npm keys in `$HERMES_HOME/npmrc` (`%LOCALAPPDATA%\hermes\npmrc` on Windows, `~/.hermes/npmrc` elsewhere) — for example `node_get_windows_binary_host_mirror=https://<mirror>/sindresorhus/get-windows/releases/download/`. Every `npm ci`/`npm run` the updater spawns (desktop, web and TUI builds) points `NPM_CONFIG_USERCONFIG` at that file when it exists, so the config survives `hermes update`; the repo-root `.npmrc` is git-tracked and gets autostashed on every update, and `~/.npmrc` may be missed because the desktop hand-off inherits the GUI's environment. An `NPM_CONFIG_USERCONFIG` you set yourself is never overridden.
|
||||||
|
|
||||||
To clear a corrupt cached zip by hand:
|
To clear a corrupt cached zip by hand:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Reference in New Issue
Block a user