spirv_tools/
cmd.rs

1use crate::error::Message;
2use std::process::{Command, Stdio};
3
4pub enum CmdError {
5    /// The binary failed to spawn, probably because it's not installed
6    /// or not in PATH
7    BinaryNotFound(std::io::Error),
8    /// An I/O error occurred accessing the process' pipes
9    Io(std::io::Error),
10    /// The binary ran, but returned a non-zero exit code and (hopefully)
11    /// diagnostics
12    ToolErrors {
13        exit_code: i32,
14        /// Messages that were parsed from the output
15        messages: Vec<Message>,
16    },
17}
18
19impl From<CmdError> for crate::error::Error {
20    fn from(ce: CmdError) -> Self {
21        use crate::SpirvResult;
22
23        match ce {
24            CmdError::BinaryNotFound(err) => Self {
25                inner: SpirvResult::Unsupported,
26                diagnostic: Some(format!("failed to spawn executable: {err}").into()),
27            },
28            CmdError::Io(err) => Self {
29                inner: SpirvResult::EndOfStream,
30                diagnostic: Some(
31                    format!("i/o error occurred communicating with executable: {err}").into(),
32                ),
33            },
34            CmdError::ToolErrors {
35                exit_code,
36                messages,
37            } => {
38                // The C API just puts the last message as the diagnostic, so just do the
39                // same for now
40                let diagnostic = messages.into_iter().next_back().map_or_else(
41                    || {
42                        crate::error::Diagnostic::from(format!(
43                            "tool exited with code `{exit_code}` and no output"
44                        ))
45                    },
46                    crate::error::Diagnostic::from,
47                );
48
49                Self {
50                    // this isn't really correct, but the spirv binaries don't
51                    // provide the error code in any meaningful way, either by the
52                    // status code of the binary, or in diagnostic output
53                    inner: SpirvResult::InternalError,
54                    diagnostic: Some(diagnostic),
55                }
56            }
57        }
58    }
59}
60
61pub struct CmdOutput {
62    /// The output the command is actually supposed to give back
63    pub binary: Vec<u8>,
64    /// Warning or Info level diagnostics that were gathered during execution
65    pub messages: Vec<Message>,
66}
67
68#[derive(PartialEq, Eq, Copy, Clone)]
69pub enum Output {
70    /// Doesn't try to read stdout for tool output (other than diagnostics)
71    Ignore,
72    /// Attempts to retrieve the tool's output from stdout
73    Retrieve,
74}
75
76pub fn exec(
77    mut cmd: Command,
78    input: Option<&[u8]>,
79    retrieve_output: Output,
80) -> Result<CmdOutput, CmdError> {
81    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
82
83    // Create a temp dir for the input and/or output of the tool
84    let temp_dir = tempfile::tempdir().map_err(CmdError::Io)?;
85
86    // Output
87    let output_path = temp_dir.path().join("output");
88    if retrieve_output == Output::Retrieve {
89        cmd.arg("-o").arg(&output_path);
90    }
91
92    // Input
93    if let Some(input) = input {
94        let input_path = temp_dir.path().join("input");
95        std::fs::write(&input_path, input).map_err(CmdError::Io)?;
96
97        cmd.arg(&input_path);
98    }
99
100    let child = cmd.spawn().map_err(CmdError::BinaryNotFound)?;
101
102    let output = child.wait_with_output().map_err(CmdError::Io)?;
103
104    let code = if let Some(code) = output.status.code() {
105        code
106    } else {
107        #[cfg(unix)]
108        let message = {
109            use std::os::unix::process::ExitStatusExt;
110            format!(
111                "process terminated by signal: {}",
112                output.status.signal().unwrap_or(666)
113            )
114        };
115        #[cfg(not(unix))]
116        let message = "process ended in an unknown state".to_owned();
117
118        return Err(CmdError::ToolErrors {
119            exit_code: -1,
120            messages: vec![Message::fatal(message)],
121        });
122    };
123
124    // stderr should only ever contain error+ level diagnostics
125    if code != 0 {
126        use crate::error::*;
127        let messages: Vec<_> = match String::from_utf8(output.stderr) {
128            Ok(errors) => {
129                let mut messages = Vec::new();
130
131                for line in errors.lines() {
132                    if let Some(msg) = Message::parse(line) {
133                        messages.push(msg);
134                    } else if let Some(msg) = messages.last_mut() {
135                        if !msg.notes.is_empty() {
136                            msg.notes.push('\n');
137                        }
138
139                        msg.notes.push_str(line);
140                    } else {
141                        // We somewhow got a message that didn't conform to how
142                        // messages are supposed to look, as the first one
143                        messages.push(Message {
144                            level: MessageLevel::Error,
145                            source: None,
146                            line: 0,
147                            column: 0,
148                            index: 0,
149                            message: line.to_owned(),
150                            notes: String::new(),
151                        });
152                    }
153                }
154
155                messages
156            }
157            Err(err) => vec![Message::fatal(format!(
158                "unable to read stderr ({err}) but process exited with code {code}",
159            ))],
160        };
161
162        return Err(CmdError::ToolErrors {
163            exit_code: code,
164            messages,
165        });
166    }
167
168    fn split(haystack: &[u8], needle: u8) -> impl Iterator<Item = &[u8]> {
169        struct Split<'a> {
170            haystack: &'a [u8],
171            needle: u8,
172        }
173
174        impl<'a> Iterator for Split<'a> {
175            type Item = &'a [u8];
176
177            fn next(&mut self) -> Option<&'a [u8]> {
178                if self.haystack.is_empty() {
179                    return None;
180                }
181                let (ret, remaining) = match memchr::memchr(self.needle, self.haystack) {
182                    Some(pos) => (
183                        self.haystack.get(..pos).unwrap(),
184                        self.haystack.get(pos + 1..).unwrap(),
185                    ),
186                    None => (self.haystack, &[][..]),
187                };
188                self.haystack = remaining;
189                Some(ret)
190            }
191        }
192
193        Split { haystack, needle }
194    }
195
196    let binary = match retrieve_output {
197        Output::Retrieve => std::fs::read(&output_path).map_err(CmdError::Io)?,
198        Output::Ignore => Vec::new(),
199    };
200
201    // Since we are retrieving the results via stdout, but it can also contain
202    // diagnostic messages, we need to be careful
203    let mut messages = Vec::new();
204
205    for line in split(&output.stdout, b'\n') {
206        if let Ok(s) = std::str::from_utf8(line) {
207            if let Some(msg) = crate::error::Message::parse(s) {
208                messages.push(msg);
209                continue;
210            }
211        }
212
213        break;
214    }
215
216    Ok(CmdOutput { binary, messages })
217}