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 nanoid::nanoid;
11use tokio::sync::OnceCell;
12
13use crate::HostTargetType;
14use crate::progress::ProgressTracker;
15
16#[derive(PartialEq, Eq, Hash, Clone)]
18pub struct BuildParams {
19 src: PathBuf,
22 bin: Option<String>,
24 example: Option<String>,
26 profile: Option<String>,
28 rustflags: Option<String>,
29 target_dir: Option<PathBuf>,
30 no_default_features: bool,
31 target_type: HostTargetType,
33 features: Option<Vec<String>>,
35}
36impl BuildParams {
37 #[expect(clippy::too_many_arguments, reason = "internal code")]
39 pub fn new(
40 src: impl AsRef<Path>,
41 bin: Option<String>,
42 example: Option<String>,
43 profile: Option<String>,
44 rustflags: Option<String>,
45 target_dir: Option<PathBuf>,
46 no_default_features: bool,
47 target_type: HostTargetType,
48 features: Option<Vec<String>>,
49 ) -> Self {
50 let src = dunce::canonicalize(src).expect("Failed to canonicalize path for build.");
57
58 BuildParams {
59 src,
60 bin,
61 example,
62 profile,
63 rustflags,
64 target_dir,
65 no_default_features,
66 target_type,
67 features,
68 }
69 }
70}
71
72pub struct BuildOutput {
74 pub unique_id: String,
76 pub bin_data: Vec<u8>,
78 pub bin_path: PathBuf,
80}
81
82static BUILDS: OnceLock<MemoMap<BuildParams, OnceCell<BuildOutput>>> = OnceLock::new();
84
85pub async fn build_crate_memoized(params: BuildParams) -> Result<&'static BuildOutput, BuildError> {
86 BUILDS
87 .get_or_init(MemoMap::new)
88 .get_or_insert(¶ms, Default::default)
89 .get_or_try_init(move || {
90 ProgressTracker::rich_leaf("build", move |set_msg| async move {
91 tokio::task::spawn_blocking(move || {
92 let mut command = Command::new("cargo");
93 command.args(["build"]);
94
95 if let Some(profile) = params.profile.as_ref() {
96 command.args(["--profile", profile]);
97 }
98
99 if let Some(bin) = params.bin.as_ref() {
100 command.args(["--bin", bin]);
101 }
102
103 if let Some(example) = params.example.as_ref() {
104 command.args(["--example", example]);
105 }
106
107 match params.target_type {
108 HostTargetType::Local => {}
109 HostTargetType::Linux => {
110 command.args(["--target", "x86_64-unknown-linux-musl"]);
111 }
112 }
113
114 if params.no_default_features {
115 command.arg("--no-default-features");
116 }
117
118 if let Some(features) = params.features {
119 command.args(["--features", &features.join(",")]);
120 }
121
122 command.arg("--message-format=json-diagnostic-rendered-ansi");
123
124 if let Some(rustflags) = params.rustflags.as_ref() {
125 command.env("RUSTFLAGS", rustflags);
126 }
127
128 if let Some(target_dir) = params.target_dir.as_ref() {
129 command.env("CARGO_TARGET_DIR", target_dir);
130 }
131
132 let mut spawned = command
133 .current_dir(¶ms.src)
134 .stdout(Stdio::piped())
135 .stderr(Stdio::piped())
136 .stdin(Stdio::null())
137 .spawn()
138 .unwrap();
139
140 let reader = std::io::BufReader::new(spawned.stdout.take().unwrap());
141 let stderr_reader = std::io::BufReader::new(spawned.stderr.take().unwrap());
142
143 let stderr_worker = std::thread::spawn(move || {
144 let mut stderr_lines = Vec::new();
145 for line in stderr_reader.lines() {
146 let Ok(line) = line else {
147 break;
148 };
149 set_msg(line.clone());
150 stderr_lines.push(line);
151 }
152 stderr_lines
153 });
154
155 let mut diagnostics = Vec::new();
156 let mut text_lines = Vec::new();
157 for message in cargo_metadata::Message::parse_stream(reader) {
158 match message.unwrap() {
159 cargo_metadata::Message::CompilerArtifact(artifact) => {
160 let is_output = if params.example.is_some() {
161 artifact.target.kind.contains(&"example".to_string())
162 } else {
163 artifact.target.kind.contains(&"bin".to_string())
164 };
165
166 if is_output {
167 let path = artifact.executable.unwrap();
168 let path_buf: PathBuf = path.clone().into();
169 let path = path.into_string();
170 let data = std::fs::read(path).unwrap();
171 assert!(spawned.wait().unwrap().success());
172 return Ok(BuildOutput {
173 unique_id: nanoid!(8),
174 bin_data: data,
175 bin_path: path_buf,
176 });
177 }
178 }
179 cargo_metadata::Message::CompilerMessage(msg) => {
180 ProgressTracker::println(msg.message.rendered.as_deref().unwrap());
181 diagnostics.push(msg.message);
182 }
183 cargo_metadata::Message::TextLine(line) => {
184 ProgressTracker::println(&line);
185 text_lines.push(line);
186 }
187 cargo_metadata::Message::BuildFinished(_) => {}
188 cargo_metadata::Message::BuildScriptExecuted(_) => {}
189 msg => panic!("Unexpected message type: {:?}", msg),
190 }
191 }
192
193 let exit_status = spawned.wait().unwrap();
194 if exit_status.success() {
195 Err(BuildError::NoBinaryEmitted)
196 } else {
197 let stderr_lines = stderr_worker
198 .join()
199 .expect("Stderr worker unexpectedly panicked.");
200 Err(BuildError::FailedToBuildCrate {
201 exit_status,
202 diagnostics,
203 text_lines,
204 stderr_lines,
205 })
206 }
207 })
208 .await
209 .map_err(|_| BuildError::TokioJoinError)?
210 })
211 })
212 .await
213}
214
215#[derive(Clone, Debug)]
216pub enum BuildError {
217 FailedToBuildCrate {
218 exit_status: ExitStatus,
219 diagnostics: Vec<Diagnostic>,
220 text_lines: Vec<String>,
221 stderr_lines: Vec<String>,
222 },
223 TokioJoinError,
224 NoBinaryEmitted,
225}
226
227impl Display for BuildError {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 match self {
230 Self::FailedToBuildCrate {
231 exit_status,
232 diagnostics,
233 text_lines,
234 stderr_lines,
235 } => {
236 writeln!(f, "Failed to build crate ({})", exit_status)?;
237 writeln!(f, "Diagnostics ({}):", diagnostics.len())?;
238 for diagnostic in diagnostics {
239 write!(f, "{}", diagnostic)?;
240 }
241 writeln!(f, "Text output ({} lines):", text_lines.len())?;
242 for line in text_lines {
243 writeln!(f, "{}", line)?;
244 }
245 writeln!(f, "Stderr output ({} lines):", stderr_lines.len())?;
246 for line in stderr_lines {
247 writeln!(f, "{}", line)?;
248 }
249 }
250 Self::TokioJoinError => {
251 write!(f, "Failed to spawn tokio blocking task.")?;
252 }
253 Self::NoBinaryEmitted => {
254 write!(f, "`cargo build` succeeded but no binary was emitted.")?;
255 }
256 }
257 Ok(())
258 }
259}
260
261impl Error for BuildError {}