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 $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 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#[derive(Debug, Default, Deserialize)]
44pub struct TaskRoot {
45 pub tasks: HashMap<String, Task>,
47
48 #[serde(default)]
50 pub use_npm: Option<UseNpm>,
51
52 #[serde(default)]
54 pub use_cargo: Option<UseCargo>,
55
56 #[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 let mut value: serde_yaml::Value = serde_yaml::from_reader(reader)?;
97 value.apply_merge()?;
98
99 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 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 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 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 let lua = Lua::new();
148
149 let value = lua.load(contents).eval()?;
151
152 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}