From e0faf4eba8e2247be904b75c926874b485674346 Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:31:39 +0900 Subject: [PATCH 1/9] fix(rust): normalize rustup profile aliases Entire-Checkpoint: 01M1QXC89YR5S6MCVXAXVDYNDP --- e2e/core/test_rust_components_reconcile | 17 ++++++++++++++--- src/plugins/core/rust.rs | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/e2e/core/test_rust_components_reconcile b/e2e/core/test_rust_components_reconcile index 7653083c35d..c632fafb583 100644 --- a/e2e/core/test_rust_components_reconcile +++ b/e2e/core/test_rust_components_reconcile @@ -65,7 +65,7 @@ case "$1 $2" in ;; esac done - if ! $toolchain_exists && [[ -z "$profile" || "$profile" == "default" ]]; then + if ! $toolchain_exists && [[ -z "$profile" || "$profile" == "default" || "$profile" == "d" ]]; then printf '%s\n' clippy rust-docs rustfmt >>"$MISE_CARGO_HOME/components" fi sort -u "$MISE_CARGO_HOME/components" -o "$MISE_CARGO_HOME/components" 2>/dev/null || true @@ -123,6 +123,17 @@ assert_contains "cat '$MISE_CARGO_HOME/components'" "clippy" assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-docs" assert_contains "cat '$MISE_CARGO_HOME/components'" "rustfmt" +cat >mise.toml <&1" "would install" +mise install +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "5" +assert_contains "cat '$MISE_CARGO_HOME/components'" "rustfmt" + cat >mise.toml <&1" "would install" mise exec -- rustc -V -assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "5" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "6" assert_contains "cat '$MISE_CARGO_HOME/components'" "rustfmt" assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-src" assert_contains "cat '$MISE_CARGO_HOME/targets'" "wasm32-unknown-unknown" mise install -assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "5" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "6" assert_contains "mise install rust --dry-run-code 2>&1" "already installed" diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index 580b8c366a2..ebbf4191d98 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -347,6 +347,7 @@ impl Backend for RustPlugin { Some(profile) => profile, None => self.rustup_default_profile(tv, &runtime)?, }; + let effective_profile = normalize_rustup_profile(&effective_profile)?; // Query components even when none were explicitly requested. This // verifies that rustup still has the toolchain represented by mise's @@ -523,6 +524,7 @@ impl Backend for RustPlugin { Some(profile) => profile.to_string(), None => self.rustup_default_profile(&tv, &runtime)?, }; + let effective_profile = normalize_rustup_profile(&effective_profile)?; let mut components = components.unwrap_or_default(); if effective_profile == "default" { components.extend( @@ -1088,6 +1090,17 @@ fn rustup_path_env(runtime: &RustRuntime) -> Result { )?) } +fn normalize_rustup_profile(profile: &str) -> Result<&'static str> { + match profile { + "minimal" | "m" => Ok("minimal"), + "default" | "d" | "" => Ok("default"), + "complete" | "c" => Ok("complete"), + _ => bail!( + "unknown rustup profile name: {profile}; valid profile names are: minimal, default, complete" + ), + } +} + fn rustup_component_installed(installed: &BTreeSet, component: &str) -> bool { installed.iter().any(|item| { item == component @@ -1289,6 +1302,18 @@ mod tests { assert!(!rustup_component_installed(&installed, "llvm")); } + #[test] + fn profile_aliases_match_rustup() { + assert_eq!(normalize_rustup_profile("minimal").unwrap(), "minimal"); + assert_eq!(normalize_rustup_profile("m").unwrap(), "minimal"); + assert_eq!(normalize_rustup_profile("default").unwrap(), "default"); + assert_eq!(normalize_rustup_profile("d").unwrap(), "default"); + assert_eq!(normalize_rustup_profile("").unwrap(), "default"); + assert_eq!(normalize_rustup_profile("complete").unwrap(), "complete"); + assert_eq!(normalize_rustup_profile("c").unwrap(), "complete"); + assert!(normalize_rustup_profile("custom").is_err()); + } + #[test] fn rust_idiomatic_options_override_tool_options() { let opts = opts_with("profile", "minimal"); From 6a035c89034178d4dbf946f8b47cd20c0d45f257 Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:35:28 +0900 Subject: [PATCH 2/9] fix(rust): reconcile required profile components Entire-Checkpoint: 01M1QXK879PFM076V6DJA938XX --- docs/lang/rust.md | 3 + e2e/core/test_rust_components_reconcile | 52 +++++++-- e2e/core/test_rust_config_env_homes | 2 +- e2e/core/test_rust_external_provider | 7 ++ src/plugins/core/rust.rs | 149 +++++++++++++++++++----- 5 files changed, 171 insertions(+), 42 deletions(-) diff --git a/docs/lang/rust.md b/docs/lang/rust.md index 916a7de8fa6..ab5a0d30f20 100644 --- a/docs/lang/rust.md +++ b/docs/lang/rust.md @@ -140,6 +140,9 @@ If not set, it defaults to the profile configured in `rustup`. You can check you "rust" = { version = "1.83.0", profile = "minimal" } ``` +If the Rust toolchain is already installed, `mise install` restores missing components implied by +the `minimal` or `default` profile. + ### `targets` The `targets` option specifies platforms to install for cross-compilation. Multiple targets can diff --git a/e2e/core/test_rust_components_reconcile b/e2e/core/test_rust_components_reconcile index c632fafb583..20d95eee65d 100644 --- a/e2e/core/test_rust_components_reconcile +++ b/e2e/core/test_rust_components_reconcile @@ -22,26 +22,34 @@ cat >"$MISE_CARGO_HOME/bin/rustup" <<'EOF' #!/usr/bin/env bash set -euo pipefail +host=x86_64-unknown-linux-gnu + echo "$*" >>"$MISE_CARGO_HOME/rustup.log" case "$1 $2" in "show profile") echo default ;; + "show active-toolchain") + toolchain="${RUSTUP_TOOLCHAIN:-}" + [[ -d "$MISE_RUSTUP_HOME/toolchains/$toolchain-$host" ]] || exit 1 + echo "$toolchain-$host (environment override by RUSTUP_TOOLCHAIN)" + ;; "component list") toolchain="${*: -1}" - [[ -d "$MISE_RUSTUP_HOME/toolchains/$toolchain" ]] || exit 1 + [[ -d "$MISE_RUSTUP_HOME/toolchains/$toolchain-$host" ]] || exit 1 cat "$MISE_CARGO_HOME/components" 2>/dev/null || true ;; "target list") toolchain="${*: -1}" - [[ -d "$MISE_RUSTUP_HOME/toolchains/$toolchain" ]] || exit 1 + [[ -d "$MISE_RUSTUP_HOME/toolchains/$toolchain-$host" ]] || exit 1 cat "$MISE_CARGO_HOME/targets" 2>/dev/null || true ;; "toolchain install") shift 2 toolchain="$1" shift + toolchain="$toolchain-$host" toolchain_exists=false [[ -d "$MISE_RUSTUP_HOME/toolchains/$toolchain" ]] && toolchain_exists=true mkdir -p "$MISE_RUSTUP_HOME/toolchains/$toolchain" @@ -49,7 +57,11 @@ case "$1 $2" in while [[ $# -gt 0 ]]; do case "$1" in --component) - echo "$2" >>"$MISE_CARGO_HOME/components" + if [[ "$2" == "rust-src" ]]; then + echo "$2" >>"$MISE_CARGO_HOME/components" + else + echo "$2-$host" >>"$MISE_CARGO_HOME/components" + fi shift 2 ;; --target) @@ -65,8 +77,11 @@ case "$1 $2" in ;; esac done - if ! $toolchain_exists && [[ -z "$profile" || "$profile" == "default" || "$profile" == "d" ]]; then - printf '%s\n' clippy rust-docs rustfmt >>"$MISE_CARGO_HOME/components" + if ! $toolchain_exists; then + printf '%s\n' "cargo-$host" "rust-std-$host" "rustc-$host" >>"$MISE_CARGO_HOME/components" + if [[ -z "$profile" || "$profile" == "default" || "$profile" == "d" ]]; then + printf '%s\n' "clippy-$host" "rust-docs-$host" "rustfmt-$host" >>"$MISE_CARGO_HOME/components" + fi fi sort -u "$MISE_CARGO_HOME/components" -o "$MISE_CARGO_HOME/components" 2>/dev/null || true sort -u "$MISE_CARGO_HOME/targets" -o "$MISE_CARGO_HOME/targets" 2>/dev/null || true @@ -97,13 +112,13 @@ EOF mise install assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "1" -sed -i '/^rust-docs$/d' "$MISE_CARGO_HOME/components" +sed -i '/^rust-docs-/d' "$MISE_CARGO_HOME/components" assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" mise install assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "2" assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-docs" -rm -rf "$MISE_RUSTUP_HOME/toolchains/1.81.0" +rm -rf "$MISE_RUSTUP_HOME/toolchains/1.81.0-x86_64-unknown-linux-gnu" assert_hook_env_does_not_call_rustup assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" mise install @@ -114,7 +129,7 @@ cat >mise.toml <&1" "would install" mise install @@ -128,12 +143,27 @@ cat >mise.toml <&1" "would install" mise install assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "5" assert_contains "cat '$MISE_CARGO_HOME/components'" "rustfmt" +cat >mise.toml <>"$MISE_CARGO_HOME/components" +sed -i '/^cargo-x86_64-unknown-linux-gnu$/d; /^rustc-x86_64-unknown-linux-gnu$/d; /^rust-std-x86_64-unknown-linux-gnu$/d' \ + "$MISE_CARGO_HOME/components" +assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" +mise install +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "6" +assert_contains "cat '$MISE_CARGO_HOME/components'" "cargo-x86_64-unknown-linux-gnu" +assert_contains "cat '$MISE_CARGO_HOME/components'" "rustc-x86_64-unknown-linux-gnu" +assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-std-x86_64-unknown-linux-gnu" + cat >mise.toml <&1" "would install" mise exec -- rustc -V -assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "6" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "7" assert_contains "cat '$MISE_CARGO_HOME/components'" "rustfmt" assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-src" assert_contains "cat '$MISE_CARGO_HOME/targets'" "wasm32-unknown-unknown" mise install -assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "6" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "7" assert_contains "mise install rust --dry-run-code 2>&1" "already installed" diff --git a/e2e/core/test_rust_config_env_homes b/e2e/core/test_rust_config_env_homes index 4e49e3f9c62..9c9a9beb617 100644 --- a/e2e/core/test_rust_config_env_homes +++ b/e2e/core/test_rust_config_env_homes @@ -31,7 +31,7 @@ case "$1 $2" in echo minimal ;; "component list") - echo rustfmt + printf '%s\n' cargo rust-std rustc rustfmt ;; "toolchain install") mkdir -p "$RUSTUP_HOME/toolchains/$3" diff --git a/e2e/core/test_rust_external_provider b/e2e/core/test_rust_external_provider index 02af52d55ad..e24a6b65d2e 100644 --- a/e2e/core/test_rust_external_provider +++ b/e2e/core/test_rust_external_provider @@ -28,6 +28,9 @@ mkdir -p "$STATE" "show profile") echo minimal ;; + "show active-toolchain") + echo "$RUSTUP_TOOLCHAIN-x86_64-unknown-linux-gnu (environment override)" + ;; "toolchain install") version=$3 mkdir -p "$STATE/toolchains/$version" @@ -55,6 +58,10 @@ mkdir -p "$STATE" rm -rf "$STATE/toolchains/$3" ;; "component list") + printf '%s\n' \ + cargo-x86_64-unknown-linux-gnu \ + rust-std-x86_64-unknown-linux-gnu \ + rustc-x86_64-unknown-linux-gnu cat "$STATE/components" 2>/dev/null || true ;; "target list") diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index ebbf4191d98..e4cc3619b5e 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -28,6 +28,7 @@ pub(super) struct RustPlugin { const RUST_NIGHTLY_MANIFEST_URL: &str = "https://static.rust-lang.org/dist/channel-rust-nightly.toml"; +const RUST_MINIMAL_PROFILE_COMPONENTS: &[&str] = &["cargo", "rust-std", "rustc"]; const RUST_DEFAULT_PROFILE_COMPONENTS: &[&str] = &["clippy", "rust-docs", "rustfmt"]; fn parse_nightly_manifest(manifest: &str) -> Result { @@ -286,14 +287,53 @@ impl RustPlugin { Ok(profile) } + fn rustup_active_toolchain( + &self, + tv: &ToolVersion, + runtime: &RustRuntime, + ) -> Result> { + let args = vec!["show".to_string(), "active-toolchain".to_string()]; + let mut cmd = cmd(runtime.bin_dir.join(RUSTUP_BIN), args) + .env("PATH", rustup_path_env(runtime)?) + .stdout_capture() + .stderr_capture() + .unchecked(); + for (key, value) in rustup_env(&runtime.homes, &tv.version) { + cmd = cmd.env(key, value); + } + let output = match cmd.run() { + Ok(output) if output.status.success() => output, + Ok(output) => { + debug!( + "rustup show active-toolchain failed for {}: {}", + tv.style(), + String::from_utf8_lossy(&output.stderr).trim() + ); + return Ok(None); + } + Err(err) => { + debug!( + "rustup show active-toolchain failed for {}: {err:#}", + tv.style() + ); + return Ok(None); + } + }; + Ok(String::from_utf8_lossy(&output.stdout) + .split_whitespace() + .next() + .map(String::from)) + } + fn missing_components( &self, requested: &[String], installed: &BTreeSet, + host: Option<&str>, ) -> Vec { requested .iter() - .filter(|component| !rustup_component_installed(installed, component)) + .filter(|component| !rustup_component_installed(installed, component, host)) .cloned() .collect() } @@ -358,18 +398,15 @@ impl Backend for RustPlugin { }; let mut required_components = components.unwrap_or_default(); - if effective_profile == "default" { - required_components.extend( - RUST_DEFAULT_PROFILE_COMPONENTS - .iter() - .map(|component| (*component).to_string()), - ); - } + required_components.extend(fallback_rustup_profile_components(effective_profile)); required_components.sort(); required_components.dedup(); if !required_components.is_empty() { - let missing = self.missing_components(&required_components, &installed_components); + let active_toolchain = self.rustup_active_toolchain(tv, &runtime)?; + let host = active_toolchain.as_deref().and_then(rustup_toolchain_host); + let missing = + self.missing_components(&required_components, &installed_components, host); if !missing.is_empty() { debug!( "{} missing rustup component(s): {}", @@ -526,15 +563,9 @@ impl Backend for RustPlugin { }; let effective_profile = normalize_rustup_profile(&effective_profile)?; let mut components = components.unwrap_or_default(); - if effective_profile == "default" { - components.extend( - RUST_DEFAULT_PROFILE_COMPONENTS - .iter() - .map(|component| (*component).to_string()), - ); - components.sort(); - components.dedup(); - } + components.extend(fallback_rustup_profile_components(effective_profile)); + components.sort(); + components.dedup(); let mut cmd = CmdLineRunner::new(runtime.bin_dir.join(RUSTUP_BIN)) .with_pr(ctx.pr.as_ref()) @@ -1101,13 +1132,47 @@ fn normalize_rustup_profile(profile: &str) -> Result<&'static str> { } } -fn rustup_component_installed(installed: &BTreeSet, component: &str) -> bool { +fn fallback_rustup_profile_components(profile: &str) -> Vec { + let mut components = match profile { + "minimal" | "default" => RUST_MINIMAL_PROFILE_COMPONENTS + .iter() + .map(|component| (*component).to_string()) + .collect::>(), + "complete" => Vec::new(), + _ => unreachable!("profile was normalized"), + }; + if profile == "default" { + components.extend( + RUST_DEFAULT_PROFILE_COMPONENTS + .iter() + .map(|component| (*component).to_string()), + ); + } + components +} + +fn rustup_toolchain_host(toolchain: &str) -> Option<&str> { + if rustup_component_suffix_is_host_triple(toolchain) { + return Some(toolchain); + } + toolchain.match_indices('-').find_map(|(index, _)| { + let suffix = &toolchain[index + 1..]; + rustup_component_suffix_is_host_triple(suffix).then_some(suffix) + }) +} + +fn rustup_component_installed( + installed: &BTreeSet, + component: &str, + host: Option<&str>, +) -> bool { installed.iter().any(|item| { item == component - || item - .strip_prefix(component) - .and_then(|suffix| suffix.strip_prefix('-')) - .is_some_and(rustup_component_suffix_is_host_triple) + || host.is_some_and(|host| { + item.strip_prefix(component) + .and_then(|suffix| suffix.strip_prefix('-')) + == Some(host) + }) }) } @@ -1289,17 +1354,41 @@ mod tests { } #[test] - fn rustup_component_matching_allows_host_suffixes() { - let installed = BTreeSet::from([ + fn rustup_component_matching_requires_the_selected_host() { + let mut installed = BTreeSet::from([ "rust-src".to_string(), "llvm-tools-x86_64-unknown-linux-gnu".to_string(), + "rust-std-wasm32-unknown-unknown".to_string(), ]); + let host = rustup_toolchain_host("1.81.0-x86_64-unknown-linux-gnu"); - assert!(rustup_component_installed(&installed, "rust-src")); - assert!(rustup_component_installed(&installed, "llvm-tools")); - assert!(!rustup_component_installed(&installed, "rustfmt")); - assert!(!rustup_component_installed(&installed, "rust")); - assert!(!rustup_component_installed(&installed, "llvm")); + assert_eq!(host, Some("x86_64-unknown-linux-gnu")); + assert!(rustup_component_installed(&installed, "rust-src", host)); + assert!(rustup_component_installed(&installed, "llvm-tools", host)); + assert!(!rustup_component_installed(&installed, "rust-std", host)); + assert!(!rustup_component_installed(&installed, "rustfmt", host)); + installed.insert("rust-std-x86_64-unknown-linux-gnu".to_string()); + assert!(rustup_component_installed(&installed, "rust-std", host)); + } + + #[test] + fn required_profile_components_match_rustup() { + assert_eq!( + fallback_rustup_profile_components("minimal"), + ["cargo", "rust-std", "rustc"] + ); + assert_eq!( + fallback_rustup_profile_components("default"), + [ + "cargo", + "rust-std", + "rustc", + "clippy", + "rust-docs", + "rustfmt" + ] + ); + assert!(fallback_rustup_profile_components("complete").is_empty()); } #[test] From 2621ef7d82db883f3c779c9901d2035171559920 Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:54:22 +0900 Subject: [PATCH 3/9] fix(rust): restore GNU Windows profile components Entire-Checkpoint: 01M1QYNW3TX7FX8YRFWMM6WEBB --- src/plugins/core/rust.rs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index e4cc3619b5e..dddc3675c07 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -388,6 +388,8 @@ impl Backend for RustPlugin { None => self.rustup_default_profile(tv, &runtime)?, }; let effective_profile = normalize_rustup_profile(&effective_profile)?; + let active_toolchain = self.rustup_active_toolchain(tv, &runtime)?; + let host = active_toolchain.as_deref().and_then(rustup_toolchain_host); // Query components even when none were explicitly requested. This // verifies that rustup still has the toolchain represented by mise's @@ -398,13 +400,11 @@ impl Backend for RustPlugin { }; let mut required_components = components.unwrap_or_default(); - required_components.extend(fallback_rustup_profile_components(effective_profile)); + required_components.extend(fallback_rustup_profile_components(effective_profile, host)); required_components.sort(); required_components.dedup(); if !required_components.is_empty() { - let active_toolchain = self.rustup_active_toolchain(tv, &runtime)?; - let host = active_toolchain.as_deref().and_then(rustup_toolchain_host); let missing = self.missing_components(&required_components, &installed_components, host); if !missing.is_empty() { @@ -562,8 +562,10 @@ impl Backend for RustPlugin { None => self.rustup_default_profile(&tv, &runtime)?, }; let effective_profile = normalize_rustup_profile(&effective_profile)?; + let active_toolchain = self.rustup_active_toolchain(&tv, &runtime)?; + let host = active_toolchain.as_deref().and_then(rustup_toolchain_host); let mut components = components.unwrap_or_default(); - components.extend(fallback_rustup_profile_components(effective_profile)); + components.extend(fallback_rustup_profile_components(effective_profile, host)); components.sort(); components.dedup(); @@ -1132,7 +1134,7 @@ fn normalize_rustup_profile(profile: &str) -> Result<&'static str> { } } -fn fallback_rustup_profile_components(profile: &str) -> Vec { +fn fallback_rustup_profile_components(profile: &str, host: Option<&str>) -> Vec { let mut components = match profile { "minimal" | "default" => RUST_MINIMAL_PROFILE_COMPONENTS .iter() @@ -1148,6 +1150,9 @@ fn fallback_rustup_profile_components(profile: &str) -> Vec { .map(|component| (*component).to_string()), ); } + if host.is_some_and(|host| host.ends_with("-pc-windows-gnu")) { + components.push("rust-mingw".to_string()); + } components } @@ -1374,11 +1379,11 @@ mod tests { #[test] fn required_profile_components_match_rustup() { assert_eq!( - fallback_rustup_profile_components("minimal"), + fallback_rustup_profile_components("minimal", Some("x86_64-unknown-linux-gnu")), ["cargo", "rust-std", "rustc"] ); assert_eq!( - fallback_rustup_profile_components("default"), + fallback_rustup_profile_components("default", Some("x86_64-unknown-linux-gnu")), [ "cargo", "rust-std", @@ -1388,7 +1393,11 @@ mod tests { "rustfmt" ] ); - assert!(fallback_rustup_profile_components("complete").is_empty()); + assert_eq!( + fallback_rustup_profile_components("minimal", Some("x86_64-pc-windows-gnu")), + ["cargo", "rust-std", "rustc", "rust-mingw"] + ); + assert!(fallback_rustup_profile_components("complete", None).is_empty()); } #[test] From 458b50fa75479189e86f5f004429f49bb827ec28 Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:04:13 +0900 Subject: [PATCH 4/9] fix(rust): keep complete fallback deferred Entire-Checkpoint: 01M1QZ7WZ2SR1S9XG1QCYNG7VA --- src/plugins/core/rust.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index dddc3675c07..521c31cdfa5 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -1150,7 +1150,7 @@ fn fallback_rustup_profile_components(profile: &str, host: Option<&str>) -> Vec< .map(|component| (*component).to_string()), ); } - if host.is_some_and(|host| host.ends_with("-pc-windows-gnu")) { + if profile != "complete" && host.is_some_and(|host| host.ends_with("-pc-windows-gnu")) { components.push("rust-mingw".to_string()); } components @@ -1398,6 +1398,10 @@ mod tests { ["cargo", "rust-std", "rustc", "rust-mingw"] ); assert!(fallback_rustup_profile_components("complete", None).is_empty()); + assert!( + fallback_rustup_profile_components("complete", Some("x86_64-pc-windows-gnu")) + .is_empty() + ); } #[test] From 377cd587e01cdf8dc1ceabf6597a4853789f2317 Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:22:02 +0900 Subject: [PATCH 5/9] fix(rust): recognize RISC-V host architectures Entire-Checkpoint: 01M1R3PCF2YCFZ6VMF6XW25T0K --- src/plugins/core/rust.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index 521c31cdfa5..b5f3a741bac 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -1221,6 +1221,8 @@ const RUST_TARGET_ARCHES: &[&str] = &[ "powerpc64le", "riscv32", "riscv64", + "riscv64a23", + "riscv64gc", "s390x", "sparc", "sparc64", @@ -1376,6 +1378,29 @@ mod tests { assert!(rustup_component_installed(&installed, "rust-std", host)); } + #[test] + fn rustup_required_components_recognize_riscv_hosts() { + let plugin = RustPlugin::new(); + for host in [ + "riscv64gc-unknown-linux-gnu", + "riscv64a23-unknown-linux-gnu", + ] { + let toolchain = format!("nightly-2026-09-01-{host}"); + let selected_host = rustup_toolchain_host(&toolchain); + assert_eq!(selected_host, Some(host)); + let required = fallback_rustup_profile_components("minimal", selected_host); + let installed = required + .iter() + .map(|component| format!("{component}-{host}")) + .collect(); + assert!( + plugin + .missing_components(&required, &installed, selected_host) + .is_empty() + ); + } + } + #[test] fn required_profile_components_match_rustup() { assert_eq!( From 7e32b0067d843c78d18e55a0538c6e8059d7126a Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:40:37 +0900 Subject: [PATCH 6/9] fix(rust): reconcile complete profile components Entire-Checkpoint: 01M1QXWP1HJC80MQD3E04R5F3N --- docs/lang/rust.md | 6 +- e2e/core/test_rust_components_reconcile | 94 ++++++- src/plugins/core/rust.rs | 330 +++++++++++++++++++++++- 3 files changed, 417 insertions(+), 13 deletions(-) diff --git a/docs/lang/rust.md b/docs/lang/rust.md index ab5a0d30f20..069f964549a 100644 --- a/docs/lang/rust.md +++ b/docs/lang/rust.md @@ -141,7 +141,11 @@ If not set, it defaults to the profile configured in `rustup`. You can check you ``` If the Rust toolchain is already installed, `mise install` restores missing components implied by -the `minimal` or `default` profile. +the `minimal`, `default`, or `complete` profile. Complete-profile membership comes from the +installed toolchain's rustup manifest because it can vary between Rust releases. + +Rustup supports only those three named profiles and discourages using `complete`. To customize a +profile, use the `components` and `targets` options with `minimal` or `default`. ### `targets` diff --git a/e2e/core/test_rust_components_reconcile b/e2e/core/test_rust_components_reconcile index 20d95eee65d..a1f770cbd9d 100644 --- a/e2e/core/test_rust_components_reconcile +++ b/e2e/core/test_rust_components_reconcile @@ -52,7 +52,67 @@ case "$1 $2" in toolchain="$toolchain-$host" toolchain_exists=false [[ -d "$MISE_RUSTUP_HOME/toolchains/$toolchain" ]] && toolchain_exists=true - mkdir -p "$MISE_RUSTUP_HOME/toolchains/$toolchain" + mkdir -p "$MISE_RUSTUP_HOME/toolchains/$toolchain/lib/rustlib" + cat >"$MISE_RUSTUP_HOME/toolchains/$toolchain/lib/rustlib/multirust-channel-manifest.toml" <<'MANIFEST' +[renames.clippy] +to = "clippy-preview" + +[renames.miri] +to = "miri-preview" + +[renames.rust-analyzer] +to = "rust-analyzer-preview" + +[renames.rustfmt] +to = "rustfmt-preview" + +[profiles] +minimal = ["rustc", "cargo", "rust-std"] +default = ["rustc", "cargo", "rust-std", "rust-docs", "rustfmt-preview", "clippy-preview"] +complete = ["rustc", "cargo", "rust-std", "rust-docs", "rustfmt-preview", "clippy-preview", "rust-analyzer-preview", "rust-src", "miri-preview"] + +[pkg.rust.target.x86_64-unknown-linux-gnu] + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "rustc" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "cargo" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "rust-std" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "rust-docs" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "rust-std" +target = "wasm32-unknown-unknown" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "rustfmt-preview" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "clippy-preview" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "rust-analyzer-preview" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "rust-src" +target = "*" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "miri-preview" +target = "x86_64-unknown-linux-gnu" +MANIFEST profile="" while [[ $# -gt 0 ]]; do case "$1" in @@ -164,6 +224,34 @@ assert_contains "cat '$MISE_CARGO_HOME/components'" "cargo-x86_64-unknown-linux- assert_contains "cat '$MISE_CARGO_HOME/components'" "rustc-x86_64-unknown-linux-gnu" assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-std-x86_64-unknown-linux-gnu" +cat >mise.toml <"$MISE_CARGO_HOME/components" +sed -i '/^miri-/d' "$MISE_CARGO_HOME/components" +assert_hook_env_does_not_call_rustup +assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" +mise install +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "7" +assert_contains "cat '$MISE_CARGO_HOME/components'" "miri-x86_64-unknown-linux-gnu" + +manifest="$MISE_RUSTUP_HOME/toolchains/1.81.0-x86_64-unknown-linux-gnu/lib/rustlib/multirust-channel-manifest.toml" +cp "$manifest" "$manifest.valid" +echo 'not valid toml = [' >"$manifest" +assert_fail_contains "mise install rust --dry-run-code 2>&1" "installed manifest is unusable" +mv "$manifest.valid" "$manifest" + cat >mise.toml <&1" "would install" mise exec -- rustc -V -assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "7" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "8" assert_contains "cat '$MISE_CARGO_HOME/components'" "rustfmt" assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-src" assert_contains "cat '$MISE_CARGO_HOME/targets'" "wasm32-unknown-unknown" mise install -assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "7" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "8" assert_contains "mise install rust --dry-run-code 2>&1" "already installed" diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index b5f3a741bac..0dafc52d2e3 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -31,6 +31,56 @@ const RUST_NIGHTLY_MANIFEST_URL: &str = const RUST_MINIMAL_PROFILE_COMPONENTS: &[&str] = &["cargo", "rust-std", "rustc"]; const RUST_DEFAULT_PROFILE_COMPONENTS: &[&str] = &["clippy", "rust-docs", "rustfmt"]; +#[derive(Debug, serde::Deserialize)] +struct RustupManifest { + #[serde(default)] + profiles: BTreeMap>, + #[serde(default)] + renames: BTreeMap, + pkg: RustupManifestPackages, +} + +#[derive(Debug, serde::Deserialize)] +struct RustupManifestRename { + to: String, +} + +#[derive(Debug, serde::Deserialize)] +struct RustupManifestPackages { + rust: RustupManifestPackage, +} + +#[derive(Debug, serde::Deserialize)] +struct RustupManifestPackage { + target: BTreeMap, +} + +#[derive(Debug, serde::Deserialize)] +struct RustupManifestTarget { + #[serde(default)] + components: Vec, + #[serde(default)] + extensions: Vec, +} + +#[derive(Debug, serde::Deserialize)] +struct RustupManifestComponent { + pkg: String, + target: Option, +} + +#[derive(Debug, PartialEq)] +struct RustupProfileComponents { + components: Vec, + host: String, +} + +#[derive(Debug)] +struct InstalledRustupManifest { + toolchain: String, + contents: Option, +} + fn parse_nightly_manifest(manifest: &str) -> Result { let manifest: toml::Value = toml::from_str(manifest).wrap_err("failed to parse the Rust nightly channel manifest")?; @@ -319,10 +369,82 @@ impl RustPlugin { return Ok(None); } }; - Ok(String::from_utf8_lossy(&output.stdout) - .split_whitespace() - .next() - .map(String::from)) + let output = String::from_utf8_lossy(&output.stdout); + let Some(toolchain) = output.split_whitespace().next() else { + debug!( + "rustup show active-toolchain returned no toolchain for {}", + tv.style() + ); + return Ok(None); + }; + Ok(Some(toolchain.to_string())) + } + + fn rustup_toolchain_manifest( + &self, + tv: &ToolVersion, + runtime: &RustRuntime, + ) -> Result> { + let Some(toolchain) = self.rustup_active_toolchain(tv, runtime)? else { + return Ok(None); + }; + let manifest = runtime + .homes + .rustup + .join("toolchains") + .join(&toolchain) + .join("lib") + .join("rustlib") + .join("multirust-channel-manifest.toml"); + if !manifest.is_file() { + debug!( + "rustup manifest missing for {} at {}", + tv.style(), + manifest.display() + ); + return Ok(Some(InstalledRustupManifest { + toolchain, + contents: None, + })); + } + let contents = match file::read_to_string(&manifest) { + Ok(contents) => Some(contents), + Err(err) => { + debug!( + "failed to read rustup manifest for {} at {}: {err:#}", + tv.style(), + manifest.display() + ); + None + } + }; + Ok(Some(InstalledRustupManifest { + toolchain, + contents, + })) + } + + fn rustup_complete_profile_components( + &self, + tv: &ToolVersion, + runtime: &RustRuntime, + ) -> Result> { + let Some(installed) = self.rustup_toolchain_manifest(tv, runtime)? else { + return Ok(None); + }; + if let Some(manifest) = installed.contents { + match parse_rustup_profile_components(&manifest, &installed.toolchain, "complete") { + Ok(components) => return Ok(Some(components)), + Err(err) => debug!( + "failed to resolve the rustup complete profile for {} from its installed manifest: {err:#}", + tv.style() + ), + } + } + bail!( + "cannot reconcile the rustup complete profile for {} because its installed manifest is unusable", + tv.style() + ) } fn missing_components( @@ -388,7 +510,11 @@ impl Backend for RustPlugin { None => self.rustup_default_profile(tv, &runtime)?, }; let effective_profile = normalize_rustup_profile(&effective_profile)?; - let active_toolchain = self.rustup_active_toolchain(tv, &runtime)?; + let active_toolchain = if effective_profile == "complete" { + None + } else { + self.rustup_active_toolchain(tv, &runtime)? + }; let host = active_toolchain.as_deref().and_then(rustup_toolchain_host); // Query components even when none were explicitly requested. This @@ -400,7 +526,19 @@ impl Backend for RustPlugin { }; let mut required_components = components.unwrap_or_default(); - required_components.extend(fallback_rustup_profile_components(effective_profile, host)); + let manifest_host = if effective_profile == "complete" { + let Some(profile_components) = + self.rustup_complete_profile_components(tv, &runtime)? + else { + return Ok(false); + }; + required_components.extend(profile_components.components); + Some(profile_components.host) + } else { + required_components.extend(fallback_rustup_profile_components(effective_profile, host)); + None + }; + let host = manifest_host.as_deref().or(host); required_components.sort(); required_components.dedup(); @@ -562,10 +700,22 @@ impl Backend for RustPlugin { None => self.rustup_default_profile(&tv, &runtime)?, }; let effective_profile = normalize_rustup_profile(&effective_profile)?; - let active_toolchain = self.rustup_active_toolchain(&tv, &runtime)?; + let active_toolchain = if effective_profile == "complete" { + None + } else { + self.rustup_active_toolchain(&tv, &runtime)? + }; let host = active_toolchain.as_deref().and_then(rustup_toolchain_host); let mut components = components.unwrap_or_default(); - components.extend(fallback_rustup_profile_components(effective_profile, host)); + if effective_profile == "complete" { + if let Some(profile_components) = + self.rustup_complete_profile_components(&tv, &runtime)? + { + components.extend(profile_components.components); + } + } else { + components.extend(fallback_rustup_profile_components(effective_profile, host)); + } components.sort(); components.dedup(); @@ -1140,7 +1290,7 @@ fn fallback_rustup_profile_components(profile: &str, host: Option<&str>) -> Vec< .iter() .map(|component| (*component).to_string()) .collect::>(), - "complete" => Vec::new(), + "complete" => return Vec::new(), _ => unreachable!("profile was normalized"), }; if profile == "default" { @@ -1166,6 +1316,70 @@ fn rustup_toolchain_host(toolchain: &str) -> Option<&str> { }) } +fn parse_rustup_profile_components( + manifest: &str, + toolchain: &str, + profile: &str, +) -> Result { + let manifest: RustupManifest = + toml::from_str(manifest).wrap_err("failed to parse the installed rustup manifest")?; + let host = manifest + .pkg + .rust + .target + .keys() + .filter(|target| { + target.as_str() != "*" + && (toolchain == target.as_str() + || toolchain + .strip_suffix(target.as_str()) + .is_some_and(|prefix| prefix.ends_with('-'))) + }) + .max_by_key(|target| target.len()) + .cloned() + .ok_or_else(|| eyre::eyre!("unable to determine the host for toolchain {toolchain}"))?; + let target = manifest + .pkg + .rust + .target + .get(&host) + .ok_or_else(|| eyre::eyre!("rustup manifest is missing host target {host}"))?; + let all_components = || target.components.iter().chain(&target.extensions); + let selected: Vec<&RustupManifestComponent> = if manifest.profiles.is_empty() { + target.components.iter().collect() + } else { + manifest + .profiles + .get(profile) + .ok_or_else(|| eyre::eyre!("rustup manifest is missing the {profile} profile"))? + .iter() + .filter_map(|name| { + all_components().find(|component| { + component.pkg == *name + && component + .target + .as_deref() + .is_none_or(|target| target == "*" || target == host) + }) + }) + .collect() + }; + let mut components = selected + .into_iter() + .map(|component| { + manifest + .renames + .iter() + .rev() + .find_map(|(name, rename)| (rename.to == component.pkg).then(|| name.clone())) + .unwrap_or_else(|| component.pkg.clone()) + }) + .collect::>(); + components.sort(); + components.dedup(); + Ok(RustupProfileComponents { components, host }) +} + fn rustup_component_installed( installed: &BTreeSet, component: &str, @@ -1366,6 +1580,7 @@ mod tests { "rust-src".to_string(), "llvm-tools-x86_64-unknown-linux-gnu".to_string(), "rust-std-wasm32-unknown-unknown".to_string(), + "rustc-x86_64-unknown-linux-gnu".to_string(), ]); let host = rustup_toolchain_host("1.81.0-x86_64-unknown-linux-gnu"); @@ -1429,6 +1644,103 @@ mod tests { ); } + #[test] + fn parses_profile_components_for_the_selected_host() { + let manifest = r#" +[renames.a-clippy] +to = "clippy-preview" + +[renames.clippy] +to = "clippy-preview" + +[renames.rust-analyzer] +to = "rust-analyzer-preview" + +[profiles] +minimal = ["rustc", "cargo", "rust-std", "rust-mingw"] +default = ["rustc", "cargo", "rust-std", "rust-mingw", "clippy-preview"] +complete = ["rustc", "rust-mingw", "clippy-preview", "rust-analyzer-preview", "rust-src"] + +[pkg.rust.target.x86_64-unknown-linux-gnu] + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "rustc" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "cargo" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "rust-std" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "rust-std" +target = "wasm32-unknown-unknown" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "clippy-preview" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "rust-analyzer-preview" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "rust-src" +target = "*" +"#; + + assert_eq!( + parse_rustup_profile_components( + manifest, + "1.81.0-x86_64-unknown-linux-gnu", + "complete" + ) + .unwrap(), + RustupProfileComponents { + components: vec![ + "clippy".to_string(), + "rust-analyzer".to_string(), + "rust-src".to_string(), + "rustc".to_string() + ], + host: "x86_64-unknown-linux-gnu".to_string() + } + ); + } + + #[test] + fn profileless_manifests_use_legacy_components() { + let manifest = r#" +[pkg.rust.target.x86_64-unknown-linux-gnu] + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "rustc" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.components]] +pkg = "cargo" +target = "x86_64-unknown-linux-gnu" + +[[pkg.rust.target.x86_64-unknown-linux-gnu.extensions]] +pkg = "clippy-preview" +target = "x86_64-unknown-linux-gnu" +"#; + + assert_eq!( + parse_rustup_profile_components( + manifest, + "1.19.0-x86_64-unknown-linux-gnu", + "complete" + ) + .unwrap() + .components, + vec!["cargo", "rustc"] + ); + } + #[test] fn profile_aliases_match_rustup() { assert_eq!(normalize_rustup_profile("minimal").unwrap(), "minimal"); From cb4922b5a0a2b2648ae9bb0d35f655e78a1737e7 Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:53:18 +0900 Subject: [PATCH 7/9] fix(rust): reject incomplete repairs without toolchain identity Entire-Checkpoint: 01M1R21S1A54FM5PDMDBVGWDD2 --- e2e/core/test_rust_components_reconcile | 18 ++++++++++++------ src/plugins/core/rust.rs | 12 ++++++++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/e2e/core/test_rust_components_reconcile b/e2e/core/test_rust_components_reconcile index a1f770cbd9d..7bdf92c57c3 100644 --- a/e2e/core/test_rust_components_reconcile +++ b/e2e/core/test_rust_components_reconcile @@ -31,6 +31,7 @@ case "$1 $2" in echo default ;; "show active-toolchain") + [[ ! -f "$MISE_CARGO_HOME/fail-active-toolchain" ]] || exit 1 toolchain="${RUSTUP_TOOLCHAIN:-}" [[ -d "$MISE_RUSTUP_HOME/toolchains/$toolchain-$host" ]] || exit 1 echo "$toolchain-$host (environment override by RUSTUP_TOOLCHAIN)" @@ -172,7 +173,7 @@ EOF mise install assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "1" -sed -i '/^rust-docs-/d' "$MISE_CARGO_HOME/components" +sed -i.bak '/^rust-docs-/d' "$MISE_CARGO_HOME/components" assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" mise install assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "2" @@ -189,7 +190,7 @@ cat >mise.toml <&1" "would install" mise install @@ -203,7 +204,7 @@ cat >mise.toml <&1" "would install" mise install assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "5" @@ -215,7 +216,7 @@ rust = { version = "1.81.0", profile = "minimal" } EOF printf '%s\n' rust-std-wasm32-unknown-unknown >>"$MISE_CARGO_HOME/components" -sed -i '/^cargo-x86_64-unknown-linux-gnu$/d; /^rustc-x86_64-unknown-linux-gnu$/d; /^rust-std-x86_64-unknown-linux-gnu$/d' \ +sed -i.bak '/^cargo-x86_64-unknown-linux-gnu$/d; /^rustc-x86_64-unknown-linux-gnu$/d; /^rust-std-x86_64-unknown-linux-gnu$/d' \ "$MISE_CARGO_HOME/components" assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" mise install @@ -239,7 +240,11 @@ printf '%s\n' \ rust-std-x86_64-unknown-linux-gnu \ rustc-x86_64-unknown-linux-gnu \ rustfmt-x86_64-unknown-linux-gnu >"$MISE_CARGO_HOME/components" -sed -i '/^miri-/d' "$MISE_CARGO_HOME/components" +sed -i.bak '/^miri-/d' "$MISE_CARGO_HOME/components" +touch "$MISE_CARGO_HOME/fail-active-toolchain" +assert_fail_contains "mise install rust 2>&1" "active toolchain could not be determined" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "6" +mv "$MISE_CARGO_HOME/fail-active-toolchain" "$MISE_CARGO_HOME/active-toolchain-recovered" assert_hook_env_does_not_call_rustup assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" mise install @@ -249,7 +254,8 @@ assert_contains "cat '$MISE_CARGO_HOME/components'" "miri-x86_64-unknown-linux-g manifest="$MISE_RUSTUP_HOME/toolchains/1.81.0-x86_64-unknown-linux-gnu/lib/rustlib/multirust-channel-manifest.toml" cp "$manifest" "$manifest.valid" echo 'not valid toml = [' >"$manifest" -assert_fail_contains "mise install rust --dry-run-code 2>&1" "installed manifest is unusable" +assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" +assert_fail_contains "mise install rust 2>&1" "installed manifest is unusable" mv "$manifest.valid" "$manifest" cat >mise.toml < Result> { let Some(installed) = self.rustup_toolchain_manifest(tv, runtime)? else { + if self + .rustup_installed_items(tv, "component", runtime)? + .is_some() + { + bail!( + "cannot reconcile the rustup complete profile for {} because its active toolchain could not be determined", + tv.style() + ); + } return Ok(None); }; if let Some(manifest) = installed.contents { @@ -527,8 +536,7 @@ impl Backend for RustPlugin { let mut required_components = components.unwrap_or_default(); let manifest_host = if effective_profile == "complete" { - let Some(profile_components) = - self.rustup_complete_profile_components(tv, &runtime)? + let Some(profile_components) = self.rustup_complete_profile_components(tv, &runtime)? else { return Ok(false); }; From 46477d3ff879713493cfdcf6a0e86f4167320e70 Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:42:59 +0900 Subject: [PATCH 8/9] fix(rust): recover missing complete profile metadata Entire-Checkpoint: 01M1QY10A73H7QGMJYR5FHM8QP --- e2e/core/test_rust_components_reconcile | 18 +++- src/plugins/core/rust.rs | 122 ++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 13 deletions(-) diff --git a/e2e/core/test_rust_components_reconcile b/e2e/core/test_rust_components_reconcile index 7bdf92c57c3..1e9c17d265a 100644 --- a/e2e/core/test_rust_components_reconcile +++ b/e2e/core/test_rust_components_reconcile @@ -252,11 +252,19 @@ assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "7" assert_contains "cat '$MISE_CARGO_HOME/components'" "miri-x86_64-unknown-linux-gnu" manifest="$MISE_RUSTUP_HOME/toolchains/1.81.0-x86_64-unknown-linux-gnu/lib/rustlib/multirust-channel-manifest.toml" -cp "$manifest" "$manifest.valid" +mkdir -p "$PWD/rust-dist/dist" +cp "$manifest" "$PWD/rust-dist/dist/channel-rust-1.81.0.toml" +cat >mise.toml <"$manifest" +sed -i.bak '/^rust-analyzer-/d' "$MISE_CARGO_HOME/components" assert_fail_contains "mise install rust --dry-run-code 2>&1" "would install" -assert_fail_contains "mise install rust 2>&1" "installed manifest is unusable" -mv "$manifest.valid" "$manifest" +mise install +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "8" +assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-analyzer-x86_64-unknown-linux-gnu" cat >mise.toml <&1" "would install" mise exec -- rustc -V -assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "8" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "9" assert_contains "cat '$MISE_CARGO_HOME/components'" "rustfmt" assert_contains "cat '$MISE_CARGO_HOME/components'" "rust-src" assert_contains "cat '$MISE_CARGO_HOME/targets'" "wasm32-unknown-unknown" mise install -assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "8" +assert_contains "grep -c '^toolchain install' '$MISE_CARGO_HOME/rustup.log'" "9" assert_contains "mise install rust --dry-run-code 2>&1" "already installed" diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index 18efee2205e..ff4b23b1198 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -28,6 +28,7 @@ pub(super) struct RustPlugin { const RUST_NIGHTLY_MANIFEST_URL: &str = "https://static.rust-lang.org/dist/channel-rust-nightly.toml"; +const RUST_DIST_ROOT: &str = "https://static.rust-lang.org/dist"; const RUST_MINIMAL_PROFILE_COMPONENTS: &[&str] = &["cargo", "rust-std", "rustc"]; const RUST_DEFAULT_PROFILE_COMPONENTS: &[&str] = &["clippy", "rust-docs", "rustfmt"]; @@ -424,7 +425,7 @@ impl RustPlugin { })) } - fn rustup_complete_profile_components( + async fn rustup_complete_profile_components( &self, tv: &ToolVersion, runtime: &RustRuntime, @@ -450,10 +451,25 @@ impl RustPlugin { ), } } - bail!( - "cannot reconcile the rustup complete profile for {} because its installed manifest is unusable", - tv.style() - ) + let url = rustup_channel_manifest_url( + &tv.version, + rustup_dist_var(tv, "RUSTUP_DIST_SERVER"), + rustup_dist_var(tv, "RUSTUP_DIST_ROOT"), + ); + let manifest = read_rustup_channel_manifest(&url).await.wrap_err_with(|| { + format!( + "cannot reconcile the rustup complete profile for {} because its installed manifest is unusable and {url} could not be fetched", + tv.style() + ) + })?; + parse_rustup_profile_components(&manifest, &installed.toolchain, "complete") + .map(Some) + .wrap_err_with(|| { + format!( + "cannot reconcile the rustup complete profile for {} from {url}", + tv.style() + ) + }) } fn missing_components( @@ -536,7 +552,9 @@ impl Backend for RustPlugin { let mut required_components = components.unwrap_or_default(); let manifest_host = if effective_profile == "complete" { - let Some(profile_components) = self.rustup_complete_profile_components(tv, &runtime)? + let Some(profile_components) = self + .rustup_complete_profile_components(tv, &runtime) + .await? else { return Ok(false); }; @@ -716,8 +734,9 @@ impl Backend for RustPlugin { let host = active_toolchain.as_deref().and_then(rustup_toolchain_host); let mut components = components.unwrap_or_default(); if effective_profile == "complete" { - if let Some(profile_components) = - self.rustup_complete_profile_components(&tv, &runtime)? + if let Some(profile_components) = self + .rustup_complete_profile_components(&tv, &runtime) + .await? { components.extend(profile_components.components); } @@ -1292,6 +1311,52 @@ fn normalize_rustup_profile(profile: &str) -> Result<&'static str> { } } +fn rustup_dist_var(tv: &ToolVersion, key: &str) -> Option { + match tv.install_env().shift_remove(key) { + Some(value) => value.into_string(), + None => env::var(key).ok(), + } + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} + +fn rustup_channel_manifest_url( + version: &str, + dist_server: Option, + legacy_dist_root: Option, +) -> String { + let dist_root = if let Some(server) = dist_server { + format!("{}/dist", server.trim_end_matches('/')) + } else if let Some(root) = legacy_dist_root { + let root = root.trim_end_matches('/'); + format!("{}/dist", root.strip_suffix("/dist").unwrap_or(root)) + } else { + RUST_DIST_ROOT.to_string() + }; + if let Some(date) = version + .strip_prefix("nightly-") + .filter(|date| date.parse::().is_ok()) + { + format!("{dist_root}/{date}/channel-rust-nightly.toml") + } else { + format!("{dist_root}/channel-rust-{version}.toml") + } +} + +async fn read_rustup_channel_manifest(url: &str) -> Result { + if let Ok(url) = url::Url::parse(url) + && url.scheme() == "file" + { + let path = url + .to_file_path() + .map_err(|_| eyre::eyre!("invalid rustup file URL: {url}"))?; + return file::read_to_string(&path).wrap_err_with(|| { + format!("failed to read Rust channel manifest at {}", path.display()) + }); + } + HTTP_FETCH.get_text_cached(url).await +} + fn fallback_rustup_profile_components(profile: &str, host: Option<&str>) -> Vec { let mut components = match profile { "minimal" | "default" => RUST_MINIMAL_PROFILE_COMPONENTS @@ -1749,6 +1814,47 @@ target = "x86_64-unknown-linux-gnu" ); } + #[test] + fn builds_rustup_channel_manifest_urls() { + assert_eq!( + rustup_channel_manifest_url("1.81.0", None, None), + "https://static.rust-lang.org/dist/channel-rust-1.81.0.toml" + ); + assert_eq!( + rustup_channel_manifest_url("nightly-2026-08-12", None, None), + "https://static.rust-lang.org/dist/2026-08-12/channel-rust-nightly.toml" + ); + assert_eq!( + rustup_channel_manifest_url( + "1.81.0", + Some("https://mirror.example.com/".to_string()), + Some("https://ignored.example.com/dist".to_string()) + ), + "https://mirror.example.com/dist/channel-rust-1.81.0.toml" + ); + assert_eq!( + rustup_channel_manifest_url( + "1.81.0", + None, + Some("https://legacy.example.com/dist".to_string()) + ), + "https://legacy.example.com/dist/channel-rust-1.81.0.toml" + ); + } + + #[tokio::test] + async fn reads_rustup_channel_manifest_from_file_server() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("channel-rust-1.81.0.toml"); + std::fs::write(&path, "manifest-version = '2'").unwrap(); + let url = url::Url::from_file_path(path).unwrap(); + + assert_eq!( + read_rustup_channel_manifest(url.as_str()).await.unwrap(), + "manifest-version = '2'" + ); + } + #[test] fn profile_aliases_match_rustup() { assert_eq!(normalize_rustup_profile("minimal").unwrap(), "minimal"); From 1e660f8a31e38f55b51fa6645dfa215f2c327aaf Mon Sep 17 00:00:00 2001 From: Taku Kodama <79110363+risu729@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:06:34 +0900 Subject: [PATCH 9/9] refactor(rust): read distribution overrides without mutation Entire-Checkpoint: 01M1QZC720NP2WWYCVAX218MWN --- src/plugins/core/rust.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/core/rust.rs b/src/plugins/core/rust.rs index ff4b23b1198..b9e45efa260 100644 --- a/src/plugins/core/rust.rs +++ b/src/plugins/core/rust.rs @@ -1312,7 +1312,7 @@ fn normalize_rustup_profile(profile: &str) -> Result<&'static str> { } fn rustup_dist_var(tv: &ToolVersion, key: &str) -> Option { - match tv.install_env().shift_remove(key) { + match tv.install_env().get(key).cloned() { Some(value) => value.into_string(), None => env::var(key).ok(), }