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", "--locked"]);
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 if let Some(rustflags) = params.rustflags.as_ref() {
158 command.env("RUSTFLAGS", rustflags);
159 }
160
161 for (k, v) in params.build_env {
162 command.env(k, v);
163 }
164
165 let mut spawned = command
166 .current_dir(¶ms.src)
167 .stdout(Stdio::piped())
168 .stderr(Stdio::piped())
169 .stdin(Stdio::null())
170 .spawn()
171 .unwrap();
172
173 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
174 let stderr_reader = std::io::BufReader::new(spawned.stderr.take().unwrap());
175
176 let stderr_worker = std::thread::spawn(move || {
177 let mut stderr_lines = Vec::new();
178 for line in stderr_reader.lines() {
179 let Ok(line) = line else {
180 break;
181 };
182 set_msg(line.clone());
183 stderr_lines.push(line);
184 }
185 stderr_lines
186 });
187
188 let mut diagnostics = Vec::new();
189 let mut text_lines = Vec::new();
190 for message in cargo_metadata::Message::parse_stream(reader) {
191 match message.unwrap() {
192 cargo_metadata::Message::CompilerArtifact(artifact) => {
193 let is_output = if params.example.is_some() {
194 artifact.target.kind.iter().any(|k| "example" == k)
195 } else {
196 artifact.target.kind.iter().any(|k| "bin" == k)
197 };
198
199 if is_output {
200 let path = artifact.executable.unwrap();
201 let path_buf: PathBuf = path.clone().into();
202 let path = path.into_string();
203 let data = std::fs::read(path).unwrap();
204 assert!(spawned.wait().unwrap().success());
205 return Ok(BuildOutput {
206 bin_data: data,
207 bin_path: path_buf,
208 shared_library_path: if params.is_dylib {
209 Some(
210 params
211 .target_dir
212 .as_ref()
213 .unwrap_or(¶ms.src.join("target"))
214 .join("debug")
215 .join("deps"),
216 )
217 } else {
218 None
219 },
220 });
221 }
222 }
223 cargo_metadata::Message::CompilerMessage(mut msg) => {
224 if let Some(rendered) = msg.message.rendered.as_mut() {
227 let file_names = msg
228 .message
229 .spans
230 .iter()
231 .map(|s| &s.file_name)
232 .collect::<std::collections::BTreeSet<_>>();
233 for file_name in file_names {
234 *rendered = rendered.replace(
235 file_name,
236 &format!(
237 "(full path) {}/{file_name}",
238 params.src.display(),
239 ),
240 )
241 }
242 }
243 ProgressTracker::println(msg.message.to_string());
244 diagnostics.push(msg.message);
245 }
246 cargo_metadata::Message::TextLine(line) => {
247 ProgressTracker::println(&line);
248 text_lines.push(line);
249 }
250 cargo_metadata::Message::BuildFinished(_) => {}
251 cargo_metadata::Message::BuildScriptExecuted(_) => {}
252 msg => panic!("Unexpected message type: {:?}", msg),
253 }
254 }
255
256 let exit_status = spawned.wait().unwrap();
257 if exit_status.success() {
258 Err(BuildError::NoBinaryEmitted)
259 } else {
260 let stderr_lines = stderr_worker
261 .join()
262 .expect("Stderr worker unexpectedly panicked.");
263 Err(BuildError::FailedToBuildCrate {
264 exit_status,
265 diagnostics,
266 text_lines,
267 stderr_lines,
268 })
269 }
270 })
271 .await
272 .map_err(|_| BuildError::TokioJoinError)?
273 })
274 })
275 .await
276}
277
278#[derive(Clone, Debug)]
279pub enum BuildError {
280 FailedToBuildCrate {
281 exit_status: ExitStatus,
282 diagnostics: Vec<Diagnostic>,
283 text_lines: Vec<String>,
284 stderr_lines: Vec<String>,
285 },
286 TokioJoinError,
287 NoBinaryEmitted,
288}
289
290impl Display for BuildError {
291 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292 match self {
293 Self::FailedToBuildCrate {
294 exit_status,
295 diagnostics,
296 text_lines,
297 stderr_lines,
298 } => {
299 writeln!(f, "Failed to build crate ({})", exit_status)?;
300 writeln!(f, "Diagnostics ({}):", diagnostics.len())?;
301 for diagnostic in diagnostics {
302 write!(f, "{}", diagnostic)?;
303 }
304 writeln!(f, "Text output ({} lines):", text_lines.len())?;
305 for line in text_lines {
306 writeln!(f, "{}", line)?;
307 }
308 writeln!(f, "Stderr output ({} lines):", stderr_lines.len())?;
309 for line in stderr_lines {
310 writeln!(f, "{}", line)?;
311 }
312 }
313 Self::TokioJoinError => {
314 write!(f, "Failed to spawn tokio blocking task.")?;
315 }
316 Self::NoBinaryEmitted => {
317 write!(f, "`cargo build` succeeded but no binary was emitted.")?;
318 }
319 }
320 Ok(())
321 }
322}
323
324impl Error for BuildError {}