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