1use std::{
2 path::{Path, PathBuf},
3 process::Stdio,
4};
5
6use miette::{IntoDiagnostic, Result, miette};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use tempfile::TempDir;
10use tokio::io::{AsyncBufReadExt, BufReader};
11use tokio::process::Command;
12use tokio::sync::mpsc::UnboundedSender;
13
14use crate::builders::{ArtifactPath, Builder, LogEvent, LogSender};
15
16fn default_parallel() -> bool {
17 true
18}
19
20#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
22pub struct CargoBuilderConfig {
23 #[serde(default)]
28 pub targets: Vec<String>,
29
30 pub manifest_path: Option<PathBuf>,
35
36 #[serde(default)]
48 pub bins: Vec<String>,
49
50 #[serde(default = "default_parallel")]
78 pub parallel: bool,
79}
80
81impl Default for CargoBuilderConfig {
82 fn default() -> Self {
83 Self {
84 targets: Vec::new(),
85 manifest_path: None,
86 bins: Vec::new(),
87 parallel: default_parallel(),
88 }
89 }
90}
91
92pub struct CargoBuilder;
94
95impl Builder for CargoBuilder {
96 type ConfigType = CargoBuilderConfig;
97
98 async fn build(
99 &self,
100 config: Self::ConfigType,
101 abbaye_version: &str,
102 log: LogSender,
103 ) -> Result<Vec<ArtifactPath>> {
104 let crate_version = read_crate_version(config.manifest_path.as_deref()).await?;
105
106 if config.targets.is_empty() {
107 let host = get_host_target().await?;
109 let line_tx = line_bridge(log, LogEvent::Line);
110 run_cargo_build(
111 &config,
112 None,
113 &host,
114 &crate_version,
115 abbaye_version,
116 line_tx,
117 None,
118 )
119 .await
120 } else {
121 let mut join_set = tokio::task::JoinSet::new();
125
126 for target in &config.targets {
127 let config = config.clone();
128 let crate_version = crate_version.clone();
129 let abbaye_version = abbaye_version.to_owned();
130 let target = target.clone();
131 let log = log.clone();
132
133 join_set.spawn(async move {
134 let _ = log.send(LogEvent::ChildStart {
136 id: target.clone(),
137 label: target.clone(),
138 });
139
140 let target_id = target.clone();
143 let line_tx = line_bridge(log.clone(), move |l| LogEvent::ChildLine {
144 id: target_id.clone(),
145 line: l,
146 });
147
148 let result = if config.parallel {
149 let tmpdir = TempDir::new().into_diagnostic()?;
152 let r = run_cargo_build(
153 &config,
154 Some(target.as_str()),
155 &target,
156 &crate_version,
157 &abbaye_version,
158 line_tx,
159 Some(tmpdir.path()),
160 )
161 .await;
162 match r {
165 Ok(artifacts) => relocate_artifacts(artifacts, tmpdir.path()).await,
166 Err(e) => Err(e),
167 }
168 } else {
169 run_cargo_build(
173 &config,
174 Some(target.as_str()),
175 &target,
176 &crate_version,
177 &abbaye_version,
178 line_tx,
179 None,
180 )
181 .await
182 };
183
184 let _ = log.send(LogEvent::ChildFinish {
185 id: target.clone(),
186 success: result.is_ok(),
187 summary: match &result {
188 Ok(artifacts) => format!("{} artifact(s)", artifacts.len()),
189 Err(e) => e.to_string(),
190 },
191 });
192
193 result
194 });
195 }
196
197 let mut all_artifacts = Vec::new();
198 while let Some(res) = join_set.join_next().await {
199 all_artifacts.extend(res.into_diagnostic()??);
200 }
201 Ok(all_artifacts)
202 }
203 }
204}
205
206fn line_bridge(
211 log: LogSender,
212 f: impl Fn(String) -> LogEvent + Send + 'static,
213) -> UnboundedSender<String> {
214 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
215 tokio::spawn(async move {
216 while let Some(line) = rx.recv().await {
217 let _ = log.send(f(line));
218 }
219 });
220 tx
221}
222
223#[derive(Deserialize)]
226struct CargoMessage {
227 reason: String,
228 package_id: Option<String>,
232 target: Option<CargoMessageTarget>,
233 filenames: Option<Vec<String>>,
234}
235
236#[derive(Deserialize)]
237struct CargoMessageTarget {
238 name: String,
239 #[serde(default)]
241 kind: Vec<String>,
242}
243
244async fn run_cargo_build(
251 config: &CargoBuilderConfig,
252 target: Option<&str>,
253 triple: &str,
254 version: &str,
255 abbaye_version: &str,
256 line_tx: UnboundedSender<String>,
257 target_dir: Option<&Path>,
258) -> Result<Vec<ArtifactPath>> {
259 let mut cmd = Command::new("cargo");
260 cmd.args(["build", "--release", "--message-format=json"]);
261 cmd.env("ABBAYE_BUILDING_VERSION", abbaye_version);
262
263 if let Some(t) = target {
264 cmd.args(["--target", t]);
265 }
266
267 if let Some(manifest) = &config.manifest_path {
268 cmd.arg("--manifest-path").arg(manifest);
269 }
270
271 if let Some(dir) = target_dir {
272 cmd.arg("--target-dir").arg(dir);
273 }
274
275 let mut child = cmd
276 .stdout(Stdio::piped())
277 .stderr(Stdio::piped())
278 .spawn()
279 .into_diagnostic()?;
280
281 let stderr = child.stderr.take().expect("stderr was piped");
284 tokio::spawn(async move {
285 let mut stderr_lines = BufReader::new(stderr).lines();
286 while let Ok(Some(line)) = stderr_lines.next_line().await {
287 let _ = line_tx.send(line);
288 }
289 });
290
291 let stdout = child.stdout.take().expect("stdout was piped");
292 let mut lines = BufReader::new(stdout).lines();
293
294 let mut artifacts = Vec::new();
295
296 while let Some(line) = lines.next_line().await.into_diagnostic()? {
297 let Ok(msg) = serde_json::from_str::<CargoMessage>(&line) else {
298 continue;
299 };
300
301 if msg.reason != "compiler-artifact" {
302 continue;
303 }
304
305 if !msg
310 .package_id
311 .as_deref()
312 .is_some_and(|id| id.contains("path+file://"))
313 {
314 continue;
315 }
316
317 if msg
319 .target
320 .as_ref()
321 .is_some_and(|t| t.kind.iter().any(|k| k == "custom-build"))
322 {
323 continue;
324 }
325
326 if !config.bins.is_empty() {
328 let target_name = msg.target.as_ref().map(|t| t.name.as_str()).unwrap_or("");
329 if !config.bins.iter().any(|b| b == target_name) {
330 continue;
331 }
332 }
333
334 for filename in msg.filenames.unwrap_or_default() {
335 let path = PathBuf::from(&filename);
336
337 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
339 if matches!(ext, "rlib" | "rmeta" | "d") {
340 continue;
341 }
342
343 if !path.exists() {
344 continue;
345 }
346
347 let stem = path
350 .file_stem()
351 .map(|s| s.to_string_lossy().into_owned())
352 .unwrap_or_default();
353 let dot_ext = path
354 .extension()
355 .map(|e| format!(".{}", e.to_string_lossy()))
356 .unwrap_or_default();
357 let name = format!("{stem}-{version}-{triple}{dot_ext}");
358
359 artifacts.push(ArtifactPath {
360 path,
361 name,
362 hash: None,
363 category: None,
364 group_name: None,
365 group_comment: None,
366 });
367 }
368 }
369
370 let status = child.wait().await.into_diagnostic()?;
371
372 if !status.success() {
373 return Err(miette!(
374 "cargo build --release failed with exit status: {status}"
375 ));
376 }
377
378 Ok(artifacts)
379}
380
381async fn relocate_artifacts(
390 artifacts: Vec<ArtifactPath>,
391 tmp_root: &Path,
392) -> Result<Vec<ArtifactPath>> {
393 let mut relocated = Vec::with_capacity(artifacts.len());
394 for artifact in artifacts {
395 let relative = artifact.path.strip_prefix(tmp_root).into_diagnostic()?;
396 let stable = PathBuf::from("target").join(relative);
397 if let Some(parent) = stable.parent() {
398 tokio::fs::create_dir_all(parent).await.into_diagnostic()?;
399 }
400 tokio::fs::copy(&artifact.path, &stable)
401 .await
402 .into_diagnostic()?;
403 relocated.push(ArtifactPath {
404 path: stable,
405 name: artifact.name,
406 hash: artifact.hash,
407 category: artifact.category,
408 group_name: None,
409 group_comment: None,
410 });
411 }
412 Ok(relocated)
413}
414
415async fn get_host_target() -> Result<String> {
418 let output = Command::new("rustc")
419 .args(["-vV"])
420 .output()
421 .await
422 .into_diagnostic()?;
423
424 if !output.status.success() {
425 return Err(miette!("rustc -vV failed"));
426 }
427
428 let stdout = String::from_utf8(output.stdout).into_diagnostic()?;
429 stdout
430 .lines()
431 .find(|l| l.starts_with("host:"))
432 .and_then(|l| l.split_whitespace().nth(1))
433 .map(str::to_owned)
434 .ok_or_else(|| miette!("could not parse host triple from `rustc -vV` output"))
435}
436
437async fn read_crate_version(manifest_path: Option<&Path>) -> Result<String> {
440 let path = manifest_path.unwrap_or(Path::new("Cargo.toml"));
441 let content = tokio::fs::read_to_string(path).await.into_diagnostic()?;
442
443 #[derive(Deserialize)]
444 struct Manifest {
445 package: Option<Package>,
446 }
447 #[derive(Deserialize)]
448 struct Package {
449 version: Option<String>,
450 }
451
452 let manifest: Manifest = toml::from_str(&content).into_diagnostic()?;
453 manifest
454 .package
455 .ok_or_else(|| miette!("{} has no [package] section", path.display()))?
456 .version
457 .ok_or_else(|| miette!("no version field in [package] in {}", path.display()))
458}
459
460#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
462pub struct CargoDocBuilderConfig {
463 pub manifest_path: Option<PathBuf>,
468
469 #[serde(default)]
471 pub no_deps: bool,
472}
473
474pub struct CargoDocBuilder;
476
477impl Builder for CargoDocBuilder {
478 type ConfigType = CargoDocBuilderConfig;
479
480 async fn build(
481 &self,
482 config: Self::ConfigType,
483 abbaye_version: &str,
484 log: LogSender,
485 ) -> Result<Vec<ArtifactPath>> {
486 let mut cmd = Command::new("cargo");
487 cmd.arg("doc");
488 cmd.env("ABBAYE_BUILDING_VERSION", abbaye_version);
489
490 if config.no_deps {
491 cmd.arg("--no-deps");
492 }
493
494 if let Some(manifest) = &config.manifest_path {
495 cmd.arg("--manifest-path").arg(manifest);
496 }
497
498 let mut child = cmd.stderr(Stdio::piped()).spawn().into_diagnostic()?;
499
500 let stderr = child.stderr.take().expect("stderr was piped");
501 tokio::spawn(async move {
502 let mut stderr_lines = BufReader::new(stderr).lines();
503 while let Ok(Some(line)) = stderr_lines.next_line().await {
504 let _ = log.send(LogEvent::Line(line));
505 }
506 });
507
508 let status = child.wait().await.into_diagnostic()?;
509
510 if !status.success() {
511 return Err(miette!("cargo doc failed with exit status: {status}"));
512 }
513
514 let doc_dir = config
517 .manifest_path
518 .as_deref()
519 .and_then(|p| p.parent())
520 .unwrap_or_else(|| std::path::Path::new("."))
521 .join("target/doc");
522
523 if !doc_dir.exists() {
524 return Err(miette!("doc directory not found at {}", doc_dir.display()));
525 }
526
527 Ok(vec![ArtifactPath {
531 path: doc_dir,
532 name: "doc".to_owned(),
533 hash: None,
534 category: None,
535 group_name: None,
536 group_comment: None,
537 }])
538 }
539}