1use std::fmt;
8use std::path::{Path, PathBuf};
9use std::process::{Command, ExitStatus, Stdio};
10
11use crate::Result;
12use crate::error::CliError;
13
14pub mod abstract_runner;
15pub mod git_runner;
16pub mod npm_runner;
17pub mod pnpm_runner;
18pub mod runner;
19pub mod runner_factory;
20pub mod schematic_runner;
21pub mod yarn_runner;
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
24pub enum RunnerKind {
25 Schematic,
26 Npm,
27 Yarn,
28 Pnpm,
29 Git,
30 Cargo,
31}
32
33impl RunnerKind {
34 pub const fn binary(self) -> &'static str {
35 match self {
36 Self::Schematic => "node",
37 Self::Npm => "npm",
38 Self::Yarn => "yarn",
39 Self::Pnpm => "pnpm",
40 Self::Git => "git",
41 Self::Cargo => "cargo",
42 }
43 }
44}
45
46impl fmt::Display for RunnerKind {
47 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48 formatter.write_str(match self {
49 Self::Schematic => "schematic",
50 Self::Npm => "npm",
51 Self::Yarn => "yarn",
52 Self::Pnpm => "pnpm",
53 Self::Git => "git",
54 Self::Cargo => "cargo",
55 })
56 }
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub struct RunnerCommand {
61 pub binary: String,
62 pub prefix_args: Vec<String>,
63 pub command: String,
64 pub collect: bool,
65 pub cwd: Option<PathBuf>,
66 pub shell: bool,
67 pub env: Vec<(String, String)>,
68}
69
70impl RunnerCommand {
71 pub fn raw_full_command(&self) -> String {
72 let mut parts = Vec::with_capacity(2 + self.prefix_args.len());
73 parts.push(self.binary.as_str());
74 parts.extend(self.prefix_args.iter().map(String::as_str));
75 parts.push(self.command.as_str());
76 parts.join(" ")
77 }
78
79 pub fn execute(&self) -> Result<RunnerExecution> {
84 let command_line = self.command_line_for_execution();
85 let mut command = if self.shell {
86 shell_command(&command_line)
87 } else {
88 let mut command = Command::new(&self.binary);
89 command.args(&self.prefix_args).arg(&self.command);
90 command
91 };
92
93 if let Some(cwd) = &self.cwd {
94 command.current_dir(cwd);
95 }
96 command.envs(self.env.iter().map(|(key, value)| (key, value)));
97
98 if self.collect {
99 command.stdout(Stdio::piped()).stderr(Stdio::piped());
100 let output = command.output()?;
101 let stdout =
102 strip_one_line_ending(String::from_utf8_lossy(&output.stdout).into_owned());
103 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
104
105 if output.status.success() {
106 Ok(RunnerExecution {
107 status: output.status,
108 stdout: Some(stdout),
109 stderr,
110 })
111 } else {
112 Err(CliError::RunnerFailed {
113 command: self.raw_full_command(),
114 reason: failure_reason(output.status, stderr),
115 })
116 }
117 } else {
118 command
119 .stdin(Stdio::inherit())
120 .stdout(Stdio::inherit())
121 .stderr(Stdio::inherit());
122 let status = command.status()?;
123
124 if status.success() {
125 Ok(RunnerExecution {
126 status,
127 stdout: None,
128 stderr: String::new(),
129 })
130 } else {
131 Err(CliError::RunnerFailed {
132 command: self.raw_full_command(),
133 reason: failure_reason(status, String::new()),
134 })
135 }
136 }
137 }
138
139 pub fn run(&self) -> Result<Option<String>> {
141 Ok(self.execute()?.stdout)
142 }
143
144 fn command_line_for_execution(&self) -> String {
145 let mut parts = Vec::with_capacity(2 + self.prefix_args.len());
146 parts.push(quote_shell_part(&self.binary));
147 parts.extend(self.prefix_args.iter().map(|arg| quote_shell_part(arg)));
148 parts.push(self.command.clone());
149 parts.join(" ")
150 }
151}
152
153#[derive(Debug)]
154pub struct RunnerExecution {
155 pub status: ExitStatus,
156 pub stdout: Option<String>,
157 pub stderr: String,
158}
159
160impl RunnerExecution {
161 pub fn success(&self) -> bool {
162 self.status.success()
163 }
164}
165
166pub trait Runner {
167 fn kind(&self) -> RunnerKind;
168 fn binary(&self) -> &str;
169 fn prefix_args(&self) -> &[String];
170
171 fn describe(
172 &self,
173 command: impl Into<String>,
174 collect: bool,
175 cwd: Option<PathBuf>,
176 ) -> RunnerCommand {
177 RunnerCommand {
178 binary: self.binary().to_owned(),
179 prefix_args: self.prefix_args().to_vec(),
180 command: command.into(),
181 collect,
182 cwd,
183 shell: true,
184 env: Vec::new(),
185 }
186 }
187
188 fn raw_full_command(&self, command: impl AsRef<str>) -> String {
189 self.describe(command.as_ref(), false, None)
190 .raw_full_command()
191 }
192
193 fn execute(
194 &self,
195 command: impl Into<String>,
196 collect: bool,
197 cwd: Option<PathBuf>,
198 ) -> Result<RunnerExecution> {
199 self.describe(command, collect, cwd).execute()
200 }
201
202 fn run(
203 &self,
204 command: impl Into<String>,
205 collect: bool,
206 cwd: Option<PathBuf>,
207 ) -> Result<Option<String>> {
208 self.describe(command, collect, cwd).run()
209 }
210}
211
212#[derive(Clone, Debug, Eq, PartialEq)]
213pub struct ProcessRunner {
214 kind: RunnerKind,
215 binary: String,
216 prefix_args: Vec<String>,
217}
218
219impl ProcessRunner {
220 pub fn new(kind: RunnerKind) -> Self {
221 Self {
222 kind,
223 binary: kind.binary().to_owned(),
224 prefix_args: Vec::new(),
225 }
226 }
227
228 pub fn with_binary(kind: RunnerKind, binary: impl Into<String>) -> Self {
229 Self {
230 kind,
231 binary: binary.into(),
232 prefix_args: Vec::new(),
233 }
234 }
235
236 pub fn schematic(schematics_binary: impl AsRef<Path>) -> Self {
237 Self {
238 kind: RunnerKind::Schematic,
239 binary: RunnerKind::Schematic.binary().to_owned(),
240 prefix_args: vec![quote_path(schematics_binary.as_ref())],
241 }
242 }
243}
244
245impl Runner for ProcessRunner {
246 fn kind(&self) -> RunnerKind {
247 self.kind
248 }
249
250 fn binary(&self) -> &str {
251 &self.binary
252 }
253
254 fn prefix_args(&self) -> &[String] {
255 &self.prefix_args
256 }
257}
258
259#[derive(Clone, Copy, Debug, Default)]
260pub struct RunnerFactory;
261
262impl RunnerFactory {
263 pub fn create(kind: RunnerKind) -> ProcessRunner {
264 ProcessRunner::new(kind)
265 }
266
267 pub fn create_schematic(schematics_binary: impl AsRef<Path>) -> ProcessRunner {
268 ProcessRunner::schematic(schematics_binary)
269 }
270}
271
272fn quote_path(path: &Path) -> String {
273 format!("\"{}\"", path.display())
274}
275
276fn shell_command(command_line: &str) -> Command {
277 #[cfg(windows)]
278 {
279 let mut command =
280 Command::new(std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into()));
281 command.arg("/C").arg(command_line);
282 command
283 }
284
285 #[cfg(not(windows))]
286 {
287 let mut command = Command::new("sh");
288 command.arg("-c").arg(command_line);
289 command
290 }
291}
292
293fn quote_shell_part(part: &str) -> String {
294 if part.is_empty()
295 || part.starts_with('"')
296 || part.starts_with('\'')
297 || !part.chars().any(char::is_whitespace)
298 {
299 return part.to_owned();
300 }
301
302 #[cfg(windows)]
303 {
304 format!("\"{}\"", part.replace('"', "\\\""))
305 }
306
307 #[cfg(not(windows))]
308 {
309 format!("'{}'", part.replace('\'', "'\\''"))
310 }
311}
312
313fn strip_one_line_ending(mut value: String) -> String {
314 if value.ends_with("\r\n") {
315 value.truncate(value.len() - 2);
316 } else if value.ends_with('\n') {
317 value.truncate(value.len() - 1);
318 }
319 value
320}
321
322fn failure_reason(status: ExitStatus, stderr: String) -> String {
323 let status = match status.code() {
324 Some(code) => format!("process exited with status code {code}"),
325 None => "process terminated by signal".to_owned(),
326 };
327
328 let stderr = stderr.trim();
329 if stderr.is_empty() {
330 status
331 } else {
332 format!("{status}: {stderr}")
333 }
334}