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    context.emit_event(&serde_json::json!({
38      "event": "command_started",
39      "task": context.current_task_name.clone().unwrap_or_else(|| "<task>".to_string()),
40      "kind": self.kind(),
41    }))?;
42
43    let result = match self {
44      CommandRunner::ContainerBuild(container_build) => container_build.execute(context),
45      CommandRunner::ContainerRun(container_run) => container_run.execute(context),
46      CommandRunner::LocalRun(local_run) => local_run.execute(context),
47      CommandRunner::TaskRun(task_run) => task_run.execute(context),
48      CommandRunner::CommandRun(command) => self.execute_command(context, command),
49    };
50
51    context.emit_event(&serde_json::json!({
52      "event": "command_finished",
53      "task": context.current_task_name.clone().unwrap_or_else(|| "<task>".to_string()),
54      "kind": self.kind(),
55      "success": result.is_ok(),
56    }))?;
57
58    result
59  }
60
61  fn execute_command(&self, context: &TaskContext, command: &str) -> anyhow::Result<()> {
62    assert!(!command.is_empty());
63
64    let ignore_errors = context.ignore_errors();
65    let verbose = context.verbose();
66    let shell = context.shell();
67
68    let stdout = get_output_handler(verbose);
69    let stderr = get_output_handler(verbose);
70
71    let mut cmd = shell.proc();
72    cmd.arg(command).stdout(stdout).stderr(stderr);
73
74    // Inject environment variables
75    for (key, value) in context.env_vars.iter() {
76      cmd.env(key, value);
77    }
78
79    let mut cmd = cmd.spawn()?;
80    if verbose {
81      handle_output!(cmd.stdout, context);
82      handle_output!(cmd.stderr, context);
83    }
84
85    let status = cmd.wait()?;
86    if !status.success() && !ignore_errors {
87      anyhow::bail!("Command failed - {}", command);
88    }
89
90    Ok(())
91  }
92
93  fn kind(&self) -> &'static str {
94    match self {
95      CommandRunner::ContainerBuild(_) => "container_build",
96      CommandRunner::ContainerRun(_) => "container_run",
97      CommandRunner::LocalRun(_) => "local_run",
98      CommandRunner::TaskRun(_) => "task_run",
99      CommandRunner::CommandRun(_) => "command_run",
100    }
101  }
102}
103
104#[cfg(test)]
105mod test {
106  use super::*;
107
108  #[test]
109  fn test_command_1() -> anyhow::Result<()> {
110    {
111      let yaml = "
112        command: 'echo \"Hello, World!\"'
113        ignore_errors: false
114        verbose: false
115      ";
116      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
117
118      if let CommandRunner::LocalRun(local_run) = command {
119        assert_eq!(local_run.command, "echo \"Hello, World!\"");
120        assert_eq!(local_run.work_dir, None);
121        assert_eq!(local_run.ignore_errors, Some(false));
122        assert_eq!(local_run.verbose, Some(false));
123      } else {
124        panic!("Expected CommandRunner::LocalRun");
125      }
126
127      Ok(())
128    }
129  }
130
131  #[test]
132  fn test_command_2() -> anyhow::Result<()> {
133    {
134      let yaml = "
135        command: 'echo \"Hello, World!\"'
136      ";
137      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
138
139      if let CommandRunner::LocalRun(local_run) = command {
140        assert_eq!(local_run.command, "echo \"Hello, World!\"");
141        assert_eq!(local_run.work_dir, None);
142        assert_eq!(local_run.ignore_errors, None);
143        assert_eq!(local_run.verbose, None);
144      } else {
145        panic!("Expected CommandRunner::LocalRun");
146      }
147
148      Ok(())
149    }
150  }
151
152  #[test]
153  fn test_command_3() -> anyhow::Result<()> {
154    {
155      let yaml = "
156        command: 'echo \"Hello, World!\"'
157        ignore_errors: true
158      ";
159      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
160      if let CommandRunner::LocalRun(local_run) = command {
161        assert_eq!(local_run.command, "echo \"Hello, World!\"");
162        assert_eq!(local_run.work_dir, None);
163        assert_eq!(local_run.ignore_errors, Some(true));
164        assert_eq!(local_run.verbose, None);
165      } else {
166        panic!("Expected CommandRunner::LocalRun");
167      }
168
169      Ok(())
170    }
171  }
172
173  #[test]
174  fn test_command_4() -> anyhow::Result<()> {
175    {
176      let yaml = "
177        command: 'echo \"Hello, World!\"'
178        verbose: false
179      ";
180      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
181      if let CommandRunner::LocalRun(local_run) = command {
182        assert_eq!(local_run.command, "echo \"Hello, World!\"");
183        assert_eq!(local_run.work_dir, None);
184        assert_eq!(local_run.ignore_errors, None);
185        assert_eq!(local_run.verbose, Some(false));
186      } else {
187        panic!("Expected CommandRunner::LocalRun");
188      }
189
190      Ok(())
191    }
192  }
193
194  #[test]
195  fn test_command_5() -> anyhow::Result<()> {
196    {
197      let yaml = "
198        command: 'echo \"Hello, World!\"'
199        work_dir: /tmp
200      ";
201      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
202      if let CommandRunner::LocalRun(local_run) = command {
203        assert_eq!(local_run.command, "echo \"Hello, World!\"");
204        assert_eq!(local_run.work_dir, Some("/tmp".into()));
205        assert_eq!(local_run.ignore_errors, None);
206        assert_eq!(local_run.verbose, None);
207      } else {
208        panic!("Expected CommandRunner::LocalRun");
209      }
210
211      Ok(())
212    }
213  }
214
215  #[test]
216  fn test_command_6() -> anyhow::Result<()> {
217    {
218      let yaml = "
219        echo 'Hello, World!'
220      ";
221      let command = serde_yaml::from_str::<CommandRunner>(yaml)?;
222      if let CommandRunner::CommandRun(command) = command {
223        assert_eq!(command, "echo 'Hello, World!'");
224      } else {
225        panic!("Expected CommandRunner::CommandRun");
226      }
227
228      Ok(())
229    }
230  }
231}