fix(install): don't cache nonexistent install paths#9553
Conversation
`ToolVersion::install_path()` cached the resolved path even when the path didn't yet exist on disk. During `mise install --system`, the dependency_env build for a `go:` backend tool computed go's install path before go itself installed, causing `find_in_shared_installs()` to return the user-dir path (no shared dir contained go yet). That path got cached. Once go installed at the system dir, callers like core go's `exec_env` still got the stale user path back, exported it as `GOROOT`, and `go install ...` for the dependent tool failed with "cannot find GOROOT directory". Now the cache only stores paths that actually exist, so pre-install lookups recompute on each call until the tool lands somewhere concrete. Adds a unit test for the cache behavior and a slow e2e covering the original repro (go + a go-backend tool installed together with `--system`). Fixes #9526 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Code Review
This pull request fixes a bug where install_path() would cache a path before it actually existed on disk, leading to stale environment variables (like GOROOT) when tools were installed into shared directories mid-run. The changes ensure that the INSTALL_PATH_CACHE is only populated if the resolved path exists. The PR also includes a new E2E regression test and a unit test to verify this behavior. I have no feedback to provide as there were no review comments.
Greptile SummaryThis PR fixes a cache-poisoning bug in Confidence Score: 5/5Safe to merge — minimal, targeted fix with a well-scoped unit test and e2e regression test. The change is a one-liner guard ( No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant Installer
participant ToolVersion
participant Cache as INSTALL_PATH_CACHE
participant FS as Filesystem
Note over Installer,FS: Before fix — cache poisoned pre-install
Installer->>ToolVersion: install_path() [go not yet installed]
ToolVersion->>FS: find_in_shared_installs → user-dir (missing)
ToolVersion->>Cache: insert(stale user-dir path) ❌
Installer->>Installer: install go → system dir
Installer->>ToolVersion: install_path() [go now in system dir]
ToolVersion->>Cache: get() → stale user-dir path ❌
Note over Installer,FS: After fix — only cache when path exists
Installer->>ToolVersion: install_path() [go not yet installed]
ToolVersion->>FS: find_in_shared_installs → user-dir (missing)
ToolVersion->>FS: path.exists()? → false
Note right of ToolVersion: skip cache insert ✓
Installer->>Installer: install go → system dir
Installer->>ToolVersion: install_path() [go now in system dir]
ToolVersion->>FS: find_in_shared_installs → system dir (exists)
ToolVersion->>FS: path.exists()? → true
ToolVersion->>Cache: insert(system dir path) ✓
Cache-->>ToolVersion: system dir path ✓
Reviews (1): Last reviewed commit: "fix(install): don't cache nonexistent in..." | Re-trigger Greptile |
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.28 x -- echo |
23.8 ± 0.4 | 23.2 | 28.5 | 1.00 |
mise x -- echo |
24.5 ± 0.7 | 23.7 | 33.7 | 1.03 ± 0.03 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.28 env |
23.2 ± 0.5 | 22.5 | 29.1 | 1.00 |
mise env |
23.9 ± 0.6 | 23.1 | 29.9 | 1.03 ± 0.03 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.28 hook-env |
24.0 ± 0.3 | 23.2 | 26.1 | 1.00 |
mise hook-env |
25.2 ± 0.6 | 23.9 | 27.0 | 1.05 ± 0.03 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.28 ls |
22.2 ± 0.9 | 20.6 | 31.7 | 1.00 |
mise ls |
22.9 ± 0.5 | 21.2 | 24.2 | 1.03 ± 0.04 |
xtasks/test/perf
| Command | mise-2026.4.28 | mise | Variance |
|---|---|---|---|
| install (cached) | 160ms | 164ms | -2% |
| ls (cached) | 81ms | 83ms | -2% |
| bin-paths (cached) | 87ms | 88ms | -1% |
| task-ls (cached) | 805ms | 795ms | +1% |
### 🚀 Features - **(conda)** graduate conda backend out of experimental by @jdx in [#9544](#9544) - **(deps)** Add dart and flutter providers by @tjarvstrand in [#9505](#9505) - **(registry)** add neo4j by @mnm364 in [#9525](#9525) - **(registry)** add rustfs by @mnm364 in [#9530](#9530) - **(task)** support exclusion patterns in task sources by @jlarmstrongiv in [#9496](#9496) - **(vfox)** add stat function to lua file module by @esteve in [#9497](#9497) ### 🐛 Bug Fixes - **(backend)** flag regex prerelease versions by @jdx in [#9500](#9500) - **(backend)** mark -nightly/-canary/-experimental as prereleases by @jdx in [#9523](#9523) - **(backend)** suppress no-versions warning for unresolved-latest backends by @jdx in [#9548](#9548) - **(backend)** include dotnet prereleases from package flags by @jdx in [#9551](#9551) - **(backend)** scope PEP 440 prerelease detection to Python backends by @jdx in [#9558](#9558) - **(cargo)** Apply install_env during cargo install by @c22 in [#9502](#9502) - **(copr)** drop epel-9 chroots since rust >= 1.91 is unavailable by @jdx in [#9484](#9484) - **(github)** skip attestations on non-default api_url by @jdx in [#9486](#9486) - **(github)** retry ip allow list errors without auth by @risu729 in [#9506](#9506) - **(http)** update versions host tracking endpoint by @jdx in [#9527](#9527) - **(install)** don't warn for configured tools when version is passed via CLI by @jdx in [#9522](#9522) - **(install)** refresh latest before installing missing tools by @jdx in [#9545](#9545) - **(install)** don't cache nonexistent install paths by @jdx in [#9553](#9553) - **(lockfile)** don't propagate ad-hoc CLI overrides into the project lockfile by @jdx in [#9562](#9562) - **(plugin)** detect plugin types after cloning by @risu729 in [#9540](#9540) - **(release)** pass --no-git-checks to aube publish by @jdx in [#9483](#9483) - **(task)** convert PATH to MSYS Unix form when spawning POSIX shells on Windows by @JamBalaya56562 in [#9547](#9547) ### 📚 Documentation - **(contributing)** require popularity check for registry PRs by @jdx in [7bbeebe](7bbeebe) - **(watch)** update pitchfork domain to en.dev by @risu729 in [#9536](#9536) - document ghtkn GitHub token setup by @jdx in [#9546](#9546) - clarify registry backend acceptance policy by @jdx in [#9543](#9543) - Change exec command to use bash for variable echo by @kuboon in [#9567](#9567) ### 🧪 Testing - **(e2e)** run test-tool targets in parallel by @jdx in [#9564](#9564) - **(e2e)** run tests in parallel by @jdx in [#9563](#9563) - **(e2e)** bind-mount /tmp on disk and surface failed tests in CI summary by @jdx in [#9570](#9570) - **(tasks)** migrate test_task_help atask to usage field by @jdx in [#9549](#9549) ### 📦️ Dependency Updates - update fedora:45 docker digest to 8b838b3 by @renovate[bot] in [#9507](#9507) - update ghcr.io/jdx/mise:deb docker digest to f02194c by @renovate[bot] in [#9509](#9509) - update taiki-e/install-action digest to 7769b73 by @renovate[bot] in [#9512](#9512) - update ghcr.io/jdx/mise:alpine docker digest to 581f8a8 by @renovate[bot] in [#9508](#9508) - update rust crate ctor to v0.10.1 by @renovate[bot] in [#9515](#9515) - update ghcr.io/jdx/mise:rpm docker digest to a5c9655 by @renovate[bot] in [#9510](#9510) - update rust docker digest to a9cfb75 by @renovate[bot] in [#9511](#9511) - update rust crate age to v0.11.3 by @renovate[bot] in [#9514](#9514) - update rust crate jiff to v0.2.24 by @renovate[bot] in [#9516](#9516) - update dependency vitepress-plugin-tabs to ^0.9.0 by @renovate[bot] in [#9518](#9518) - update autofix-ci/action action to v1.3.4 by @renovate[bot] in [#9513](#9513) - update rust crate usage-lib to v3.2.1 by @renovate[bot] in [#9517](#9517) - update apple-actions/import-codesign-certs action to v7 by @renovate[bot] in [#9519](#9519) - update taiki-e/install-action digest to 51cd0b8 by @renovate[bot] in [#9531](#9531) - exclude taiki-e/install-action from renovate by @jdx in [#9532](#9532) - update rust crate blake3 to v1.8.5 by @renovate[bot] in [#9533](#9533) ### 📦 Registry - enable shellcheck on windows by @zeitlinger in [#9487](#9487) - add google-java-format by @zeitlinger in [#9488](#9488) - add expert ([aqua:expert-lsp/expert](https://github.com/expert-lsp/expert)) by @AlternateRT in [#9498](#9498) - update entry for checkmake by @eread in [#9504](#9504) - add systemctl-tui ([aqua:rgwood/systemctl-tui](https://github.com/rgwood/systemctl-tui)) by @2xdevv in [#9521](#9521) - add codon by @3w36zj6 in [#9538](#9538) - add tool yr (backend:github:VirusTotal/yara-x) by @adam-moss in [#9542](#9542) - add tool betterleaks (backend:aqua/betterleaks/betterleaks) by @adam-moss in [#9541](#9541) - add `git-filter-repo` by @garysassano in [#9550](#9550) - add umoci ([aqua:opencontainers/umoci](https://github.com/opencontainers/umoci)) by @2xdevv in [#9555](#9555) - add aqua backend for elixir-ls by @AlternateRT in [#9557](#9557) - deny inline backend options by @risu729 in [#9565](#9565) ### Chore - **(ci)** fail registry tests without summary by @jdx in [#9559](#9559) - **(ci)** use !cancelled() instead of always() for test-ci aggregator by @jdx in [#9569](#9569) - **(ci)** use namespace runners for ci jobs by @jdx in [#9561](#9561) - **(config)** deprecate shorthands_file setting by @risu729 in [#9534](#9534) - **(docs)** remove shrill.en.dev analytics script by @jdx in [#9539](#9539) - **(release)** replace bc with awk in release-plz star formatting by @jdx in [d7f177f](d7f177f) - bump hk to 1.44.3 by @jdx in [#9493](#9493) - invert CLAUDE.md/AGENTS.md so AGENTS.md is canonical by @jdx in [#9560](#9560) - set dev profile debug to 1 by @jdx in [#9572](#9572) ### New Contributors - @kuboon made their first contribution in [#9567](#9567) - @AlternateRT made their first contribution in [#9557](#9557) - @2xdevv made their first contribution in [#9555](#9555) - @adam-moss made their first contribution in [#9541](#9541) - @jlarmstrongiv made their first contribution in [#9496](#9496) - @tjarvstrand made their first contribution in [#9505](#9505)
Summary
mise install --systemfailed when installing bothgoand ago:backend tool in the same invocation, withgo: cannot find GOROOT directory: $MISE_DATA_DIR/installs/go/<ver>.ToolVersion::install_path()now only caches resolved paths that actually exist on disk, so pre-install lookups don't poison the cache with the user-dir fallback.Why
During
dependency_envfor thego:backend tool, mise resolved go'sinstall_pathbefore go was installed. At that pointfind_in_shared_installs()couldn't find go anywhere (the--systeminstalls dir didn't exist yet) and returned the user-dir path — whichinstall_path()then cached. After go installed at the system dir, callers like core go'sexec_envgot the stale user path back from the cache and exported it asGOROOT, breaking every go-backend install in the same run.The cache key is
(backend, version), so a freshly-builtToolVersionfromdependency_toolsetcollided with the earlier (stale) entry. The minimal correct fix is to only cache when the resolved path is real — pre-install lookups recompute on each call (cheap, only matters during install workflows) until the tool actually lands somewhere concrete.Test plan
install_path_does_not_cache_nonexistent_paths(src/toolset/tool_version.rs) verifies the cache stays empty for non-existent paths and populates after the dir exists.e2e/cli/test_install_system_go_backend_slowreproduces the original scenario:go+go:github.com/jdx/go-exampleinstalled together with--system --jobs 1, asserts both end up in the system installs dir.cargo test— all 819 unit tests pass.mise run lintclean.test_install_system,test_install_system_readonly,test_shared_install_dirsstill pass.🤖 Generated with Claude Code
Note
Medium Risk
Touches core
ToolVersion::install_path()path resolution/caching used broadly during installs, so mistakes could mis-route tool lookups; the change is small and covered by new unit/e2e regression tests.Overview
Fixes a regression where
mise install --system/--sharedcould cache an install path before a tool was actually installed, causing later lookups (notably GoGOROOT/GOPATHforgo:backends) to use a stale user-dir path.ToolVersion::install_path()now only writes toINSTALL_PATH_CACHEwhen the resolved path exists on disk, and adds a unit test plus a new slow e2e test that installsgoand ago:backend tool in the same--systeminvocation and asserts both land in the system installs dir.Reviewed by Cursor Bugbot for commit c99fc3f. Bugbot is set up for automated code reviews on this repo. Configure here.