Skip to main content

pyro_artifacts/
command.rs

1// ! The cargo and macro expand tools
2
3use std::path::Path;
4use thiserror::Error;
5
6use tokio::process::Command;
7
8#[derive(Error, Debug)]
9pub enum CommandError {
10    #[error("IO error: {0}")]
11    Io(#[from] std::io::Error),
12    #[error(
13        "Cargo command failed with status {status}. Args: {args:?}\nStdout: {stdout}\nStderr: {stderr}"
14    )]
15    Cargo {
16        status: std::process::ExitStatus,
17        args: Vec<String>,
18        stdout: String,
19        stderr: String,
20    },
21}
22
23/// Run a cargo command within this environment
24#[tracing::instrument(skip(root), fields(root = %root.display(), tool_args = ?tool_args))]
25pub async fn run_command(
26    root: &Path,
27    tool_args: &[&str],
28    capture: bool,
29) -> Result<String, CommandError> {
30    tracing::debug!("Executing cargo command: cargo {:?}", tool_args);
31    let mut cmd = Command::new("cargo");
32    cmd.args(tool_args).current_dir(root);
33
34    if capture {
35        let output = match cmd.output().await {
36            Ok(o) => o,
37            Err(e) => {
38                tracing::error!(error = ?e, "Failed to launch cargo command");
39                return Err(CommandError::Io(e));
40            }
41        };
42
43        if !output.status.success() {
44            let err = CommandError::Cargo {
45                status: output.status,
46                args: tool_args.iter().map(|s| s.to_string()).collect(),
47                stdout: String::from_utf8_lossy(&output.stdout).to_string(),
48                stderr: String::from_utf8_lossy(&output.stderr).to_string(),
49            };
50            tracing::error!(status = ?output.status, "Cargo command failed with exit status");
51            return Err(err);
52        }
53        tracing::debug!("Cargo command completed successfully (captured output)");
54        Ok(String::from_utf8_lossy(&output.stdout).to_string())
55    } else {
56        let status = match cmd.status().await {
57            Ok(s) => s,
58            Err(e) => {
59                tracing::error!(error = ?e, "Failed to launch cargo command");
60                return Err(CommandError::Io(e));
61            }
62        };
63
64        if !status.success() {
65            let err = CommandError::Cargo {
66                status,
67                args: tool_args.iter().map(|s| s.to_string()).collect(),
68                stdout: String::from("Not captured"),
69                stderr: String::from("Not captured"),
70            };
71            tracing::error!(status = ?status, "Cargo command failed with exit status");
72            return Err(err);
73        }
74        tracing::debug!("Cargo command completed successfully");
75        Ok(String::new())
76    }
77}
78
79/// Format a syn::Error with source context
80pub fn format_syn_error(source: &str, err: syn::Error) -> String {
81    let span = err.span();
82    let start = span.start();
83    let msg = err.to_string();
84
85    let lines: Vec<&str> = source.lines().collect();
86    let line_num = start.line;
87    let col = start.column;
88
89    let mut output = String::new();
90    output.push_str(&format!("error: {}\n", msg));
91    output.push_str(&format!("  --> src/lib.rs:{}:{}\n", line_num, col + 1));
92    output.push_str("   |\n");
93
94    // Show context: line before, error line, line after
95    let start_line = line_num.saturating_sub(2);
96    let end_line = (line_num + 1).min(lines.len());
97
98    for i in start_line..end_line {
99        let line_content = lines.get(i).unwrap_or(&"");
100        let display_num = i + 1;
101
102        if display_num == line_num {
103            output.push_str(&format!("{:3} | {}\n", display_num, line_content));
104            // Add caret pointing to the column
105            output.push_str(&format!("    | {}^\n", " ".repeat(col)));
106        } else {
107            output.push_str(&format!("{:3} | {}\n", display_num, line_content));
108        }
109    }
110    output.push_str("   |\n");
111
112    output
113}