1use 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 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 pdb: PathBuf,
98 },
99 ElfBin {
100 bin: PathBuf,
101 },
102 LinuxStaticLib {
103 a: PathBuf,
104 },
105 LinuxDynamicLib {
106 so: PathBuf,
107 },
108 WindowsStaticLib {
109 lib: PathBuf,
110 pdb: PathBuf,
111 },
112 WindowsDynamicLib {
113 dll: PathBuf,
114 dll_lib: PathBuf,
115 pdb: PathBuf,
116 },
117 UefiBin {
118 efi: PathBuf,
119 pdb: PathBuf,
120 },
121}
122
123flowey_request! {
124 pub struct Request {
125 pub in_folder: ReadVar<PathBuf>,
126 pub crate_name: String,
127 pub out_name: String,
128 pub profile: CargoBuildProfile,
129 pub features: CargoFeatureSet,
130 pub output_kind: CargoCrateType,
131 pub target: Option<target_lexicon::Triple>,
132 pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
133 pub config: Vec<String>,
134 pub pre_build_deps: Vec<ReadVar<SideEffect>>,
139 pub output: WriteVar<CargoBuildOutput>,
140 }
141}
142
143new_flow_node!(struct Node);
144
145impl FlowNode for Node {
146 type Request = Request;
147
148 fn imports(ctx: &mut ImportCtx<'_>) {
149 ctx.import::<crate::cfg_cargo_common_flags::Node>();
150 ctx.import::<crate::install_rust::Node>();
151 }
152
153 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
154 let rust_toolchain = ctx.reqv(crate::install_rust::Request::GetRustupToolchain);
155 let flags = ctx.reqv(crate::cfg_cargo_common_flags::Request::GetFlags);
156
157 for Request {
158 in_folder,
159 crate_name,
160 out_name,
161 profile,
162 features,
163 output_kind,
164 target,
165 extra_env,
166 config,
167 pre_build_deps,
168 output,
169 } in requests
170 {
171 if let Some(target) = &target {
172 ctx.req(crate::install_rust::Request::InstallTargetTriple(
173 target.clone(),
174 ));
175 }
176
177 ctx.emit_rust_step(format!("cargo build {crate_name}"), |ctx| {
178 pre_build_deps.claim(ctx);
179 let rust_toolchain = rust_toolchain.clone().claim(ctx);
180 let flags = flags.clone().claim(ctx);
181 let in_folder = in_folder.claim(ctx);
182 let output = output.claim(ctx);
183 let extra_env = extra_env.claim(ctx);
184 move |rt| {
185 let rust_toolchain = rt.read(rust_toolchain);
186 let flags = rt.read(flags);
187 let in_folder = rt.read(in_folder);
188 let with_env = rt.read(extra_env).unwrap_or_default();
189
190 let crate::cfg_cargo_common_flags::Flags { locked, verbose } = flags;
191
192 let cargo_profile = match &profile {
193 CargoBuildProfile::Debug => "dev",
194 CargoBuildProfile::Release => "release",
195 CargoBuildProfile::Custom(s) => s,
196 };
197
198 let argv0 = if rust_toolchain.is_some() {
201 "rustup"
202 } else {
203 "cargo"
204 };
205
206 let cmd = CargoBuildCommand {
212 argv0: argv0.into(),
213 params: {
214 let mut v = Vec::new();
215 if let Some(rust_toolchain) = &rust_toolchain {
216 v.push("run".into());
217 v.push(rust_toolchain.into());
218 v.push("cargo".into());
219 }
220 v.push("build".into());
221 v.push("--message-format=json-render-diagnostics".into());
222 if verbose {
223 v.push("--verbose".into());
224 }
225 if locked {
226 v.push("--locked".into());
227 }
228 v.push("-p".into());
229 v.push(crate_name.clone());
230 v.extend(features.to_cargo_arg_strings().into_iter());
231 if let Some(target) = &target {
232 v.push("--target".into());
233 v.push(target.to_string());
234 }
235 v.push("--profile".into());
236 v.push(cargo_profile.into());
237 v.extend(config.iter().flat_map(|x| ["--config", x]).map(Into::into));
238 match output_kind {
239 CargoCrateType::Bin => {
240 v.push("--bin".into());
241 v.push(out_name.clone());
242 }
243 CargoCrateType::StaticLib | CargoCrateType::DynamicLib => {
244 v.push("--lib".into());
245 }
246 }
247 v
248 },
249 with_env,
250 cargo_work_dir: in_folder.clone(),
251 out_name,
252 crate_type: output_kind,
253 };
254
255 let CargoBuildCommand {
256 argv0,
257 params,
258 mut with_env,
259 cargo_work_dir,
260 out_name,
261 crate_type,
262 } = cmd;
263
264 let sh = xshell::Shell::new()?;
265
266 let out_dir = sh.current_dir();
267
268 sh.change_dir(cargo_work_dir);
269 let mut cmd = xshell::cmd!(sh, "{argv0} {params...}");
270 if !matches!(rt.backend(), FlowBackend::Local) {
271 with_env.insert("CARGO_INCREMENTAL".to_owned(), "0".to_owned());
274 } else {
275 cmd = cmd
279 .arg("--target-dir")
280 .arg(in_folder.join("target").join(&crate_name));
281 }
282 cmd = cmd.envs(&with_env);
283
284 log::info!(
285 "$ {}{cmd}",
286 with_env
287 .iter()
288 .map(|(k, v)| format!("{k}={v} "))
289 .collect::<Vec<_>>()
290 .concat()
291 );
292 let json = cmd.read()?;
293 let messages: Vec<cargo_output::Message> =
294 serde_json::Deserializer::from_str(&json)
295 .into_iter()
296 .collect::<Result<_, _>>()
297 .context("failed to deserialize cargo output")?;
298
299 sh.change_dir(out_dir.clone());
300
301 let build_output =
302 rename_output(&messages, &crate_name, &out_name, crate_type, &out_dir)?;
303
304 rt.write(output, &build_output);
305
306 Ok(())
307 }
308 });
309 }
310
311 Ok(())
312 }
313}
314
315struct CargoBuildCommand {
316 argv0: String,
317 params: Vec<String>,
318 with_env: BTreeMap<String, String>,
319 cargo_work_dir: PathBuf,
320 out_name: String,
321 crate_type: CargoCrateType,
322}
323
324fn rename_output(
325 messages: &[cargo_output::Message],
326 crate_name: &str,
327 out_name: &str,
328 crate_type: CargoCrateType,
329 out_dir: &Path,
330) -> Result<CargoBuildOutput, anyhow::Error> {
331 let filenames = messages
332 .iter()
333 .find_map(|msg| match msg {
334 cargo_output::Message::CompilerArtifact {
335 target: cargo_output::Target { name, kind },
336 filenames,
337 } if name == crate_name && kind.iter().any(|k| k == crate_type.as_str()) => {
338 Some(filenames)
339 }
340 _ => None,
341 })
342 .with_context(|| {
343 format!(
344 "failed to find artifact {crate_name} of kind {kind}",
345 kind = crate_type.as_str()
346 )
347 })?;
348
349 let find_source = |name: &str| {
350 filenames
351 .iter()
352 .find(|path| path.file_name().is_some_and(|f| f == name))
353 };
354
355 fn rename_or_copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
356 let res = fs_err::rename(from.as_ref(), to.as_ref());
357
358 let needs_copy = match res {
359 Ok(_) => false,
360 Err(e) => match e.kind() {
361 std::io::ErrorKind::CrossesDevices => true,
362 _ => return Err(e),
363 },
364 };
365
366 if needs_copy {
367 fs_err::copy(from, to)?;
368 }
369
370 Ok(())
371 }
372
373 let do_rename = |ext: &str, no_dash: bool| -> anyhow::Result<_> {
374 let mut file_name = if !no_dash {
375 out_name.into()
376 } else {
377 out_name.replace('-', "_")
378 };
379 if !ext.is_empty() {
380 file_name.push('.');
381 file_name.push_str(ext);
382 }
383
384 let rename_path_base = out_dir.join(&file_name);
385 rename_or_copy(
386 find_source(&file_name)
387 .with_context(|| format!("failed to find artifact file {file_name}"))?,
388 &rename_path_base,
389 )?;
390 anyhow::Ok(rename_path_base)
391 };
392
393 let expected_output = match crate_type {
394 CargoCrateType::Bin => {
395 if find_source(&format!("{out_name}.exe")).is_some() {
396 let exe = do_rename("exe", false)?;
397 let pdb = do_rename("pdb", true)?;
398 CargoBuildOutput::WindowsBin { exe, pdb }
399 } else if find_source(&format!("{out_name}.efi")).is_some() {
400 let efi = do_rename("efi", false)?;
401 let pdb = do_rename("pdb", true)?;
402 CargoBuildOutput::UefiBin { efi, pdb }
403 } else if find_source(out_name).is_some() {
404 let bin = do_rename("", false)?;
405 CargoBuildOutput::ElfBin { bin }
406 } else {
407 anyhow::bail!("failed to find binary artifact for {out_name}");
408 }
409 }
410 CargoCrateType::DynamicLib => {
411 if find_source(&format!("{out_name}.dll")).is_some() {
412 let dll = do_rename("dll", false)?;
413 let dll_lib = do_rename("dll.lib", false)?;
414 let pdb = do_rename("pdb", true)?;
415
416 CargoBuildOutput::WindowsDynamicLib { dll, dll_lib, pdb }
417 } else if let Some(source) = find_source(&format!("lib{out_name}.so")) {
418 let so = {
419 let rename_path = out_dir.join(format!("lib{out_name}.so"));
420 rename_or_copy(source, &rename_path)?;
421 rename_path
422 };
423
424 CargoBuildOutput::LinuxDynamicLib { so }
425 } else {
426 anyhow::bail!("failed to find dynamic library artifact for {out_name}");
427 }
428 }
429 CargoCrateType::StaticLib => {
430 if find_source(&format!("{out_name}.lib")).is_some() {
431 let lib = do_rename("lib", false)?;
432 let pdb = do_rename("pdb", true)?;
433
434 CargoBuildOutput::WindowsStaticLib { lib, pdb }
435 } else if let Some(source) = find_source(&format!("lib{out_name}.a")) {
436 let a = {
437 let rename_path = out_dir.join(format!("lib{out_name}.a"));
438 rename_or_copy(source, &rename_path)?;
439 rename_path
440 };
441
442 CargoBuildOutput::LinuxStaticLib { a }
443 } else {
444 anyhow::bail!("failed to find static library artifact for {out_name}");
445 }
446 }
447 };
448
449 Ok(expected_output)
450}