fix(config): resolve relative path: tool versions against config root#9320
fix(config): resolve relative path: tool versions against config root#9320
Conversation
Previously, `path:./packages/foo` in `[tools]` was stored as-is and later joined with the tool's installs_path, producing nonsense like `~/.local/share/mise/installs/foo/./packages/foo`. Resolve the value at parse time: expand `~/`, strip `./`, and anchor remaining relative paths to the config file's directory (or CWD for CLI args). Closes #9288 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
There was a problem hiding this comment.
Code Review
This pull request implements path resolution for path: tool versions, supporting relative paths, ./ prefixes, and ~/ expansion, along with a new E2E test. The review feedback highlights a potential issue where resolving paths to absolute values at parse time could break lockfile portability across different machines. There is also a suggestion to improve the robustness of home directory expansion by handling the bare ~ prefix.
| Some(("path", p)) => { | ||
| let path = resolve_path(p, &source); | ||
| Self::Path { | ||
| path, | ||
| options: backend.opts(), | ||
| backend, | ||
| source, | ||
| } | ||
| } |
There was a problem hiding this comment.
Resolving the path to an absolute path at parse time in ToolRequest::new breaks the portability of lockfiles. When mise lock is run, the resulting lockfile will contain an absolute path (or a ~/ prefixed path) that is specific to the current user's environment. If another user clones the repository to a different location, the path: tool will fail to resolve for them because the stored path is no longer relative to the project root.
To maintain portability while fixing the "garbage path" issue, consider storing the path as provided (relative) in the ToolRequest struct and resolving it to an absolute path only when the actual filesystem path is required (e.g., in install_path() or during ToolVersion resolution).
| if let Ok(rest) = p.strip_prefix("~/") { | ||
| return dirs::HOME.join(rest); | ||
| } |
There was a problem hiding this comment.
Using strip_prefix("~/") will not match a bare ~ (e.g., path:~). It is more robust to strip the ~ component, which handles both ~ and ~/ correctly as path components.
| if let Ok(rest) = p.strip_prefix("~/") { | |
| return dirs::HOME.join(rest); | |
| } | |
| if let Ok(rest) = p.strip_prefix("~") { | |
| return dirs::HOME.join(rest); | |
| } |
Greptile SummaryThis PR fixes a bug where relative Confidence Score: 5/5Safe to merge — fixes a real path-resolution bug with no introduced regressions or new failure modes. All changed logic is correct: paths are always resolved to absolute values at parse time, e2e/cli/test_tool_version_path — created non-executable (mode 100644); verify the test runner invokes it via Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["ToolRequest::new(backend, s, source)"] --> B{"s starts with 'path:'"}
B -- No --> C[Other variant: Version / Ref / Prefix / Sub / System]
B -- Yes --> D["resolve_path(p, &source)"]
D --> E{"starts with ~/"}
E -- Yes --> F["dirs::HOME.join(rest)"]
E -- No --> G{"is_absolute"}
G -- Yes --> H[pass through as-is]
G -- No --> I["strip leading ./"]
I --> J{"source.path() is Some"}
J -- Yes --> K["config_root(src).join(p)"]
J -- No --> L["dirs::CWD.join(p)"]
F & H & K & L --> M["Self::Path { path: absolute_path }"]
M --> N["version() calls path.display_user()"]
N --> O["/home/user/... rendered as ~/..."]
Reviews (1): Last reviewed commit: "fix(config): resolve relative path: tool..." | Re-trigger Greptile |
| @@ -0,0 +1,45 @@ | |||
| #!/usr/bin/env bash | |||
There was a problem hiding this comment.
Test file is non-executable (mode 100644)
The file is created with 100644 permissions. If the e2e runner discovers tests via glob and executes them directly (e.g., ./test_tool_version_path) rather than via bash test_tool_version_path, the script will fail with a permission-denied error. Other tests in e2e/cli/ that already pass should have mode 100755 — consider matching that convention here.
(No code change needed — just chmod +x e2e/cli/test_tool_version_path or set the git mode with git update-index --chmod=+x.)
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.19 x -- echo |
24.7 ± 3.5 | 22.9 | 57.0 | 1.00 ± 0.14 |
mise x -- echo |
24.7 ± 0.7 | 23.4 | 30.6 | 1.00 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.19 env |
23.5 ± 0.6 | 22.6 | 27.9 | 1.00 |
mise env |
24.1 ± 0.6 | 22.9 | 25.6 | 1.03 ± 0.03 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.19 hook-env |
24.6 ± 1.0 | 23.3 | 39.7 | 1.00 |
mise hook-env |
25.2 ± 1.0 | 23.7 | 37.3 | 1.02 ± 0.06 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.4.19 ls |
21.9 ± 0.4 | 21.0 | 24.7 | 1.00 |
mise ls |
22.5 ± 0.4 | 21.4 | 24.4 | 1.03 ± 0.03 |
xtasks/test/perf
| Command | mise-2026.4.19 | mise | Variance |
|---|---|---|---|
| install (cached) | 169ms | 174ms | -2% |
| ls (cached) | 79ms | 82ms | -3% |
| bin-paths (cached) | 84ms | 86ms | -2% |
| task-ls (cached) | 804ms | 796ms | +1% |
### 🐛 Bug Fixes - **(config)** resolve relative path: tool versions against config root by @jdx in [#9320](#9320) - **(lock)** resolve @latest and prune poisoned lockfile entries by @jdx in [#9321](#9321) - fix - be able to work with regex in attestation check by @monotek in [#9327](#9327) ### 🚜 Refactor - **(aqua)** bake aqua registry from merged yaml by @risu729 in [#9043](#9043) ### 📚 Documentation - add cross-site announcement banner by @jdx in [#9326](#9326) - keep banner height in sync via ResizeObserver by @jdx in [#9330](#9330) - respect banner expires field by @jdx in [#9334](#9334) ### 📦️ Dependency Updates - bump communique to 1.0.2 by @jdx in [#9313](#9313) - bump communique to 1.0.3 by @jdx in [#9332](#9332) - update actions/setup-node digest to 48b55a0 by @renovate[bot] in [#9339](#9339) - update ghcr.io/jdx/mise:alpine docker digest to a92efa5 by @renovate[bot] in [#9340](#9340) - update ghcr.io/jdx/mise:rpm docker digest to 5c24f69 by @renovate[bot] in [#9343](#9343) - update rust docker digest to e4f09e8 by @renovate[bot] in [#9345](#9345) - update rui314/setup-mold digest to 9c9c13b by @renovate[bot] in [#9344](#9344) - update ghcr.io/jdx/mise:deb docker digest to a3afe3e by @renovate[bot] in [#9342](#9342) - update ghcr.io/jdx/mise:copr docker digest to 4098d5a by @renovate[bot] in [#9341](#9341) - update taiki-e/install-action digest to 74e87cb by @renovate[bot] in [#9346](#9346) ### Chore - **(ci)** remove cargo-vendor install from ppa publish by @jdx in [#9312](#9312) - **(release)** publish snap to stable channel by @jdx in [#9318](#9318) - remove FUNDING.yml in favor of jdx/.github default by @jdx in [#9331](#9331) ## 📦 Aqua Registry Updated [aqua-registry](https://github.com/aquaproj/aqua-registry): [v4.492.0](https://github.com/aquaproj/aqua-registry/releases/tag/v4.492.0) -> [v4.498.0](https://github.com/aquaproj/aqua-registry/releases/tag/v4.498.0). Included aqua-registry releases: - [v4.493.0](https://github.com/aquaproj/aqua-registry/releases/tag/v4.493.0) - [v4.494.0](https://github.com/aquaproj/aqua-registry/releases/tag/v4.494.0) - [v4.494.1](https://github.com/aquaproj/aqua-registry/releases/tag/v4.494.1) - [v4.495.0](https://github.com/aquaproj/aqua-registry/releases/tag/v4.495.0) - [v4.496.0](https://github.com/aquaproj/aqua-registry/releases/tag/v4.496.0) - [v4.497.0](https://github.com/aquaproj/aqua-registry/releases/tag/v4.497.0) - [v4.498.0](https://github.com/aquaproj/aqua-registry/releases/tag/v4.498.0)
Summary
Fixes #9288.
path:values in[tools]were stored as-is, then later joined with the tool'sinstalls_pathwhen resolving the install directory. For absolute paths this works becausePath::joinreplaces when given an absolute path, but for relative paths likepath:./packages/logrit produced garbage:~/.local/share/mise/installs/logr/./packages/logrNow
ToolRequest::newresolvespath:at parse time:~/expands to$HOME./is strippedconfig_root(source)when the source is a config file, or CWD for CLI argsToolRequest::version()now renders the stored PathBuf viadisplay_user()so~/substitution is preserved inmise lsoutput and lockfiles.Test plan
e2e/cli/test_tool_version_pathcoveringpath:./x, bare relativepath:x,path:~/x, and absolute pathse2e/cli/test_usepath:~/workdir/mydummyassertion still passescargo checkandmise run lint-fixcleantoolset::tool_requestpass🤖 Generated with Claude Code
Note
Medium Risk
Changes how
path:tool versions are parsed and displayed, which can affect tool resolution, lockfile entries, andmise where/lsoutput for existing configs using relative paths.Overview
Fixes
path:tool versions so relative paths are resolved at parse time against the config file’s root (or CWD for CLI usage), with~/expansion and./stripping, preventing bogus install-path joins.Updates
ToolRequest::version()to renderpath:values viadisplay_user()(preserving~/in output/lockfiles) and adds an e2e test covering relative,~/, and absolutepath:cases.Reviewed by Cursor Bugbot for commit 27e0c90. Bugbot is set up for automated code reviews on this repo. Configure here.