mk_lib/schema/command/
local_run.rs

1use 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  /// The command to run
26  pub command: String,
27
28  /// The shell to use to run the command
29  #[serde(default)]
30  pub shell: Option<Shell>,
31
32  /// The test to run before running command
33  /// If the test fails, the command will not run
34  #[serde(default)]
35  pub test: Option<String>,
36
37  /// The working directory to run the command in
38  #[serde(default)]
39  pub work_dir: Option<String>,
40
41  /// Interactive mode
42  /// If true, the command will be interactive accepting user input
43  #[serde(default)]
44  pub interactive: Option<bool>,
45
46  /// Ignore errors if the command fails
47  #[serde(default)]
48  pub ignore_errors: Option<bool>,
49
50  /// Show verbose output
51  #[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    // If interactive mode is enabled, we don't need to redirect the output
62    // to the parent process. This is because the command will be run in the
63    // foreground and the user will be able to see the output.
64    let verbose = interactive || self.verbose(context);
65
66    // Skip the command if the test fails
67    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    // Inject environment variables
99    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  /// Check if the local run task is parallel safe
118  /// If the task is interactive, it is not parallel safe
119  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}