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 out_dir = rt.sh.current_dir();
265
266 rt.sh.change_dir(cargo_work_dir);
267 let mut cmd = flowey::shell_cmd!(rt, "{argv0} {params...}");
268 if !matches!(rt.backend(), FlowBackend::Local) {
269 with_env.insert("CARGO_INCREMENTAL".to_owned(), "0".to_owned());
272 } else {
273 cmd = cmd
277 .arg("--target-dir")
278 .arg(in_folder.join("target").join(&crate_name));
279 }
280 cmd = cmd.envs(&with_env);
281
282 log::info!(
283 "$ {}{cmd}",
284 with_env
285 .iter()
286 .map(|(k, v)| format!("{k}={v} "))
287 .collect::<Vec<_>>()
288 .concat()
289 );
290 let json = cmd.read()?;
291 let messages: Vec<cargo_output::Message> =
292 serde_json::Deserializer::from_str(&json)
293 .into_iter()
294 .collect::<Result<_, _>>()
295 .context("failed to deserialize cargo output")?;
296
297 rt.sh.change_dir(out_dir.clone());
298
299 let build_output =
300 rename_output(&messages, &crate_name, &out_name, crate_type, &out_dir)?;
301
302 rt.write(output, &build_output);
303
304 Ok(())
305 }
306 });
307 }
308
309 Ok(())
310 }
311}
312
313struct CargoBuildCommand {
314 argv0: String,
315 params: Vec<String>,
316 with_env: BTreeMap<String, String>,
317 cargo_work_dir: PathBuf,
318 out_name: String,
319 crate_type: CargoCrateType,
320}
321
322fn rename_output(
323 messages: &[cargo_output::Message],
324 crate_name: &str,
325 out_name: &str,
326 crate_type: CargoCrateType,
327 out_dir: &Path,
328) -> Result<CargoBuildOutput, anyhow::Error> {
329 let filenames = messages
330 .iter()
331 .find_map(|msg| match msg {
332 cargo_output::Message::CompilerArtifact {
333 target: cargo_output::Target { name, kind },
334 filenames,
335 } if name == crate_name && kind.iter().any(|k| k == crate_type.as_str()) => {
336 Some(filenames)
337 }
338 _ => None,
339 })
340 .with_context(|| {
341 format!(
342 "failed to find artifact {crate_name} of kind {kind}",
343 kind = crate_type.as_str()
344 )
345 })?;
346
347 let find_source = |name: &str| {
348 filenames
349 .iter()
350 .find(|path| path.file_name().is_some_and(|f| f == name))
351 };
352
353 fn rename_or_copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
354 let res = fs_err::rename(from.as_ref(), to.as_ref());
355
356 let needs_copy = match res {
357 Ok(_) => false,
358 Err(e) => match e.kind() {
359 std::io::ErrorKind::CrossesDevices => true,
360 _ => return Err(e),
361 },
362 };
363
364 if needs_copy {
365 fs_err::copy(from, to)?;
366 }
367
368 Ok(())
369 }
370
371 let do_rename = |ext: &str, no_dash: bool| -> anyhow::Result<_> {
372 let mut file_name = if !no_dash {
373 out_name.into()
374 } else {
375 out_name.replace('-', "_")
376 };
377 if !ext.is_empty() {
378 file_name.push('.');
379 file_name.push_str(ext);
380 }
381
382 let rename_path_base = out_dir.join(&file_name);
383 rename_or_copy(
384 find_source(&file_name)
385 .with_context(|| format!("failed to find artifact file {file_name}"))?,
386 &rename_path_base,
387 )?;
388 anyhow::Ok(rename_path_base)
389 };
390
391 let expected_output = match crate_type {
392 CargoCrateType::Bin => {
393 if find_source(&format!("{out_name}.exe")).is_some() {
394 let exe = do_rename("exe", false)?;
395 let pdb = do_rename("pdb", true)?;
396 CargoBuildOutput::WindowsBin { exe, pdb }
397 } else if find_source(&format!("{out_name}.efi")).is_some() {
398 let efi = do_rename("efi", false)?;
399 let pdb = do_rename("pdb", true)?;
400 CargoBuildOutput::UefiBin { efi, pdb }
401 } else if find_source(out_name).is_some() {
402 let bin = do_rename("", false)?;
403 CargoBuildOutput::ElfBin { bin }
404 } else {
405 anyhow::bail!("failed to find binary artifact for {out_name}");
406 }
407 }
408 CargoCrateType::DynamicLib => {
409 if find_source(&format!("{out_name}.dll")).is_some() {
410 let dll = do_rename("dll", false)?;
411 let dll_lib = do_rename("dll.lib", false)?;
412 let pdb = do_rename("pdb", true)?;
413
414 CargoBuildOutput::WindowsDynamicLib { dll, dll_lib, pdb }
415 } else if let Some(source) = find_source(&format!("lib{out_name}.so")) {
416 let so = {
417 let rename_path = out_dir.join(format!("lib{out_name}.so"));
418 rename_or_copy(source, &rename_path)?;
419 rename_path
420 };
421
422 CargoBuildOutput::LinuxDynamicLib { so }
423 } else {
424 anyhow::bail!("failed to find dynamic library artifact for {out_name}");
425 }
426 }
427 CargoCrateType::StaticLib => {
428 if find_source(&format!("{out_name}.lib")).is_some() {
429 let lib = do_rename("lib", false)?;
430 let pdb = do_rename("pdb", true)?;
431
432 CargoBuildOutput::WindowsStaticLib { lib, pdb }
433 } else if let Some(source) = find_source(&format!("lib{out_name}.a")) {
434 let a = {
435 let rename_path = out_dir.join(format!("lib{out_name}.a"));
436 rename_or_copy(source, &rename_path)?;
437 rename_path
438 };
439
440 CargoBuildOutput::LinuxStaticLib { a }
441 } else {
442 anyhow::bail!("failed to find static library artifact for {out_name}");
443 }
444 }
445 };
446
447 Ok(expected_output)
448}