cargo_task/
_cargo_task_util.rs

1//! Common cargo_task_util mod will be available to all tasks.
2//!
3//! Simply include it in your task module:
4//! - `mod cargo_task_util;`
5//! - `use cargo_task_util::*;`
6//!
7//! Note, the macros `ct_info!`, `ct_warn!`, `ct_fatal!`, and `ct_check_fatal!`
8//! are all defined in this module, and thus are available to task code too.
9//! But rust hoists all macros up to the root, so their docs are up there : )
10
11use std::{collections::BTreeMap, ffi::OsString, path::PathBuf, rc::Rc};
12
13/// Force install a new CTEnv
14#[doc(hidden)]
15pub(crate) fn ct_force_new_env() -> Rc<CTEnv> {
16    CT_ENV.with(|env| {
17        let mut env = env.lock().unwrap();
18        *env = priv_new_env();
19        env.clone()
20    })
21}
22
23/// Fetch the current CTEnv
24pub fn ct_env() -> Rc<CTEnv> {
25    CT_ENV.with(|env| env.lock().unwrap().clone())
26}
27
28/// format! style helper for printing out info messages.
29#[macro_export]
30macro_rules! ct_info {
31    ($($tt:tt)*) => { $crate::_cargo_task_util::ct_info(&format!($($tt)*)) };
32}
33
34/// format! style helper for printing out warn messages.
35#[macro_export]
36macro_rules! ct_warn {
37    ($($tt:tt)*) => { $crate::_cargo_task_util::ct_warn(&format!($($tt)*)) };
38}
39
40/// format! style helper for printing out fatal messages.
41#[macro_export]
42macro_rules! ct_fatal {
43    ($($tt:tt)*) => { $crate::_cargo_task_util::ct_fatal(&format!($($tt)*)) };
44}
45
46/// takes a result, if the result is error, runs ct_fatal!
47#[macro_export]
48macro_rules! ct_check_fatal {
49    ($code:expr) => {
50        match { $code } {
51            Err(e) => $crate::ct_fatal!("{:#?}", e),
52            Ok(r) => r,
53        }
54    };
55}
56
57/// Cargo-task environment info struct.
58#[derive(Debug)]
59pub struct CTEnv {
60    /// The path to the cargo binary.
61    pub cargo_path: PathBuf,
62
63    /// The .cargo-task directory.
64    pub cargo_task_path: PathBuf,
65
66    /// The targe dir for cargo-task builds.
67    pub cargo_task_target: PathBuf,
68
69    /// The root of the cargo task execution environment.
70    pub work_dir: PathBuf,
71
72    /// Task list specified by user.
73    pub task_list: Vec<String>,
74
75    /// Additional arguments specified by user.
76    pub arg_list: Vec<String>,
77
78    /// All tasks defined in the task directory.
79    pub tasks: BTreeMap<String, CTTaskMeta>,
80}
81
82impl CTEnv {
83    /// Create a new cargo std::process::Command
84    pub fn cargo(&self) -> std::process::Command {
85        std::process::Command::new(&self.cargo_path)
86    }
87
88    /// Execute a rust std::process::Command
89    pub fn exec(&self, mut cmd: std::process::Command) -> std::io::Result<()> {
90        let non_zero_err = format!("{:?} exited non-zero", cmd);
91        if !cmd.spawn()?.wait()?.success() {
92            return Err(std::io::Error::new(
93                std::io::ErrorKind::Other,
94                non_zero_err,
95            ));
96        }
97        Ok(())
98    }
99
100    /// Export an environment variable up to the parent task runner env.
101    /// Also sets the variable in the current environment.
102    pub fn set_env<N: AsRef<str>, V: AsRef<str>>(&self, name: N, val: V) {
103        let name = name.as_ref();
104        let val = val.as_ref();
105
106        std::env::set_var(name, val);
107
108        let directive = format!("@ct-set-env@ {}={} @@\n", name, val);
109
110        let mut p = self.cargo_task_target.clone();
111        let directive_file_name =
112            format!("task-directive-{}.atat", std::process::id());
113        p.push(directive_file_name);
114
115        FRESH_DIRECTIVE.call_once(|| {
116            let _ = std::fs::remove_file(&p);
117        });
118
119        let mut f = ct_check_fatal!(std::fs::OpenOptions::new()
120            .write(true)
121            .append(true)
122            .create(true)
123            .open(&p));
124
125        use std::io::Write;
126        ct_check_fatal!(f.write_all(directive.as_bytes()));
127        ct_check_fatal!(f.sync_all());
128    }
129}
130
131static FRESH_DIRECTIVE: std::sync::Once = std::sync::Once::new();
132
133/// Cargo-task task metadata struct.
134#[derive(Debug)]
135pub struct CTTaskMeta {
136    /// task name
137    pub name: String,
138
139    /// `true` if this task was specified as a single `*.ct.rs` script file.
140    /// `false` if this task was specified as a full crate directory.
141    pub is_script: bool,
142
143    /// Minimum cargo-task utility version required for this task.
144    pub min_version: Option<String>,
145
146    /// task "crate" path
147    pub path: PathBuf,
148
149    /// does this path run on default `cargo task` execution?
150    pub default: bool,
151
152    /// should this task always run before other tasks?
153    /// task list will be reloaded after all bootstrap tasks run.
154    pub bootstrap: bool,
155
156    /// help info for this task
157    pub help: String,
158
159    /// any cargo (Cargo.toml) dependencies for a script task
160    pub cargo_deps: Option<String>,
161
162    /// any cargo-task task dependencies
163    pub task_deps: Vec<String>,
164}
165
166/// Log Level enum for CT logging
167#[derive(Clone, Copy)]
168pub enum CTLogLevel {
169    /// Informational message
170    Info,
171
172    /// Warning message
173    Warn,
174
175    /// Fatal message
176    Fatal,
177}
178
179/// Generic CT log function
180pub fn ct_log(lvl: CTLogLevel, text: &str) {
181    let with_color = std::env::var_os("CT_NO_COLOR").is_none()
182        && (std::env::var_os("CT_WITH_COLOR").is_some() || DEFAULT_WITH_COLOR);
183
184    let task_name = std::env::var_os("CT_CUR_TASK")
185        .map(|s| s.to_string_lossy().to_string())
186        .unwrap_or_else(|| "".to_string());
187
188    let t_colon = if task_name.is_empty() { "" } else { ":" };
189
190    let base = if with_color { "\x1b[97m" } else { "" };
191    let reset = if with_color { "\x1b[0m" } else { "" };
192
193    let (lvl_name, log) = match lvl {
194        CTLogLevel::Info => ("INFO", "\x1b[92m"),
195        CTLogLevel::Warn => ("WARN", "\x1b[93m"),
196        CTLogLevel::Fatal => ("FATAL", "\x1b[91m"),
197    };
198
199    let log = if with_color { log } else { "" };
200
201    for line in text.split('\n') {
202        eprintln!(
203            "{}[ct:{}{}{}{}{}]{} {}",
204            base, log, lvl_name, base, t_colon, task_name, reset, line
205        );
206    }
207
208    if let CTLogLevel::Fatal = lvl {
209        std::process::exit(1);
210    }
211}
212
213/// Info level log function
214pub fn ct_info(text: &str) {
215    ct_log(CTLogLevel::Info, text)
216}
217
218/// Warn level log function
219pub fn ct_warn(text: &str) {
220    ct_log(CTLogLevel::Warn, text)
221}
222
223/// Fatal level log function
224pub fn ct_fatal(text: &str) -> ! {
225    ct_log(CTLogLevel::Fatal, text);
226    std::process::exit(1);
227}
228
229// -- private -- //
230
231#[cfg(windows)]
232const DEFAULT_WITH_COLOR: bool = false;
233#[cfg(not(windows))]
234const DEFAULT_WITH_COLOR: bool = true;
235
236thread_local! {
237    static CT_ENV: std::sync::Mutex<Rc<CTEnv>> = std::sync::Mutex::new(priv_new_env());
238}
239
240/// Gather data from environment variables to create a cargo task "env" item.
241fn priv_new_env() -> Rc<CTEnv> {
242    let cargo_path = match std::env::var_os("CARGO").map(PathBuf::from) {
243        Some(cargo_path) => cargo_path,
244        None => ct_fatal!("CARGO binary path not set in environment"),
245    };
246    let work_dir = match std::env::var_os("CT_WORK_DIR").map(PathBuf::from) {
247        Some(work_dir) => work_dir,
248        None => ct_fatal!("CT_WORK_DIR environment variable not set"),
249    };
250    let cargo_task_path = match std::env::var_os("CT_PATH").map(PathBuf::from) {
251        Some(cargo_task_path) => cargo_task_path,
252        None => ct_fatal!("CT_PATH environment variable not set"),
253    };
254    let cargo_task_target =
255        match std::env::var_os("CT_TARGET").map(PathBuf::from) {
256            Some(cargo_task_target) => cargo_task_target,
257            None => ct_fatal!("CT_TARGET environment variable not set"),
258        };
259    let task_list = match std::env::var_os("CT_TASKS") {
260        Some(args) => args
261            .to_string_lossy()
262            .split_whitespace()
263            .map(|s| s.to_string())
264            .collect::<Vec<_>>(),
265        None => Vec::with_capacity(0),
266    };
267    let arg_list = match std::env::var_os("CT_ARGS") {
268        Some(args) => args
269            .to_string_lossy()
270            .split_whitespace()
271            .map(|s| s.to_string())
272            .collect::<Vec<_>>(),
273        None => Vec::with_capacity(0),
274    };
275    let tasks = ct_check_fatal!(enumerate_task_metadata());
276
277    Rc::new(CTEnv {
278        cargo_path,
279        work_dir,
280        cargo_task_path,
281        cargo_task_target,
282        task_list,
283        arg_list,
284        tasks,
285    })
286}
287
288/// Loads task metadata from environment.
289#[allow(clippy::unnecessary_wraps)]
290fn enumerate_task_metadata(
291) -> Result<BTreeMap<String, CTTaskMeta>, &'static str> {
292    let mut out = BTreeMap::new();
293
294    let env = std::env::vars_os().collect::<BTreeMap<OsString, OsString>>();
295    for (env_k, env_v) in env.iter() {
296        let env_k = env_k.to_string_lossy();
297        if env_k.starts_with("CT_TASK_") && env_k.ends_with("_PATH") {
298            let name = env_k[8..env_k.len() - 5].to_string();
299            let script_name = format!("CT_TASK_{}_IS_SCRIPT", name);
300            let is_script = env.contains_key(&OsString::from(script_name));
301            let mv_name = format!("CT_TASK_{}_MIN_VER", name);
302            let min_version = env
303                .get(&OsString::from(mv_name))
304                .map(|v| v.to_string_lossy().to_string());
305            let def_name = format!("CT_TASK_{}_DEFAULT", name);
306            let default = env.contains_key(&OsString::from(def_name));
307            let bs_name = format!("CT_TASK_{}_BOOTSTRAP", name);
308            let bootstrap = env.contains_key(&OsString::from(bs_name));
309            let help_name = format!("CT_TASK_{}_HELP", name);
310            let help = env
311                .get(&OsString::from(help_name))
312                .map(|s| s.to_string_lossy().to_string())
313                .unwrap_or_else(|| "".to_string());
314            let deps_name = format!("CT_TASK_{}_CARGO_DEPS", name);
315            let cargo_deps = env
316                .get(&OsString::from(deps_name))
317                .map(|v| v.to_string_lossy().to_string());
318            let deps_name = format!("CT_TASK_{}_TASK_DEPS", name);
319            let mut task_deps = Vec::new();
320            if let Some(deps) = env.get(&OsString::from(deps_name)) {
321                for dep in deps.to_string_lossy().split_whitespace() {
322                    task_deps.push(dep.to_string());
323                }
324            }
325            let path = PathBuf::from(env_v);
326            out.insert(
327                name.clone(),
328                CTTaskMeta {
329                    name,
330                    is_script,
331                    min_version,
332                    path,
333                    default,
334                    bootstrap,
335                    help,
336                    cargo_deps,
337                    task_deps,
338                },
339            );
340        }
341    }
342
343    Ok(out)
344}