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