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