1use std::error::Error;
2use std::fmt::Display;
3use std::io::BufRead;
4use std::path::{Path, PathBuf};
5use std::process::{Command, ExitStatus, Stdio};
6use std::sync::OnceLock;
7
8use cargo_metadata::diagnostic::Diagnostic;
9use memo_map::MemoMap;
10use tokio::sync::OnceCell;
11
12use crate::HostTargetType;
13use crate::progress::ProgressTracker;
14
15#[derive(PartialEq, Eq, Hash, Clone)]
17pub struct BuildParams {
18 src: PathBuf,
21 workspace_root: PathBuf,
24 bin: Option<String>,
26 example: Option<String>,
28 profile: Option<String>,
30 rustflags: Option<String>,
31 target_dir: Option<PathBuf>,
32 build_env: Vec<(String, String)>,
34 no_default_features: bool,
35 target_type: HostTargetType,
37 is_dylib: bool,
39 features: Option<Vec<String>>,
41 config: Vec<String>,
43}
44impl BuildParams {
45 #[expect(clippy::too_many_arguments, reason = "internal code")]
47 pub fn new(
48 src: impl AsRef<Path>,
49 workspace_root: impl AsRef<Path>,
50 bin: Option<String>,
51 example: Option<String>,
52 profile: Option<String>,
53 rustflags: Option<String>,
54 target_dir: Option<PathBuf>,
55 build_env: Vec<(String, String)>,
56 no_default_features: bool,
57 target_type: HostTargetType,
58 is_dylib: bool,
59 features: Option<Vec<String>>,
60 config: Vec<String>,
61 ) -> Self {
62 let src = dunce::canonicalize(src.as_ref()).unwrap_or_else(|e| {
69 panic!(
70 "Failed to canonicalize path `{}` for build: {e}.",
71 src.as_ref().display(),
72 )
73 });
74
75 let workspace_root = dunce::canonicalize(workspace_root.as_ref()).unwrap_or_else(|e| {
76 panic!(
77 "Failed to canonicalize path `{}` for build: {e}.",
78 workspace_root.as_ref().display(),
79 )
80 });
81
82 BuildParams {
83 src,
84 workspace_root,
85 bin,
86 example,
87 profile,
88 rustflags,
89 target_dir,
90 build_env,
91 no_default_features,
92 target_type,
93 is_dylib,
94 features,
95 config,
96 }
97 }
98}
99
100pub struct BuildOutput {
102 pub bin_data: Vec<u8>,
104 pub bin_path: PathBuf,
106 pub shared_library_path: Option<PathBuf>,
108}
109impl BuildOutput {
110 pub fn unique_id(&self) -> impl use<> + Display {
112 blake3::hash(&self.bin_data).to_hex()
113 }
114}
115
116static BUILDS: OnceLock<MemoMap<BuildParams, OnceCell<BuildOutput>>> = OnceLock::new();
118
119pub async fn build_crate_memoized(params: BuildParams) -> Result<&'static BuildOutput, BuildError> {
120 BUILDS
121 .get_or_init(MemoMap::new)
122 .get_or_insert(¶ms, Default::default)
123 .get_or_try_init(move || {
124 ProgressTracker::rich_leaf("build", move |set_msg| async move {
125 tokio::task::spawn_blocking(move || {
126 let base_target_dir = params
127 .target_dir
128 .as_ref()
129 .cloned()
130 .unwrap_or_else(|| params.src.join("target"));
131 let job_name = params
132 .bin
133 .as_deref()
134 .or(params.example.as_deref())
135 .unwrap_or("default");
136
137 let (per_job_target_dir, _prebuild_guard, _cargo_lock) = if params.is_dylib {
140 let shared_debug = base_target_dir.join("debug");
141 let jobs_dir = base_target_dir.join("jobs");
142 let per_job = hydro_concurrent_cargo::setup_job_dir(&jobs_dir, job_name, &shared_debug);
143
144 let features = params.features.clone().unwrap_or_default();
145 let staged_paths = vec![
146 params.src.join("src").join("__staged.rs"),
147 params.src.join("Cargo.lock"),
148 std::env::current_exe().unwrap(),
149 ];
150
151 let src = params.src.clone();
152 let profile = params.profile.clone();
153 let target_type = params.target_type;
154 let no_default_features = params.no_default_features;
155 let features_for_closure = features.clone();
156 let config = params.config.clone();
157 let rustflags = params.rustflags.clone();
158 let build_env = params.build_env.clone();
159
160 let (prebuild_guard, cargo_lock) = hydro_concurrent_cargo::run_prebuild(
161 &base_target_dir,
162 params.src.parent().unwrap().file_name().unwrap().to_str().unwrap(),
163 &features,
164 &staged_paths,
165 |prebuild_target| {
166 set_msg("building dependencies".to_owned());
167
168 let mut dep_cmd = Command::new("cargo");
169 dep_cmd.current_dir(src.join("..").join("dylib"));
170 dep_cmd.args(["build", "--locked"]);
171
172 if let Some(profile) = profile.as_ref() {
173 dep_cmd.args(["--profile", profile]);
174 }
175
176 match target_type {
177 HostTargetType::Local => {}
178 HostTargetType::Linux(crate::LinuxCompileType::Glibc) => {
179 dep_cmd.args(["--target", "x86_64-unknown-linux-gnu"]);
180 }
181 HostTargetType::Linux(crate::LinuxCompileType::Musl) => {
182 dep_cmd.args(["--target", "x86_64-unknown-linux-musl"]);
183 }
184 }
185
186 if no_default_features {
187 dep_cmd.arg("--no-default-features");
188 }
189
190 if !features_for_closure.is_empty() {
191 dep_cmd.args(["--features", &features_for_closure.join(",")]);
192 }
193
194 for c in &config {
195 dep_cmd.args(["--config", c]);
196 }
197
198 dep_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
199
200 if let Some(rustflags) = rustflags.as_ref() {
201 dep_cmd.env("RUSTFLAGS", rustflags);
202 }
203
204 for (k, v) in &build_env {
205 dep_cmd.env(k, v);
206 }
207
208 eprintln!("[hydro-build] starting deploy prebuild child cargo");
209 let status = dep_cmd
210 .stdin(Stdio::null())
211 .status()
212 .unwrap();
213 eprintln!("[hydro-build] deploy prebuild child cargo finished, success={}", status.success());
214 if !status.success() {
215 panic!("dep prebuild failed");
216 }
217
218 eprintln!("[hydro-build] starting deploy prebuild dylib-examples lib");
220 let mut lib_cmd = Command::new("cargo");
221 lib_cmd.current_dir(&src);
222 lib_cmd.args(["build", "--locked", "--lib"]);
223 if let Some(profile) = profile.as_ref() {
224 lib_cmd.args(["--profile", profile]);
225 }
226 match target_type {
227 HostTargetType::Local => {}
228 HostTargetType::Linux(crate::LinuxCompileType::Glibc) => {
229 lib_cmd.args(["--target", "x86_64-unknown-linux-gnu"]);
230 }
231 HostTargetType::Linux(crate::LinuxCompileType::Musl) => {
232 lib_cmd.args(["--target", "x86_64-unknown-linux-musl"]);
233 }
234 }
235 if no_default_features {
236 lib_cmd.arg("--no-default-features");
237 }
238 if !features_for_closure.is_empty() {
239 lib_cmd.args(["--features", &features_for_closure.join(",")]);
240 }
241 for c in &config {
242 lib_cmd.args(["--config", c]);
243 }
244 lib_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
245 if let Some(rustflags) = rustflags.as_ref() {
246 lib_cmd.env("RUSTFLAGS", rustflags);
247 }
248 for (k, v) in &build_env {
249 lib_cmd.env(k, v);
250 }
251 let lib_status = lib_cmd
252 .stdin(Stdio::null())
253 .status()
254 .unwrap();
255 if !lib_status.success() {
256 panic!("dylib-examples lib prebuild failed");
257 }
258 },
259 );
260
261 (per_job, Some(prebuild_guard), Some(cargo_lock))
262 } else {
263 (base_target_dir.clone(), None, None)
264 };
265
266 hydro_concurrent_cargo::log_build_event(&base_target_dir, "deploy: starting final build");
267
268 let _job_build_guard = if params.is_dylib {
270 let shared_debug = base_target_dir.join("debug");
271 Some(hydro_concurrent_cargo::populate_job_build_dir(&per_job_target_dir.join("debug"), &shared_debug))
272 } else {
273 None
274 };
275
276 let mut command = Command::new("cargo");
277 command.args(["build", if params.is_dylib { "--frozen" } else { "--locked" }]);
278
279 if let Some(profile) = params.profile.as_ref() {
280 command.args(["--profile", profile]);
281 }
282
283 if let Some(bin) = params.bin.as_ref() {
284 command.args(["--bin", bin]);
285 }
286
287 if let Some(example) = params.example.as_ref() {
288 command.args(["--example", example]);
289 }
290
291 match params.target_type {
292 HostTargetType::Local => {}
293 HostTargetType::Linux(crate::LinuxCompileType::Glibc) => {
294 command.args(["--target", "x86_64-unknown-linux-gnu"]);
295 }
296 HostTargetType::Linux(crate::LinuxCompileType::Musl) => {
297 command.args(["--target", "x86_64-unknown-linux-musl"]);
298 }
299 }
300
301 if params.no_default_features {
302 command.arg("--no-default-features");
303 }
304
305 if let Some(features) = params.features {
306 command.args(["--features", &features.join(",")]);
307 }
308
309 for config in ¶ms.config {
310 command.args(["--config", config]);
311 }
312
313 command.arg("--message-format=json-diagnostic-rendered-ansi");
314 command.args(["--target-dir", per_job_target_dir.to_str().unwrap()]);
315
316 if let Some(rustflags) = params.rustflags.as_ref() {
317 command.env("RUSTFLAGS", rustflags);
318 }
319
320 for (k, v) in params.build_env {
321 command.env(k, v);
322 }
323
324 let mut spawned = command
325 .current_dir(¶ms.src)
326 .stdout(Stdio::piped())
327 .stderr(Stdio::piped())
328 .stdin(Stdio::null())
329 .spawn()
330 .unwrap();
331
332 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
333 let stderr_reader = std::io::BufReader::new(spawned.stderr.take().unwrap());
334
335 let stderr_worker = std::thread::spawn(move || {
336 let mut stderr_lines = Vec::new();
337 for line in stderr_reader.lines() {
338 let Ok(line) = line else {
339 break;
340 };
341 set_msg(line.clone());
342 stderr_lines.push(line);
343 }
344 stderr_lines
345 });
346
347 let mut diagnostics = Vec::new();
348 let mut text_lines = Vec::new();
349 for message in cargo_metadata::Message::parse_stream(reader) {
350 match message.unwrap() {
351 cargo_metadata::Message::CompilerArtifact(artifact) => {
352 let is_output = if params.example.is_some() {
353 artifact.target.kind.iter().any(|k| "example" == k)
354 } else {
355 artifact.target.kind.iter().any(|k| "bin" == k)
356 };
357
358 if is_output {
359 let path = artifact.executable.unwrap();
360 let path_buf: PathBuf = path.clone().into();
361 let path = path.into_string();
362 let data = std::fs::read(path).unwrap();
363 let exit_status = spawned.wait().unwrap();
364
365 let stderr_lines = stderr_worker.join().unwrap();
366
367 if params.is_dylib {
369 for line in &stderr_lines {
370 if line.contains("Compiling") && !line.contains("dylib-examples") && !line.contains(job_name) {
371 panic!(
372 "unexpected recompilation in deploy final build: {line}\nfull stderr:\n{}",
373 stderr_lines.join("\n")
374 );
375 }
376 }
377 }
378
379 assert!(exit_status.success(), "deploy final build failed:\n{}", stderr_lines.join("\n"));
380
381 return Ok(BuildOutput {
382 bin_data: data,
383 bin_path: path_buf,
384 shared_library_path: if params.is_dylib {
385 Some(per_job_target_dir.join("debug").join("deps"))
386 } else {
387 None
388 },
389 });
390 }
391 }
392 cargo_metadata::Message::CompilerMessage(mut msg) => {
393 if let Some(rendered) = msg.message.rendered.as_mut() {
396 let file_names = msg
397 .message
398 .spans
399 .iter()
400 .map(|s| &s.file_name)
401 .collect::<std::collections::BTreeSet<_>>();
402 for file_name in file_names {
403 if Path::new(file_name).is_relative() {
404 *rendered = rendered.replace(
405 file_name,
406 &format!(
407 "(full path) {}/{file_name}",
408 params.workspace_root.display(),
409 ),
410 )
411 }
412 }
413 }
414 ProgressTracker::println(msg.message.to_string());
415 diagnostics.push(msg.message);
416 }
417 cargo_metadata::Message::TextLine(line) => {
418 ProgressTracker::println(&line);
419 text_lines.push(line);
420 }
421 cargo_metadata::Message::BuildFinished(_) => {}
422 cargo_metadata::Message::BuildScriptExecuted(_) => {}
423 msg => panic!("Unexpected message type: {:?}", msg),
424 }
425 }
426
427 let exit_status = spawned.wait().unwrap();
428 if exit_status.success() {
429 Err(BuildError::NoBinaryEmitted)
430 } else {
431 let stderr_lines = stderr_worker
432 .join()
433 .expect("Stderr worker unexpectedly panicked.");
434
435 Err(BuildError::FailedToBuildCrate {
436 exit_status,
437 diagnostics,
438 text_lines,
439 stderr_lines,
440 })
441 }
442 })
443 .await
444 .map_err(|_| BuildError::TokioJoinError)?
445 })
446 })
447 .await
448}
449
450#[derive(Clone, Debug)]
451pub enum BuildError {
452 FailedToBuildCrate {
453 exit_status: ExitStatus,
454 diagnostics: Vec<Diagnostic>,
455 text_lines: Vec<String>,
456 stderr_lines: Vec<String>,
457 },
458 TokioJoinError,
459 NoBinaryEmitted,
460}
461
462impl Display for BuildError {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 match self {
465 Self::FailedToBuildCrate {
466 exit_status,
467 diagnostics,
468 text_lines,
469 stderr_lines,
470 } => {
471 writeln!(f, "Failed to build crate ({})", exit_status)?;
472 writeln!(f, "Diagnostics ({}):", diagnostics.len())?;
473 for diagnostic in diagnostics {
474 write!(f, "{}", diagnostic)?;
475 }
476 writeln!(f, "Text output ({} lines):", text_lines.len())?;
477 for line in text_lines {
478 writeln!(f, "{}", line)?;
479 }
480 writeln!(f, "Stderr output ({} lines):", stderr_lines.len())?;
481 for line in stderr_lines {
482 writeln!(f, "{}", line)?;
483 }
484 }
485 Self::TokioJoinError => {
486 write!(f, "Failed to spawn tokio blocking task.")?;
487 }
488 Self::NoBinaryEmitted => {
489 write!(f, "`cargo build` succeeded but no binary was emitted.")?;
490 }
491 }
492 Ok(())
493 }
494}
495
496impl Error for BuildError {}