mk_lib/schema/
task_root.rs

1use anyhow::Context;
2use hashbrown::HashMap;
3use mlua::{
4  Lua,
5  LuaSerdeExt,
6};
7use serde::Deserialize;
8
9use std::fs::File;
10use std::io::{
11  BufReader,
12  Read as _,
13};
14use std::path::Path;
15
16use super::{
17  Include,
18  Task,
19  UseCargo,
20  UseNpm,
21};
22
23const MK_COMMANDS: [&str; 5] = ["run", "list", "completion", "secrets", "help"];
24
25macro_rules! process_tasks {
26  ($root:expr, $mk_commands:expr) => {
27    // Rename tasks that have the same name as mk commands
28    $root.tasks = rename_tasks($root.tasks, "task", &$mk_commands, &HashMap::new());
29
30    if let Some(npm) = &$root.use_npm {
31      let npm_tasks = npm.capture()?;
32
33      // Rename tasks that have the same name as mk commands and existing tasks
34      let renamed_npm_tasks = rename_tasks(npm_tasks, "npm", &$mk_commands, &$root.tasks);
35
36      $root.tasks.extend(renamed_npm_tasks);
37    }
38  };
39}
40
41/// This struct represents the root of the task schema. It contains all the tasks
42/// that can be executed.
43#[derive(Debug, Default, Deserialize)]
44pub struct TaskRoot {
45  /// The tasks that can be executed
46  pub tasks: HashMap<String, Task>,
47
48  /// This allows mk to use npm scripts as tasks
49  #[serde(default)]
50  pub use_npm: Option<UseNpm>,
51
52  /// This allows mk to use cargo commands as tasks
53  #[serde(default)]
54  pub use_cargo: Option<UseCargo>,
55
56  /// Includes additional files to be merged into the current file
57  #[serde(default)]
58  pub include: Option<Vec<Include>>,
59}
60
61impl TaskRoot {
62  pub fn from_file(file: &str) -> anyhow::Result<Self> {
63    let file_path = Path::new(file);
64    let file_extension = file_path
65      .extension()
66      .and_then(|ext| ext.to_str())
67      .context("Failed to get file extension")?;
68
69    match file_extension {
70      "yaml" | "yml" => load_yaml_file(file),
71      "lua" => load_lua_file(file),
72      "json" => load_json_file(file),
73      "toml" => load_toml_file(file),
74      "json5" => anyhow::bail!("JSON5 files are not supported yet"),
75      "makefile" | "mk" => anyhow::bail!("Makefiles are not supported yet"),
76      _ => anyhow::bail!("Unsupported file extension - {}", file_extension),
77    }
78  }
79
80  pub fn from_hashmap(tasks: HashMap<String, Task>) -> Self {
81    Self {
82      tasks,
83      use_npm: None,
84      use_cargo: None,
85      include: None,
86    }
87  }
88}
89
90fn load_yaml_file(file: &str) -> anyhow::Result<TaskRoot> {
91  let file = File::open(file).with_context(|| format!("Failed to open file - {}", file))?;
92  let reader = BufReader::new(file);
93
94  // Deserialize the YAML file into a serde_yaml::Value to be able to merge
95  // anchors and aliases
96  let mut value: serde_yaml::Value = serde_yaml::from_reader(reader)?;
97  value.apply_merge()?;
98
99  // Deserialize the serde_yaml::Value into a TaskRoot
100  let mut root: TaskRoot = serde_yaml::from_value(value)?;
101
102  process_tasks!(root, MK_COMMANDS);
103
104  Ok(root)
105}
106
107fn load_toml_file(file: &str) -> anyhow::Result<TaskRoot> {
108  let mut file = File::open(file).with_context(|| format!("Failed to open file - {}", file))?;
109  let mut contents = String::new();
110  file.read_to_string(&mut contents)?;
111
112  // Deserialize the TOML file into a TaskRoot
113  let mut root: TaskRoot = toml::from_str(&contents)?;
114
115  process_tasks!(root, MK_COMMANDS);
116
117  Ok(root)
118}
119
120fn load_json_file(file: &str) -> anyhow::Result<TaskRoot> {
121  let file = File::open(file).with_context(|| format!("Failed to open file - {}", file))?;
122  let reader = BufReader::new(file);
123
124  // Deserialize the JSON file into a TaskRoot
125  let mut root: TaskRoot = serde_json::from_reader(reader)?;
126
127  process_tasks!(root, MK_COMMANDS);
128
129  Ok(root)
130}
131
132fn load_lua_file(file: &str) -> anyhow::Result<TaskRoot> {
133  let mut file = File::open(file).with_context(|| format!("Failed to open file - {}", file))?;
134  let mut contents = String::new();
135  file.read_to_string(&mut contents)?;
136
137  // Deserialize the Lua value into a TaskRoot
138  let mut root: TaskRoot = get_lua_table(&contents)?;
139
140  process_tasks!(root, MK_COMMANDS);
141
142  Ok(root)
143}
144
145fn get_lua_table(contents: &str) -> anyhow::Result<TaskRoot> {
146  // Create a new Lua instance
147  let lua = Lua::new();
148
149  // Load the Lua file and evaluate it
150  let value = lua.load(contents).eval()?;
151
152  // Deserialize the Lua value into a TaskRoot
153  let root = lua.from_value(value)?;
154
155  Ok(root)
156}
157
158fn rename_tasks(
159  tasks: HashMap<String, Task>,
160  prefix: &str,
161  mk_commands: &[&str],
162  existing_tasks: &HashMap<String, Task>,
163) -> HashMap<String, Task> {
164  let mut new_tasks = HashMap::new();
165  for (task_name, task) in tasks.into_iter() {
166    let new_task_name =
167      if mk_commands.contains(&task_name.as_str()) || existing_tasks.contains_key(&task_name) {
168        format!("{}_{}", prefix, task_name)
169      } else {
170        task_name
171      };
172
173    new_tasks.insert(new_task_name, task);
174  }
175  new_tasks
176}
177
178#[cfg(test)]
179mod test {
180  use super::*;
181  use crate::schema::{
182    CommandRunner,
183    TaskDependency,
184  };
185
186  #[test]
187  fn test_task_root_1() -> anyhow::Result<()> {
188    let yaml = "
189      tasks:
190        task1:
191          commands:
192            - command: echo \"Hello, World 1!\"
193              ignore_errors: false
194              verbose: false
195          depends_on:
196            - name: task2
197          description: 'This is a task'
198          labels: {}
199          environment:
200            FOO: bar
201          env_file:
202            - test.env
203        task2:
204          commands:
205            - command: echo \"Hello, World 2!\"
206              ignore_errors: false
207              verbose: false
208          depends_on:
209            - name: task1
210          description: 'This is a task'
211          labels: {}
212          environment: {}
213        task3:
214          commands:
215            - command: echo \"Hello, World 3!\"
216              ignore_errors: false
217              verbose: false
218    ";
219
220    let task_root = serde_yaml::from_str::<TaskRoot>(yaml)?;
221
222    assert_eq!(task_root.tasks.len(), 3);
223
224    if let Task::Task(task) = &task_root.tasks["task1"] {
225      if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
226        assert_eq!(local_run.command, "echo \"Hello, World 1!\"");
227        assert_eq!(local_run.work_dir, None);
228        assert_eq!(local_run.ignore_errors, Some(false));
229        assert_eq!(local_run.verbose, Some(false));
230      } else {
231        panic!("Expected CommandRunner::LocalRun");
232      }
233
234      if let TaskDependency::TaskDependency(args) = &task.depends_on[0] {
235        assert_eq!(args.name, "task2");
236      } else {
237        panic!("Expected TaskDependency::TaskDependency");
238      }
239      assert_eq!(task.labels.len(), 0);
240      assert_eq!(task.description, "This is a task");
241      assert_eq!(task.environment.len(), 1);
242      assert_eq!(task.env_file.len(), 1);
243    } else {
244      panic!("Expected Task::Task");
245    }
246
247    if let Task::Task(task) = &task_root.tasks["task2"] {
248      if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
249        assert_eq!(local_run.command, "echo \"Hello, World 2!\"");
250        assert_eq!(local_run.work_dir, None);
251        assert_eq!(local_run.ignore_errors, Some(false));
252        assert_eq!(local_run.verbose, Some(false));
253      } else {
254        panic!("Expected CommandRunner::LocalRun");
255      }
256
257      if let TaskDependency::TaskDependency(args) = &task.depends_on[0] {
258        assert_eq!(args.name, "task1");
259      } else {
260        panic!("Expected TaskDependency::TaskDependency");
261      }
262      assert_eq!(task.labels.len(), 0);
263      assert_eq!(task.description, "This is a task");
264      assert_eq!(task.environment.len(), 0);
265      assert_eq!(task.env_file.len(), 0);
266    } else {
267      panic!("Expected Task::Task");
268    }
269
270    if let Task::Task(task) = &task_root.tasks["task3"] {
271      if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
272        assert_eq!(local_run.command, "echo \"Hello, World 3!\"");
273        assert_eq!(local_run.work_dir, None);
274        assert_eq!(local_run.ignore_errors, Some(false));
275        assert_eq!(local_run.verbose, Some(false));
276      } else {
277        panic!("Expected CommandRunner::LocalRun");
278      }
279
280      assert_eq!(task.depends_on.len(), 0);
281      assert_eq!(task.labels.len(), 0);
282      assert_eq!(task.description.len(), 0);
283      assert_eq!(task.environment.len(), 0);
284      assert_eq!(task.env_file.len(), 0);
285    } else {
286      panic!("Expected Task::Task");
287    }
288
289    Ok(())
290  }
291
292  #[test]
293  fn test_task_root_2() -> anyhow::Result<()> {
294    let yaml = "
295      tasks:
296        task1:
297          commands:
298            - command: echo \"Hello, World 1!\"
299        task2:
300          commands:
301            - echo \"Hello, World 2!\"
302        task3: echo \"Hello, World 3!\"
303    ";
304
305    let task_root = serde_yaml::from_str::<TaskRoot>(yaml)?;
306
307    assert_eq!(task_root.tasks.len(), 3);
308
309    if let Task::Task(task) = &task_root.tasks["task1"] {
310      if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
311        assert_eq!(local_run.command, "echo \"Hello, World 1!\"");
312        assert_eq!(local_run.work_dir, None);
313        assert_eq!(local_run.ignore_errors, None);
314        assert_eq!(local_run.verbose, None);
315      } else {
316        panic!("Expected CommandRunner::LocalRun");
317      }
318
319      assert_eq!(task.labels.len(), 0);
320      assert_eq!(task.description, "");
321      assert_eq!(task.environment.len(), 0);
322      assert_eq!(task.env_file.len(), 0);
323    } else {
324      panic!("Expected Task::Task");
325    }
326
327    if let Task::Task(task) = &task_root.tasks["task2"] {
328      if let CommandRunner::CommandRun(command) = &task.commands[0] {
329        assert_eq!(command, "echo \"Hello, World 2!\"");
330      } else {
331        panic!("Expected CommandRunner::CommandRun");
332      }
333
334      assert_eq!(task.labels.len(), 0);
335      assert_eq!(task.description, "");
336      assert_eq!(task.environment.len(), 0);
337      assert_eq!(task.env_file.len(), 0);
338    } else {
339      panic!("Expected Task::Task");
340    }
341
342    if let Task::String(command) = &task_root.tasks["task3"] {
343      assert_eq!(command, "echo \"Hello, World 3!\"");
344    } else {
345      panic!("Expected Task::String");
346    }
347
348    Ok(())
349  }
350
351  #[test]
352  fn test_task_root_3() -> anyhow::Result<()> {
353    let yaml = "
354      tasks:
355        task1: echo \"Hello, World 1!\"
356        task2: echo \"Hello, World 2!\"
357        task3: echo \"Hello, World 3!\"
358    ";
359
360    let task_root = serde_yaml::from_str::<TaskRoot>(yaml)?;
361
362    assert_eq!(task_root.tasks.len(), 3);
363
364    if let Task::String(command) = &task_root.tasks["task1"] {
365      assert_eq!(command, "echo \"Hello, World 1!\"");
366    } else {
367      panic!("Expected Task::String");
368    }
369
370    if let Task::String(command) = &task_root.tasks["task2"] {
371      assert_eq!(command, "echo \"Hello, World 2!\"");
372    } else {
373      panic!("Expected Task::String");
374    }
375
376    if let Task::String(command) = &task_root.tasks["task3"] {
377      assert_eq!(command, "echo \"Hello, World 3!\"");
378    } else {
379      panic!("Expected Task::String");
380    }
381
382    Ok(())
383  }
384
385  #[test]
386  fn test_task_root_4() -> anyhow::Result<()> {
387    let lua = "
388      {
389        tasks = {
390          task1 = 'echo \"Hello, World 1!\"',
391          task2 = 'echo \"Hello, World 2!\"',
392          task3 = 'echo \"Hello, World 3!\"',
393        }
394      }
395    ";
396
397    let task_root = get_lua_table(lua)?;
398
399    assert_eq!(task_root.tasks.len(), 3);
400
401    if let Task::String(command) = &task_root.tasks["task1"] {
402      assert_eq!(command, "echo \"Hello, World 1!\"");
403    } else {
404      panic!("Expected Task::String");
405    }
406
407    if let Task::String(command) = &task_root.tasks["task2"] {
408      assert_eq!(command, "echo \"Hello, World 2!\"");
409    } else {
410      panic!("Expected Task::String");
411    }
412
413    if let Task::String(command) = &task_root.tasks["task3"] {
414      assert_eq!(command, "echo \"Hello, World 3!\"");
415    } else {
416      panic!("Expected Task::String");
417    }
418
419    Ok(())
420  }
421}