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