Skip to main content

mk_lib/schema/
precondition.rs

1use anyhow::Context as _;
2use schemars::JsonSchema;
3use serde::Deserialize;
4use std::io::{
5  BufRead as _,
6  BufReader,
7};
8use std::thread;
9
10use super::{
11  Shell,
12  TaskContext,
13};
14use crate::defaults::default_verbose;
15use crate::handle_output;
16use crate::schema::get_output_handler;
17
18/// This struct represents a precondition that must be met before a task can be
19/// executed.
20#[derive(Debug, Default, Deserialize, JsonSchema)]
21pub struct Precondition {
22  /// The command to run
23  pub command: String,
24
25  /// The message to display if the command fails
26  #[serde(default)]
27  pub message: Option<String>,
28
29  /// The shell to use to run the command
30  #[serde(default)]
31  pub shell: Option<Shell>,
32
33  /// The working directory to run the command in
34  #[serde(default)]
35  pub work_dir: Option<String>,
36
37  /// Show verbose output
38  #[serde(default)]
39  pub verbose: Option<bool>,
40}
41
42impl Precondition {
43  pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
44    assert!(!self.command.is_empty());
45
46    let verbose = self.verbose(context);
47
48    let stdout = get_output_handler(verbose);
49    let stderr = get_output_handler(verbose);
50
51    let mut cmd = self
52      .shell
53      .as_ref()
54      .map(|shell| shell.proc())
55      .unwrap_or_else(|| context.shell().proc());
56
57    cmd.arg(self.command.clone()).stdout(stdout).stderr(stderr);
58
59    if let Some(work_dir) = self.resolved_work_dir(context) {
60      cmd.current_dir(work_dir);
61    }
62
63    // Inject environment variables
64    for (key, value) in context.env_vars.iter() {
65      cmd.env(key, value);
66    }
67
68    let mut cmd = cmd.spawn()?;
69
70    if verbose {
71      handle_output!(cmd.stdout, context);
72      handle_output!(cmd.stderr, context);
73    }
74
75    let status = cmd.wait()?;
76    if !status.success() {
77      if let Some(message) = &self.message {
78        anyhow::bail!("Precondition failed - {}", message);
79      } else {
80        anyhow::bail!("Precondition failed - {}", self.command);
81      }
82    }
83
84    Ok(())
85  }
86
87  fn verbose(&self, context: &TaskContext) -> bool {
88    self.verbose.or(context.verbose).unwrap_or(default_verbose())
89  }
90
91  fn resolved_work_dir(&self, context: &TaskContext) -> Option<std::path::PathBuf> {
92    self
93      .work_dir
94      .as_ref()
95      .map(|work_dir| context.resolve_from_config(work_dir))
96  }
97}
98
99#[cfg(test)]
100mod test {
101  use super::*;
102
103  #[test]
104  fn test_precondition_1() -> anyhow::Result<()> {
105    {
106      let yaml = "
107        command: 'echo \"Hello, World!\"'
108        message: 'This is a message'
109      ";
110      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
111
112      assert_eq!(precondition.command, "echo \"Hello, World!\"");
113      assert_eq!(precondition.message, Some("This is a message".into()));
114      assert_eq!(precondition.work_dir, None);
115      assert_eq!(precondition.verbose, None);
116
117      Ok(())
118    }
119  }
120
121  #[test]
122  fn test_precondition_2() -> anyhow::Result<()> {
123    {
124      let yaml = "
125        command: 'echo \"Hello, World!\"'
126      ";
127      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
128
129      assert_eq!(precondition.command, "echo \"Hello, World!\"");
130      assert_eq!(precondition.message, None);
131      assert_eq!(precondition.work_dir, None);
132      assert_eq!(precondition.verbose, None);
133
134      Ok(())
135    }
136  }
137
138  #[test]
139  fn test_precondition_3() -> anyhow::Result<()> {
140    {
141      let yaml = "
142        command: 'echo \"Hello, World!\"'
143        message: null
144      ";
145      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
146
147      assert_eq!(precondition.command, "echo \"Hello, World!\"");
148      assert_eq!(precondition.message, None);
149      assert_eq!(precondition.work_dir, None);
150      assert_eq!(precondition.verbose, None);
151
152      Ok(())
153    }
154  }
155
156  #[test]
157  fn test_precondition_4() -> anyhow::Result<()> {
158    {
159      let yaml = "
160        command: 'echo \"Hello, World!\"'
161        work_dir: /tmp
162      ";
163      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
164
165      assert_eq!(precondition.command, "echo \"Hello, World!\"");
166      assert_eq!(precondition.message, None);
167      assert_eq!(precondition.work_dir, Some("/tmp".into()));
168      assert_eq!(precondition.verbose, None);
169
170      Ok(())
171    }
172  }
173
174  #[test]
175  fn test_precondition_5() -> anyhow::Result<()> {
176    {
177      let yaml = "
178        command: 'echo \"Hello, World!\"'
179        verbose: true
180      ";
181      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
182
183      assert_eq!(precondition.command, "echo \"Hello, World!\"");
184      assert_eq!(precondition.message, None);
185      assert_eq!(precondition.work_dir, None);
186      assert_eq!(precondition.verbose, Some(true));
187
188      Ok(())
189    }
190  }
191
192  #[test]
193  fn test_precondition_6() -> anyhow::Result<()> {
194    {
195      let yaml = "
196        command: ls -la
197        message: Listing directory contents
198        shell: bash
199        work_dir: /tmp
200        verbose: false
201      ";
202      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
203
204      assert_eq!(precondition.command, "ls -la");
205      assert_eq!(
206        precondition.message,
207        Some("Listing directory contents".to_string())
208      );
209      if let Some(shell) = precondition.shell {
210        assert_eq!(shell.cmd(), "bash".to_string());
211      } else {
212        panic!("Expected shell to be Some");
213      }
214      assert_eq!(precondition.work_dir, Some("/tmp".into()));
215      assert_eq!(precondition.verbose, Some(false));
216
217      Ok(())
218    }
219  }
220}