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 project_dir = params.src.parent().unwrap();
162 let (prebuild_guard, cargo_lock) = hydro_concurrent_cargo::run_prebuild(
163 &base_target_dir,
164 project_dir.file_name().unwrap().to_str().unwrap(),
165 &features,
166 params.rustflags.as_deref().unwrap_or_default(),
167 &staged_paths,
168 |prebuild_target| {
169 set_msg("building dependencies".to_owned());
170 hydro_concurrent_cargo::set_dylib_lib_name(
171 project_dir,
172 params.rustflags.as_deref().unwrap_or_default(),
173 );
174
175 let mut lib_cmd = Command::new("cargo");
186 lib_cmd.current_dir(&src);
187 lib_cmd.args(["build", "--locked", "--lib"]);
188 if let Some(profile) = profile.as_ref() {
189 lib_cmd.args(["--profile", profile]);
190 }
191 match target_type {
192 HostTargetType::Local => {}
193 HostTargetType::Linux(crate::LinuxCompileType::Glibc) => {
194 lib_cmd.args(["--target", "x86_64-unknown-linux-gnu"]);
195 }
196 HostTargetType::Linux(crate::LinuxCompileType::Musl) => {
197 lib_cmd.args(["--target", "x86_64-unknown-linux-musl"]);
198 }
199 }
200 if no_default_features {
201 lib_cmd.arg("--no-default-features");
202 }
203 if !features_for_closure.is_empty() {
204 lib_cmd.args(["--features", &features_for_closure.join(",")]);
205 }
206 for c in &config {
207 lib_cmd.args(["--config", c]);
208 }
209 lib_cmd.args(["--target-dir", prebuild_target.to_str().unwrap()]);
210 if let Some(rustflags) = rustflags.as_ref() {
211 lib_cmd.env("RUSTFLAGS", rustflags);
212 }
213 for (k, v) in &build_env {
214 lib_cmd.env(k, v);
215 }
216 let lib_status = lib_cmd
217 .stdin(Stdio::null())
218 .status()
219 .unwrap();
220 if !lib_status.success() {
221 panic!("dep prebuild failed");
222 }
223 },
224 );
225
226 (per_job, Some(prebuild_guard), Some(cargo_lock))
227 } else {
228 (base_target_dir.clone(), None, None)
229 };
230
231 hydro_concurrent_cargo::log_build_event(&base_target_dir, "deploy: starting final build");
232
233 let _job_build_guard = if params.is_dylib {
235 let shared_debug = base_target_dir.join("debug");
236 Some(hydro_concurrent_cargo::populate_job_build_dir(&per_job_target_dir.join("debug"), &shared_debug))
237 } else {
238 None
239 };
240
241 let mut command = Command::new("cargo");
242 command.args(["build", if params.is_dylib { "--frozen" } else { "--locked" }]);
243
244 if let Some(profile) = params.profile.as_ref() {
245 command.args(["--profile", profile]);
246 }
247
248 if let Some(bin) = params.bin.as_ref() {
249 command.args(["--bin", bin]);
250 }
251
252 if let Some(example) = params.example.as_ref() {
253 command.args(["--example", example]);
254 }
255
256 match params.target_type {
257 HostTargetType::Local => {}
258 HostTargetType::Linux(crate::LinuxCompileType::Glibc) => {
259 command.args(["--target", "x86_64-unknown-linux-gnu"]);
260 }
261 HostTargetType::Linux(crate::LinuxCompileType::Musl) => {
262 command.args(["--target", "x86_64-unknown-linux-musl"]);
263 }
264 }
265
266 if params.no_default_features {
267 command.arg("--no-default-features");
268 }
269
270 if let Some(features) = params.features {
271 command.args(["--features", &features.join(",")]);
272 }
273
274 for config in ¶ms.config {
275 command.args(["--config", config]);
276 }
277
278 command.arg("--message-format=json-diagnostic-rendered-ansi");
279 command.args(["--target-dir", per_job_target_dir.to_str().unwrap()]);
280
281 if let Some(rustflags) = params.rustflags.as_ref() {
282 command.env("RUSTFLAGS", rustflags);
283 }
284
285 for (k, v) in params.build_env {
286 command.env(k, v);
287 }
288
289 let mut spawned = command
290 .current_dir(¶ms.src)
291 .stdout(Stdio::piped())
292 .stderr(Stdio::piped())
293 .stdin(Stdio::null())
294 .spawn()
295 .unwrap();
296
297 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
298 let stderr_reader = std::io::BufReader::new(spawned.stderr.take().unwrap());
299
300 let stderr_worker = std::thread::spawn(move || {
301 let mut stderr_lines = Vec::new();
302 for line in stderr_reader.lines() {
303 let Ok(line) = line else {
304 break;
305 };
306 set_msg(line.clone());
307 stderr_lines.push(line);
308 }
309 stderr_lines
310 });
311
312 let mut diagnostics = Vec::new();
313 let mut text_lines = Vec::new();
314 for message in cargo_metadata::Message::parse_stream(reader) {
315 match message.unwrap() {
316 cargo_metadata::Message::CompilerArtifact(artifact) => {
317 let is_output = if params.example.is_some() {
318 artifact.target.kind.iter().any(|k| "example" == k)
319 } else {
320 artifact.target.kind.iter().any(|k| "bin" == k)
321 };
322
323 if is_output {
324 let path = artifact.executable.unwrap();
325 let path_buf: PathBuf = path.clone().into();
326 let path = path.into_string();
327 let data = std::fs::read(path).unwrap();
328 let exit_status = spawned.wait().unwrap();
329
330 let stderr_lines = stderr_worker.join().unwrap();
331
332 if params.is_dylib {
334 for line in &stderr_lines {
335 if line.contains("Compiling") && !line.contains("dylib-examples") && !line.contains(job_name) {
336 panic!(
337 "unexpected recompilation in deploy final build: {line}\nfull stderr:\n{}",
338 stderr_lines.join("\n")
339 );
340 }
341 }
342 }
343
344 assert!(exit_status.success(), "deploy final build failed:\n{}", stderr_lines.join("\n"));
345
346 return Ok(BuildOutput {
347 bin_data: data,
348 bin_path: path_buf,
349 shared_library_path: if params.is_dylib {
350 Some(per_job_target_dir.join("debug").join("deps"))
351 } else {
352 None
353 },
354 });
355 }
356 }
357 cargo_metadata::Message::CompilerMessage(mut msg) => {
358 if let Some(rendered) = msg.message.rendered.as_mut() {
361 let file_names = msg
362 .message
363 .spans
364 .iter()
365 .map(|s| &s.file_name)
366 .collect::<std::collections::BTreeSet<_>>();
367 for file_name in file_names {
368 if Path::new(file_name).is_relative() {
369 *rendered = rendered.replace(
370 file_name,
371 &format!(
372 "(full path) {}/{file_name}",
373 params.workspace_root.display(),
374 ),
375 )
376 }
377 }
378 }
379 ProgressTracker::println(msg.message.to_string());
380 diagnostics.push(msg.message);
381 }
382 cargo_metadata::Message::TextLine(line) => {
383 ProgressTracker::println(&line);
384 text_lines.push(line);
385 }
386 cargo_metadata::Message::BuildFinished(_) => {}
387 cargo_metadata::Message::BuildScriptExecuted(_) => {}
388 msg => panic!("Unexpected message type: {:?}", msg),
389 }
390 }
391
392 let exit_status = spawned.wait().unwrap();
393 if exit_status.success() {
394 Err(BuildError::NoBinaryEmitted)
395 } else {
396 let stderr_lines = stderr_worker
397 .join()
398 .expect("Stderr worker unexpectedly panicked.");
399
400 Err(BuildError::FailedToBuildCrate {
401 exit_status,
402 diagnostics,
403 text_lines,
404 stderr_lines,
405 })
406 }
407 })
408 .await
409 .map_err(|_| BuildError::TokioJoinError)?
410 })
411 })
412 .await
413}
414
415#[derive(Clone, Debug)]
416pub enum BuildError {
417 FailedToBuildCrate {
418 exit_status: ExitStatus,
419 diagnostics: Vec<Diagnostic>,
420 text_lines: Vec<String>,
421 stderr_lines: Vec<String>,
422 },
423 TokioJoinError,
424 NoBinaryEmitted,
425}
426
427impl Display for BuildError {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 match self {
430 Self::FailedToBuildCrate {
431 exit_status,
432 diagnostics,
433 text_lines,
434 stderr_lines,
435 } => {
436 writeln!(f, "Failed to build crate ({})", exit_status)?;
437 writeln!(f, "Diagnostics ({}):", diagnostics.len())?;
438 for diagnostic in diagnostics {
439 write!(f, "{}", diagnostic)?;
440 }
441 writeln!(f, "Text output ({} lines):", text_lines.len())?;
442 for line in text_lines {
443 writeln!(f, "{}", line)?;
444 }
445 writeln!(f, "Stderr output ({} lines):", stderr_lines.len())?;
446 for line in stderr_lines {
447 writeln!(f, "{}", line)?;
448 }
449 }
450 Self::TokioJoinError => {
451 write!(f, "Failed to spawn tokio blocking task.")?;
452 }
453 Self::NoBinaryEmitted => {
454 write!(f, "`cargo build` succeeded but no binary was emitted.")?;
455 }
456 }
457 Ok(())
458 }
459}
460
461impl Error for BuildError {}