Skip to main content

ninja_xtask/commands/
mod.rs

1use std::{
2    fmt::Debug,
3    io::{self, Read},
4    process::{Child, Output},
5    thread::{self, JoinHandle},
6};
7
8use serde_json::{Value, json};
9
10mod stage;
11pub use stage::*;
12
13mod build;
14pub use build::*;
15
16use crate::{CheckFlags, Exit, WithJson};
17
18#[derive(Debug)]
19pub struct Cmd {
20    pub name: &'static str,
21    pub result: Result<Output, io::Error>,
22    pub flags: CheckFlags,
23}
24
25pub trait CmdExt {
26    fn into_cmd(self, name: &'static str, checkflags: Option<CheckFlags>) -> Cmd;
27}
28
29impl CmdExt for Result<Output, io::Error> {
30    fn into_cmd(self, name: &'static str, checkflags: Option<CheckFlags>) -> Cmd {
31        Cmd {
32            name,
33            result: self,
34            flags: checkflags.unwrap_or_default(),
35        }
36    }
37}
38
39impl From<Cmd> for Exit<WithJson<()>> {
40    fn from(cmd: Cmd) -> Self {
41        let Cmd {
42            name: task,
43            result: did_it_spawn,
44            flags,
45        } = cmd;
46
47        let output = match did_it_spawn {
48            Ok(output) => output,
49            Err(err_spawning) => {
50                let json = flags.contains(CheckFlags::JSON).then(|| {
51                    json!({
52                        "task": task,
53                        "status": "failed to spawn",
54                        "error": &err_spawning.to_string(),
55                    })
56                });
57                let msg = format!("{task} failed: {err_spawning}");
58                return match json {
59                    Some(json) => Exit::IO(WithJson {
60                        value: String::new(),
61                        json: Some(json),
62                    }),
63                    None => Self::IO(WithJson {
64                        value: msg,
65                        json: None,
66                    }),
67                };
68            }
69        };
70
71        let status = output.status;
72        let json = flags.contains(CheckFlags::JSON).then(|| {
73            let payload = String::from_utf8_lossy(&output.stdout)
74                .lines()
75                .filter(|line| line.starts_with("{"))
76                .map(serde_json::from_str::<Value>)
77                .map(|json| json.unwrap_or_else(|err| json!({"unparsable": &err.to_string()})))
78                // TODO: Add verbose flag to output build messages in json
79                .filter(|json| {
80                    json.as_object().is_some_and(|fields| {
81                        fields.get("reason").is_some_and(|reason| {
82                            [Some("build-finished"), Some("compiler-message")]
83                                .contains(&reason.as_str())
84                        }) || fields.contains_key("event") // cargo test output
85                    })
86                })
87                .collect::<Value>();
88            json!({
89                "task": task,
90                "status": &status.to_string(),
91                "payload": payload,
92            })
93        });
94        let stdout = String::from_utf8_lossy(&output.stdout);
95        let stderr = String::from_utf8_lossy(&output.stderr);
96
97        match (status.success(), json) {
98            (true, Some(json)) => Exit::Ok(WithJson {
99                value: (),
100                json: Some(json),
101            }),
102            (true, None) => {
103                println!("{task}: OK");
104                Self::Ok(WithJson {
105                    value: (),
106                    json: None,
107                })
108            }
109            (false, Some(json)) => Exit::Error(WithJson {
110                value: String::new(),
111                json: Some(json),
112            }),
113            (false, None) => Self::Error(WithJson {
114                value: format!(
115                    "====== {task} exited with {status} ======\n-- stdout: --\n{stdout}\n\n-- stderr: --\n{stderr}",
116                    status = output.status
117                ),
118                json: None,
119            }),
120        }
121    }
122}
123
124#[derive(Debug)]
125pub struct Spawned {
126    pub name: &'static str,
127    pub child: Result<Child, io::Error>,
128    pub stdout: JoinHandle<Vec<u8>>,
129    pub stderr: JoinHandle<Vec<u8>>,
130    pub flags: CheckFlags,
131}
132
133impl Spawned {
134    pub fn wait(self) -> Cmd {
135        match self.child {
136            Ok(mut child) => {
137                let status = child.wait();
138                let stdout = self.stdout.join().unwrap();
139                let stderr = self.stderr.join().unwrap();
140                match status {
141                    Ok(exit_status) => {
142                        let output = Output {
143                            status: exit_status,
144                            stdout,
145                            stderr,
146                        };
147                        Ok(output).into_cmd(self.name, Some(self.flags))
148                    }
149                    Err(error_waiting) => Cmd {
150                        name: self.name,
151                        result: Err(error_waiting),
152                        flags: self.flags,
153                    },
154                }
155            }
156            Err(error_spawning) => {
157                let _ = self.stdout.join().unwrap();
158                let _ = self.stderr.join().unwrap();
159                Cmd {
160                    name: self.name,
161                    result: Err(error_spawning),
162                    flags: self.flags,
163                }
164            }
165        }
166    }
167}
168
169pub trait SpawnedExt {
170    fn into_spawned(self, name: &'static str, flags: Option<CheckFlags>) -> Spawned;
171}
172
173impl SpawnedExt for Result<Child, io::Error> {
174    fn into_spawned(mut self, name: &'static str, flags: Option<CheckFlags>) -> Spawned {
175        let stdout_pipe = self.as_mut().ok().and_then(|child| child.stdout.take());
176        let stderr_pipe = self.as_mut().ok().and_then(|child| child.stderr.take());
177
178        let stdout_reader = thread::spawn(|| {
179            let mut buf = Vec::<u8>::with_capacity(65536);
180            if let Some(mut stdout) = stdout_pipe {
181                stdout.read_to_end(&mut buf).unwrap(); // Panic will end up in Result after .join()
182            }
183            buf
184        });
185
186        let stderr_reader = thread::spawn(|| {
187            let mut buf = Vec::<u8>::with_capacity(65536);
188            if let Some(mut stderr) = stderr_pipe {
189                stderr.read_to_end(&mut buf).unwrap(); // Panic will end up in Result after .join()
190            }
191            buf
192        });
193
194        Spawned {
195            name,
196            child: self,
197            stdout: stdout_reader,
198            stderr: stderr_reader,
199            flags: flags.unwrap_or_default(),
200        }
201    }
202}
203
204impl FromIterator<Spawned> for Exit<WithJson<()>> {
205    fn from_iter<I: IntoIterator<Item = Spawned>>(spawns: I) -> Self {
206        spawns.into_iter().map(Exit::from).collect()
207    }
208}
209
210impl From<Spawned> for Exit<WithJson<()>> {
211    fn from(spawn: Spawned) -> Self {
212        spawn.wait().into()
213    }
214}