mk_lib/schema/command/
local_run.rs1use std::io::{
2 BufRead as _,
3 BufReader,
4};
5use std::process::Stdio;
6use std::thread;
7
8use anyhow::Context as _;
9use indicatif::ProgressDrawTarget;
10use serde::Deserialize;
11
12use crate::defaults::{
13 default_ignore_errors,
14 default_verbose,
15};
16use crate::handle_output;
17use crate::schema::{
18 get_output_handler,
19 Shell,
20 TaskContext,
21};
22
23#[derive(Debug, Deserialize, Clone)]
24pub struct LocalRun {
25 pub command: String,
27
28 #[serde(default)]
30 pub shell: Option<Shell>,
31
32 #[serde(default)]
35 pub test: Option<String>,
36
37 #[serde(default)]
39 pub work_dir: Option<String>,
40
41 #[serde(default)]
44 pub interactive: Option<bool>,
45
46 #[serde(default)]
48 pub ignore_errors: Option<bool>,
49
50 #[serde(default)]
52 pub verbose: Option<bool>,
53}
54
55impl LocalRun {
56 pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
57 assert!(!self.command.is_empty());
58
59 let interactive = self.interactive();
60 let ignore_errors = self.ignore_errors(context);
61 let verbose = interactive || self.verbose(context);
65
66 if self.test(context).is_err() {
68 return Ok(());
69 }
70
71 let mut cmd = self
72 .shell
73 .as_ref()
74 .map(|shell| shell.proc())
75 .unwrap_or_else(|| context.shell().proc());
76
77 cmd.arg(&self.command);
78
79 if verbose {
80 if interactive {
81 context.multi.set_draw_target(ProgressDrawTarget::hidden());
82
83 cmd
84 .stdin(Stdio::inherit())
85 .stdout(Stdio::inherit())
86 .stderr(Stdio::inherit());
87 } else {
88 let stdout = get_output_handler(verbose);
89 let stderr = get_output_handler(verbose);
90 cmd.stdout(stdout).stderr(stderr);
91 }
92 }
93
94 if let Some(work_dir) = &self.work_dir.clone() {
95 cmd.current_dir(work_dir);
96 }
97
98 for (key, value) in context.env_vars.iter() {
100 cmd.env(key, value);
101 }
102
103 let mut cmd = cmd.spawn()?;
104 if verbose && !interactive {
105 handle_output!(cmd.stdout, context);
106 handle_output!(cmd.stderr, context);
107 }
108
109 let status = cmd.wait()?;
110 if !status.success() && !ignore_errors {
111 anyhow::bail!("Command failed - {}", self.command);
112 }
113
114 Ok(())
115 }
116
117 pub fn is_parallel_safe(&self) -> bool {
120 !self.interactive()
121 }
122
123 fn test(&self, context: &TaskContext) -> anyhow::Result<()> {
124 let verbose = self.verbose(context);
125
126 let stdout = get_output_handler(verbose);
127 let stderr = get_output_handler(verbose);
128
129 if let Some(test) = &self.test {
130 let mut cmd = self
131 .shell
132 .as_ref()
133 .map(|shell| shell.proc())
134 .unwrap_or_else(|| context.shell().proc());
135 cmd.arg(test).stdout(stdout).stderr(stderr);
136
137 let mut cmd = cmd.spawn()?;
138 if verbose {
139 handle_output!(cmd.stdout, context);
140 handle_output!(cmd.stderr, context);
141 }
142
143 let status = cmd.wait()?;
144
145 log::trace!("Test status: {:?}", status.success());
146 if !status.success() {
147 anyhow::bail!("Command test failed - {}", test);
148 }
149 }
150
151 Ok(())
152 }
153
154 fn interactive(&self) -> bool {
155 self.interactive.unwrap_or(false)
156 }
157
158 fn ignore_errors(&self, context: &TaskContext) -> bool {
159 self
160 .ignore_errors
161 .or(context.ignore_errors)
162 .unwrap_or(default_ignore_errors())
163 }
164
165 fn verbose(&self, context: &TaskContext) -> bool {
166 self.verbose.or(context.verbose).unwrap_or(default_verbose())
167 }
168}
169
170#[cfg(test)]
171mod test {
172 use super::*;
173
174 #[test]
175 fn test_local_run_1() -> anyhow::Result<()> {
176 {
177 let yaml = "
178 command: echo 'Hello, World!'
179 ignore_errors: false
180 verbose: false
181 ";
182 let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
183
184 assert_eq!(local_run.command, "echo 'Hello, World!'");
185 assert_eq!(local_run.work_dir, None);
186 assert_eq!(local_run.ignore_errors, Some(false));
187 assert_eq!(local_run.verbose, Some(false));
188
189 Ok(())
190 }
191 }
192
193 #[test]
194 fn test_local_run_2() -> anyhow::Result<()> {
195 {
196 let yaml = "
197 command: echo 'Hello, World!'
198 test: test $(uname) = 'Linux'
199 ignore_errors: false
200 verbose: false
201 ";
202 let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
203
204 assert_eq!(local_run.command, "echo 'Hello, World!'");
205 assert_eq!(local_run.test, Some("test $(uname) = 'Linux'".to_string()));
206 assert_eq!(local_run.work_dir, None);
207 assert_eq!(local_run.ignore_errors, Some(false));
208 assert_eq!(local_run.verbose, Some(false));
209
210 Ok(())
211 }
212 }
213
214 #[test]
215 fn test_local_run_3() -> anyhow::Result<()> {
216 {
217 let yaml = "
218 command: echo 'Hello, World!'
219 test: test $(uname) = 'Linux'
220 shell: bash
221 ignore_errors: false
222 verbose: false
223 interactive: true
224 ";
225 let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
226
227 assert_eq!(local_run.command, "echo 'Hello, World!'");
228 assert_eq!(local_run.test, Some("test $(uname) = 'Linux'".to_string()));
229 assert_eq!(local_run.shell, Some(Shell::String("bash".to_string())));
230 assert_eq!(local_run.work_dir, None);
231 assert_eq!(local_run.ignore_errors, Some(false));
232 assert_eq!(local_run.verbose, Some(false));
233 assert_eq!(local_run.interactive, Some(true));
234
235 Ok(())
236 }
237 }
238}