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