cargo_task/
exec.rs

1use crate::*;
2use std::{
3    collections::HashSet,
4    path::{Path, PathBuf},
5};
6
7/// Main entrypoint for cargo-task binary.
8pub fn exec_cargo_task() {
9    // any pre-env-load tasks to execute?
10    task::check_pre_env_task();
11
12    // load our environment into environment variables
13    if env_loader::load().is_err() {
14        ct_fatal!(
15            r"ERROR: Could not find '{}' directory.
16Have you run 'cargo task ct-init'?",
17            CARGO_TASK_DIR,
18        );
19    }
20
21    // parse environment vars into env struct
22    let mut env = _cargo_task_util::ct_env();
23
24    ct_info!("cargo-task running...");
25
26    clean_build_workspace(&env);
27    let mut did_build_workspace = false;
28
29    // check for bootstrap tasks
30    let mut task_list = Vec::new();
31    for (task, task_meta) in env.tasks.iter() {
32        if task_meta.bootstrap {
33            fill_task_deps(
34                &env,
35                &mut task_list,
36                task.to_string(),
37                HashSet::new(),
38            );
39        }
40    }
41
42    // if we are bootstrapping
43    if !task_list.is_empty() {
44        ct_info!("executing bootstrap list: {:?}", task_list);
45        for task in task_list {
46            if !task::check_system_task(task.as_str(), &env) {
47                // run ct-init to ensure our cargo_task_util crate is up-to-date
48                task::ct_init();
49
50                run_task(&env, &task, &mut did_build_workspace);
51            }
52        }
53        ct_info!("reloading env post-bootstrap");
54        if env_loader::load().is_err() {
55            ct_fatal!(
56                r"ERROR: Could not find '{}' directory.
57    Have you run 'cargo task ct-init'?",
58                CARGO_TASK_DIR,
59            );
60        }
61        clean_build_workspace(&env);
62        did_build_workspace = false;
63
64        env = _cargo_task_util::ct_force_new_env();
65    }
66
67    // load up specified tasks
68    let mut task_list = Vec::new();
69    for task in env.task_list.iter() {
70        fill_task_deps(&env, &mut task_list, task.to_string(), HashSet::new());
71    }
72
73    // if no specified tasks - load default tasks
74    if task_list.is_empty() {
75        for (task, task_meta) in env.tasks.iter() {
76            if task_meta.default {
77                fill_task_deps(
78                    &env,
79                    &mut task_list,
80                    task.to_string(),
81                    HashSet::new(),
82                );
83            }
84        }
85    }
86
87    ct_info!("task order: {:?}", task_list);
88
89    for task in task_list {
90        if !task::check_system_task(task.as_str(), &env) {
91            // run ct-init to ensure our cargo_task_util crate is up-to-date
92            task::ct_init();
93
94            run_task(&env, &task, &mut did_build_workspace);
95        }
96    }
97
98    clean_build_workspace(&env);
99
100    ct_info!("cargo-task complete : )");
101}
102
103/// fill task deps
104fn fill_task_deps(
105    env: &_cargo_task_util::CTEnv,
106    task_list: &mut Vec<String>,
107    task: String,
108    mut visited: HashSet<String>,
109) {
110    visited.insert(task.clone());
111    if !env.tasks.contains_key(&task) {
112        // this may be a psuedo task - add it, but don't check deps
113        if !task_list.contains(&task) {
114            task_list.push(task);
115        }
116        return;
117    }
118    for dep in env.tasks.get(&task).unwrap().task_deps.iter() {
119        if visited.contains(dep) {
120            ct_fatal!("circular task dependency within {:?}", visited);
121        }
122        fill_task_deps(env, task_list, dep.to_string(), visited.clone());
123    }
124    if !task_list.contains(&task) {
125        task_list.push(task);
126    }
127}
128
129/// delete the cargo-task build workspace
130fn clean_build_workspace(env: &_cargo_task_util::CTEnv) {
131    let mut ws = env.cargo_task_target.clone();
132    ws.push("ct-workspace");
133    let _ = std::fs::remove_dir_all(&ws);
134}
135
136/// prep the cargo-task build workspace
137fn generate_build_workspace(env: &_cargo_task_util::CTEnv) {
138    let mut all_tasks = Vec::new();
139    let mut ws = env.cargo_task_target.clone();
140    ws.push("ct-workspace");
141    ct_check_fatal!(std::fs::create_dir_all(&ws));
142
143    // copy in our cargo_task_util crate
144    let mut ctu_src = env.cargo_task_path.clone();
145    ctu_src.push("cargo_task_util");
146    let mut ctu_dest = ws.clone();
147    ctu_dest.push("cargo_task_util");
148    if let Ok(meta) = std::fs::metadata(&ctu_src) {
149        if meta.is_dir() {
150            copy_dir(&ctu_src, &ctu_dest);
151        }
152    }
153
154    for (task, task_meta) in env.tasks.iter() {
155        all_tasks.push(task);
156
157        let mut task_dir = ws.clone();
158        task_dir.push(task);
159
160        if task_meta.is_script {
161            ct_check_fatal!(std::fs::create_dir_all(&task_dir));
162            let mut cargo_toml = task_dir.clone();
163            cargo_toml.push("Cargo.toml");
164            let deps = if let Some(deps) = &task_meta.cargo_deps {
165                deps
166            } else {
167                ""
168            };
169            ct_check_fatal!(std::fs::write(
170                &cargo_toml,
171                format!(
172                    r#"[package]
173name = "{}"
174version = "0.0.1"
175edition = "2018"
176
177[dependencies]
178cargo_task_util = "*"
179{}
180"#,
181                    task, deps,
182                )
183            ));
184            let mut src_dir = task_dir.clone();
185            src_dir.push("src");
186            ct_check_fatal!(std::fs::create_dir_all(&src_dir));
187            let mut main_file = src_dir;
188            main_file.push("main.rs");
189            ct_check_fatal!(std::fs::copy(&task_meta.path, &main_file));
190        } else {
191            copy_dir(&task_meta.path, &task_dir);
192        }
193    }
194
195    // also add our cargo_task_util dep crate to the workspace
196    let ctu = "cargo_task_util".to_string();
197    all_tasks.push(&ctu);
198
199    ws.push("Cargo.toml");
200    ct_check_fatal!(std::fs::write(
201        &ws,
202        format!(
203            r#"[workspace]
204members = {:?}
205
206[patch.crates-io]
207cargo_task_util = {{ path = "cargo_task_util" }}
208"#,
209            all_tasks
210        ),
211    ));
212}
213
214/// recursively copy a whole directory
215fn copy_dir<S: AsRef<Path>, D: AsRef<Path>>(src: S, dest: D) {
216    ct_check_fatal!(std::fs::create_dir_all(&dest));
217    for item in ct_check_fatal!(std::fs::read_dir(src)).flatten() {
218        let meta = ct_check_fatal!(item.metadata());
219        let mut dest = dest.as_ref().to_owned();
220        dest.push(item.file_name());
221        if meta.is_dir() {
222            copy_dir(item.path(), &dest);
223        } else if meta.is_file() {
224            ct_check_fatal!(std::fs::copy(item.path(), &dest));
225        }
226    }
227}
228
229/// run a specific task
230fn run_task(
231    env: &_cargo_task_util::CTEnv,
232    task_name: &str,
233    did_build_workspace: &mut bool,
234) {
235    if !env.tasks.contains_key(task_name) {
236        ct_fatal!("invalid task name '{}'", task_name);
237    }
238
239    let task_meta = env.tasks.get(task_name).unwrap();
240    if let Some(min_version) = &task_meta.min_version {
241        if parse_semver(crate::CARGO_TASK_VER) < parse_semver(min_version) {
242            ct_fatal!(
243                "cargo-task {} < required min version {}",
244                crate::CARGO_TASK_VER,
245                min_version,
246            );
247        }
248    }
249
250    let task = task_build(env, task_name, did_build_workspace);
251
252    ct_info!("run task: '{}'", task_name);
253    std::env::set_var("CT_CUR_TASK", task_name);
254
255    let mut cmd = std::process::Command::new(task);
256    cmd.current_dir(&env.work_dir);
257    for arg in env.arg_list.iter() {
258        cmd.arg(arg);
259    }
260    cmd.stdin(std::process::Stdio::piped());
261    let res: Result<(), String> = (move || {
262        let mut child = cmd.spawn().map_err(|e| format!("{:?}", e))?;
263
264        // drop stdin to ensure child exit
265        drop(child.stdin.take().unwrap());
266
267        let res: Result<(), String> = (|| {
268            let status = child.wait().map_err(|e| format!("{:?}", e))?;
269
270            if !status.success() {
271                return Err(format!("{} exited non-zero", task_name));
272            }
273
274            Ok(())
275        })();
276
277        let mut p = env.cargo_task_target.clone();
278        let directive_file_name = format!("task-directive-{}.atat", child.id());
279        p.push(directive_file_name);
280
281        if let Ok(file) = std::fs::File::open(&p) {
282            let mut parser = at_at::AtAtParser::new(file);
283            while let Some(res) = parser.parse() {
284                for item in res {
285                    if let at_at::AtAtParseItem::KeyValue(k, v) = item {
286                        match k.as_str() {
287                            "ct-set-env" => {
288                                let idx = match v.find('=') {
289                                    Some(idx) => idx,
290                                    None => ct_fatal!(
291                                        "no '=' found in ct-set-env directive"
292                                    ),
293                                };
294                                let n = &v[..idx];
295                                let v = &v[idx + 1..];
296                                std::env::set_var(n, v);
297                                ct_info!("CT-SET-ENV: {}={}", n, v);
298                            }
299                            _ => ct_fatal!("unrecognized AtAt command '{}'", k),
300                        }
301                    }
302                }
303            }
304        }
305
306        let _ = std::fs::remove_file(&p);
307
308        res
309    })();
310    std::env::remove_var("CT_CUR_TASK");
311
312    if let Err(e) = res {
313        ct_fatal!("{}", e);
314    }
315}
316
317/// build a specific task crate
318fn task_build(
319    env: &_cargo_task_util::CTEnv,
320    task_name: &str,
321    did_build_workspace: &mut bool,
322) -> PathBuf {
323    let task_meta = env.tasks.get(task_name).unwrap();
324
325    let target_dir = env.cargo_task_target.clone();
326
327    let mut artifact_path = target_dir.clone();
328    artifact_path.push("release");
329    artifact_path.push(task_name);
330
331    if let Ok(meta) = std::fs::metadata(&artifact_path) {
332        let artifact_time = meta
333            .modified()
334            .expect("failed to get artifact modified time");
335        let dir_time = get_newest_time(&task_meta.path);
336
337        if artifact_time >= dir_time {
338            return artifact_path;
339        }
340    }
341
342    ct_info!("build task '{}'", task_name);
343
344    if !*did_build_workspace {
345        *did_build_workspace = true;
346        generate_build_workspace(env);
347    }
348
349    let mut crate_path = env.cargo_task_target.clone();
350    crate_path.push("ct-workspace");
351    crate_path.push(task_name);
352
353    let mut cmd = env.cargo();
354    cmd.arg("build");
355    cmd.arg("--release");
356
357    let mut manifest_path = crate_path;
358    manifest_path.push("Cargo.toml");
359
360    cmd.arg("--manifest-path");
361    cmd.arg(manifest_path);
362
363    cmd.arg("--target-dir");
364    cmd.arg(&target_dir);
365
366    ct_check_fatal!(env.exec(cmd));
367
368    artifact_path
369}
370
371/// recursively get the newest update time for any file/dir
372fn get_newest_time<P: AsRef<Path>>(path: P) -> std::time::SystemTime {
373    let mut newest_time = std::time::SystemTime::UNIX_EPOCH;
374
375    if let Ok(metadata) = std::fs::metadata(&path) {
376        if metadata.is_file() {
377            return metadata.modified().expect("failed to get modified time");
378        }
379    }
380
381    for item in std::fs::read_dir(&path)
382        .expect("failed to read directory")
383        .flatten()
384    {
385        let t = item.file_type().expect("failed to get file type");
386
387        if t.is_dir() {
388            let updated = get_newest_time(item.path());
389            if updated > newest_time {
390                newest_time = updated;
391            }
392        } else if t.is_file() {
393            let updated = item
394                .metadata()
395                .expect("failed to get metadata")
396                .modified()
397                .expect("failed to get modified time");
398            if updated > newest_time {
399                newest_time = updated;
400            }
401        }
402    }
403
404    newest_time
405}
406
407/// Parse a semver string into a (usize, usize, usize)
408fn parse_semver(s: &str) -> (usize, usize, usize) {
409    let r = s.split('.').collect::<Vec<_>>();
410    if r.len() != 3 {
411        ct_fatal!("invalid semver: {}", s);
412    }
413    (
414        r[0].parse().unwrap(),
415        r[1].parse().unwrap(),
416        r[2].parse().unwrap(),
417    )
418}