Skip to content

fix(env): use runtime symlink paths for fuzzy versions#9143

Merged
jdx merged 1 commit intomainfrom
codex/runtime-symlink-paths
Apr 16, 2026
Merged

fix(env): use runtime symlink paths for fuzzy versions#9143
jdx merged 1 commit intomainfrom
codex/runtime-symlink-paths

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Apr 16, 2026

Summary

Use runtime symlink paths when building environment-facing tool paths for fuzzy or rolling requests, while keeping concrete install paths for install state and filesystem operations.

This fixes a case where python = "3.14" resolved to 3.14.4 and PATH used .../installs/python/3.14.4/bin instead of the stable requested-version symlink .../installs/python/3.14/bin. Tools that cache interpreter paths, such as virtualenvs, can then survive patch upgrades where the concrete install directory changes.

Root Cause

ToolVersion::install_path() is intentionally based on the resolved version and is used broadly for install directories, downloads, cache paths, incomplete markers, and uninstall state. PATH generation also used that same concrete install path through backend list_bin_paths(), so runtime symlinks created by runtime_symlinks::rebuild() were not used for active PATH entries.

Changes

  • Add ToolVersion::runtime_path() for PATH-facing paths.
  • Use runtime_path() in the default backend bin path implementation and asdf bin paths.
  • Ensure runtime_path() only returns traversable directories, and resolve Windows file-based pseudo-symlinks to their concrete install directory.
  • Keep lockfile-resolved tool versions on their concrete install path so fuzzy runtime symlinks cannot drift past the locked version.
  • Include the requested version in the tool path cache key so exact and fuzzy requests resolving to the same concrete version do not share stale cached paths.
  • Add e2e coverage for dummy@1.0 and dummy@prefix:1 returning runtime symlink bin paths.
  • Add a unit regression test for file-based runtime symlink paths.
  • Update latest bin-path expectations to use the existing latest runtime symlink.
  • Update docs examples for runtime symlink paths and refresh visible Node/Python examples to Node 26 and Python 3.15.

Validation

  • cargo test runtime_path_does_not_return_file_based_runtime_symlink
  • mise run format
  • mise run test:e2e e2e/lockfile/test_lockfile_exec
  • mise run test:e2e e2e/tools/test_runtime_symlinks e2e/cli/test_bin_paths e2e/tools/test_path_order
  • markdownlint README.md docs/demo.md docs/dev-tools/index.md docs/dev-tools/mise-lock.md docs/dev-tools/shims.md docs/environments/index.md docs/getting-started.md docs/lang/node.md docs/lang/python.md docs/mise-cookbook/docker.md docs/mise-cookbook/nodejs.md docs/tips-and-tricks.md docs/troubleshooting.md docs/walkthrough.md
  • Commit hook hk lint set, including cargo check --all-features

This PR was generated by Codex.


Note

Medium Risk
Changes how PATH/bin-paths are computed for non-pinned tool requests, which can affect runtime selection and downstream tooling that relies on specific filesystem paths. While scoped and covered by new tests, it touches core environment construction across backends.

Overview
Ensures environment-facing tool paths (e.g., PATH/mise bin-paths) use the requested-version runtime symlink (like .../installs/python/3.15/bin or .../installs/node/lts/bin) instead of the resolved patch install directory, by introducing ToolVersion::runtime_path() and routing backend list_bin_paths() through it (including the asdf backend).

Updates toolset path caching to key on requested=resolved to avoid stale path reuse when different requests resolve to the same concrete version, adds/adjusts unit + e2e coverage for runtime symlink bin paths (including latest, prefix, and version requests), and refreshes docs/demo examples to Node 26 / Python 3.15 while documenting the runtime-symlink PATH behavior.

Reviewed by Cursor Bugbot for commit a65c8aa. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a runtime_path method to prioritize runtime symlinks (such as latest) over specific versioned installation paths when resolving tool binaries. This change ensures that environment paths remain stable even when the underlying version is updated. The PR also updates the path caching logic to include the requested version in the cache key. Feedback was provided to use is_dir() instead of exists() when validating the runtime path to prevent potential issues on Windows where symlinks might be incorrectly handled as files.

Comment thread src/toolset/tool_version.rs Outdated
};
let path = self.ba().installs_path.join(&pathname);
let path = env::find_in_shared_installs(path, &self.ba().tool_dir_name(), &pathname);
if path.exists() && is_runtime_symlink(&path) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using path.is_dir() is preferred over path.exists() here. On Windows, if mise is unable to create real symlinks (e.g., due to missing permissions), it may create a text file containing the target path instead. While is_runtime_symlink might return true for such a file, including a file path in PATH (e.g., .../latest/bin) will not work as expected for the shell. is_dir() ensures that the path is a valid directory (or a symlink to one) before it is used for environment-facing tool paths.

Suggested change
if path.exists() && is_runtime_symlink(&path) {
if path.is_dir() && is_runtime_symlink(&path) {

@jdx jdx marked this pull request as ready for review April 16, 2026 14:00
@jdx jdx changed the title [codex] fix(env): use runtime symlink paths for fuzzy versions fix(env): use runtime symlink paths for fuzzy versions Apr 16, 2026
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 405615c. Configure here.

Comment thread src/toolset/tool_version.rs
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 16, 2026

Greptile Summary

Introduces ToolVersion::runtime_path() to return the version-symlink path (e.g. installs/python/3.14/bin) for fuzzy version requests instead of the concrete install path (installs/python/3.14.4/bin), so that tools like virtualenvs that cache interpreter paths survive patch upgrades. The LIST_PATHS_CACHE key is also updated to include the requested version alongside the resolved version, preventing stale cache sharing when multiple requests (e.g. python@3.14 vs python@3.15) resolve to the same concrete version in the same process.

Confidence Score: 5/5

Safe to merge — all remaining findings from prior rounds are P2 scope/perf notes that the author has explicitly acknowledged, and no new P0/P1 issues were found.

The core logic in runtime_path() is correct: locked versions fall back to the concrete install path, exact-pinned versions do the same (via runtime_pathname() returning None when requested==resolved), and the fallback chain handles missing symlinks gracefully. The cache-key fix in toolset_paths.rs directly addresses the stale-entry problem for same-concrete-version requests. The unit regression test for the Windows pseudo-symlink edge case and the new e2e assertions give solid coverage of the changed paths. Prior reviewer concerns (lack of a RUNTIME_PATH_CACHE and backends with custom list_bin_paths being unscoped) are P2 follow-ups, not blockers.

No files require special attention.

Important Files Changed

Filename Overview
src/toolset/tool_version.rs Adds runtime_path() and runtime_pathname(), plus the locked field; fallback logic to install_path() is correct for locked, exact-pinned, and path-based requests; Windows file-symlink handling is correctly guarded and tested.
src/toolset/toolset_paths.rs Cache key updated to backend@requested=resolved to prevent stale sharing when different fuzzy requests resolve to the same concrete version; change is minimal and correct.
src/backend/mod.rs Default list_bin_paths now uses tv.runtime_path() instead of tv.install_path(); single-line change with correct semantics.
src/backend/asdf.rs Correctly computes runtime_path outside the asdf bin-path cache so that cached relative paths are joined with the current symlink path on each call.
e2e/cli/test_bin_paths Expectations updated from concrete version paths to latest symlink paths, correctly reflecting the new runtime_path behaviour for @latest requests.
e2e/tools/test_runtime_symlinks New assertions cover dummy@1.0 (version symlink) and dummy@prefix:1 (prefix symlink), providing direct e2e coverage for the two affected request types.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[runtime_path called] --> B{tv.locked?}
    B -- yes --> Z[return install_path]
    B -- no --> C{runtime_pathname returns Some?}
    C -- no --> Z
    C -- yes --> D[path = installs_path / pathname]
    D --> E[find_in_shared_installs]
    E --> F{path.is_dir AND is_runtime_symlink?}
    F -- yes --> G[return symlink path e.g. installs/python/3.14/bin]
    F -- no --> H{Windows AND path.is_file AND is_runtime_symlink?}
    H -- yes --> I{target.is_dir?}
    I -- yes --> J[return absolutized target]
    I -- no --> Z
    H -- no --> Z

    subgraph runtime_pathname
        R1{ToolRequest type?}
        R1 -- Version AND version != tv.version --> R2[return requested version string]
        R1 -- Prefix --> R3[return prefix string]
        R1 -- Other --> R4[return None]
    end
Loading

Reviews (4): Last reviewed commit: "fix(env): use runtime symlink paths for ..." | Re-trigger Greptile

Comment thread src/toolset/tool_version.rs
Comment thread src/toolset/tool_version.rs
@jdx jdx force-pushed the codex/runtime-symlink-paths branch 2 times, most recently from 6d759c4 to a65c8aa Compare April 16, 2026 14:20
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 16, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.15 x -- echo 21.9 ± 0.5 20.9 24.0 1.00
mise x -- echo 22.2 ± 0.4 21.4 23.5 1.01 ± 0.03

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.15 env 21.2 ± 0.7 20.5 28.0 1.00
mise env 21.9 ± 0.4 21.0 23.6 1.03 ± 0.04

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.15 hook-env 21.7 ± 0.4 21.0 26.6 1.00
mise hook-env 22.2 ± 0.3 21.6 25.5 1.02 ± 0.03

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.15 ls 19.1 ± 0.3 18.5 21.2 1.00
mise ls 19.7 ± 0.3 19.1 21.0 1.03 ± 0.02

xtasks/test/perf

Command mise-2026.4.15 mise Variance
install (cached) 145ms 146ms +0%
ls (cached) 76ms 76ms +0%
bin-paths (cached) 80ms 80ms +0%
task-ls (cached) 818ms 783ms +4%

@jdx jdx force-pushed the codex/runtime-symlink-paths branch from a65c8aa to e4127b2 Compare April 16, 2026 15:30
@jdx jdx merged commit dc84086 into main Apr 16, 2026
38 checks passed
@jdx jdx deleted the codex/runtime-symlink-paths branch April 16, 2026 16:30
mise-en-dev added a commit that referenced this pull request Apr 17, 2026
### 🚀 Features

- **(registry)** add .perl-version support for perl by @ergofriend in
[#9102](#9102)
- **(task)** add Tera template support for inline table run tasks by
@iamkroot in [#9079](#9079)

### 🐛 Bug Fixes

- **(env)** use runtime symlink paths for fuzzy versions by @jdx in
[#9143](#9143)
- **(github)** use full token resolution chain for attestation
verification by @jdx in [#9154](#9154)
- **(go)** Remove install-time version override for subpath packages by
@c22 in [#9135](#9135)
- **(npm)** respect install_before when resolving dist-tag versions by
@webkaz in [#9145](#9145)
- **(self-update)** ensure subcommand exists by @salim-b in
[#9144](#9144)
- **(task)** show available tasks when run target missing by @jdx in
[#9141](#9141)
- **(task)** forward task help args and add raw_args by @jdx in
[#9118](#9118)
- **(task)** remove red/yellow from task prefix colors by
@lechuckcaptain in [#8782](#8782)
- **(task)** merge TOML task block into same-named file task and surface
resolved dir by @jdx in [#9147](#9147)
- **(toolset)** round-trip serialized tool options by @atharvasingh7007
in [#9124](#9124)
- **(vfox)** fallback to absolute bin path if env_keys not set by
@80avin in [#9151](#9151)

### 📚 Documentation

- make agent guide wording generic by @jdx in
[#9142](#9142)

### 📦️ Dependency Updates

- update ghcr.io/jdx/mise:deb docker digest to e019cb9 by @renovate[bot]
in [#9160](#9160)
- update ghcr.io/jdx/mise:copr docker digest to 8d25608 by
@renovate[bot] in [#9159](#9159)
- update ghcr.io/jdx/mise:rpm docker digest to 22e52da by @renovate[bot]
in [#9161](#9161)
- update ghcr.io/jdx/mise:alpine docker digest to a3da97c by
@renovate[bot] in [#9158](#9158)
- update rust docker digest to 4a2ef38 by @renovate[bot] in
[#9162](#9162)
- update ubuntu:24.04 docker digest to c4a8d55 by @renovate[bot] in
[#9164](#9164)
- update rust crate aws-lc-rs to v1.16.3 by @renovate[bot] in
[#9165](#9165)
- update ubuntu docker tag to resolute-20260413 by @renovate[bot] in
[#9169](#9169)
- update rust crate clap to v4.6.1 by @renovate[bot] in
[#9166](#9166)
- update taiki-e/install-action digest to a2352fc by @renovate[bot] in
[#9163](#9163)
- update rust crate ctor to 0.10 by @renovate[bot] in
[#9170](#9170)
- update rust crate tokio to v1.52.1 by @renovate[bot] in
[#9167](#9167)
- update rust crate rmcp-macros to 0.17 by @renovate[bot] in
[#9173](#9173)
- update rust crate signal-hook to 0.4 by @renovate[bot] in
[#9177](#9177)
- update rust crate zipsign-api to 0.2 by @renovate[bot] in
[#9180](#9180)
- update rust crate toml_edit to 0.25 by @renovate[bot] in
[#9179](#9179)
- update rust crate strum to 0.28 by @renovate[bot] in
[#9178](#9178)

### 📦 Registry

- add ibmcloud by @dnwe in
[#9139](#9139)
- add rush by @jdx in [#9146](#9146)

### New Contributors

- @80avin made their first contribution in
[#9151](#9151)
- @atharvasingh7007 made their first contribution in
[#9124](#9124)
- @lechuckcaptain made their first contribution in
[#8782](#8782)
- @ergofriend made their first contribution in
[#9102](#9102)
- @dnwe made their first contribution in
[#9139](#9139)

## 📦 Aqua Registry Updates

#### New Packages (3)

-
[`controlplaneio-fluxcd/flux-operator`](https://github.com/controlplaneio-fluxcd/flux-operator)
-
[`dependency-check/DependencyCheck`](https://github.com/dependency-check/DependencyCheck)
- [`kiro.dev/kiro-cli`](https://github.com/kiro.dev/kiro-cli)

#### Updated Packages (2)

-
[`jreleaser/jreleaser/standalone`](https://github.com/jreleaser/jreleaser/standalone)
- [`sigstore/cosign`](https://github.com/sigstore/cosign)
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.

1 participant