Skip to content

feat(env): honor system-only in shims and installers - #3233

Open
myuron wants to merge 6 commits into
voidzero-dev:mainfrom
myuron:feat/system-only-shim
Open

myuron wants to merge 6 commits into
voidzero-dev:mainfrom
myuron:feat/system-only-shim

Conversation

@myuron

@myuron myuron commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Part of #1899 (system-only management mode for NixOS). Follows #3143.

VP_*_MANAGER=system-only restricts Node.js or a package-manager family to the system installation. Shims then use only tools found on PATH: they never fall back to a managed version, download, or reuse a cached managed install. Every other managed entry point fails with a clear error instead of downloading.

system-only is intentionally undocumented until the remaining paths are covered (vp commands, reinstall/upgrade dependency installation, and installation-time setup). Without a system-only variable, nothing changes: yes/no, config.json modes, and the existing installer probe response behave as before.

What's Changed

Parsing and safety net

  • ManagerPreference::SystemOnly matches only the exact lowercase value. A family variable (VP_PNPM_MANAGER, ...) takes precedence over VP_PM_MANAGER. At run time, system-only overrides config.json. The other values still apply only at installation.
  • Managed Node.js and package-manager entry points fail with DisabledBySystemOnly before their cache lookups: runtime download, the Node.js version index, package-manager download and binary lookup, and registry version resolution. The error is not an integrity failure.
  • Installation ignores system-only for now: self_setup treats it like an unknown value.

Shims

  • vp_command::find_external_tool gets an exclude_managed option. With it, installs under <DATA>/js_runtime and <DATA>/package_manager, and the fallback-bin directory, never count as the system tool. Paths are compared after canonicalization, so /nix/store/... tools are accepted.
  • New EffectiveMode { Managed, SystemFirst, SystemOnly } combines the config.json shim mode with the environment.
  • The core-shim selection moves from dispatch() into select_core_tool, which does not exec and can be tested directly. With system-only Node.js, it ignores session overrides, .node-version-style pins, and a managed runtime inherited from a parent shim. A system-only family ignores packageManager pins and makes no registry requests.
  • The VP_BYPASS path, the reuse of a tool selected by a parent shim, global package binaries, and vpx global binaries follow the same rules. The npm bundled with a managed runtime is treated as part of that runtime.
  • System-only directories are never added to the child PATH. A canonical path such as /nix/store/...-nodejs/bin matches no PATH entry, so adding it would reorder the user's PATH. Children reach the same tools through PATH or the shims.
  • When no system tool is found, the shim fails with VP_NODE_MANAGER=system-only requires a system 'node' on PATH, but none was found.

Installers and vp upgrade

A vp that predates this change would ignore system-only and download managed tools. Installers and vp upgrade therefore refuse to hand over to such a binary while any system-only variable is set.

  • vp answers VP_SELF_SETUP_SUPPORT_CHECK=system-only with a second line, vite-plus-self-setup-system-only. The response to the existing check (vite-plus-self-setup-v1) is unchanged, because released installers compare it exactly.
  • install.sh, install.ps1, and vp_installer probe with the new value only when a system-only variable is set. They fail before the legacy installer path, which would download Node.js itself.
  • vp upgrade checks the extracted binary before installing dependencies, and vp upgrade --rollback checks the previous version before switching.
  • vp_installer tracks the Node.js choice as NodeManager { Enabled, Disabled, SystemOnly }. system-only takes precedence over --no-node-manager and cannot be toggled in the menu. When package managers inherit the Node.js choice, system-only becomes no, so only an explicit variable makes them system-only.

Current limitations (follow-up PRs)

With system-only set, after this PR:

  • Shims (node, npm, pnpm, ...) use system tools.
  • vp install, vp run, vp env *, and vpx package downloads fail with DisabledBySystemOnly instead of using or downloading managed tools.
  • Reinstalling or running vp upgrade with system-only exported fails while installing dependencies, for the same reason.
  • Setting system-only at installation is not saved yet.

Testing

On the current main:

  • Each of the three commits on its own: cargo fmt --all -- --check, cargo check --workspace --locked, cargo clippy -- -D warnings and cargo test for the affected crates
  • New snapshot cases: shim_system_first_fallback/system_only_overrides_managed_shims (an unreachable Node.js mirror and registry, checks that no managed install is created and that the child PATH is unchanged), and command_self_setup/command_self_setup_system_only_probe

Before rebasing onto the current main (the only conflict was an import in shim/dispatch.rs):

  • just check, just lint, just test
  • .github/scripts/test-install-bootstrap.sh: all scenarios, including five new system-only scenarios

Not run locally:

  • .github/scripts/test-install-bootstrap.ps1 and test-install-legacy-remote.ps1 (CI; no PowerShell locally)
  • Windows (CI)

Some existing command_self_setup and shim snapshot cases fail locally on NixOS both before and after this change (same cases); relying on CI for those.

@liangmiQwQ
liangmiQwQ self-requested a review October 25, 2026 14:55
@fengmk2

fengmk2 commented Oct 27, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-27T06:34:49.474516Z
We couldn’t complete this review. Comment @codex review to try again.
b15ffe5 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

An unknown error occurred
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +726 to +731
let node_system_only = vp_shared::EnvConfig::get().node_system_only();
if !node_system_only {
env.prepend(node_bin_dir, &["node"], PrependOptions::default())?;
}
if vp_pm_cli::system_only_variable(PackageManagerType::Npm).is_some() {
return Ok(env);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Enforce system-only selection in the child PATH

With a managed runtime inherited first on PATH, setting both VP_NODE_MANAGER=system-only and VP_NPM_MANAGER=system-only selects the system npm executable but leaves that managed runtime first in its environment. The normal #!/usr/bin/env node shebang therefore executes managed Node, violating the new restriction despite successful system-tool discovery. The reverse combination also fails: with only npm set to system-only, prepending the permitted managed Node directory exposes its bundled npm, so nested npm calls use the managed copy. Preserve the restrictions through interpreter and child-process execution, including inherited-tool shortcuts, and add tests that execute the selected tools instead of only checking their paths.

Reproduced with the built PR binary: the system npm shebang printed MANAGED_NODE; the separate npm-only case printed SYSTEM_NPM_PARENT followed by MANAGED_NPM_CHILD. Both exited 0. These are two manifestations of the same PATH enforcement issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — fixed in ba2d71a.

The shims now remove managed directories from the child PATH, instead of only choosing the system tool. When Node.js or npm is system-only, the managed runtime directory is removed, which also covers its bundled npm. When a package-manager family is system-only, that family's managed directories are removed. Other entries keep their order. This runs wherever the shim starts a tool (exec_tool / spawn_tool), so the inherited-tool shortcut is covered too. With only npm system-only, children still reach managed Node.js through the node shim.

The new snapshot case system_only_child_processes runs a #!/usr/bin/env node system npm that starts a nested npm. It covers both of your scenarios, and every step fails without the fix.

vpx (6fb3e80): I found the same issue in vpx; it predates this PR. vpx picks the executable from PATH before starting it, so restricting the child environment isn't enough. For system-only tools, vpx node / vpx npm and script mode now skip managed installs on PATH. When there is no system install, vpx fails instead of falling back to dlx, which could download the tool.

Comment on lines +803 to +805
let system_node = if node_system_only {
resolve_effective_tool("node", EffectiveMode::SystemOnly)
.map_err(|e| vec![e.to_string()])?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Stop re-entering an external manager that forwards to vp

An external Node manager can forward to the vp fallback shim without removing itself from PATH; the existing foreign_manager_falls_back_without_recursion fixture covers this arrangement. Under VP_NODE_MANAGER=system-only, this branch selects the same external manager again each time vp receives that forwarded call. node --version then loops through repeated execs instead of reporting that no usable system Node exists. Track the external selection across re-entry and stop or exclude it when it forwards back to vp.

Reproduced with the built PR binary: using the forwarding script from setup-foreign-manager.cjs, the system-only invocation timed out after three seconds and the harness killed it. The same PATH with VP_NODE_MANAGER=yes and a cached managed runtime exited 0 and printed MANAGED_FALLBACK_NODE.

Comment on lines +1446 to +1448
fn find_system_only_tool(tool: &str, cwd: &AbsolutePath) -> Option<AbsolutePathBuf> {
let inputs = ExternalToolInputs::from_process_env()?;
vp_command::find_external_tool(tool, &inputs.query(true), cwd)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep foreign executables eligible in a shared bin directory

The new system-only lookup calls find_external_tool, which removes the whole configured VP_BIN_DIR before checking executable identity. A supported split layout can place a foreign Node in that shared directory; setup preserves such tools, and the existing split_layout_preserves_foreign_tools fixture covers them. If that is the only system Node on PATH, a shim that needs Node now reports that no system Node exists. Keep foreign executables eligible while rejecting vp-owned shims and managed installations by identity.

Reproduced with the built PR binary: a foreign Node in VP_BIN_DIR produced exit 1 with VP_NODE_MANAGER=system-only requires a system 'node' on PATH, but none was found. An equivalent executable outside VP_BIN_DIR exited 0.

Comment on lines +230 to +231
// A target without system-only support would download managed tools.
ensure_system_only_support(&binary_path, new_version).await?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve retained versions when rejecting an incompatible upgrade

If the requested target already exists as the retained previous version, VP_NODE_MANAGER=system-only vp upgrade <target-version> extracts into that installation before this new compatibility check. Rejecting an unsupported target returns an error to the caller, whose cleanup deletes the entire target directory while leaving .previous-version pointing to it. The refusal therefore destroys the rollback installation; unsetting the variable does not restore it. Probe the downloaded binary in a staging directory before changing a retained installation, and preserve existing directories on rejection. Although generic failed-install cleanup predates this PR, this new policy refusal now triggers that destructive path before dependency installation.

Reproduced with the built PR binary and a local registry: the retained directory existed before the command and disappeared afterward; the command exited 1 with the unsupported-system-only error, while current and .previous-version stayed unchanged. The server received only metadata and payload requests.

@liangmiQwQ liangmiQwQ left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(🙇 apogolise for late replying)


/// The variable that restricts `tool` (Node.js or a package-manager family)
/// to the system, if any. Package binaries have no such variable.
fn system_only_variable_for_tool(tool: &str) -> Option<&'static str> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried about if the logic finding in the system-PATH in a Vite+ shim call got re-introduced. PATH-forwarding is hacky and has been proven to cause some tricky problems like #2728.

Is it possible to only allow system-only when the tool is in system-first mode, then directly throw an error for shim calls?

/// System-only tools add no directories: a resolved system directory may be a
/// canonical path missing from PATH, and adding it would reorder the user's
/// PATH. Children reach the same system tools through PATH or the shims, after
/// [`restrict_child_env`] removes the managed directories they must not run.

@liangmiQwQ liangmiQwQ Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with Nix, may I ask why managed directories have to be removed here?

My understanding is that system-only is relatively stable. If it is already set, the only mission is to avoid an incorrect prepend (just skip this function call). And since these directories have never been prepended, there is no need to remove it either.

This branch has not been deployed

No deployments
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.

3 participants