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