11//! toolchain installation logic
22
3+ use crate :: user_output;
34use anyhow:: Context as _;
45#[ cfg( feature = "tty" ) ]
56use 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