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