pistonite_cu/process/pio/
cargo_preset.rs1use std::process::Stdio;
2use std::sync::{Arc, LazyLock};
3
4use regex::Regex;
5use tokio::io::AsyncBufReadExt as _;
6use tokio::process::{Child as TokioChild, ChildStderr, ChildStdout, Command as TokioCommand};
7
8use crate::BoxedFuture;
9use crate::cli::{ProgressBar, ProgressBarBuilder};
10use crate::lv::Lv;
11use crate::process::{Command, Preset, pio};
12
13#[inline(always)]
87pub fn cargo(progress_message: impl Into<String>) -> Cargo {
88 Cargo {
89 error_lv: Lv::Error,
90 warning_lv: Lv::Warn,
91 other_lv: Lv::Debug,
92 diagnostic_hook: None,
93 progress_builder: crate::progress(progress_message),
94 }
95}
96pub struct Cargo {
97 error_lv: Lv,
98 warning_lv: Lv,
99 other_lv: Lv,
100 diagnostic_hook: Option<DianogsticHook>,
101 progress_builder: ProgressBarBuilder,
102}
103
104impl Cargo {
105 pub fn error(mut self, lv: Lv) -> Self {
107 self.error_lv = lv;
108 self
109 }
110 pub fn warning(mut self, lv: Lv) -> Self {
112 self.warning_lv = lv;
113 self
114 }
115 pub fn other(mut self, lv: Lv) -> Self {
117 self.other_lv = lv;
118 self
119 }
120 pub fn on_diagnostic<F: Fn(bool, &str) + Send + 'static>(mut self, f: F) -> Self {
126 self.diagnostic_hook = Some(Box::new(f));
127 self
128 }
129
130 #[inline(always)]
132 pub fn configure_spinner<F: FnOnce(ProgressBarBuilder) -> ProgressBarBuilder>(
133 mut self,
134 f: F,
135 ) -> Self {
136 self.progress_builder = f(self.progress_builder);
137 self
138 }
139}
140
141impl Preset for Cargo {
142 type Output = Command<Cargo, CargoStubStdErr, pio::Null>;
143
144 fn configure<O, E, I>(self, command: crate::Command<O, E, I>) -> Self::Output {
145 command
146 .args(["--message-format=json-diagnostic-rendered-ansi"])
147 .stderr(CargoStubStdErr)
148 .stdout(self)
149 .stdin_null()
150 }
151}
152
153pub struct CargoTask {
154 error_lv: Lv,
155 warning_lv: Lv,
156 other_lv: Lv,
157 bar: Arc<ProgressBar>,
158 out: ChildStdout,
159 err: ChildStderr,
160 diagnostic_hook: Option<DianogsticHook>,
161}
162
163impl pio::ChildOutConfig for Cargo {
164 type Task = CargoTask;
165 type __Null = super::__OCNonNull;
166 fn configure_stdout(&mut self, command: &mut TokioCommand) {
167 command.stdout(Stdio::piped());
168 }
169 fn configure_stderr(&mut self, _: &mut TokioCommand) {}
170 fn take(self, child: &mut TokioChild, _: Option<&str>, _: bool) -> crate::Result<Self::Task> {
171 let stdout = super::take_child_stdout(child)?;
172 let stderr = super::take_child_stderr(child)?;
173 let bar = self.progress_builder.spawn();
174 Ok(CargoTask {
175 error_lv: self.error_lv,
176 warning_lv: self.warning_lv,
177 other_lv: self.other_lv,
178 bar,
179 out: stdout,
180 err: stderr,
181 diagnostic_hook: self.diagnostic_hook,
182 })
183 }
184}
185pub struct CargoStubStdErr;
186impl pio::ChildOutConfig for CargoStubStdErr {
187 type Task = ();
188 type __Null = super::__OCNull;
189 fn configure_stdout(&mut self, _: &mut TokioCommand) {}
190 fn configure_stderr(&mut self, command: &mut TokioCommand) {
191 command.stderr(Stdio::piped());
192 }
193 fn take(self, _: &mut TokioChild, _: Option<&str>, _: bool) -> crate::Result<Self::Task> {
194 Ok(())
195 }
196}
197
198impl pio::ChildOutTask for CargoTask {
199 type Output = Arc<ProgressBar>;
200
201 fn run(self) -> (Option<BoxedFuture<()>>, Self::Output) {
202 let bar = Arc::clone(&self.bar);
203 (Some(Box::pin(self.main())), bar)
204 }
205}
206
207impl CargoTask {
208 async fn main(self) {
209 let read_out = tokio::io::BufReader::new(self.out);
210 let mut out_lines = Some(read_out.lines());
211 let read_err = tokio::io::BufReader::new(self.err);
212 let mut err_lines = Some(read_err.lines());
213
214 let bar = self.bar;
215
216 crate::progress!(bar, "preparing");
217
218 let mut state = PrintState::new(
219 self.error_lv,
220 self.warning_lv,
221 self.other_lv,
222 bar,
223 self.diagnostic_hook,
224 );
225
226 loop {
227 let read_res = match (&mut out_lines, &mut err_lines) {
228 (None, None) => break,
229 (Some(out), None) => Ok(out.next_line().await),
230 (None, Some(err)) => Err(err.next_line().await),
231 (Some(out), Some(err)) => {
232 tokio::select! {
233 x = out.next_line() => Ok(x),
234 x = err.next_line() => Err(x)
235 }
236 }
237 };
238 let line: Result<String, String> = match read_res {
239 Ok(x) => match x {
240 Ok(Some(x)) => Ok(x),
241 _ => {
242 out_lines = None;
243 continue;
244 }
245 },
246 Err(x) => match x {
247 Ok(Some(x)) => Err(x),
248 _ => {
249 err_lines = None;
250 continue;
251 }
252 },
253 };
254 match line {
255 Ok(line) => state.handle_stdout(&line),
256 Err(line) => state.handle_stderr(&line),
257 }
258 }
259 }
260}
261
262struct PrintState {
263 error_lv: Lv,
264 warning_lv: Lv,
265 other_lv: Lv,
266 bar: Arc<ProgressBar>,
267 done_count: usize,
268 in_progress: Vec<String>, buf: String,
270 diagnostic_hook: Option<DianogsticHook>,
271 stderr_printing_message_lv: Option<Lv>,
272}
273
274impl PrintState {
275 fn new(
276 error_lv: Lv,
277 warning_lv: Lv,
278 other_lv: Lv,
279 bar: Arc<ProgressBar>,
280 diagnostic_hook: Option<DianogsticHook>,
281 ) -> Self {
282 Self {
283 error_lv,
284 warning_lv,
285 other_lv,
286 bar,
287 done_count: 0,
288 in_progress: Default::default(),
289 buf: Default::default(),
290 diagnostic_hook,
291 stderr_printing_message_lv: None,
292 }
293 }
294 fn handle_stdout(&mut self, line: &str) {
295 if !line.starts_with('{') {
297 crate::trace!("{line}");
298 return;
299 }
300
301 let payload = match crate::json::parse::<Payload>(line) {
302 Ok(x) => x,
303 Err(e) => {
304 crate::trace!("failed to parse cargo json output: {e:?}");
305 return;
306 }
307 };
308 match payload.reason {
309 "compiler-artifact" => {
310 let Some(target) = payload.target else {
311 return;
312 };
313 if target.name == "build-script-build" {
314 return;
316 }
317 self.done_count += 1;
318 self.in_progress.retain(|x| x != target.name);
319 self.update_bar();
320 }
321 "compiler-message" => {
322 let Some(message) = payload.message else {
323 return;
324 };
325 let Some(rendered) = message.rendered else {
326 return;
327 };
328 match message.level {
329 Some("warning") => match &self.diagnostic_hook {
330 None => {
331 crate::cli::__print_with_level(
332 self.warning_lv,
333 format_args!("{rendered}"),
334 );
335 }
336 Some(hook) => hook(true, &rendered),
337 },
338 Some("error") => match &self.diagnostic_hook {
339 None => {
340 crate::cli::__print_with_level(
341 self.error_lv,
342 format_args!("{rendered}"),
343 );
344 }
345 Some(hook) => hook(false, &rendered),
346 },
347 _ => {
348 crate::cli::__print_with_level(self.other_lv, format_args!("{rendered}"));
349 }
350 }
351 }
352 "build-finished" => match payload.success {
353 Some(true) => {
354 self.bar.done_by_ref();
355 crate::trace!("cargo build successful");
356 }
357 _ => {
358 crate::trace!("cargo build failed");
359 }
360 },
361 "build-script-executed" => {}
362 _ => {
363 crate::trace!("unhandled cargo message reason: {}", payload.reason);
364 }
365 }
366 }
367
368 fn handle_stderr(&mut self, line: &str) {
369 static STATUS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
370 Regex::new("^((\x1b[^m]*m)|\\s)*(Compiling|Checking)((\x1b[^m]*m)|\\s)*").unwrap()
371 });
372 static OTHER_STATUS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
373 Regex::new("^((\x1b[^m]*m)|\\s)*(Downloading|Downloaded)((\x1b[^m]*m)|\\s)*").unwrap()
374 });
375 static ERROR_REGEX: LazyLock<Regex> =
376 LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*error").unwrap());
377 static WARNING_REGEX: LazyLock<Regex> =
378 LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*warning").unwrap());
379 let Some(m) = STATUS_REGEX.find(line) else {
380 if OTHER_STATUS_REGEX.is_match(line) {
381 crate::cli::__print_with_level(self.other_lv, format_args!("{line}"));
382 self.stderr_printing_message_lv = None;
383 return;
384 }
385 if let Some(lv) = self.stderr_printing_message_lv {
388 crate::cli::__print_with_level(lv, format_args!("{line}"));
391 return;
392 }
393 if ERROR_REGEX.is_match(line) {
395 crate::cli::__print_with_level(self.error_lv, format_args!("{line}"));
396 self.stderr_printing_message_lv = Some(self.error_lv);
397 return;
398 }
399 if WARNING_REGEX.is_match(line) {
400 crate::cli::__print_with_level(self.warning_lv, format_args!("{line}"));
401 self.stderr_printing_message_lv = Some(self.warning_lv);
402 return;
403 }
404 crate::cli::__print_with_level(self.other_lv, format_args!("{line}"));
406 return;
407 };
408 crate::cli::__print_with_level(self.other_lv, format_args!("{line}"));
410 self.stderr_printing_message_lv = None;
411
412 let line = &line[m.end()..].trim();
414 let crate_name = match line.find(' ') {
416 None => line,
417 Some(i) => &line[..i],
418 };
419 self.in_progress.push(crate_name.replace('-', "_"));
420 self.update_bar();
421 }
422
423 fn update_bar(&mut self) {
424 let count = self.done_count;
425 let bar = &self.bar;
426
427 self.buf.clear();
428 let mut iter = self.in_progress.iter();
429 if let Some(x) = iter.next() {
430 self.buf.push_str(x);
431 for c in iter {
432 self.buf.push_str(", ");
433 self.buf.push_str(c);
434 }
435 crate::progress!(bar, "{count} done, compiling: {}", self.buf);
436 } else if count != 0 {
437 crate::progress!(bar, "{count} done");
438 }
439 }
440}
441
442type DianogsticHook = Box<dyn Fn(bool, &str) + Send>;
444
445#[derive(serde::Deserialize)]
446struct Payload<'a> {
447 reason: &'a str,
448 target: Option<PayloadTarget<'a>>,
449 message: Option<PayloadMessage<'a>>,
450 success: Option<bool>,
451}
452
453#[derive(serde::Deserialize)]
454struct PayloadTarget<'a> {
455 name: &'a str,
456}
457
458#[derive(serde::Deserialize)]
459struct PayloadMessage<'a> {
460 level: Option<&'a str>,
461 rendered: Option<String>,
463}