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