cargo_metask/
lib.rs

1use ::clap::Parser;
2use ::std::{io, fs, env};
3use ::std::process::{Command, Stdio, exit};
4
5#[derive(Parser)]
6#[command(version, about)]
7struct Args {
8    task_names: Vec<String>,
9}
10
11pub fn run() -> io::Result<()> {
12    let Args {
13        task_names,
14    } = {
15        let mut args = env::args().collect::<Vec<_>>();
16        if matches!(args.get(1).map(String::as_str), Some("task" | "metask")) {
17            // when invoked as `cargo task` or `cargo metask`
18            args.remove(1);
19        }
20        Args::parse_from(args)
21    };
22    if task_names.is_empty() {
23        eprintln!("[cargo-metask] no task names provided.");
24        return Ok(());
25    }
26
27    let cargo_toml = {
28        toml::from_str::<toml::Value>(&fs::read_to_string("Cargo.toml")?)
29            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
30    };
31
32    let task_def = get_task_def(&cargo_toml).ok_or_else(|| io::Error::new(
33        io::ErrorKind::InvalidData,
34        "`{package, workspace}.metadata.tasks` not found"
35    ))?;
36
37    let tasks = {
38        let mut tasks = Vec::with_capacity(task_names.len());
39        for task_name in &task_names {
40            let task = task_def
41                .get(task_name)
42                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, format!("task `{task_name}` not found")))?
43                .as_str()
44                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, format!("task `{task_name}` is not a string")))?;
45            tasks.push(task);
46        }
47        tasks
48    };
49
50    // execute tasks in parallel...
51
52    let mut handles = std::collections::VecDeque::with_capacity(tasks.len());
53    #[cfg(not(target_os = "windows"))] {
54        let shell = env::var("SHELL");
55        for task in &tasks {
56            handles.push_back(
57                Command::new(shell.as_deref().unwrap_or("/bin/sh"))
58                .args(["-c", &format!("set -Cue\n{task}")])
59                .stdout(Stdio::inherit())
60                .stderr(Stdio::inherit())
61                .spawn()?
62            );
63        }
64    }
65    #[cfg(target_os = "windows")] {
66        for task in &tasks {
67            handles.push_back(
68                Command::new("cmd")
69                .args(["/C", task])
70                .stdout(Stdio::inherit())
71                .stderr(Stdio::inherit())
72                .spawn()?
73            );
74        }
75    }
76
77    match handles.len() {
78        0 => {
79            Ok(())
80        }
81        1 => {
82            let status = handles.pop_front().unwrap().wait()?;
83            let code = status.code().unwrap_or_else(|| {
84                eprintln!("[cargo-metask] task terminated by signal");
85                1
86            });
87            exit(code);
88        }
89        _ => {
90            let mut error_code = None;
91            while let Some(mut next) = handles.pop_front() {
92                match next.try_wait()? {
93                    // task is still running, so push it back to the queue
94                    None => handles.push_back(next),
95
96                    // task has finished, so check its exit status
97                    Some(status) => match status.code() {
98                        Some(code) => {
99                            if code != 0 && error_code.is_none() {
100                                error_code = Some(code);
101                            }
102                        }
103                        None => {
104                            eprintln!("[cargo-metask] task terminated by signal");
105                            if error_code.is_none() {
106                                error_code = Some(1);
107                            }
108                        }
109                    }
110                }
111
112                // Sleep for a short time to avoid busy waiting.
113                // 
114                // This will not be a problem in practice :
115                // * the tasks are usually short-lived
116                // * the queue is small
117                std::thread::sleep(std::time::Duration::from_millis(10));
118            }
119            exit(error_code.unwrap_or(0));
120        }
121    }
122}
123
124fn get_task_def(cargo_toml: &toml::Value) -> Option<&toml::Table> {
125    (cargo_toml.get("workspace")).or(cargo_toml.get("package"))?
126        .get("metadata")?
127        .get("tasks")?
128        .as_table()
129}