flowey_lib_common/
run_cargo_build.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Encapsulates the logic of both invoking `cargo build`, and tracking where
5//! built artifacts are emitted (which varies depending on the crate's type and
6//! platform).
7//!
8//! Takes into account bits of "global" configuration and dependency management,
9//! such as setting global cargo flags (e.g: --verbose, --locked), ensuring any
10//! required Rust dependencies are installed (i.e: toolchain, triples), etc...
11
12use crate::_util::cargo_output;
13use flowey::node::prelude::*;
14use std::collections::BTreeMap;
15use std::collections::BTreeSet;
16
17#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
18pub enum CargoBuildProfile {
19    /// Project-specific profile (will only work is profile is set up correctly
20    /// in the project's `Cargo.toml` file).
21    Custom(String),
22    Debug,
23    Release,
24}
25
26impl CargoBuildProfile {
27    pub fn from_release(value: bool) -> Self {
28        match value {
29            true => CargoBuildProfile::Release,
30            false => CargoBuildProfile::Debug,
31        }
32    }
33}
34
35#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
36pub enum CargoCrateType {
37    Bin,
38    StaticLib,
39    DynamicLib,
40}
41
42impl CargoCrateType {
43    fn as_str(&self) -> &str {
44        match self {
45            CargoCrateType::Bin => "bin",
46            CargoCrateType::StaticLib => "staticlib",
47            CargoCrateType::DynamicLib => "cdylib",
48        }
49    }
50}
51
52#[derive(Serialize, Deserialize)]
53pub enum CargoBuildOutput {
54    WindowsBin {
55        exe: PathBuf,
56        pdb: PathBuf,
57    },
58    ElfBin {
59        bin: PathBuf,
60    },
61    LinuxStaticLib {
62        a: PathBuf,
63    },
64    LinuxDynamicLib {
65        so: PathBuf,
66    },
67    WindowsStaticLib {
68        lib: PathBuf,
69        pdb: PathBuf,
70    },
71    WindowsDynamicLib {
72        dll: PathBuf,
73        dll_lib: PathBuf,
74        pdb: PathBuf,
75    },
76    UefiBin {
77        efi: PathBuf,
78        pdb: PathBuf,
79    },
80}
81
82flowey_request! {
83    pub struct Request {
84        pub in_folder: ReadVar<PathBuf>,
85        pub crate_name: String,
86        pub out_name: String,
87        pub profile: CargoBuildProfile,
88        pub features: BTreeSet<String>,
89        pub output_kind: CargoCrateType,
90        pub target: Option<target_lexicon::Triple>,
91        pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
92        pub config: Vec<String>,
93        /// Wait for specified side-effects to resolve before running cargo-run.
94        ///
95        /// (e.g: to allow for some ambient packages / dependencies to get
96        /// installed).
97        pub pre_build_deps: Vec<ReadVar<SideEffect>>,
98        pub output: WriteVar<CargoBuildOutput>,
99    }
100}
101
102new_flow_node!(struct Node);
103
104impl FlowNode for Node {
105    type Request = Request;
106
107    fn imports(ctx: &mut ImportCtx<'_>) {
108        ctx.import::<crate::cfg_cargo_common_flags::Node>();
109        ctx.import::<crate::install_rust::Node>();
110    }
111
112    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
113        let rust_toolchain = ctx.reqv(crate::install_rust::Request::GetRustupToolchain);
114        let flags = ctx.reqv(crate::cfg_cargo_common_flags::Request::GetFlags);
115
116        for Request {
117            in_folder,
118            crate_name,
119            out_name,
120            profile,
121            features,
122            output_kind,
123            target,
124            extra_env,
125            config,
126            pre_build_deps,
127            output,
128        } in requests
129        {
130            if let Some(target) = &target {
131                ctx.req(crate::install_rust::Request::InstallTargetTriple(
132                    target.clone(),
133                ));
134            }
135
136            ctx.emit_rust_step(format!("cargo build {crate_name}"), |ctx| {
137                pre_build_deps.claim(ctx);
138                let rust_toolchain = rust_toolchain.clone().claim(ctx);
139                let flags = flags.clone().claim(ctx);
140                let in_folder = in_folder.claim(ctx);
141                let output = output.claim(ctx);
142                let extra_env = extra_env.claim(ctx);
143                move |rt| {
144                    let rust_toolchain = rt.read(rust_toolchain);
145                    let flags = rt.read(flags);
146                    let in_folder = rt.read(in_folder);
147                    let with_env = rt.read(extra_env).unwrap_or_default();
148
149                    let crate::cfg_cargo_common_flags::Flags { locked, verbose } = flags;
150
151                    let features = features.into_iter().collect::<Vec<_>>().join(",");
152
153                    let cargo_profile = match &profile {
154                        CargoBuildProfile::Debug => "dev",
155                        CargoBuildProfile::Release => "release",
156                        CargoBuildProfile::Custom(s) => s,
157                    };
158
159                    // would be nice to use +{toolchain} syntax instead, but that
160                    // doesn't work on windows via xshell for some reason...
161                    let argv0 = if rust_toolchain.is_some() {
162                        "rustup"
163                    } else {
164                        "cargo"
165                    };
166
167                    // FIXME: this flow is vestigial from a time when this node
168                    // would return `CargoBuildCommand` back to the caller.
169                    //
170                    // this should be replaced with a easier to read + maintain
171                    // `xshell` invocation
172                    let cmd = CargoBuildCommand {
173                        argv0: argv0.into(),
174                        params: {
175                            let mut v = Vec::new();
176                            if let Some(rust_toolchain) = &rust_toolchain {
177                                v.push("run".into());
178                                v.push(rust_toolchain.into());
179                                v.push("cargo".into());
180                            }
181                            v.push("build".into());
182                            v.push("--message-format=json-render-diagnostics".into());
183                            if verbose {
184                                v.push("--verbose".into());
185                            }
186                            if locked {
187                                v.push("--locked".into());
188                            }
189                            v.push("-p".into());
190                            v.push(crate_name.clone());
191                            if !features.is_empty() {
192                                v.push("--features".into());
193                                v.push(features);
194                            }
195                            if let Some(target) = &target {
196                                v.push("--target".into());
197                                v.push(target.to_string());
198                            }
199                            v.push("--profile".into());
200                            v.push(cargo_profile.into());
201                            v.extend(config.iter().flat_map(|x| ["--config", x]).map(Into::into));
202                            match output_kind {
203                                CargoCrateType::Bin => {
204                                    v.push("--bin".into());
205                                    v.push(out_name.clone());
206                                }
207                                CargoCrateType::StaticLib | CargoCrateType::DynamicLib => {
208                                    v.push("--lib".into());
209                                }
210                            }
211                            v
212                        },
213                        with_env,
214                        cargo_work_dir: in_folder.clone(),
215                        out_name,
216                        crate_type: output_kind,
217                    };
218
219                    let CargoBuildCommand {
220                        argv0,
221                        params,
222                        with_env,
223                        cargo_work_dir,
224                        out_name,
225                        crate_type,
226                    } = cmd;
227
228                    let sh = xshell::Shell::new()?;
229
230                    let out_dir = sh.current_dir();
231
232                    sh.change_dir(cargo_work_dir);
233                    let mut cmd = xshell::cmd!(sh, "{argv0} {params...}");
234                    // if running in CI, no need to waste time with incremental
235                    // build artifacts
236                    if !matches!(rt.backend(), FlowBackend::Local) {
237                        cmd = cmd.env("CARGO_INCREMENTAL", "0");
238                    }
239                    for (key, val) in with_env {
240                        log::info!("extra_env: {key}={val}");
241                        cmd = cmd.env(key, val);
242                    }
243                    let json = cmd.read()?;
244                    let messages: Vec<cargo_output::Message> =
245                        serde_json::Deserializer::from_str(&json)
246                            .into_iter()
247                            .collect::<Result<_, _>>()
248                            .context("failed to deserialize cargo output")?;
249
250                    sh.change_dir(out_dir.clone());
251
252                    let build_output =
253                        rename_output(&messages, &crate_name, &out_name, crate_type, &out_dir)?;
254
255                    rt.write(output, &build_output);
256
257                    Ok(())
258                }
259            });
260        }
261
262        Ok(())
263    }
264}
265
266struct CargoBuildCommand {
267    argv0: String,
268    params: Vec<String>,
269    with_env: BTreeMap<String, String>,
270    cargo_work_dir: PathBuf,
271    out_name: String,
272    crate_type: CargoCrateType,
273}
274
275fn rename_output(
276    messages: &[cargo_output::Message],
277    crate_name: &str,
278    out_name: &str,
279    crate_type: CargoCrateType,
280    out_dir: &Path,
281) -> Result<CargoBuildOutput, anyhow::Error> {
282    let filenames = messages
283        .iter()
284        .find_map(|msg| match msg {
285            cargo_output::Message::CompilerArtifact {
286                target: cargo_output::Target { name, kind },
287                filenames,
288            } if name == crate_name && kind.iter().any(|k| k == crate_type.as_str()) => {
289                Some(filenames)
290            }
291            _ => None,
292        })
293        .with_context(|| {
294            format!(
295                "failed to find artifact {crate_name} of kind {kind}",
296                kind = crate_type.as_str()
297            )
298        })?;
299
300    let find_source = |name: &str| {
301        filenames
302            .iter()
303            .find(|path| path.file_name().is_some_and(|f| f == name))
304    };
305
306    fn rename_or_copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
307        let res = fs_err::rename(from.as_ref(), to.as_ref());
308
309        let needs_copy = match res {
310            Ok(_) => false,
311            Err(e) => match e.kind() {
312                std::io::ErrorKind::CrossesDevices => true,
313                _ => return Err(e),
314            },
315        };
316
317        if needs_copy {
318            fs_err::copy(from, to)?;
319        }
320
321        Ok(())
322    }
323
324    let do_rename = |ext: &str, no_dash: bool| -> anyhow::Result<_> {
325        let mut file_name = if !no_dash {
326            out_name.into()
327        } else {
328            out_name.replace('-', "_")
329        };
330        if !ext.is_empty() {
331            file_name.push('.');
332            file_name.push_str(ext);
333        }
334
335        let rename_path_base = out_dir.join(&file_name);
336        rename_or_copy(
337            find_source(&file_name)
338                .with_context(|| format!("failed to find artifact file {file_name}"))?,
339            &rename_path_base,
340        )?;
341        anyhow::Ok(rename_path_base)
342    };
343
344    let expected_output = match crate_type {
345        CargoCrateType::Bin => {
346            if find_source(&format!("{out_name}.exe")).is_some() {
347                let exe = do_rename("exe", false)?;
348                let pdb = do_rename("pdb", true)?;
349                CargoBuildOutput::WindowsBin { exe, pdb }
350            } else if find_source(&format!("{out_name}.efi")).is_some() {
351                let efi = do_rename("efi", false)?;
352                let pdb = do_rename("pdb", true)?;
353                CargoBuildOutput::UefiBin { efi, pdb }
354            } else if find_source(out_name).is_some() {
355                let bin = do_rename("", false)?;
356                CargoBuildOutput::ElfBin { bin }
357            } else {
358                anyhow::bail!("failed to find binary artifact for {out_name}");
359            }
360        }
361        CargoCrateType::DynamicLib => {
362            if find_source(&format!("{out_name}.dll")).is_some() {
363                let dll = do_rename("dll", false)?;
364                let dll_lib = do_rename("dll.lib", false)?;
365                let pdb = do_rename("pdb", true)?;
366
367                CargoBuildOutput::WindowsDynamicLib { dll, dll_lib, pdb }
368            } else if let Some(source) = find_source(&format!("lib{out_name}.so")) {
369                let so = {
370                    let rename_path = out_dir.join(format!("lib{out_name}.so"));
371                    rename_or_copy(source, &rename_path)?;
372                    rename_path
373                };
374
375                CargoBuildOutput::LinuxDynamicLib { so }
376            } else {
377                anyhow::bail!("failed to find dynamic library artifact for {out_name}");
378            }
379        }
380        CargoCrateType::StaticLib => {
381            if find_source(&format!("{out_name}.lib")).is_some() {
382                let lib = do_rename("lib", false)?;
383                let pdb = do_rename("pdb", true)?;
384
385                CargoBuildOutput::WindowsStaticLib { lib, pdb }
386            } else if let Some(source) = find_source(&format!("lib{out_name}.a")) {
387                let a = {
388                    let rename_path = out_dir.join(format!("lib{out_name}.a"));
389                    rename_or_copy(source, &rename_path)?;
390                    rename_path
391                };
392
393                CargoBuildOutput::LinuxStaticLib { a }
394            } else {
395                anyhow::bail!("failed to find static library artifact for {out_name}");
396            }
397        }
398    };
399
400    Ok(expected_output)
401}