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: 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 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 let argv0 = if rust_toolchain.is_some() {
210 "rustup"
211 } else {
212 "cargo"
213 };
214
215 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 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 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}