1use std::{
2 path::{Path, PathBuf},
3 process::Stdio,
4};
5
6use miette::{IntoDiagnostic, Result, miette};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use tokio::io::{AsyncBufReadExt, BufReader};
10use tokio::process::Command;
11
12use crate::builders::{ArtifactPath, Builder};
13
14#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
16pub struct CargoBuilderConfig {
17 #[serde(default)]
22 pub targets: Vec<String>,
23
24 pub manifest_path: Option<PathBuf>,
29
30 #[serde(default)]
42 pub bins: Vec<String>,
43}
44
45pub struct CargoBuilder;
47
48impl Builder for CargoBuilder {
49 type ConfigType = CargoBuilderConfig;
50
51 async fn build(
52 &self,
53 config: Self::ConfigType,
54 abbaye_version: &str,
55 ) -> Result<Vec<ArtifactPath>> {
56 let crate_version = read_crate_version(config.manifest_path.as_deref()).await?;
57
58 if config.targets.is_empty() {
59 let host = get_host_target().await?;
60 run_cargo_build(&config, None, &host, &crate_version, abbaye_version).await
61 } else {
62 let mut all_artifacts = Vec::new();
63 for target in &config.targets {
64 let artifacts = run_cargo_build(
65 &config,
66 Some(target.as_str()),
67 target,
68 &crate_version,
69 abbaye_version,
70 )
71 .await?;
72 all_artifacts.extend(artifacts);
73 }
74 Ok(all_artifacts)
75 }
76 }
77}
78
79#[derive(Deserialize)]
82struct CargoMessage {
83 reason: String,
84 package_id: Option<String>,
88 target: Option<CargoMessageTarget>,
89 filenames: Option<Vec<String>>,
90}
91
92#[derive(Deserialize)]
93struct CargoMessageTarget {
94 name: String,
95}
96
97async fn run_cargo_build(
101 config: &CargoBuilderConfig,
102 target: Option<&str>,
103 triple: &str,
104 version: &str,
105 abbaye_version: &str,
106) -> Result<Vec<ArtifactPath>> {
107 let mut cmd = Command::new("cargo");
108 cmd.args(["build", "--release", "--message-format=json"]);
109 cmd.env("ABBAYE_BUILDING_VERSION", abbaye_version);
110
111 if let Some(t) = target {
112 cmd.args(["--target", t]);
113 }
114
115 if let Some(manifest) = &config.manifest_path {
116 cmd.arg("--manifest-path").arg(manifest);
117 }
118
119 let mut child = cmd
120 .stdout(Stdio::piped())
121 .stderr(Stdio::inherit()) .spawn()
123 .into_diagnostic()?;
124
125 let stdout = child.stdout.take().expect("stdout was piped");
126 let mut lines = BufReader::new(stdout).lines();
127
128 let mut artifacts = Vec::new();
129
130 while let Some(line) = lines.next_line().await.into_diagnostic()? {
131 let Ok(msg) = serde_json::from_str::<CargoMessage>(&line) else {
132 continue;
133 };
134
135 if msg.reason != "compiler-artifact" {
136 continue;
137 }
138
139 if !msg
144 .package_id
145 .as_deref()
146 .is_some_and(|id| id.contains("path+file://"))
147 {
148 continue;
149 }
150
151 if !config.bins.is_empty() {
153 let target_name = msg.target.as_ref().map(|t| t.name.as_str()).unwrap_or("");
154 if !config.bins.iter().any(|b| b == target_name) {
155 continue;
156 }
157 }
158
159 for filename in msg.filenames.unwrap_or_default() {
160 let path = PathBuf::from(&filename);
161
162 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
164 if matches!(ext, "rlib" | "rmeta" | "d") {
165 continue;
166 }
167
168 if !path.exists() {
169 continue;
170 }
171
172 let stem = path
175 .file_stem()
176 .map(|s| s.to_string_lossy().into_owned())
177 .unwrap_or_default();
178 let dot_ext = path
179 .extension()
180 .map(|e| format!(".{}", e.to_string_lossy()))
181 .unwrap_or_default();
182 let name = format!("{stem}-{version}-{triple}{dot_ext}");
183
184 artifacts.push(ArtifactPath {
185 path,
186 name,
187 hash: None,
188 });
189 }
190 }
191
192 let status = child.wait().await.into_diagnostic()?;
193
194 if !status.success() {
195 return Err(miette!(
196 "cargo build --release failed with exit status: {status}"
197 ));
198 }
199
200 Ok(artifacts)
201}
202
203async fn get_host_target() -> Result<String> {
206 let output = Command::new("rustc")
207 .args(["-vV"])
208 .output()
209 .await
210 .into_diagnostic()?;
211
212 if !output.status.success() {
213 return Err(miette!("rustc -vV failed"));
214 }
215
216 let stdout = String::from_utf8(output.stdout).into_diagnostic()?;
217 stdout
218 .lines()
219 .find(|l| l.starts_with("host:"))
220 .and_then(|l| l.split_whitespace().nth(1))
221 .map(str::to_owned)
222 .ok_or_else(|| miette!("could not parse host triple from `rustc -vV` output"))
223}
224
225async fn read_crate_version(manifest_path: Option<&Path>) -> Result<String> {
228 let path = manifest_path.unwrap_or(Path::new("Cargo.toml"));
229 let content = tokio::fs::read_to_string(path).await.into_diagnostic()?;
230
231 #[derive(Deserialize)]
232 struct Manifest {
233 package: Option<Package>,
234 }
235 #[derive(Deserialize)]
236 struct Package {
237 version: Option<String>,
238 }
239
240 let manifest: Manifest = toml::from_str(&content).into_diagnostic()?;
241 manifest
242 .package
243 .ok_or_else(|| miette!("{} has no [package] section", path.display()))?
244 .version
245 .ok_or_else(|| miette!("no version field in [package] in {}", path.display()))
246}
247
248#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
250pub struct CargoDocBuilderConfig {
251 pub manifest_path: Option<PathBuf>,
256
257 #[serde(default)]
259 pub no_deps: bool,
260}
261
262pub struct CargoDocBuilder;
264
265impl Builder for CargoDocBuilder {
266 type ConfigType = CargoDocBuilderConfig;
267
268 async fn build(
269 &self,
270 config: Self::ConfigType,
271 abbaye_version: &str,
272 ) -> Result<Vec<ArtifactPath>> {
273 let mut cmd = Command::new("cargo");
274 cmd.arg("doc");
275 cmd.env("ABBAYE_BUILDING_VERSION", abbaye_version);
276
277 if config.no_deps {
278 cmd.arg("--no-deps");
279 }
280
281 if let Some(manifest) = &config.manifest_path {
282 cmd.arg("--manifest-path").arg(manifest);
283 }
284
285 let status = cmd
286 .stderr(Stdio::inherit())
287 .status()
288 .await
289 .into_diagnostic()?;
290
291 if !status.success() {
292 return Err(miette!("cargo doc failed with exit status: {status}"));
293 }
294
295 let doc_dir = config
298 .manifest_path
299 .as_deref()
300 .and_then(|p| p.parent())
301 .unwrap_or_else(|| std::path::Path::new("."))
302 .join("target/doc");
303
304 if !doc_dir.exists() {
305 return Err(miette!("doc directory not found at {}", doc_dir.display()));
306 }
307
308 Ok(vec![ArtifactPath {
312 path: doc_dir,
313 name: "doc".to_owned(),
314 hash: None,
315 }])
316 }
317}