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 interpolate_template_string,
20 Shell,
21 TaskContext,
22};
23
24#[derive(Debug, Deserialize, Clone)]
25pub struct LocalRun {
26 pub command: String,
28
29 #[serde(default)]
31 pub shell: Option<Shell>,
32
33 #[serde(default)]
36 pub test: Option<String>,
37
38 #[serde(default)]
40 pub work_dir: Option<String>,
41
42 #[serde(default)]
45 pub interactive: Option<bool>,
46
47 #[serde(default)]
49 pub ignore_errors: Option<bool>,
50
51 #[serde(default)]
53 pub save_output_as: Option<String>,
54
55 #[serde(default)]
57 pub verbose: Option<bool>,
58}
59
60impl LocalRun {
61 pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
62 assert!(!self.command.is_empty());
63
64 let command = interpolate_template_string(&self.command, context)?;
65 let interactive = self.interactive();
66 let ignore_errors = self.ignore_errors(context);
67 let capture_output = self.save_output_as.is_some();
68 let verbose = interactive || self.verbose(context);
72
73 if self.test(context).is_err() {
75 return Ok(());
76 }
77
78 let mut cmd = self
79 .shell
80 .as_ref()
81 .map(|shell| shell.proc())
82 .unwrap_or_else(|| context.shell().proc());
83
84 cmd.arg(&command);
85
86 if capture_output {
87 cmd.stdout(Stdio::piped());
88 if interactive {
89 context.multi.set_draw_target(ProgressDrawTarget::hidden());
90 cmd.stdin(Stdio::inherit()).stderr(Stdio::inherit());
91 } else {
92 cmd.stderr(get_output_handler(verbose));
93 }
94 } else if verbose {
95 if interactive {
96 context.multi.set_draw_target(ProgressDrawTarget::hidden());
97
98 cmd
99 .stdin(Stdio::inherit())
100 .stdout(Stdio::inherit())
101 .stderr(Stdio::inherit());
102 } else {
103 let stdout = get_output_handler(verbose);
104 let stderr = get_output_handler(verbose);
105 cmd.stdout(stdout).stderr(stderr);
106 }
107 }
108
109 if let Some(work_dir) = self.resolved_work_dir(context) {
110 cmd.current_dir(work_dir);
111 }
112
113 for (key, value) in context.env_vars.iter() {
115 cmd.env(key, value);
116 }
117
118 let mut cmd = cmd.spawn()?;
119 let stdout_handle = if capture_output {
120 let stdout = cmd.stdout.take().context("Failed to open stdout")?;
121 let multi = context.multi.clone();
122 Some(thread::spawn(move || -> anyhow::Result<String> {
123 let reader = BufReader::new(stdout);
124 let mut output = String::new();
125 for line in reader.lines() {
126 let line = line?;
127 if verbose {
128 let _ = multi.println(line.clone());
129 }
130 output.push_str(&line);
131 output.push('\n');
132 }
133 Ok(output.trim_end_matches(['\r', '\n']).to_string())
134 }))
135 } else {
136 None
137 };
138
139 if verbose && !interactive && !capture_output {
140 handle_output!(cmd.stdout, context);
141 handle_output!(cmd.stderr, context);
142 } else if verbose && !interactive && capture_output {
143 handle_output!(cmd.stderr, context);
144 }
145
146 let status = cmd.wait()?;
147 let captured_stdout = match stdout_handle {
148 Some(handle) => Some(
149 handle
150 .join()
151 .map_err(|_| anyhow::anyhow!("Failed to join stdout capture thread"))??,
152 ),
153 None => None,
154 };
155 if !status.success() && !ignore_errors {
156 anyhow::bail!("Command failed - {}", command);
157 }
158
159 if status.success() {
160 if let (Some(output_name), Some(output_value)) = (&self.save_output_as, captured_stdout) {
161 context.insert_task_output(output_name.clone(), output_value)?;
162 }
163 }
164
165 Ok(())
166 }
167
168 pub fn is_parallel_safe(&self) -> bool {
171 !self.interactive()
172 }
173
174 fn test(&self, context: &TaskContext) -> anyhow::Result<()> {
175 let verbose = self.verbose(context);
176
177 let stdout = get_output_handler(verbose);
178 let stderr = get_output_handler(verbose);
179
180 if let Some(test) = &self.test {
181 let test = interpolate_template_string(test, context)?;
182 let mut cmd = self
183 .shell
184 .as_ref()
185 .map(|shell| shell.proc())
186 .unwrap_or_else(|| context.shell().proc());
187 cmd.arg(&test).stdout(stdout).stderr(stderr);
188
189 if let Some(work_dir) = self.resolved_work_dir(context) {
190 cmd.current_dir(work_dir);
191 }
192
193 let mut cmd = cmd.spawn()?;
194 if verbose {
195 handle_output!(cmd.stdout, context);
196 handle_output!(cmd.stderr, context);
197 }
198
199 let status = cmd.wait()?;
200
201 log::trace!("Test status: {:?}", status.success());
202 if !status.success() {
203 anyhow::bail!("Command test failed - {}", test);
204 }
205 }
206
207 Ok(())
208 }
209
210 fn interactive(&self) -> bool {
211 self.interactive.unwrap_or(false)
212 }
213
214 fn ignore_errors(&self, context: &TaskContext) -> bool {
215 self
216 .ignore_errors
217 .or(context.ignore_errors)
218 .unwrap_or(default_ignore_errors())
219 }
220
221 fn verbose(&self, context: &TaskContext) -> bool {
222 self.verbose.or(context.verbose).unwrap_or(default_verbose())
223 }
224
225 pub fn resolved_work_dir(&self, context: &TaskContext) -> Option<std::path::PathBuf> {
226 self
227 .work_dir
228 .as_ref()
229 .map(|work_dir| context.resolve_from_config(work_dir))
230 }
231}
232
233#[cfg(test)]
234mod test {
235 use super::*;
236
237 #[test]
238 fn test_local_run_1() -> anyhow::Result<()> {
239 {
240 let yaml = "
241 command: echo 'Hello, World!'
242 ignore_errors: false
243 verbose: false
244 ";
245 let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
246
247 assert_eq!(local_run.command, "echo 'Hello, World!'");
248 assert_eq!(local_run.work_dir, None);
249 assert_eq!(local_run.ignore_errors, Some(false));
250 assert_eq!(local_run.verbose, Some(false));
251 assert_eq!(local_run.save_output_as, None);
252
253 Ok(())
254 }
255 }
256
257 #[test]
258 fn test_local_run_2() -> anyhow::Result<()> {
259 {
260 let yaml = "
261 command: echo 'Hello, World!'
262 test: test $(uname) = 'Linux'
263 ignore_errors: false
264 verbose: false
265 ";
266 let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
267
268 assert_eq!(local_run.command, "echo 'Hello, World!'");
269 assert_eq!(local_run.test, Some("test $(uname) = 'Linux'".to_string()));
270 assert_eq!(local_run.work_dir, None);
271 assert_eq!(local_run.ignore_errors, Some(false));
272 assert_eq!(local_run.verbose, Some(false));
273 assert_eq!(local_run.save_output_as, None);
274
275 Ok(())
276 }
277 }
278
279 #[test]
280 fn test_local_run_3() -> anyhow::Result<()> {
281 {
282 let yaml = "
283 command: echo 'Hello, World!'
284 test: test $(uname) = 'Linux'
285 shell: bash
286 ignore_errors: false
287 verbose: false
288 interactive: true
289 ";
290 let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
291
292 assert_eq!(local_run.command, "echo 'Hello, World!'");
293 assert_eq!(local_run.test, Some("test $(uname) = 'Linux'".to_string()));
294 assert_eq!(local_run.shell, Some(Shell::String("bash".to_string())));
295 assert_eq!(local_run.work_dir, None);
296 assert_eq!(local_run.ignore_errors, Some(false));
297 assert_eq!(local_run.verbose, Some(false));
298 assert_eq!(local_run.interactive, Some(true));
299 assert_eq!(local_run.save_output_as, None);
300
301 Ok(())
302 }
303 }
304}