1
use anyhow::Context;
2
use hashbrown::HashMap;
3
use mlua::{
4
  Lua,
5
  LuaSerdeExt,
6
};
7
use serde::Deserialize;
8

            
9
use std::fs::File;
10
use std::io::{
11
  BufReader,
12
  Read as _,
13
};
14
use std::path::Path;
15

            
16
use super::{
17
  Include,
18
  Task,
19
  UseCargo,
20
  UseNpm,
21
};
22

            
23
const MK_COMMANDS: [&str; 5] = ["run", "list", "completion", "secrets", "help"];
24

            
25
macro_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
189
    }
38
189
  };
39
189
}
40
189

            
41
189
/// This struct represents the root of the task schema. It contains all the tasks
42
189
/// that can be executed.
43
#[derive(Debug, Default, Deserialize)]
44
189
pub struct TaskRoot {
45
189
  /// The tasks that can be executed
46
  pub tasks: HashMap<String, Task>,
47

            
48
189
  /// This allows mk to use npm scripts as tasks
49
  #[serde(default)]
50
18
  pub use_npm: Option<UseNpm>,
51
18

            
52
18
  /// This allows mk to use cargo commands as tasks
53
18
  #[serde(default)]
54
18
  pub use_cargo: Option<UseCargo>,
55
18

            
56
18
  /// Includes additional files to be merged into the current file
57
18
  #[serde(default)]
58
  pub include: Option<Vec<Include>>,
59
}
60
189

            
61
189
impl TaskRoot {
62
217
  pub fn from_file(file: &str) -> anyhow::Result<Self> {
63
28
    let file_path = Path::new(file);
64
28
    let file_extension = file_path
65
28
      .extension()
66
217
      .and_then(|ext| ext.to_str())
67
217
      .context("Failed to get file extension")?;
68

            
69
217
    match file_extension {
70
28
      "yaml" | "yml" => load_yaml_file(file),
71
      "lua" => load_lua_file(file),
72
189
      "json" => load_json_file(file),
73
189
      "toml" => load_toml_file(file),
74
      "json5" => anyhow::bail!("JSON5 files are not supported yet"),
75
189
      "makefile" | "mk" => anyhow::bail!("Makefiles are not supported yet"),
76
      _ => anyhow::bail!("Unsupported file extension - {}", file_extension),
77
    }
78
28
  }
79

            
80
2
  pub fn from_hashmap(tasks: HashMap<String, Task>) -> Self {
81
2
    Self {
82
191
      tasks,
83
2
      use_npm: None,
84
191
      use_cargo: None,
85
191
      include: None,
86
2
    }
87
191
  }
88
189
}
89
189

            
90
217
fn load_yaml_file(file: &str) -> anyhow::Result<TaskRoot> {
91
217
  let file = File::open(file).with_context(|| format!("Failed to open file - {}", file))?;
92
217
  let reader = BufReader::new(file);
93
189

            
94
549
  // Deserialize the YAML file into a serde_yaml::Value to be able to merge
95
549
  // anchors and aliases
96
577
  let mut value: serde_yaml::Value = serde_yaml::from_reader(reader)?;
97
55
  value.apply_merge()?;
98

            
99
522
  // Deserialize the serde_yaml::Value into a TaskRoot
100
28
  let mut root: TaskRoot = serde_yaml::from_value(value)?;
101

            
102
577
  process_tasks!(root, MK_COMMANDS);
103

            
104
217
  Ok(root)
105
217
}
106

            
107
fn 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
18

            
117
18
  Ok(root)
118
18
}
119
18

            
120
18
fn load_json_file(file: &str) -> anyhow::Result<TaskRoot> {
121
18
  let file = File::open(file).with_context(|| format!("Failed to open file - {}", file))?;
122
18
  let reader = BufReader::new(file);
123
18

            
124
18
  // Deserialize the JSON file into a TaskRoot
125
18
  let mut root: TaskRoot = serde_json::from_reader(reader)?;
126
18

            
127
18
  process_tasks!(root, MK_COMMANDS);
128
18

            
129
18
  Ok(root)
130
18
}
131
18

            
132
18
fn load_lua_file(file: &str) -> anyhow::Result<TaskRoot> {
133
18
  let mut file = File::open(file).with_context(|| format!("Failed to open file - {}", file))?;
134
18
  let mut contents = String::new();
135
18
  file.read_to_string(&mut contents)?;
136
18

            
137
18
  // Deserialize the Lua value into a TaskRoot
138
18
  let mut root: TaskRoot = get_lua_table(&contents)?;
139
18

            
140
18
  process_tasks!(root, MK_COMMANDS);
141
18

            
142
18
  Ok(root)
143
18
}
144
18

            
145
20
fn get_lua_table(contents: &str) -> anyhow::Result<TaskRoot> {
146
20
  // Create a new Lua instance
147
20
  let lua = Lua::new();
148

            
149
18
  // Load the Lua file and evaluate it
150
2
  let value = lua.load(contents).eval()?;
151
18

            
152
  // Deserialize the Lua value into a TaskRoot
153
20
  let root = lua.from_value(value)?;
154
18

            
155
20
  Ok(root)
156
20
}
157
18

            
158
46
fn rename_tasks(
159
46
  tasks: HashMap<String, Task>,
160
28
  prefix: &str,
161
28
  mk_commands: &[&str],
162
28
  existing_tasks: &HashMap<String, Task>,
163
28
) -> HashMap<String, Task> {
164
46
  let mut new_tasks = HashMap::new();
165
102
  for (task_name, task) in tasks.into_iter() {
166
84
    let new_task_name =
167
84
      if mk_commands.contains(&task_name.as_str()) || existing_tasks.contains_key(&task_name) {
168
4
        format!("{}_{}", prefix, task_name)
169
18
      } else {
170
98
        task_name
171
18
      };
172
18

            
173
84
    new_tasks.insert(new_task_name, task);
174
  }
175
28
  new_tasks
176
28
}
177
18

            
178
18
#[cfg(test)]
179
18
mod test {
180
18
  use super::*;
181
18
  use crate::schema::{
182
18
    CommandRunner,
183
18
    TaskDependency,
184
  };
185

            
186
  #[test]
187
2
  fn test_task_root_1() -> anyhow::Result<()> {
188
20
    let yaml = "
189
20
      tasks:
190
2
        task1:
191
2
          commands:
192
2
            - command: echo \"Hello, World 1!\"
193
20
              ignore_errors: false
194
20
              verbose: false
195
20
          depends_on:
196
20
            - name: task2
197
2
          description: 'This is a task'
198
2
          labels: {}
199
2
          environment:
200
2
            FOO: bar
201
20
          env_file:
202
20
            - test.env
203
20
        task2:
204
20
          commands:
205
20
            - command: echo \"Hello, World 2!\"
206
20
              ignore_errors: false
207
20
              verbose: false
208
2
          depends_on:
209
2
            - name: task1
210
2
          description: 'This is a task'
211
2
          labels: {}
212
20
          environment: {}
213
20
        task3:
214
20
          commands:
215
20
            - command: echo \"Hello, World 3!\"
216
20
              ignore_errors: false
217
2
              verbose: false
218
2
    ";
219

            
220
2
    let task_root = serde_yaml::from_str::<TaskRoot>(yaml)?;
221
18

            
222
20
    assert_eq!(task_root.tasks.len(), 3);
223

            
224
2
    if let Task::Task(task) = &task_root.tasks["task1"] {
225
20
      if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
226
20
        assert_eq!(local_run.command, "echo \"Hello, World 1!\"");
227
20
        assert_eq!(local_run.work_dir, None);
228
20
        assert_eq!(local_run.shell, "sh");
229
20
        assert_eq!(local_run.ignore_errors, Some(false));
230
20
        assert_eq!(local_run.verbose, Some(false));
231
18
      } else {
232
18
        panic!("Expected CommandRunner::LocalRun");
233
18
      }
234
18

            
235
20
      if let TaskDependency::TaskDependency(args) = &task.depends_on[0] {
236
2
        assert_eq!(args.name, "task2");
237
18
      } else {
238
        panic!("Expected TaskDependency::TaskDependency");
239
18
      }
240
2
      assert_eq!(task.labels.len(), 0);
241
20
      assert_eq!(task.description, "This is a task");
242
20
      assert_eq!(task.environment.len(), 1);
243
20
      assert_eq!(task.env_file.len(), 1);
244
18
    } else {
245
18
      panic!("Expected Task::Task");
246
18
    }
247
18

            
248
2
    if let Task::Task(task) = &task_root.tasks["task2"] {
249
2
      if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
250
2
        assert_eq!(local_run.command, "echo \"Hello, World 2!\"");
251
2
        assert_eq!(local_run.work_dir, None);
252
20
        assert_eq!(local_run.shell, "sh");
253
20
        assert_eq!(local_run.ignore_errors, Some(false));
254
20
        assert_eq!(local_run.verbose, Some(false));
255
18
      } else {
256
        panic!("Expected CommandRunner::LocalRun");
257
      }
258

            
259
2
      if let TaskDependency::TaskDependency(args) = &task.depends_on[0] {
260
20
        assert_eq!(args.name, "task1");
261
18
      } else {
262
18
        panic!("Expected TaskDependency::TaskDependency");
263
      }
264
2
      assert_eq!(task.labels.len(), 0);
265
2
      assert_eq!(task.description, "This is a task");
266
2
      assert_eq!(task.environment.len(), 0);
267
20
      assert_eq!(task.env_file.len(), 0);
268
18
    } else {
269
18
      panic!("Expected Task::Task");
270
18
    }
271

            
272
2
    if let Task::Task(task) = &task_root.tasks["task3"] {
273
2
      if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
274
2
        assert_eq!(local_run.command, "echo \"Hello, World 3!\"");
275
20
        assert_eq!(local_run.work_dir, None);
276
20
        assert_eq!(local_run.shell, "sh");
277
2
        assert_eq!(local_run.ignore_errors, Some(false));
278
2
        assert_eq!(local_run.verbose, Some(false));
279
      } else {
280
        panic!("Expected CommandRunner::LocalRun");
281
18
      }
282
18

            
283
2
      assert_eq!(task.depends_on.len(), 0);
284
2
      assert_eq!(task.labels.len(), 0);
285
20
      assert_eq!(task.description.len(), 0);
286
20
      assert_eq!(task.environment.len(), 0);
287
20
      assert_eq!(task.env_file.len(), 0);
288
18
    } else {
289
18
      panic!("Expected Task::Task");
290
18
    }
291
18

            
292
2
    Ok(())
293
20
  }
294

            
295
18
  #[test]
296
2
  fn test_task_root_2() -> anyhow::Result<()> {
297
20
    let yaml = "
298
20
      tasks:
299
2
        task1:
300
2
          commands:
301
2
            - command: echo \"Hello, World 1!\"
302
2
        task2:
303
20
          commands:
304
20
            - echo \"Hello, World 2!\"
305
2
        task3: echo \"Hello, World 3!\"
306
2
    ";
307

            
308
2
    let task_root = serde_yaml::from_str::<TaskRoot>(yaml)?;
309
18

            
310
20
    assert_eq!(task_root.tasks.len(), 3);
311

            
312
2
    if let Task::Task(task) = &task_root.tasks["task1"] {
313
2
      if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
314
2
        assert_eq!(local_run.command, "echo \"Hello, World 1!\"");
315
20
        assert_eq!(local_run.work_dir, None);
316
20
        assert_eq!(local_run.shell, "sh");
317
2
        assert_eq!(local_run.ignore_errors, None);
318
2
        assert_eq!(local_run.verbose, None);
319
      } else {
320
        panic!("Expected CommandRunner::LocalRun");
321
      }
322

            
323
2
      assert_eq!(task.labels.len(), 0);
324
2
      assert_eq!(task.description, "");
325
2
      assert_eq!(task.environment.len(), 0);
326
2
      assert_eq!(task.env_file.len(), 0);
327
    } else {
328
      panic!("Expected Task::Task");
329
    }
330

            
331
2
    if let Task::Task(task) = &task_root.tasks["task2"] {
332
2
      if let CommandRunner::CommandRun(command) = &task.commands[0] {
333
2
        assert_eq!(command, "echo \"Hello, World 2!\"");
334
      } else {
335
        panic!("Expected CommandRunner::CommandRun");
336
      }
337

            
338
2
      assert_eq!(task.labels.len(), 0);
339
2
      assert_eq!(task.description, "");
340
2
      assert_eq!(task.environment.len(), 0);
341
2
      assert_eq!(task.env_file.len(), 0);
342
    } else {
343
      panic!("Expected Task::Task");
344
    }
345

            
346
2
    if let Task::String(command) = &task_root.tasks["task3"] {
347
2
      assert_eq!(command, "echo \"Hello, World 3!\"");
348
    } else {
349
      panic!("Expected Task::String");
350
    }
351

            
352
2
    Ok(())
353
2
  }
354

            
355
  #[test]
356
2
  fn test_task_root_3() -> anyhow::Result<()> {
357
2
    let yaml = "
358
2
      tasks:
359
2
        task1: echo \"Hello, World 1!\"
360
2
        task2: echo \"Hello, World 2!\"
361
2
        task3: echo \"Hello, World 3!\"
362
2
    ";
363

            
364
2
    let task_root = serde_yaml::from_str::<TaskRoot>(yaml)?;
365

            
366
2
    assert_eq!(task_root.tasks.len(), 3);
367

            
368
2
    if let Task::String(command) = &task_root.tasks["task1"] {
369
2
      assert_eq!(command, "echo \"Hello, World 1!\"");
370
    } else {
371
      panic!("Expected Task::String");
372
    }
373

            
374
2
    if let Task::String(command) = &task_root.tasks["task2"] {
375
2
      assert_eq!(command, "echo \"Hello, World 2!\"");
376
    } else {
377
      panic!("Expected Task::String");
378
    }
379

            
380
2
    if let Task::String(command) = &task_root.tasks["task3"] {
381
2
      assert_eq!(command, "echo \"Hello, World 3!\"");
382
    } else {
383
      panic!("Expected Task::String");
384
    }
385

            
386
2
    Ok(())
387
2
  }
388

            
389
  #[test]
390
2
  fn test_task_root_4() -> anyhow::Result<()> {
391
2
    let lua = "
392
2
      {
393
2
        tasks = {
394
2
          task1 = 'echo \"Hello, World 1!\"',
395
2
          task2 = 'echo \"Hello, World 2!\"',
396
2
          task3 = 'echo \"Hello, World 3!\"',
397
2
        }
398
2
      }
399
2
    ";
400

            
401
2
    let task_root = get_lua_table(lua)?;
402

            
403
2
    assert_eq!(task_root.tasks.len(), 3);
404

            
405
2
    if let Task::String(command) = &task_root.tasks["task1"] {
406
2
      assert_eq!(command, "echo \"Hello, World 1!\"");
407
    } else {
408
      panic!("Expected Task::String");
409
    }
410

            
411
2
    if let Task::String(command) = &task_root.tasks["task2"] {
412
2
      assert_eq!(command, "echo \"Hello, World 2!\"");
413
    } else {
414
      panic!("Expected Task::String");
415
    }
416

            
417
2
    if let Task::String(command) = &task_root.tasks["task3"] {
418
2
      assert_eq!(command, "echo \"Hello, World 3!\"");
419
    } else {
420
      panic!("Expected Task::String");
421
    }
422

            
423
2
    Ok(())
424
2
  }
425
}