Skip to content

fix(install): create runtime symlinks in system/shared install directories#8722

Merged
jdx merged 3 commits intomainfrom
fix/system-install-symlinks
Mar 23, 2026
Merged

fix(install): create runtime symlinks in system/shared install directories#8722
jdx merged 3 commits intomainfrom
fix/system-install-symlinks

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 23, 2026

Summary

  • Fixes mise install --system failing to create runtime symlinks (latest, partial version aliases) in the system/shared installs directory
  • runtime_symlinks::rebuild() now scans all install directories (user + shared/system) and creates symlinks in each one based on the versions actually present there, instead of only using backend.ba().installs_path
  • Adds e2e test that installs to both user and system dirs and verifies each gets correct symlinks

Fixes #8596

Test plan

  • New e2e test test_install_system passes with fix, fails without it
  • mise run lint passes
  • cargo check passes
  • Verify with core tools (node/python) installed via --system — the fix handles the case where arg_to_backend discards the system installs_path for core plugins

🤖 Generated with Claude Code


Note

Medium Risk
Touches runtime symlink generation/removal across multiple install roots and changes error-handling to tolerate permission issues in system/shared dirs, which could affect which versions latest/partial aliases resolve to.

Overview
Fixes runtime symlink creation for --system/shared installs by rebuilding latest and partial-version symlinks per install directory (user plus env::shared_install_dirs()), based only on the versions physically present in each directory.

Refactors runtime_symlinks::rebuild() into per-directory helpers, adds permission/read-only tolerance when updating shared/system locations, and includes a new e2e regression test ensuring user and system install roots get correct, non-broken latest symlinks.

Written by Cursor Bugbot for commit 34280bf. This will update automatically on new commits. Configure here.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical bug where system-wide installations of tools were not correctly generating runtime symlinks, leading to potential usability issues for users relying on shared or system-level tool management. The changes ensure that mise now properly manages symlinks across all configured installation paths, improving consistency and reliability for both user and system-level tool versions.

Highlights

  • System Install Symlink Fix: Resolved an issue where mise install --system failed to create runtime symlinks (e.g., latest, partial version aliases) in system or shared installation directories.
  • Enhanced Symlink Generation Logic: The runtime_symlinks::rebuild() function was updated to scan all relevant install directories (user, shared, and system) and generate symlinks based on the versions actually present in each specific directory, rather than relying solely on the backend's default installs path.
  • New End-to-End Test: Added a new end-to-end test (test_install_system) to verify that mise install --system correctly creates symlinks in the system install directory, and that user-specific symlinks remain unaffected.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR fixes mise install --system failing to create runtime symlinks (latest, partial version aliases) in shared/system install directories. Previously, runtime_symlinks::rebuild() only created symlinks under backend.ba().installs_path (the user dir); versions installed in system/shared dirs got no symlinks at all.

Key changes:

  • rebuild() now collects all relevant install roots for each backend: the user dir plus any shared/system dirs from env::shared_install_dirs(). Each directory is processed independently.
  • A new rebuild_symlinks_in_dir() helper encapsulates the per-directory logic, replacing the inline loop that only operated on the backend's primary path.
  • installed_versions_in_dir() replaces installed_versions() (which delegated to the backend): it scans the directory directly for real (non-symlink, non-incomplete) version subdirectories, ensuring each dir only gets symlinks for versions present in that dir.
  • Permission errors (EACCES, EROFS) from shared/system dir processing are caught via is_permission_error() and logged as warnings rather than aborting the install. The user dir continues to propagate errors normally.
  • A focused e2e regression test installs different tool versions to user and system dirs and verifies each gets its own correct latest symlink.

The all three concerns from prior reviews — permission propagation, EROFS handling, and ReadOnlyFilesystem MSRV compatibility — are fully addressed in this revision (MSRV is 1.88, well above Rust 1.83 where ErrorKind::ReadOnlyFilesystem was stabilized). The remaining redundant sort inside installed_versions_in_dir (versions sorted there and again in list_symlinks_for_dir) is harmless but was noted in a previous review thread.

Confidence Score: 4/5

  • Safe to merge with minor housekeeping remaining; the fix is correct and all prior review concerns are addressed.
  • All three concerns raised in prior review rounds (permission-error propagation from shared dirs, EROFS not caught, ReadOnlyFilesystem MSRV gap) are fully resolved. The core logic — scanning each install root independently and tolerating permission failures on non-user dirs — is sound. The e2e regression test directly exercises the bug scenario. The one remaining nit (redundant sort) is harmless and was already flagged in a prior thread; it doesn't affect correctness. No new logic or security issues were found.
  • No files require special attention; src/runtime_symlinks.rs is the critical path but the changes are well-scoped.

Important Files Changed

Filename Overview
src/runtime_symlinks.rs Core fix: rebuild() now discovers all install roots (user + shared/system) and independently rebuilds symlinks in each; permission errors on shared dirs are tolerated. Previous review concerns (EROFS, permission propagation) are addressed. Minor: redundant sort in installed_versions_in_dir (already flagged in prior review) is still present but harmless.
e2e/cli/test_install_system New regression test that installs different tool versions to user and system dirs and verifies each dir gets the correct latest symlink. Setup via MISE_SYSTEM_DATA_DIR is correct and the assertions directly exercise the fix.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[rebuild called] --> B[for each backend]
    B --> C[Collect installs dirs\nuser dir + shared_install_dirs]
    C --> D[Process user dir\nrebuild_symlinks_in_dir?]
    D -->|error| E[propagate error]
    D -->|ok| F[Process each shared/system dir\nrebuild_symlinks_in_dir?]
    F -->|permission error| G[warn + skip]
    F -->|other error| H[propagate error]
    F -->|ok| I[continue]

    subgraph rebuild_symlinks_in_dir
        J[list_symlinks_for_dir\nversions from dir scan] --> K[for each symlink\ncreate/update]
        K --> L[remove_missing_symlinks_in_dir\nclean dangling symlinks]
    end

    D & F --> rebuild_symlinks_in_dir
Loading

Reviews (3): Last reviewed commit: "fix: restore semver sort for installed v..." | Re-trigger Greptile

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

The pull request modifies the runtime symlink creation logic to support shared install directories. The changes involve collecting install directories from user and shared locations, iterating through them to create symlinks, and removing missing symlinks. Review feedback suggests verifying that file::dir_subdirs provides equivalent information to backend.list_installed_versions(), renaming the ba variable to backend_abstraction for better clarity, using a HashSet for installs_dirs to improve lookup complexity, handling the None case explicitly to avoid removing potentially valid symlinks, and handling the None case explicitly and ensuring that invalid versions are sorted appropriately.

I am having trouble creating individual review comments. Click here to see my feedback.

src/runtime_symlinks.rs (75-80)

high

The original installed_versions function used backend.list_installed_versions(). The new installed_versions_in_dir function replaces this with file::dir_subdirs(installs_dir). It's crucial to ensure that file::dir_subdirs provides the same information as backend.list_installed_versions() in all cases. If backend.list_installed_versions() performs additional filtering or processing, the new function might not be equivalent, potentially leading to incorrect symlink creation or removal.

Suggestion: Verify that file::dir_subdirs provides equivalent information to backend.list_installed_versions() or reimplement the filtering logic.

src/runtime_symlinks.rs (108)

high

The remove_missing_symlinks function now calls remove_missing_symlinks_in_dir with backend.ba().installs_path. This change ensures that missing symlinks are removed from the user install directory. However, it's important to ensure that this function correctly handles symlinks in all install directories (user + shared/system). If the logic for removing missing symlinks is different for shared/system directories, this change could introduce a bug.

Suggestion: Verify that remove_missing_symlinks_in_dir correctly handles symlinks in all install directories.

src/runtime_symlinks.rs (17)

medium

This change introduces a new variable ba without a clear explanation of its purpose. While it's derived from backend.ba(), adding a comment clarifying what ba represents (e.g., "backend abstraction") would improve readability.

Consider renaming ba to backend_abstraction for better clarity.

let backend_abstraction = backend.ba();

src/runtime_symlinks.rs (23-25)

medium

This check prevents adding duplicate install directories, which is good. However, the contains method on Vec has O(n) complexity. If the number of shared install directories grows, this could become a performance bottleneck. Consider using a HashSet for installs_dirs to improve the lookup complexity to O(1).

Suggestion: Use HashSet for installs_dirs.

use std::collections::HashSet;

// Collect all install directories for this backend: user dir + shared/system dirs
        let mut installs_dirs: HashSet<_> = vec![ba.installs_path.clone()].into_iter().collect();
        let tool_dir_name = ba.tool_dir_name();
        for shared_dir in env::shared_install_dirs() {
            let dir = shared_dir.join(&tool_dir_name);
            if dir.is_dir() && !installs_dirs.contains(&dir) {
                installs_dirs.insert(dir);
            }
        }

src/runtime_symlinks.rs (33-34)

medium

The condition file::resolve_symlink(&from)?.unwrap_or_default() != to checks if an existing symlink points to the correct target. However, unwrap_or_default() will return a default PathBuf if resolve_symlink returns None (i.e., the symlink doesn't exist or is broken). This could lead to unnecessary symlink removals if a broken symlink already exists.

Suggestion: Handle the None case explicitly to avoid removing potentially valid symlinks.

if is_runtime_symlink(&from) {
                        match file::resolve_symlink(&from)? {
                            Some(existing_target) if existing_target != to => {
                                trace!("Removing existing symlink: {}", from.display());
                                file::remove_file(&from)?;
                            }
                            _ => continue,
                        }
                    } else {

src/runtime_symlinks.rs (103)

medium

The sorted_by_cached_key function sorts the versions. This sorting is important for determining the latest symlink. However, the sorting key (Versioning::new(v), v.to_string()) might not be the most efficient. If Versioning::new(v) fails (returns None), it defaults to Versioning::new(0.0.0), which could lead to incorrect sorting if some versions are invalid.

Suggestion: Handle the None case explicitly and ensure that invalid versions are sorted appropriately (e.g., by placing them at the end of the list or logging an error).

.sorted_by_cached_key(|v| {
            Versioning::new(v).map_or_else(
                || {
                    eprintln!("Invalid version found: {}", v);
                    Versioning::new("0.0.0").unwrap()
                },
                |version| version
            )
        })

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 23, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.12 x -- echo 24.1 ± 0.4 23.4 26.6 1.00 ± 0.02
mise x -- echo 24.1 ± 0.4 23.4 26.8 1.00

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.12 env 23.8 ± 0.8 22.8 29.5 1.00 ± 0.04
mise env 23.6 ± 0.3 23.0 25.5 1.00

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.12 hook-env 24.2 ± 0.3 23.6 25.8 1.00
mise hook-env 24.5 ± 0.4 23.8 26.5 1.01 ± 0.02

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.3.12 ls 23.8 ± 0.4 22.9 29.5 1.00
mise ls 23.9 ± 0.3 23.0 25.3 1.00 ± 0.02

xtasks/test/perf

Command mise-2026.3.12 mise Variance
install (cached) 152ms 159ms -4%
ls (cached) 84ms 84ms +0%
bin-paths (cached) 87ms 86ms +1%
task-ls (cached) 848ms 833ms +1%

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.

jdx and others added 3 commits March 23, 2026 12:07
…ories

When tools are installed with `--system`, runtime symlinks (like `latest`)
were only created in the user installs directory, not the system directory.
This caused errors when the user dir didn't have the tool, or broken
symlinks when versions existed in different directories.

The fix makes `rebuild()` scan all install directories (user + shared/system)
and create symlinks in each one based on the versions actually present there.

Fixes #8596

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…build

Gracefully handle permission errors when updating symlinks in shared/system
install directories (e.g., root-owned dirs). Warns and continues instead of
aborting the entire rebuild. Also removes redundant sort in
installed_versions_in_dir.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restore the semantic version sort in installed_versions_in_dir — it
determines which version wins for `latest` via insertion order, so
lexicographic ordering from BTreeSet would produce wrong symlinks for
multi-digit version components (e.g. "8.0.0" after "18.0.0").

Also widen the permission error check to handle ReadOnlyFilesystem
(EROFS), which occurs on read-only mounted shared dirs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jdx jdx force-pushed the fix/system-install-symlinks branch from 618d71b to 34280bf Compare March 23, 2026 12:07
@jdx jdx merged commit 5e5bdc8 into main Mar 23, 2026
36 checks passed
@jdx jdx deleted the fix/system-install-symlinks branch March 23, 2026 12:38
mise-en-dev added a commit that referenced this pull request Mar 23, 2026
### 🐛 Bug Fixes

- **(env)** improve hook-env watch_files tracking and early-exits by
@rpendleton in [#8716](#8716)
- **(install)** create runtime symlinks in system/shared install
directories by @jdx in [#8722](#8722)
- apply --silent flag to global settings to suppress output by
@nkakouros in [#8720](#8720)

### 📦️ Dependency Updates

- ignore RUSTSEC-2026-0066 astral-tokio-tar advisory by @jdx in
[#8723](#8723)

### 📦 Registry

- add acli by @ggoggam in [#8721](#8721)

### New Contributors

- @rpendleton made their first contribution in
[#8716](#8716)
- @ggoggam made their first contribution in
[#8721](#8721)

## 📦 Aqua Registry Updates

#### Updated Packages (1)

- [`astral-sh/ty`](https://github.com/astral-sh/ty)
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