flowey_lib_common/
install_rust.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Globally install a Rust toolchain, and ensure those tools are available on
5//! the user's $PATH
6
7use flowey::node::prelude::*;
8use std::collections::BTreeSet;
9use std::io::Write;
10
11new_flow_node!(struct Node);
12
13flowey_request! {
14    pub enum Request {
15        /// Automatically install all required Rust tools and components.
16        ///
17        /// If false - will check for pre-existing Rust installation, and fail
18        /// if it doesn't meet the current job's requirements.
19        AutoInstall(bool),
20
21        /// Ignore the Version requirement, and build using whatever version of
22        /// the Rust toolchain the user has installed locally.
23        IgnoreVersion(bool),
24
25        /// Install a specific Rust toolchain version.
26        // FUTURE: support installing / using multiple versions of the Rust
27        // toolchain at the same time, e.g: for stable and nightly, or to
28        // support regression tests between a pinned rust version and current
29        // stable.
30        Version(String),
31
32        /// Specify an additional target-triple to install the toolchain for.
33        ///
34        /// By default, only the native target will be installed.
35        InstallTargetTriple(target_lexicon::Triple),
36
37        /// If Rust was installed via Rustup, return the rustup toolchain that
38        /// was installed (e.g: when specifting `+stable` or `+nightly` to
39        /// commands)
40        GetRustupToolchain(WriteVar<Option<String>>),
41
42        /// Install the specified component.
43        InstallComponent(String),
44
45        /// Get the path to $CARGO_HOME
46        GetCargoHome(WriteVar<PathBuf>),
47
48        /// Ensure that Rust was installed and is available on the $PATH
49        EnsureInstalled(WriteVar<SideEffect>),
50    }
51}
52
53impl FlowNode for Node {
54    type Request = Request;
55
56    fn imports(dep: &mut ImportCtx<'_>) {
57        dep.import::<crate::check_needs_relaunch::Node>();
58    }
59
60    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
61        let mut ensure_installed = Vec::new();
62        let mut rust_toolchain = None;
63        let mut auto_install = None;
64        let mut ignore_version = None;
65        let mut additional_target_triples = BTreeSet::new();
66        let mut additional_components = BTreeSet::new();
67        let mut get_rust_toolchain = Vec::new();
68        let mut get_cargo_home = Vec::new();
69
70        for req in requests {
71            match req {
72                Request::EnsureInstalled(v) => ensure_installed.push(v),
73                Request::AutoInstall(v) => {
74                    same_across_all_reqs("AutoInstall", &mut auto_install, v)?
75                }
76                Request::IgnoreVersion(v) => {
77                    same_across_all_reqs("IgnoreVersion", &mut ignore_version, v)?
78                }
79                Request::Version(v) => same_across_all_reqs("Version", &mut rust_toolchain, v)?,
80                Request::InstallTargetTriple(s) => {
81                    additional_target_triples.insert(s.to_string());
82                }
83                Request::InstallComponent(v) => {
84                    additional_components.insert(v);
85                }
86                Request::GetRustupToolchain(v) => get_rust_toolchain.push(v),
87                Request::GetCargoHome(v) => get_cargo_home.push(v),
88            }
89        }
90
91        let ensure_installed = ensure_installed;
92        let auto_install =
93            auto_install.ok_or(anyhow::anyhow!("Missing essential request: AutoInstall",))?;
94        if !auto_install && matches!(ctx.backend(), FlowBackend::Github) {
95            anyhow::bail!("`AutoInstall` must be true when using the Github backend");
96        }
97        let ignore_version =
98            ignore_version.ok_or(anyhow::anyhow!("Missing essential request: IgnoreVersion",))?;
99        if ignore_version && matches!(ctx.backend(), FlowBackend::Github) {
100            anyhow::bail!("`IgnoreVersion` must be false when using the Github backend");
101        }
102        let rust_toolchain =
103            rust_toolchain.ok_or(anyhow::anyhow!("Missing essential request: RustToolchain"))?;
104        let additional_target_triples = additional_target_triples;
105        let additional_components = additional_components;
106        let get_rust_toolchain = get_rust_toolchain;
107        let get_cargo_home = get_cargo_home;
108
109        // -- end of req processing -- //
110
111        let rust_toolchain = (!ignore_version).then_some(rust_toolchain);
112
113        let check_rust_install = {
114            let rust_toolchain = rust_toolchain.clone();
115            let additional_target_triples = additional_target_triples.clone();
116            let additional_components = additional_components.clone();
117
118            move |rt: &mut RustRuntimeServices<'_>| {
119                if which::which("cargo").is_err() {
120                    anyhow::bail!("did not find `cargo` on $PATH");
121                }
122
123                let rust_toolchain = rust_toolchain.map(|s| format!("+{s}"));
124                let rust_toolchain = rust_toolchain.as_ref();
125
126                // make sure the specific rust version was installed
127                flowey::shell_cmd!(rt, "rustc {rust_toolchain...} -vV").run()?;
128
129                // make sure the additional target triples were installed
130                if let Ok(rustup) = which::which("rustup") {
131                    for (thing, expected_things) in [
132                        ("target", &additional_target_triples),
133                        ("component", &additional_components),
134                    ] {
135                        let output = flowey::shell_cmd!(
136                            rt,
137                            "{rustup} {rust_toolchain...} {thing} list --installed"
138                        )
139                        .ignore_status()
140                        .output()?;
141                        let stderr = String::from_utf8(output.stderr)?;
142                        let stdout = String::from_utf8(output.stdout)?;
143
144                        // This error message may occur if the user has rustup
145                        // installed, but is using a custom custom toolchain.
146                        //
147                        // NOTE: not thrilled that we are sniffing a magic string
148                        // from stderr... but I'm also not sure if there's a better
149                        // way to detect this...
150                        if stderr.contains("does not support components") {
151                            log::warn!("Detected a non-standard `rustup default` toolchain!");
152                            log::warn!(
153                                "Will not be able to double-check that all required target-triples and components are available."
154                            );
155                        } else {
156                            let mut installed_things = BTreeSet::new();
157
158                            for line in stdout.lines() {
159                                let triple = line.trim();
160                                installed_things.insert(triple);
161                            }
162
163                            for expected_thing in expected_things {
164                                if !installed_things.contains(expected_thing.as_str()) {
165                                    anyhow::bail!(
166                                        "missing required {thing}: {expected_thing}; to install: `rustup {thing} add {expected_thing}`"
167                                    )
168                                }
169                            }
170                        }
171                    }
172                } else {
173                    log::warn!("`rustup` was not found!");
174                    log::warn!(
175                        "Unable to double-check that all target-triples and components are available."
176                    )
177                }
178
179                anyhow::Ok(())
180            }
181        };
182
183        let check_is_installed = |write_cargo_bin: Option<
184            WriteVar<Option<crate::check_needs_relaunch::BinOrEnv>>,
185        >,
186                                  ensure_installed: Vec<WriteVar<SideEffect>>,
187                                  auto_install: bool,
188                                  ctx: &mut NodeCtx<'_>| {
189            if write_cargo_bin.is_some() || !ensure_installed.is_empty() {
190                if auto_install || matches!(ctx.backend(), FlowBackend::Github) {
191                    let added_to_path = if matches!(ctx.backend(), FlowBackend::Github) {
192                        Some(ctx.emit_rust_step("add default cargo home to path", |_| {
193                            |_| {
194                                let default_cargo_home = home::home_dir()
195                                    .context("Unable to get home dir")?
196                                    .join(".cargo")
197                                    .join("bin");
198                                let github_path = std::env::var("GITHUB_PATH")?;
199                                let mut github_path =
200                                    fs_err::File::options().append(true).open(github_path)?;
201                                github_path
202                                    .write_all(default_cargo_home.as_os_str().as_encoded_bytes())?;
203                                log::info!("Added {} to PATH", default_cargo_home.display());
204                                Ok(())
205                            }
206                        }))
207                    } else {
208                        None
209                    };
210
211                    let rust_toolchain = rust_toolchain.clone();
212                    ctx.emit_rust_step("install Rust", |ctx| {
213                        let write_cargo_bin = if let Some(write_cargo_bin) = write_cargo_bin {
214                            Some(write_cargo_bin.claim(ctx))
215                        } else {
216                            ensure_installed.claim(ctx);
217                            None
218                        };
219                        added_to_path.claim(ctx);
220
221                        move |rt: &mut RustRuntimeServices<'_>| {
222                            if let Some(write_cargo_bin) = write_cargo_bin {
223                                rt.write(write_cargo_bin, &Some(crate::check_needs_relaunch::BinOrEnv::Bin("cargo".to_string())));
224                            }
225                            let rust_toolchain = rust_toolchain.clone();
226                            if check_rust_install.clone()(rt).is_ok() {
227                                return Ok(());
228                            }
229
230                            match rt.platform() {
231                                FlowPlatform::Linux(_) => {
232                                    let interactive_prompt = Some("-y");
233                                    let mut default_toolchain = Vec::new();
234                                    if let Some(ver) = rust_toolchain {
235                                        default_toolchain.push("--default-toolchain".into());
236                                        default_toolchain.push(ver)
237                                    };
238
239                                    flowey::shell_cmd!(
240                                        rt,
241                                        "curl --fail --proto =https --tlsv1.2 -sSf https://sh.rustup.rs -o rustup-init.sh"
242                                    )
243                                    .run()?;
244                                    flowey::shell_cmd!(rt, "chmod +x ./rustup-init.sh").run()?;
245                                    flowey::shell_cmd!(
246                                        rt,
247                                        "./rustup-init.sh {interactive_prompt...} {default_toolchain...}"
248                                    )
249                                    .run()?;
250                                }
251                                FlowPlatform::Windows => {
252                                    let interactive_prompt = Some("-y");
253                                    let mut default_toolchain = Vec::new();
254                                    if let Some(ver) = rust_toolchain {
255                                        default_toolchain.push("--default-toolchain".into());
256                                        default_toolchain.push(ver)
257                                    };
258
259                                    let arch = match rt.arch() {
260                                        FlowArch::X86_64 => "x86_64",
261                                        FlowArch::Aarch64 => "aarch64",
262                                        arch => anyhow::bail!("unsupported arch {arch}"),
263                                    };
264
265                                    flowey::shell_cmd!(
266                                        rt,
267                                        "curl --fail -sSfLo rustup-init.exe https://win.rustup.rs/{arch} --output rustup-init"
268                                    ).run()?;
269                                    flowey::shell_cmd!(
270                                        rt,
271                                        "./rustup-init.exe {interactive_prompt...} {default_toolchain...}"
272                                    )
273                                    .run()?;
274                                },
275                                platform => anyhow::bail!("unsupported platform {platform}"),
276                            }
277
278                            if !additional_target_triples.is_empty() {
279                                flowey::shell_cmd!(rt, "rustup target add {additional_target_triples...}")
280                                    .run()?;
281                            }
282                            if !additional_components.is_empty() {
283                                flowey::shell_cmd!(rt, "rustup component add {additional_components...}")
284                                    .run()?;
285                            }
286
287                            Ok(())
288                        }
289                    })
290                } else if let Some(write_cargo_bin) = write_cargo_bin {
291                    ctx.emit_rust_step("ensure Rust is installed", |ctx| {
292                        let write_cargo_bin = write_cargo_bin.claim(ctx);
293                        move |rt| {
294                            rt.write(
295                                write_cargo_bin,
296                                &Some(crate::check_needs_relaunch::BinOrEnv::Bin(
297                                    "cargo".to_string(),
298                                )),
299                            );
300
301                            check_rust_install(rt)?;
302                            Ok(())
303                        }
304                    })
305                } else {
306                    ReadVar::from_static(()).into_side_effect()
307                }
308            } else {
309                ReadVar::from_static(()).into_side_effect()
310            }
311        };
312
313        // The reason we need to check for relaunch on Local but not GH Actions is that GH Actions
314        // spawns a new shell for each step, so the new shell will have the new $PATH. On the local backend,
315        // the same shell is reused and needs to be relaunched to pick up the new $PATH.
316        let is_installed =
317            if !ensure_installed.is_empty() && matches!(ctx.backend(), FlowBackend::Local) {
318                let (read_bin, write_cargo_bin) = ctx.new_var();
319                ctx.req(crate::check_needs_relaunch::Params {
320                    check: read_bin,
321                    done: ensure_installed,
322                });
323                check_is_installed(Some(write_cargo_bin), Vec::new(), auto_install, ctx)
324            } else {
325                check_is_installed(None, ensure_installed, auto_install, ctx)
326            };
327
328        if !get_rust_toolchain.is_empty() {
329            ctx.emit_rust_step("detect active toolchain", |ctx| {
330                is_installed.clone().claim(ctx);
331                let get_rust_toolchain = get_rust_toolchain.claim(ctx);
332
333                move |rt| {
334                    let rust_toolchain = match rust_toolchain {
335                        Some(toolchain) => Some(toolchain),
336                        None => {
337                            if let Ok(rustup) = which::which("rustup") {
338                                // Unfortunately, `rustup` still doesn't have any stable way to emit
339                                // machine-readable output. See https://github.com/rust-lang/rustup/issues/450
340                                //
341                                // As a result, this logic is written to work with multiple rustup
342                                // versions, both prior-to, and after 1.28.0.
343                                //
344                                // Prior to 1.28.0:
345                                //   $ rustup show active-toolchain
346                                //   stable-x86_64-unknown-linux-gnu (default)
347                                //
348                                // Starting from 1.28.0:
349                                //   $ rustup show active-toolchain
350                                //   stable-x86_64-unknown-linux-gnu
351                                //   active because: it's the default toolchain
352                                let output =
353                                    flowey::shell_cmd!(rt, "{rustup} show active-toolchain")
354                                        .output()?;
355                                let stdout = String::from_utf8(output.stdout)?;
356                                let line = stdout.lines().next().unwrap();
357                                Some(line.split(' ').next().unwrap().into())
358                            } else {
359                                None
360                            }
361                        }
362                    };
363
364                    rt.write_all(get_rust_toolchain, &rust_toolchain);
365
366                    Ok(())
367                }
368            });
369        }
370
371        if !get_cargo_home.is_empty() {
372            ctx.emit_rust_step("report $CARGO_HOME", |ctx| {
373                is_installed.claim(ctx);
374                let get_cargo_home = get_cargo_home.claim(ctx);
375                move |rt| {
376                    let cargo_home = home::cargo_home()?;
377                    rt.write_all(get_cargo_home, &cargo_home);
378
379                    Ok(())
380                }
381            });
382        }
383
384        Ok(())
385    }
386}