Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.

Commit 5b8748d

Browse files
committed
toolchain: refactor, check installed components, install everything as one cmd
1 parent b265ae0 commit 5b8748d

File tree

1 file changed

+125
-72
lines changed

1 file changed

+125
-72
lines changed

crates/cargo-gpu-install/src/install_toolchain.rs

Lines changed: 125 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
//! toolchain installation logic
22
3+
use crate::user_output;
34
use anyhow::Context as _;
45
#[cfg(feature = "tty")]
56
use crossterm::tty::IsTty as _;
7+
use std::collections::HashSet;
8+
use std::process::Command;
9+
use std::string::FromUtf8Error;
610

7-
use crate::user_output;
11+
/// list of required rustup components
12+
pub const REQUIRED_COMPONENTS: &[&str] =
13+
["rust-src", "rustc-dev", "llvm-tools", "clippy"].as_slice();
814

915
/// Use `rustup` to install the toolchain and components, if not already installed.
1016
///
@@ -16,87 +22,134 @@ pub fn ensure_toolchain_and_components_exist(
1622
channel: &str,
1723
skip_toolchain_install_consent: bool,
1824
) -> anyhow::Result<()> {
19-
// Check for the required toolchain
20-
let output_toolchain_list = std::process::Command::new("rustup")
21-
.args(["toolchain", "list"])
22-
.output()
23-
.context("running rustup command")?;
24-
anyhow::ensure!(
25-
output_toolchain_list.status.success(),
26-
"could not list installed toolchains"
27-
);
28-
let string_toolchain_list = String::from_utf8_lossy(&output_toolchain_list.stdout);
29-
if string_toolchain_list
30-
.split_whitespace()
31-
.any(|toolchain| toolchain.starts_with(channel))
32-
{
33-
log::debug!("toolchain {channel} is already installed");
34-
} else {
35-
let message = format!("Rust {channel} with `rustup`");
25+
// While our channel may be `nightly-2024-04-24`, it'll be resolved to the full toolchain name of e.g.
26+
// `nightly-2024-04-24-aarch64-unknown-linux-gnu` and that's also what `rustup toolchain list` will print.
27+
// Only checking whether the toolchain starts with the channel name may incorrectly pass if you have a toolchain
28+
// installed that you're not able to run on your system via `rustup toolchain install --force-non-host ...`.
29+
// CMD: `rustc --print host-tuple`
30+
// TODO: What if the user has no toolchain installed? You can't query this with rustup sady.
31+
let (host_tuple, _) = run_cmd(Command::new("rustc").args(["--print", "host-tuple"]))?;
32+
let host_tuple = host_tuple.trim_ascii();
33+
let toolchain = format!("{channel}-{host_tuple}");
34+
35+
if !is_toolchain_installed(&toolchain, host_tuple)? {
36+
let message = format!(
37+
"toolchain {channel} with components {}",
38+
intersperse(", ", REQUIRED_COMPONENTS.iter().copied())
39+
);
3640
get_consent_for_toolchain_install(
3741
format!("Install {message}").as_ref(),
3842
skip_toolchain_install_consent,
3943
)?;
40-
crate::user_output!("Installing {message}\n");
41-
42-
let output_toolchain_add = std::process::Command::new("rustup")
43-
.args(["toolchain", "add"])
44-
.arg(channel)
45-
.stdout(std::process::Stdio::inherit())
46-
.stderr(std::process::Stdio::inherit())
47-
.output()
48-
.context("adding toolchain")?;
49-
anyhow::ensure!(
50-
output_toolchain_add.status.success(),
51-
"could not install required toolchain"
44+
user_output!("Installing {message}\n");
45+
46+
// component list may be out of sync
47+
// CMD: `rustup toolchain install nightly-2024-04-24 -c clippy,rust-src,rustc-dev,llvm-tools`
48+
run_cmd(
49+
Command::new("rustup")
50+
.args([
51+
"toolchain",
52+
"install",
53+
&toolchain,
54+
"-c",
55+
&intersperse(",", REQUIRED_COMPONENTS.iter().copied()),
56+
])
57+
.stdout(std::process::Stdio::inherit())
58+
.stderr(std::process::Stdio::inherit()),
59+
)?;
60+
}
61+
62+
Ok(())
63+
}
64+
65+
/// Returns true if the toolchain and required components are installed.
66+
fn is_toolchain_installed(toolchain: &str, host_tuple: &str) -> anyhow::Result<bool> {
67+
// check if toolchain is installed
68+
// CMD: `rustup toolchain list -q`
69+
let (list_toolchains, _) = run_cmd(Command::new("rustup").args(["toolchain", "list", "-q"]))?;
70+
if !list_toolchains
71+
.split_ascii_whitespace()
72+
.any(|s| s == toolchain)
73+
{
74+
log::info!("toolchain {toolchain} is not installed");
75+
return Ok(false);
76+
}
77+
78+
// check if required components are installed
79+
// NOTE: checking for components will install the toolchain with the default profile, if not already installed!
80+
// So we must check beforehand whether the toolchain is installed, to not accidentally install it here.
81+
// Passing *just* `-q` will list available components, so add `--installed` for installed components.
82+
// CMD: `rustup component list --toolchain nightly-2024-04-24-aarch64-unknown-linux-gnu -q --installed`
83+
let (components, _) = run_cmd(Command::new("rustup").args([
84+
"component",
85+
"list",
86+
"--toolchain",
87+
toolchain,
88+
"-q",
89+
"--installed",
90+
]))?;
91+
92+
// components are listed as:
93+
// * `llvm-tools-aarch64-unknown-linux-gnu` and we need to snippet off the host tuple from the end
94+
// * `rust-src` since source code isn't target dependent
95+
let component_host_suffix = format!("-{host_tuple}");
96+
let mut required = REQUIRED_COMPONENTS.iter().copied().collect::<HashSet<_>>();
97+
for component in components.split_ascii_whitespace() {
98+
required.remove(
99+
component
100+
.strip_suffix(&component_host_suffix)
101+
.unwrap_or(component),
52102
);
53103
}
104+
if !required.is_empty() {
105+
log::info!("components {required:?} missing for toolchain {toolchain}");
106+
return Ok(false);
107+
}
54108

55-
// Check for the required components
56-
let output_component_list = std::process::Command::new("rustup")
57-
.args(["component", "list", "--toolchain"])
58-
.arg(channel)
59-
.output()
60-
.context("getting toolchain list")?;
61-
anyhow::ensure!(
62-
output_component_list.status.success(),
63-
"could not list installed components"
64-
);
65-
let string_component_list = String::from_utf8_lossy(&output_component_list.stdout);
66-
let required_components = ["rust-src", "rustc-dev", "llvm-tools"];
67-
let installed_components = string_component_list.lines().collect::<Vec<_>>();
68-
let all_components_installed = required_components.iter().all(|component| {
69-
installed_components.iter().any(|installed_component| {
70-
let is_component = installed_component.starts_with(component);
71-
let is_installed = installed_component.ends_with("(installed)");
72-
is_component && is_installed
73-
})
74-
});
75-
if all_components_installed {
76-
log::debug!("all required components are installed");
77-
} else {
78-
let message = "toolchain components [rust-src, rustc-dev, llvm-tools] with `rustup`";
79-
get_consent_for_toolchain_install(
80-
format!("Install {message}").as_ref(),
81-
skip_toolchain_install_consent,
82-
)?;
83-
crate::user_output!("Installing {message}\n");
84-
85-
let output_component_add = std::process::Command::new("rustup")
86-
.args(["component", "add", "--toolchain"])
87-
.arg(channel)
88-
.args(["rust-src", "rustc-dev", "llvm-tools"])
89-
.stdout(std::process::Stdio::inherit())
90-
.stderr(std::process::Stdio::inherit())
91-
.output()
92-
.context("adding rustup component")?;
93-
anyhow::ensure!(
94-
output_component_add.status.success(),
95-
"could not install required components"
109+
log::info!("toolchain and required components are already installed");
110+
Ok(true)
111+
}
112+
113+
pub fn run_cmd(cmd: &mut Command) -> anyhow::Result<(String, String)> {
114+
let output = cmd.output();
115+
let fmt_cmd = || {
116+
intersperse(
117+
" ",
118+
std::iter::once(cmd.get_program())
119+
.chain(cmd.get_args())
120+
.map(|s| s.to_str().unwrap()),
121+
)
122+
};
123+
let output = output.with_context(|| format!("Failed to launch cmd `{}`", fmt_cmd()))?;
124+
125+
let utf8_error = |e: FromUtf8Error, kind: &str| {
126+
anyhow::anyhow!(
127+
"Command `{}` {} contains invalid UTF-8: {} \n {:?}",
128+
kind,
129+
fmt_cmd(),
130+
e.utf8_error(),
131+
e.into_bytes()
132+
)
133+
};
134+
let stdout = String::from_utf8(output.stdout).map_err(|e| utf8_error(e, "stdout"))?;
135+
let stderr = String::from_utf8(output.stderr).map_err(|e| utf8_error(e, "stderr"))?;
136+
137+
if !output.status.success() {
138+
anyhow::bail!(
139+
"Command `{}` failed with {}:\n-- stdout\n{stdout}\n-- stderr\n{stderr}",
140+
fmt_cmd(),
141+
&output.status,
96142
);
97143
}
144+
Ok((stdout, stderr))
145+
}
98146

99-
Ok(())
147+
/// Folds an [`Iterator`] of `&str` into a [`String`] while interspersing some `&str` between each element
148+
#[expect(clippy::string_add, reason = "Deliberately using String::add")]
149+
fn intersperse<'a>(intersperse: &str, iter: impl Iterator<Item = &'a str>) -> String {
150+
let mut s = iter.fold(String::new(), |a, b| a + b + intersperse);
151+
s.truncate(s.len() - intersperse.len());
152+
s
100153
}
101154

102155
#[cfg(not(feature = "tty"))]

0 commit comments

Comments
 (0)