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