Skip to main content

mk_lib/schema/command/
mod.rs

1use std::io::{
2  BufRead as _,
3  BufReader,
4};
5use std::thread;
6
7use crate::handle_output;
8use crate::schema::get_output_handler;
9use anyhow::Context;
10use schemars::JsonSchema;
11use serde::Deserialize;
12
13use super::TaskContext;
14
15mod container_build;
16mod container_run;
17mod container_runtime;
18mod local_run;
19mod task_run;
20
21pub use container_runtime::ContainerRuntime;
22pub use local_run::LocalRun;
23
24#[derive(Debug, Deserialize, Clone, JsonSchema)]
25#[serde(untagged)]
26/// A single command entry in a task. Can be a shell string or a structured runner object.
27pub enum CommandRunner {
28  ContainerBuild(container_build::ContainerBuild),
29  ContainerRun(container_run::ContainerRun),
30  LocalRun(local_run::LocalRun),
31  TaskRun(task_run::TaskRun),
32  CommandRun(String),
33}
34
35impl CommandRunner {
36  pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
37    self.execute_inner(context, false)
38  }
39
40  pub fn execute_cancellable(&self, context: &TaskContext) -> anyhow::Result<()> {
41    self.execute_inner(context, true)
42  }
43
44  fn execute_inner(&self, context: &TaskContext, allow_cancellation: bool) -> anyhow::Result<()> {
45    context.emit_event(&serde_json::json!({
46      "event": "command_started",
47      "task": context.current_task_name.clone().unwrap_or_else(|| "<task>".to_string()),
48      "kind": self.kind(),
49    }))?;
50
51    let result = match self {
52      CommandRunner::ContainerBuild(container_build) => container_build.execute(context),
53      CommandRunner::ContainerRun(container_run) => container_run.execute(context),
54      CommandRunner::LocalRun(local_run) if allow_cancellation => local_run.execute_cancellable(context),
55      CommandRunner::LocalRun(local_run) => local_run.execute(context),
56      CommandRunner::TaskRun(task_run) => task_run.execute(context),
57      CommandRunner::CommandRun(command) => self.execute_command(context, command),
58    };
59
60    context.emit_event(&serde_json::json!({
61      "event": "command_finished",
62      "task": context.current_task_name.clone().unwrap_or_else(|| "<task>".to_string()),
63      "kind": self.kind(),
64      "success": result.is_ok(),
65    }))?;
66
67    result
68  }
69
70  fn execute_command(&self, context: &TaskContext, command: &str) -> anyhow::Result<()> {
71    assert!(!command.is_empty());
72
73    let ignore_errors = context.ignore_errors();
74    let verbose = context.verbose();
75    let shell = context.shell();
76
77    let stdout = get_output_handler(verbose);
78    let stderr = get_output_handler(verbose);
79
80    let mut cmd = shell.proc();
81    cmd.arg(command).stdout(stdout).stderr(stderr);
82
83    // Inject environment variables
84    for (key, value) in context.env_vars.iter() {
85      cmd.env(key, value);
86    }
87
88    let mut cmd = cmd.spawn()?;
89    if verbose {
90      handle_output!(cmd.stdout, context);
91      handle_output!(cmd.stderr, context);
92    }
93
94    let status = cmd.wait()?;
95    if !status.success() && !ignore_errors {
96      anyhow::bail!("Command failed - {}", command);
97    }
98
99    Ok(())
100  }
101
102  fn kind(&self) -> &'static str {
103    match self {
104      CommandRunner::ContainerBuild(_) => "container_build",
105      CommandRunner::ContainerRun(_) => "container_run",
106      CommandRunner::LocalRun(_) => "local_run",
107      CommandRunner::TaskRun(_) => "task_run",
108      CommandRunner::CommandRun(_) => "command_run",
109    }
110  }
111}
112
113#[cfg(test)]
114mod test {
115  use super::*;
116
117  #[test]
118  fn test_command_1() -> anyhow::Result<()> {
119    {
120      let yaml = "
121        command: 'echo \"Hello, World!\"'
122        ignore_errors: false
123        verbose: false
124      ";
125      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
126
127      if let CommandRunner::LocalRun(local_run) = command {
128        assert_eq!(local_run.command, "echo \"Hello, World!\"");
129        assert_eq!(local_run.work_dir, None);
130        assert_eq!(local_run.ignore_errors, Some(false));
131        assert_eq!(local_run.verbose, Some(false));
132      } else {
133        panic!("Expected CommandRunner::LocalRun");
134      }
135
136      Ok(())
137    }
138  }
139
140  #[test]
141  fn test_command_2() -> anyhow::Result<()> {
142    {
143      let yaml = "
144        command: 'echo \"Hello, World!\"'
145      ";
146      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
147
148      if let CommandRunner::LocalRun(local_run) = command {
149        assert_eq!(local_run.command, "echo \"Hello, World!\"");
150        assert_eq!(local_run.work_dir, None);
151        assert_eq!(local_run.ignore_errors, None);
152        assert_eq!(local_run.verbose, None);
153      } else {
154        panic!("Expected CommandRunner::LocalRun");
155      }
156
157      Ok(())
158    }
159  }
160
161  #[test]
162  fn test_command_3() -> anyhow::Result<()> {
163    {
164      let yaml = "
165        command: 'echo \"Hello, World!\"'
166        ignore_errors: true
167      ";
168      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
169      if let CommandRunner::LocalRun(local_run) = command {
170        assert_eq!(local_run.command, "echo \"Hello, World!\"");
171        assert_eq!(local_run.work_dir, None);
172        assert_eq!(local_run.ignore_errors, Some(true));
173        assert_eq!(local_run.verbose, None);
174      } else {
175        panic!("Expected CommandRunner::LocalRun");
176      }
177
178      Ok(())
179    }
180  }
181
182  #[test]
183  fn test_command_4() -> anyhow::Result<()> {
184    {
185      let yaml = "
186        command: 'echo \"Hello, World!\"'
187        verbose: false
188      ";
189      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
190      if let CommandRunner::LocalRun(local_run) = command {
191        assert_eq!(local_run.command, "echo \"Hello, World!\"");
192        assert_eq!(local_run.work_dir, None);
193        assert_eq!(local_run.ignore_errors, None);
194        assert_eq!(local_run.verbose, Some(false));
195      } else {
196        panic!("Expected CommandRunner::LocalRun");
197      }
198
199      Ok(())
200    }
201  }
202
203  #[test]
204  fn test_command_5() -> anyhow::Result<()> {
205    {
206      let yaml = "
207        command: 'echo \"Hello, World!\"'
208        work_dir: /tmp
209      ";
210      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
211      if let CommandRunner::LocalRun(local_run) = command {
212        assert_eq!(local_run.command, "echo \"Hello, World!\"");
213        assert_eq!(local_run.work_dir, Some("/tmp".into()));
214        assert_eq!(local_run.ignore_errors, None);
215        assert_eq!(local_run.verbose, None);
216      } else {
217        panic!("Expected CommandRunner::LocalRun");
218      }
219
220      Ok(())
221    }
222  }
223
224  #[test]
225  fn test_command_6() -> anyhow::Result<()> {
226    {
227      let yaml = "
228        echo 'Hello, World!'
229      ";
230      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
231      if let CommandRunner::CommandRun(command) = command {
232        assert_eq!(command, "echo 'Hello, World!'");
233      } else {
234        panic!("Expected CommandRunner::CommandRun");
235      }
236
237      Ok(())
238    }
239  }
240}