flowey_lib_common/
install_rust.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Globally install a Rust toolchain, and ensure those tools are available on
//! the user's $PATH

use flowey::node::prelude::*;
use std::collections::BTreeSet;
use std::io::Write;

new_flow_node!(struct Node);

flowey_request! {
    pub enum Request {
        /// Automatically install all required Rust tools and components.
        ///
        /// If false - will check for pre-existing Rust installation, and fail
        /// if it doesn't meet the current job's requirements.
        AutoInstall(bool),

        /// Ignore the Version requirement, and build using whatever version of
        /// the Rust toolchain the user has installed locally.
        IgnoreVersion(bool),

        /// Install a specific Rust toolchain version.
        // FUTURE: support installing / using multiple versions of the Rust
        // toolchain at the same time, e.g: for stable and nightly, or to
        // support regression tests between a pinned rust version and current
        // stable.
        Version(String),

        /// Specify an additional target-triple to install the toolchain for.
        ///
        /// By default, only the native target will be installed.
        InstallTargetTriple(target_lexicon::Triple),

        /// If Rust was installed via Rustup, return the rustup toolchain that
        /// was installed (e.g: when specifting `+stable` or `+nightly` to
        /// commands)
        GetRustupToolchain(WriteVar<Option<String>>),

        /// Get the path to $CARGO_HOME
        GetCargoHome(WriteVar<PathBuf>),

        /// Ensure that Rust was installed and is available on the $PATH
        EnsureInstalled(WriteVar<SideEffect>),
    }
}

impl FlowNode for Node {
    type Request = Request;

    fn imports(dep: &mut ImportCtx<'_>) {
        dep.import::<crate::check_needs_relaunch::Node>();
    }

    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
        if !matches!(ctx.backend(), FlowBackend::Local | FlowBackend::Github) {
            anyhow::bail!("only supported on the local and github backends at this time");
        }

        let mut ensure_installed = Vec::new();
        let mut rust_toolchain = None;
        let mut auto_install = None;
        let mut ignore_version = None;
        let mut additional_target_triples = BTreeSet::new();
        let mut get_rust_toolchain = Vec::new();
        let mut get_cargo_home = Vec::new();

        for req in requests {
            match req {
                Request::EnsureInstalled(v) => ensure_installed.push(v),
                Request::AutoInstall(v) => {
                    same_across_all_reqs("AutoInstall", &mut auto_install, v)?
                }
                Request::IgnoreVersion(v) => {
                    same_across_all_reqs("IgnoreVersion", &mut ignore_version, v)?
                }
                Request::Version(v) => same_across_all_reqs("Version", &mut rust_toolchain, v)?,
                Request::InstallTargetTriple(s) => {
                    additional_target_triples.insert(s.to_string());
                }
                Request::GetRustupToolchain(v) => get_rust_toolchain.push(v),
                Request::GetCargoHome(v) => get_cargo_home.push(v),
            }
        }

        let ensure_installed = ensure_installed;
        let auto_install =
            auto_install.ok_or(anyhow::anyhow!("Missing essential request: AutoInstall",))?;
        if !auto_install && matches!(ctx.backend(), FlowBackend::Github) {
            anyhow::bail!("`AutoInstall` must be true when using the Github backend");
        }
        let ignore_version =
            ignore_version.ok_or(anyhow::anyhow!("Missing essential request: IgnoreVersion",))?;
        if ignore_version && matches!(ctx.backend(), FlowBackend::Github) {
            anyhow::bail!("`IgnoreVersion` must be false when using the Github backend");
        }
        let rust_toolchain =
            rust_toolchain.ok_or(anyhow::anyhow!("Missing essential request: RustToolchain"))?;
        let additional_target_triples = additional_target_triples;
        let get_rust_toolchain = get_rust_toolchain;
        let get_cargo_home = get_cargo_home;

        // -- end of req processing -- //

        let rust_toolchain = (!ignore_version).then_some(rust_toolchain);

        let check_rust_install = {
            let rust_toolchain = rust_toolchain.clone();
            let additional_target_triples = additional_target_triples.clone();

            move |_: &mut RustRuntimeServices<'_>| {
                if which::which("cargo").is_err() {
                    anyhow::bail!("did not find `cargo` on $PATH");
                }

                let rust_toolchain = rust_toolchain.map(|s| format!("+{s}"));

                // make sure the specific rust version was installed
                let sh = xshell::Shell::new()?;
                {
                    let rust_toolchain = rust_toolchain.clone();
                    xshell::cmd!(sh, "rustc {rust_toolchain...} -vV").run()?;
                }

                // make sure the additional target triples were installed
                if let Ok(rustup) = which::which("rustup") {
                    let output =
                        xshell::cmd!(sh, "{rustup} {rust_toolchain...} target list --installed")
                            .ignore_status()
                            .output()?;
                    let stderr = String::from_utf8(output.stderr)?;
                    let stdout = String::from_utf8(output.stdout)?;

                    // This error message may occur if the user has rustup
                    // installed, but is using a custom custom toolchain.
                    //
                    // NOTE: not thrilled that we are sniffing a magic string
                    // from stderr... but I'm also not sure if there's a better
                    // way to detect this...
                    if stderr.contains("does not support components") {
                        log::warn!("Detected a non-standard `rustup default` toolchain!");
                        log::warn!(
                            "Will not be able to double-check that all required target-triples are available."
                        );
                    } else {
                        let mut installed_target_triples = BTreeSet::new();

                        for line in stdout.lines() {
                            let triple = line.trim();
                            installed_target_triples.insert(triple);
                        }

                        for expected_target in additional_target_triples {
                            if !installed_target_triples.contains(expected_target.as_str()) {
                                anyhow::bail!(
                                    "missing required target-triple: {expected_target}; to intsall: `rustup target add {expected_target}`"
                                )
                            }
                        }
                    }
                } else {
                    log::warn!("`rustup` was not found!");
                    log::warn!("Unable to double-check that all target-triples are available.")
                }

                anyhow::Ok(())
            }
        };

        let check_is_installed = |write_cargo_bin: Option<
            WriteVar<Option<crate::check_needs_relaunch::BinOrEnv>>,
        >,
                                  ensure_installed: Vec<WriteVar<SideEffect>>,
                                  auto_install: bool,
                                  ctx: &mut NodeCtx<'_>| {
            if write_cargo_bin.is_some() || !ensure_installed.is_empty() {
                if auto_install || matches!(ctx.backend(), FlowBackend::Github) {
                    let added_to_path = if matches!(ctx.backend(), FlowBackend::Github) {
                        Some(ctx.emit_rust_step("add default cargo home to path", |_| {
                            |_| {
                                let default_cargo_home = home::home_dir()
                                    .context("Unable to get home dir")?
                                    .join(".cargo")
                                    .join("bin");
                                let github_path = std::env::var("GITHUB_PATH")?;
                                let mut github_path =
                                    fs_err::File::options().append(true).open(github_path)?;
                                github_path
                                    .write_all(default_cargo_home.as_os_str().as_encoded_bytes())?;
                                log::info!("Added {} to PATH", default_cargo_home.display());
                                Ok(())
                            }
                        }))
                    } else {
                        None
                    };

                    let rust_toolchain = rust_toolchain.clone();
                    ctx.emit_rust_step("install Rust", |ctx| {
                        let write_cargo_bin = if let Some(write_cargo_bin) = write_cargo_bin {
                            Some(write_cargo_bin.claim(ctx))
                        } else {
                            ensure_installed.claim(ctx);
                            None
                        };
                        added_to_path.claim(ctx);

                        move |rt: &mut RustRuntimeServices<'_>| {
                            if let Some(write_cargo_bin) = write_cargo_bin {
                                rt.write(write_cargo_bin, &Some(crate::check_needs_relaunch::BinOrEnv::Bin("cargo".to_string())));
                            }
                            let rust_toolchain = rust_toolchain.clone();
                            if check_rust_install.clone()(rt).is_ok() {
                                return Ok(());
                            }

                            let sh = xshell::Shell::new()?;
                            match rt.platform() {
                                FlowPlatform::Linux(_) => {
                                    let interactive_prompt = Some("-y");
                                    let mut default_toolchain = Vec::new();
                                    if let Some(ver) = rust_toolchain {
                                        default_toolchain.push("--default-toolchain".into());
                                        default_toolchain.push(ver)
                                    };

                                    xshell::cmd!(
                                        sh,
                                        "curl --fail --proto =https --tlsv1.2 -sSf https://sh.rustup.rs -o rustup-init.sh"
                                    )
                                    .run()?;
                                    xshell::cmd!(sh, "chmod +x ./rustup-init.sh").run()?;
                                    xshell::cmd!(
                                        sh,
                                        "./rustup-init.sh {interactive_prompt...} {default_toolchain...}"
                                    )
                                    .run()?;
                                }
                                FlowPlatform::Windows => {
                                    let interactive_prompt = Some("-y");
                                    let mut default_toolchain = Vec::new();
                                    if let Some(ver) = rust_toolchain {
                                        default_toolchain.push("--default-toolchain".into());
                                        default_toolchain.push(ver)
                                    };

                                    let arch = match rt.arch() {
                                        FlowArch::X86_64 => "x86_64",
                                        FlowArch::Aarch64 => "aarch64",
                                        arch => anyhow::bail!("unsupported arch {arch}"),
                                    };

                                    xshell::cmd!(
                                        sh,
                                        "curl --fail -sSfLo rustup-init.exe https://win.rustup.rs/{arch} --output rustup-init"
                                    ).run()?;
                                    xshell::cmd!(
                                        sh,
                                        "./rustup-init.exe {interactive_prompt...} {default_toolchain...}"
                                    )
                                    .run()?;
                                },
                                platform => anyhow::bail!("unsupported platform {platform}"),
                            }

                            if !additional_target_triples.is_empty() {
                                xshell::cmd!(sh, "rustup target add {additional_target_triples...}")
                                    .run()?;
                            }

                            Ok(())
                        }
                    })
                } else if let Some(write_cargo_bin) = write_cargo_bin {
                    ctx.emit_rust_step("ensure Rust is installed", |ctx| {
                        let write_cargo_bin = write_cargo_bin.claim(ctx);
                        move |rt| {
                            rt.write(
                                write_cargo_bin,
                                &Some(crate::check_needs_relaunch::BinOrEnv::Bin(
                                    "cargo".to_string(),
                                )),
                            );

                            check_rust_install(rt)?;
                            Ok(())
                        }
                    })
                } else {
                    ReadVar::from_static(()).into_side_effect()
                }
            } else {
                ReadVar::from_static(()).into_side_effect()
            }
        };

        // The reason we need to check for relaunch on Local but not GH Actions is that GH Actions
        // spawns a new shell for each step, so the new shell will have the new $PATH. On the local backend,
        // the same shell is reused and needs to be relaunched to pick up the new $PATH.
        let is_installed =
            if !ensure_installed.is_empty() && matches!(ctx.backend(), FlowBackend::Local) {
                let (read_bin, write_cargo_bin) = ctx.new_var();
                ctx.req(crate::check_needs_relaunch::Params {
                    check: read_bin,
                    done: ensure_installed,
                });
                check_is_installed(Some(write_cargo_bin), Vec::new(), auto_install, ctx)
            } else {
                check_is_installed(None, ensure_installed, auto_install, ctx)
            };

        if !get_rust_toolchain.is_empty() {
            ctx.emit_rust_step("detect active toolchain", |ctx| {
                is_installed.clone().claim(ctx);
                let get_rust_toolchain = get_rust_toolchain.claim(ctx);

                move |rt| {
                    let rust_toolchain = match rust_toolchain {
                        Some(toolchain) => Some(toolchain),
                        None => {
                            let sh = xshell::Shell::new()?;
                            if let Ok(rustup) = which::which("rustup") {
                                // Unfortunately, `rustup` still doesn't have any stable way to emit
                                // machine-readable output. See https://github.com/rust-lang/rustup/issues/450
                                //
                                // As a result, this logic is written to work with multiple rustup
                                // versions, both prior-to, and after 1.28.0.
                                //
                                // Prior to 1.28.0:
                                //   $ rustup show active-toolchain
                                //   stable-x86_64-unknown-linux-gnu (default)
                                //
                                // Starting from 1.28.0:
                                //   $ rustup show active-toolchain
                                //   stable-x86_64-unknown-linux-gnu
                                //   active because: it's the default toolchain
                                let output =
                                    xshell::cmd!(sh, "{rustup} show active-toolchain").output()?;
                                let stdout = String::from_utf8(output.stdout)?;
                                let line = stdout.lines().next().unwrap();
                                Some(line.split(' ').next().unwrap().into())
                            } else {
                                None
                            }
                        }
                    };

                    rt.write_all(get_rust_toolchain, &rust_toolchain);

                    Ok(())
                }
            });
        }

        if !get_cargo_home.is_empty() {
            ctx.emit_rust_step("report $CARGO_HOME", |ctx| {
                is_installed.claim(ctx);
                let get_cargo_home = get_cargo_home.claim(ctx);
                move |rt| {
                    let cargo_home = home::cargo_home()?;
                    rt.write_all(get_cargo_home, &cargo_home);

                    Ok(())
                }
            });
        }

        Ok(())
    }
}