Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,19 @@ fn codegen_registry() {
.collect::<Vec<_>>()
})
.unwrap_or_default();
let overrides = info
.get("overrides")
.map(|overrides| {
overrides
.as_array()
.unwrap()
.iter()
.map(|f| f.as_str().unwrap().to_string())
.collect::<Vec<_>>()
Comment thread
risu729 marked this conversation as resolved.
})
.unwrap_or_default();
let rt = format!(
r#"RegistryTool{{short: "{short}", description: {description}, backends: &[{backends}], aliases: &[{aliases}], test: &{test}, os: &[{os}], depends: &[{depends}], idiomatic_files: &[{idiomatic_files}]}}"#,
r#"RegistryTool{{short: "{short}", description: {description}, backends: &[{backends}], aliases: &[{aliases}], test: &{test}, os: &[{os}], depends: &[{depends}], idiomatic_files: &[{idiomatic_files}], overrides: &[{overrides}]}}"#,
description = description
.map(|d| format!("Some({})", raw_string_literal(&d)))
.unwrap_or("None".to_string()),
Expand Down Expand Up @@ -220,6 +231,11 @@ fn codegen_registry() {
.map(|f| format!("\"{f}\""))
.collect::<Vec<_>>()
.join(", "),
overrides = overrides
.iter()
.map(|f| format!("\"{f}\""))
.collect::<Vec<_>>()
.join(", "),
);
lines.push(format!(r#" ("{short}", {rt}),"#));
for alias in aliases {
Expand Down
21 changes: 21 additions & 0 deletions e2e/registry/test_overrides
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash

# Test 1: node defined before npm
cat >mise.toml <<EOF
[tools]
node = "25.2.1"
npm = "11.7.0"
EOF

mise install
assert_contains "mise x -- npm --version" "11.7.0"

# Test 2: npm defined before node
cat >mise.toml <<EOF
[tools]
npm = "11.7.0"
node = "25.2.1"
EOF

mise install
assert_contains "mise x -- npm --version" "11.7.0"
Comment thread
risu729 marked this conversation as resolved.
1 change: 1 addition & 0 deletions registry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3288,6 +3288,7 @@ description = "Find outdated or deprecated Helm charts running in your cluster"
[tools.npm]
backends = ["npm:npm"]
description = "the package manager for JavaScript"
overrides = ["node"]
test = ["npm --version", "{{version}}"]

[tools.nsc]
Expand Down
7 changes: 7 additions & 0 deletions schema/mise-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@
"type": "string"
},
"description": "Files that indicate this tool should be used in a project"
},
"overrides": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of tool IDs that this tool overrides. When a tool overrides another, its installation directory is placed earlier in PATH so its binaries take precedence over the overridden tools' binaries."
}
},
"required": ["backends"],
Expand Down
1 change: 1 addition & 0 deletions src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub struct RegistryTool {
pub backends: &'static [RegistryBackend],
#[allow(unused)]
pub aliases: &'static [&'static str],
pub overrides: &'static [&'static str],
pub test: &'static Option<(&'static str, &'static str)>,
pub os: &'static [&'static str],
pub depends: &'static [&'static str],
Expand Down
33 changes: 27 additions & 6 deletions src/toolset/toolset_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ use std::sync::LazyLock as Lazy;

use crate::config::Config;
use crate::config::env_directive::EnvResults;
use crate::registry::REGISTRY;
use crate::toolset::Toolset;
use crate::uv;
use itertools::Itertools;

// Cache Toolset::list_paths results across identical toolsets within a process.
// Keyed by project_root plus sorted list of backend@version pairs currently installed.
Expand All @@ -21,21 +23,40 @@ impl Toolset {
if let Some(root) = &config.project_root {
key_parts.push(root.to_string_lossy().to_string());
}
let mut installed: Vec<String> = self
.list_current_installed_versions(config)
.into_iter()
let mut installed = self.list_current_installed_versions(config);

let installed_strs: Vec<String> = installed
.iter()
.map(|(p, tv)| format!("{}@{}", p.id(), tv.version))
.sorted()
.collect();
installed.sort();
key_parts.extend(installed);
key_parts.extend(installed_strs);

let cache_key = key_parts.join("|");
if let Some(entry) = LIST_PATHS_CACHE.get(&cache_key) {
trace!("toolset.list_paths hit cache");
return entry.clone();
}

installed.sort_by(|(a, _), (b, _)| {
let id_a = a.id();
let id_b = b.id();

if let Some(tool_a) = REGISTRY.get(id_a)
&& tool_a.overrides.contains(&id_b)
{
return std::cmp::Ordering::Less;
}
if let Some(tool_b) = REGISTRY.get(id_b)
&& tool_b.overrides.contains(&id_a)
{
return std::cmp::Ordering::Greater;
}
Comment thread
risu729 marked this conversation as resolved.
std::cmp::Ordering::Equal
});

Comment on lines +41 to +57

Copilot AI Jan 5, 2026

Copy link

Choose a reason for hiding this comment

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

The comparison function violates the transitivity and totality requirements for a proper sort comparator. While it works for simple cases like "npm overrides node", it will fail if the registry includes chains of overrides (A overrides B, B overrides C).

For example, if npm overrides node and node overrides another tool X:

  • npm vs node returns Less (npm < node)
  • node vs X returns Less (node < X)
  • npm vs X returns Equal (should return Less to maintain transitivity)

This violates the total order requirement and can lead to undefined behavior in Rust's sort implementation, potentially causing panics or incorrect ordering.

To fix this, implement a proper topological sort that builds a dependency graph and orders tools accordingly. Alternatively, compute the full transitive closure of override relationships and use that for comparisons. Also consider adding validation in build.rs to detect and reject circular override relationships.

Suggested change
installed.sort_by(|(a, _), (b, _)| {
let id_a = a.id();
let id_b = b.id();
if let Some(tool_a) = REGISTRY.get(id_a)
&& tool_a.overrides.contains(&id_b)
{
return std::cmp::Ordering::Less;
}
if let Some(tool_b) = REGISTRY.get(id_b)
&& tool_b.overrides.contains(&id_a)
{
return std::cmp::Ordering::Greater;
}
std::cmp::Ordering::Equal
});
// Order installed tools using a topological sort based on override relationships.
// Tools that override others come earlier; unrelated tools are ordered by their IDs.
let n = installed.len();
if n > 1 {
// Precompute tool IDs for deterministic tie-breaking.
let ids: Vec<String> = installed
.iter()
.map(|(p, _)| p.id().to_string())
.collect();
// Build adjacency list and in-degree counts for Kahn's algorithm.
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut indegree: Vec<usize> = vec![0; n];
for i in 0..n {
let id_i = installed[i].0.id();
if let Some(tool_i) = REGISTRY.get(id_i) {
for j in 0..n {
if i == j {
continue;
}
let id_j = installed[j].0.id();
if tool_i.overrides.contains(&id_j) {
adj[i].push(j);
indegree[j] += 1;
}
}
}
}
// Kahn's topological sort with ID-based tie-breaking for a total, deterministic order.
let mut zero_indegree: Vec<usize> = (0..n).filter(|&i| indegree[i] == 0).collect();
zero_indegree.sort_by(|&i, &j| ids[i].cmp(&ids[j]));
let mut order: Vec<usize> = Vec::with_capacity(n);
let mut seen: Vec<bool> = vec![false; n];
while !zero_indegree.is_empty() {
// Always take the smallest ID among zero in-degree nodes for determinism.
let u = zero_indegree.remove(0);
if seen[u] {
continue;
}
seen[u] = true;
order.push(u);
for &v in &adj[u] {
if indegree[v] > 0 {
indegree[v] -= 1;
if indegree[v] == 0 {
zero_indegree.push(v);
}
}
}
// Re-sort candidates after updates to maintain ID-based ordering.
zero_indegree.sort_by(|&i, &j| ids[i].cmp(&ids[j]));
}
if order.len() < n {
// Fallback for cycles or unreachable nodes: append remaining nodes,
// ordered by ID, so we still produce a total order.
let mut remaining: Vec<usize> = (0..n).filter(|&i| !seen[i]).collect();
remaining.sort_by(|&i, &j| ids[i].cmp(&ids[j]));
order.extend(remaining);
}
// Rebuild `installed` according to the computed order.
let mut reordered = Vec::with_capacity(n);
for idx in order {
reordered.push(installed[idx].clone());
}
installed = reordered;
}

Copilot uses AI. Check for mistakes.

@risu729 risu729 Jan 5, 2026

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.

I think we don't need to support chain/circular overrides for simplicity for now.

let mut paths: Vec<PathBuf> = Vec::new();
for (p, tv) in self.list_current_installed_versions(config).into_iter() {
for (p, tv) in installed {
let start = std::time::Instant::now();
let new_paths = p.list_bin_paths(config, &tv).await.unwrap_or_else(|e| {
warn!("Error listing bin paths for {tv}: {e:#}");
Expand Down
Loading