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 bin: Option<String>,
23 example: Option<String>,
25 profile: Option<String>,
27 rustflags: Option<String>,
28 target_dir: Option<PathBuf>,
29 build_env: Vec<(String, String)>,
31 no_default_features: bool,
32 target_type: HostTargetType,
34 is_dylib: bool,
36 features: Option<Vec<String>>,
38 config: Vec<String>,
40}
41impl BuildParams {
42 #[expect(clippy::too_many_arguments, reason = "internal code")]
44 pub fn new(
45 src: impl AsRef<Path>,
46 bin: Option<String>,
47 example: Option<String>,
48 profile: Option<String>,
49 rustflags: Option<String>,
50 target_dir: Option<PathBuf>,
51 build_env: Vec<(String, String)>,
52 no_default_features: bool,
53 target_type: HostTargetType,
54 is_dylib: bool,
55 features: Option<Vec<String>>,
56 config: Vec<String>,
57 ) -> Self {
58 let src = dunce::canonicalize(src.as_ref()).unwrap_or_else(|e| {
65 panic!(
66 "Failed to canonicalize path `{}` for build: {e}.",
67 src.as_ref().display(),
68 )
69 });
70
71 BuildParams {
72 src,
73 bin,
74 example,
75 profile,
76 rustflags,
77 target_dir,
78 build_env,
79 no_default_features,
80 target_type,
81 is_dylib,
82 features,
83 config,
84 }
85 }
86}
87
88pub struct BuildOutput {
90 pub bin_data: Vec<u8>,
92 pub bin_path: PathBuf,
94 pub shared_library_path: Option<PathBuf>,
96}
97impl BuildOutput {
98 pub fn unique_id(&self) -> impl use<> + Display {
100 blake3::hash(&self.bin_data).to_hex()
101 }
102}
103
104static BUILDS: OnceLock<MemoMap<BuildParams, OnceCell<BuildOutput>>> = OnceLock::new();
106
107pub async fn build_crate_memoized(params: BuildParams) -> Result<&'static BuildOutput, BuildError> {
108 BUILDS
109 .get_or_init(MemoMap::new)
110 .get_or_insert(¶ms, Default::default)
111 .get_or_try_init(move || {
112 ProgressTracker::rich_leaf("build", move |set_msg| async move {
113 tokio::task::spawn_blocking(move || {
114 let mut command = Command::new("cargo");
115 command.args(["build", "--frozen"]);
116
117 if let Some(profile) = params.profile.as_ref() {
118 command.args(["--profile", profile]);
119 }
120
121 if let Some(bin) = params.bin.as_ref() {
122 command.args(["--bin", bin]);
123 }
124
125 if let Some(example) = params.example.as_ref() {
126 command.args(["--example", example]);
127 }
128
129 match params.target_type {
130 HostTargetType::Local => {}
131 HostTargetType::Linux(crate::LinuxCompileType::Glibc) => {
132 command.args(["--target", "x86_64-unknown-linux-gnu"]);
133 }
134 HostTargetType::Linux(crate::LinuxCompileType::Musl) => {
135 command.args(["--target", "x86_64-unknown-linux-musl"]);
136 }
137 }
138
139 if params.no_default_features {
140 command.arg("--no-default-features");
141 }
142
143 if let Some(features) = params.features {
144 command.args(["--features", &features.join(",")]);
145 }
146
147 for config in ¶ms.config {
148 command.args(["--config", config]);
149 }
150
151 command.arg("--message-format=json-diagnostic-rendered-ansi");
152
153 if let Some(target_dir) = params.target_dir.as_ref() {
154 command.args(["--target-dir", target_dir.to_str().unwrap()]);
155 }
156
157 for (k, v) in params.build_env {
158 command.env(k, v);
159 }
160
161 let mut spawned = command
162 .current_dir(¶ms.src)
163 .stdout(Stdio::piped())
164 .stderr(Stdio::piped())
165 .stdin(Stdio::null())
166 .spawn()
167 .unwrap();
168
169 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
170 let stderr_reader = std::io::BufReader::new(spawned.stderr.take().unwrap());
171
172 let stderr_worker = std::thread::spawn(move || {
173 let mut stderr_lines = Vec::new();
174 for line in stderr_reader.lines() {
175 let Ok(line) = line else {
176 break;
177 };
178 set_msg(line.clone());
179 stderr_lines.push(line);
180 }
181 stderr_lines
182 });
183
184 let mut diagnostics = Vec::new();
185 let mut text_lines = Vec::new();
186 for message in cargo_metadata::Message::parse_stream(reader) {
187 match message.unwrap() {
188 cargo_metadata::Message::CompilerArtifact(artifact) => {
189 let is_output = if params.example.is_some() {
190 artifact.target.kind.contains(&"example".to_string())
191 } else {
192 artifact.target.kind.contains(&"bin".to_string())
193 };
194
195 if is_output {
196 let path = artifact.executable.unwrap();
197 let path_buf: PathBuf = path.clone().into();
198 let path = path.into_string();
199 let data = std::fs::read(path).unwrap();
200 assert!(spawned.wait().unwrap().success());
201 return Ok(BuildOutput {
202 bin_data: data,
203 bin_path: path_buf,
204 shared_library_path: if params.is_dylib {
205 Some(
206 params
207 .target_dir
208 .as_ref()
209 .unwrap_or(¶ms.src.join("target"))
210 .join("debug")
211 .join("deps"),
212 )
213 } else {
214 None
215 },
216 });
217 }
218 }
219 cargo_metadata::Message::CompilerMessage(mut msg) => {
220 if let Some(rendered) = msg.message.rendered.as_mut() {
223 let file_names = msg
224 .message
225 .spans
226 .iter()
227 .map(|s| &s.file_name)
228 .collect::<std::collections::BTreeSet<_>>();
229 for file_name in file_names {
230 *rendered = rendered.replace(
231 file_name,
232 &format!(
233 "(full path) {}/{file_name}",
234 params.src.display(),
235 ),
236 )
237 }
238 }
239 ProgressTracker::println(msg.message.to_string());
240 diagnostics.push(msg.message);
241 }
242 cargo_metadata::Message::TextLine(line) => {
243 ProgressTracker::println(&line);
244 text_lines.push(line);
245 }
246 cargo_metadata::Message::BuildFinished(_) => {}
247 cargo_metadata::Message::BuildScriptExecuted(_) => {}
248 msg => panic!("Unexpected message type: {:?}", msg),
249 }
250 }
251
252 let exit_status = spawned.wait().unwrap();
253 if exit_status.success() {
254 Err(BuildError::NoBinaryEmitted)
255 } else {
256 let stderr_lines = stderr_worker
257 .join()
258 .expect("Stderr worker unexpectedly panicked.");
259 Err(BuildError::FailedToBuildCrate {
260 exit_status,
261 diagnostics,
262 text_lines,
263 stderr_lines,
264 })
265 }
266 })
267 .await
268 .map_err(|_| BuildError::TokioJoinError)?
269 })
270 })
271 .await
272}
273
274#[derive(Clone, Debug)]
275pub enum BuildError {
276 FailedToBuildCrate {
277 exit_status: ExitStatus,
278 diagnostics: Vec<Diagnostic>,
279 text_lines: Vec<String>,
280 stderr_lines: Vec<String>,
281 },
282 TokioJoinError,
283 NoBinaryEmitted,
284}
285
286impl Display for BuildError {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 match self {
289 Self::FailedToBuildCrate {
290 exit_status,
291 diagnostics,
292 text_lines,
293 stderr_lines,
294 } => {
295 writeln!(f, "Failed to build crate ({})", exit_status)?;
296 writeln!(f, "Diagnostics ({}):", diagnostics.len())?;
297 for diagnostic in diagnostics {
298 write!(f, "{}", diagnostic)?;
299 }
300 writeln!(f, "Text output ({} lines):", text_lines.len())?;
301 for line in text_lines {
302 writeln!(f, "{}", line)?;
303 }
304 writeln!(f, "Stderr output ({} lines):", stderr_lines.len())?;
305 for line in stderr_lines {
306 writeln!(f, "{}", line)?;
307 }
308 }
309 Self::TokioJoinError => {
310 write!(f, "Failed to spawn tokio blocking task.")?;
311 }
312 Self::NoBinaryEmitted => {
313 write!(f, "`cargo build` succeeded but no binary was emitted.")?;
314 }
315 }
316 Ok(())
317 }
318}
319
320impl Error for BuildError {}