Skip to main content

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;
15
16#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
17pub enum CargoBuildProfile {
18    /// Project-specific profile (will only work is profile is set up correctly
19    /// in the project's `Cargo.toml` file).
20    Custom(String),
21    Debug,
22    Release,
23}
24
25impl CargoBuildProfile {
26    pub fn from_release(value: bool) -> Self {
27        match value {
28            true => CargoBuildProfile::Release,
29            false => CargoBuildProfile::Debug,
30        }
31    }
32}
33
34#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Default)]
35pub enum CargoFeatureSet {
36    All,
37    Specific(Vec<String>),
38    #[default]
39    None,
40}
41
42impl<T, const N: usize> From<[T; N]> for CargoFeatureSet
43where
44    T: AsRef<str>,
45{
46    fn from(value: [T; N]) -> Self {
47        Self::from(value.as_slice())
48    }
49}
50
51impl<T> From<&[T]> for CargoFeatureSet
52where
53    T: AsRef<str>,
54{
55    fn from(value: &[T]) -> Self {
56        CargoFeatureSet::Specific(value.iter().map(|s| s.as_ref().to_owned()).collect())
57    }
58}
59
60impl CargoFeatureSet {
61    pub fn to_cargo_arg_strings(&self) -> Vec<String> {
62        match self {
63            CargoFeatureSet::All => vec!["--all-features".into()],
64            CargoFeatureSet::Specific(features) => {
65                if features.is_empty() {
66                    vec![]
67                } else {
68                    vec!["--features".into(), features.join(",")]
69                }
70            }
71            CargoFeatureSet::None => vec![],
72        }
73    }
74}
75
76#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
77pub enum CargoCrateType {
78    Bin,
79    StaticLib,
80    DynamicLib,
81}
82
83impl CargoCrateType {
84    fn as_str(&self) -> &str {
85        match self {
86            CargoCrateType::Bin => "bin",
87            CargoCrateType::StaticLib => "staticlib",
88            CargoCrateType::DynamicLib => "cdylib",
89        }
90    }
91}
92
93#[derive(Serialize, Deserialize)]
94pub enum CargoBuildOutput {
95    WindowsBin {
96        exe: PathBuf,
97        /// Path to the separate debug file (`.pdb`), if one was produced.
98        ///
99        /// The MSVC toolchain emits a `.pdb`; the GNU (mingw-w64) toolchain
100        /// embeds DWARF debug info in the `.exe` and produces no separate
101        /// file, so this is `None` for GNU builds.
102        pdb: Option<PathBuf>,
103    },
104    ElfBin {
105        bin: PathBuf,
106    },
107    LinuxStaticLib {
108        a: PathBuf,
109    },
110    LinuxDynamicLib {
111        so: PathBuf,
112    },
113    WindowsStaticLib {
114        lib: PathBuf,
115        pdb: PathBuf,
116    },
117    WindowsDynamicLib {
118        dll: PathBuf,
119        dll_lib: PathBuf,
120        pdb: PathBuf,
121    },
122    UefiBin {
123        efi: PathBuf,
124        pdb: PathBuf,
125    },
126}
127
128flowey_request! {
129    pub struct Request {
130        pub in_folder: ReadVar<PathBuf>,
131        pub crate_name: String,
132        pub out_name: String,
133        pub profile: CargoBuildProfile,
134        pub features: CargoFeatureSet,
135        pub output_kind: CargoCrateType,
136        pub target: Option<target_lexicon::Triple>,
137        pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
138        pub config: Vec<String>,
139        /// Wait for specified side-effects to resolve before running cargo-run.
140        ///
141        /// (e.g: to allow for some ambient packages / dependencies to get
142        /// installed).
143        pub pre_build_deps: Vec<ReadVar<SideEffect>>,
144        pub output: WriteVar<CargoBuildOutput>,
145    }
146}
147
148new_flow_node!(struct Node);
149
150impl FlowNode for Node {
151    type Request = Request;
152
153    fn imports(ctx: &mut ImportCtx<'_>) {
154        ctx.import::<crate::cfg_cargo_common_flags::Node>();
155        ctx.import::<crate::install_rust::Node>();
156    }
157
158    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
159        let rust_toolchain = ctx.reqv(crate::install_rust::Request::GetRustupToolchain);
160        let flags = ctx.reqv(crate::cfg_cargo_common_flags::Request::GetFlags);
161
162        for Request {
163            in_folder,
164            crate_name,
165            out_name,
166            profile,
167            features,
168            output_kind,
169            target,
170            extra_env,
171            config,
172            pre_build_deps,
173            output,
174        } in requests
175        {
176            if let Some(target) = &target {
177                ctx.req(crate::install_rust::Request::InstallTargetTriple(
178                    target.clone(),
179                ));
180            }
181
182            ctx.emit_rust_step(format!("cargo build {crate_name}"), |ctx| {
183                pre_build_deps.claim(ctx);
184                let rust_toolchain = rust_toolchain.clone().claim(ctx);
185                let flags = flags.clone().claim(ctx);
186                let in_folder = in_folder.claim(ctx);
187                let output = output.claim(ctx);
188                let extra_env = extra_env.claim(ctx);
189                move |rt| {
190                    let rust_toolchain = rt.read(rust_toolchain);
191                    let flags = rt.read(flags);
192                    let in_folder = rt.read(in_folder);
193                    let with_env = rt.read(extra_env).unwrap_or_default();
194
195                    let crate::cfg_cargo_common_flags::Flags {
196                        locked,
197                        verbose,
198                        no_incremental,
199                    } = flags;
200
201                    let cargo_profile = match &profile {
202                        CargoBuildProfile::Debug => "dev",
203                        CargoBuildProfile::Release => "release",
204                        CargoBuildProfile::Custom(s) => s,
205                    };
206
207                    // would be nice to use +{toolchain} syntax instead, but that
208                    // doesn't work on windows via xshell for some reason...
209                    let argv0 = if rust_toolchain.is_some() {
210                        "rustup"
211                    } else {
212                        "cargo"
213                    };
214
215                    // FIXME: this flow is vestigial from a time when this node
216                    // would return `CargoBuildCommand` back to the caller.
217                    //
218                    // this should be replaced with a easier to read + maintain
219                    // `xshell` invocation
220                    let cmd = CargoBuildCommand {
221                        argv0: argv0.into(),
222                        params: {
223                            let mut v = Vec::new();
224                            if let Some(rust_toolchain) = &rust_toolchain {
225                                v.push("run".into());
226                                v.push(rust_toolchain.into());
227                                v.push("cargo".into());
228                            }
229                            v.push("build".into());
230                            v.push("--message-format=json-render-diagnostics".into());
231                            if verbose {
232                                v.push("--verbose".into());
233                            }
234                            if locked {
235                                v.push("--locked".into());
236                            }
237                            v.push("-p".into());
238                            v.push(crate_name.clone());
239                            v.extend(features.to_cargo_arg_strings());
240                            if let Some(target) = &target {
241                                v.push("--target".into());
242                                v.push(target.to_string());
243                            }
244                            v.push("--profile".into());
245                            v.push(cargo_profile.into());
246                            v.extend(config.iter().flat_map(|x| ["--config", x]).map(Into::into));
247                            match output_kind {
248                                CargoCrateType::Bin => {
249                                    v.push("--bin".into());
250                                    v.push(out_name.clone());
251                                }
252                                CargoCrateType::StaticLib | CargoCrateType::DynamicLib => {
253                                    v.push("--lib".into());
254                                }
255                            }
256                            v
257                        },
258                        with_env,
259                        cargo_work_dir: in_folder.clone(),
260                        out_name,
261                        crate_type: output_kind,
262                    };
263
264                    let CargoBuildCommand {
265                        argv0,
266                        params,
267                        mut with_env,
268                        cargo_work_dir,
269                        out_name,
270                        crate_type,
271                    } = cmd;
272
273                    let out_dir = rt.sh.current_dir();
274
275                    rt.sh.change_dir(cargo_work_dir);
276                    let mut cmd = flowey::shell_cmd!(rt, "{argv0} {params...}");
277                    if no_incremental {
278                        with_env.insert("CARGO_INCREMENTAL".to_owned(), "0".to_owned());
279                    } else if matches!(rt.backend(), FlowBackend::Local) {
280                        // if building locally, use per-package target dirs
281                        // to avoid rebuilding
282                        // TODO: remove this once cargo's caching improves
283                        cmd = cmd
284                            .arg("--target-dir")
285                            .arg(in_folder.join("target").join(&crate_name));
286                    }
287                    cmd = cmd.envs(&with_env);
288
289                    log::info!(
290                        "$ {}{cmd}",
291                        with_env
292                            .iter()
293                            .map(|(k, v)| format!("{k}={v} "))
294                            .collect::<Vec<_>>()
295                            .concat()
296                    );
297                    let json = cmd.read()?;
298                    let messages: Vec<cargo_output::Message> =
299                        serde_json::Deserializer::from_str(&json)
300                            .into_iter()
301                            .collect::<Result<_, _>>()
302                            .context("failed to deserialize cargo output")?;
303
304                    rt.sh.change_dir(out_dir.clone());
305
306                    let build_output =
307                        rename_output(&messages, &crate_name, &out_name, crate_type, &out_dir)?;
308
309                    rt.write(output, &build_output);
310
311                    Ok(())
312                }
313            });
314        }
315
316        Ok(())
317    }
318}
319
320struct CargoBuildCommand {
321    argv0: String,
322    params: Vec<String>,
323    with_env: BTreeMap<String, String>,
324    cargo_work_dir: PathBuf,
325    out_name: String,
326    crate_type: CargoCrateType,
327}
328
329fn rename_output(
330    messages: &[cargo_output::Message],
331    crate_name: &str,
332    out_name: &str,
333    crate_type: CargoCrateType,
334    out_dir: &Path,
335) -> Result<CargoBuildOutput, anyhow::Error> {
336    let filenames = messages
337        .iter()
338        .find_map(|msg| match msg {
339            cargo_output::Message::CompilerArtifact {
340                target: cargo_output::Target { name, kind },
341                filenames,
342            } if name == crate_name && kind.iter().any(|k| k == crate_type.as_str()) => {
343                Some(filenames)
344            }
345            _ => None,
346        })
347        .with_context(|| {
348            format!(
349                "failed to find artifact {crate_name} of kind {kind}",
350                kind = crate_type.as_str()
351            )
352        })?;
353
354    let find_source = |name: &str| {
355        filenames
356            .iter()
357            .find(|path| path.file_name().is_some_and(|f| f == name))
358    };
359
360    fn rename_or_copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
361        let res = fs_err::rename(from.as_ref(), to.as_ref());
362
363        let needs_copy = match res {
364            Ok(_) => false,
365            Err(e) => match e.kind() {
366                std::io::ErrorKind::CrossesDevices => true,
367                _ => return Err(e),
368            },
369        };
370
371        if needs_copy {
372            fs_err::copy(from, to)?;
373        }
374
375        Ok(())
376    }
377
378    let do_rename = |ext: &str, no_dash: bool| -> anyhow::Result<_> {
379        let mut file_name = if !no_dash {
380            out_name.into()
381        } else {
382            out_name.replace('-', "_")
383        };
384        if !ext.is_empty() {
385            file_name.push('.');
386            file_name.push_str(ext);
387        }
388
389        let rename_path_base = out_dir.join(&file_name);
390        rename_or_copy(
391            find_source(&file_name)
392                .with_context(|| format!("failed to find artifact file {file_name}"))?,
393            &rename_path_base,
394        )?;
395        anyhow::Ok(rename_path_base)
396    };
397
398    let expected_output = match crate_type {
399        CargoCrateType::Bin => {
400            if find_source(&format!("{out_name}.exe")).is_some() {
401                let exe = do_rename("exe", false)?;
402                // No `.pdb` is produced for GNU (mingw-w64) builds.
403                let pdb_name = format!("{}.pdb", out_name.replace('-', "_"));
404                let pdb = if find_source(&pdb_name).is_some() {
405                    Some(do_rename("pdb", true)?)
406                } else {
407                    None
408                };
409                CargoBuildOutput::WindowsBin { exe, pdb }
410            } else if find_source(&format!("{out_name}.efi")).is_some() {
411                let efi = do_rename("efi", false)?;
412                let pdb = do_rename("pdb", true)?;
413                CargoBuildOutput::UefiBin { efi, pdb }
414            } else if find_source(out_name).is_some() {
415                let bin = do_rename("", false)?;
416                CargoBuildOutput::ElfBin { bin }
417            } else {
418                anyhow::bail!("failed to find binary artifact for {out_name}");
419            }
420        }
421        CargoCrateType::DynamicLib => {
422            if find_source(&format!("{out_name}.dll")).is_some() {
423                let dll = do_rename("dll", false)?;
424                let dll_lib = do_rename("dll.lib", false)?;
425                let pdb = do_rename("pdb", true)?;
426
427                CargoBuildOutput::WindowsDynamicLib { dll, dll_lib, pdb }
428            } else if let Some(source) = find_source(&format!("lib{out_name}.so")) {
429                let so = {
430                    let rename_path = out_dir.join(format!("lib{out_name}.so"));
431                    rename_or_copy(source, &rename_path)?;
432                    rename_path
433                };
434
435                CargoBuildOutput::LinuxDynamicLib { so }
436            } else {
437                anyhow::bail!("failed to find dynamic library artifact for {out_name}");
438            }
439        }
440        CargoCrateType::StaticLib => {
441            if find_source(&format!("{out_name}.lib")).is_some() {
442                let lib = do_rename("lib", false)?;
443                let pdb = do_rename("pdb", true)?;
444
445                CargoBuildOutput::WindowsStaticLib { lib, pdb }
446            } else if let Some(source) = find_source(&format!("lib{out_name}.a")) {
447                let a = {
448                    let rename_path = out_dir.join(format!("lib{out_name}.a"));
449                    rename_or_copy(source, &rename_path)?;
450                    rename_path
451                };
452
453                CargoBuildOutput::LinuxStaticLib { a }
454            } else {
455                anyhow::bail!("failed to find static library artifact for {out_name}");
456            }
457        }
458    };
459
460    Ok(expected_output)
461}