• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kaspar030 / laze / 15781979994

20 Jun 2025 03:08PM UTC coverage: 80.227% (+0.3%) from 79.955%
15781979994

push

github

web-flow
feat(tasks): implement tasks calling subtasks (#725)

132 of 173 new or added lines in 7 files covered. (76.3%)

1 existing line in 1 file now uncovered.

3327 of 4147 relevant lines covered (80.23%)

100.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.43
/src/model/task.rs
1
use std::cell::LazyCell;
2
use std::ffi::OsStr;
3
use std::path::Path;
4

5
use anyhow::{Context, Error, Result};
6
use im::Vector;
7
use indexmap::IndexMap;
8
use itertools::Itertools;
9
use serde::{Deserialize, Serialize};
10
use thiserror::Error;
11

12
use crate::nested_env;
13
use crate::serde_bool_helpers::{default_as_false, default_as_true};
14
use crate::subst_ext::{substitute, IgnoreMissing, LocalVec};
15
use crate::EXIT_ON_SIGINT;
16

17
use super::shared::VarExportSpec;
18

19
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
20
#[serde(deny_unknown_fields)]
21
pub struct Task {
22
    pub cmd: Vec<String>,
23
    pub help: Option<String>,
24
    pub required_vars: Option<Vec<String>>,
25
    pub required_modules: Option<Vec<String>>,
26
    pub export: Option<Vector<VarExportSpec>>,
27
    #[serde(default = "default_as_true")]
28
    pub build: bool,
29
    #[serde(default = "default_as_false")]
30
    pub ignore_ctrl_c: bool,
31
    pub workdir: Option<String>,
32
}
33

34
#[derive(Error, Debug, Serialize, Deserialize, Clone)]
35
pub enum TaskError {
36
    #[error("required variable `{var}` not set")]
37
    RequiredVarMissing { var: String },
38
    #[error("required module `{module}` not selected")]
39
    RequiredModuleMissing { module: String },
40
    #[error("task command had non-zero exit code")]
41
    CmdExitError,
42
    #[error("task `{task_name}` not found")]
43
    TaskNotFound { task_name: String },
44
}
45

46
impl Task {
47
    pub fn build_app(&self) -> bool {
6✔
48
        self.build
6✔
49
    }
6✔
50

51
    pub fn execute(
24✔
52
        &self,
24✔
53
        start_dir: &Path,
24✔
54
        args: Option<&Vec<&str>>,
24✔
55
        verbose: u8,
24✔
56
        all_tasks: &IndexMap<String, Result<Task, TaskError>>,
24✔
57
        parent_exports: &Vector<VarExportSpec>,
24✔
58
    ) -> Result<(), Error> {
24✔
59
        let argv_str = LazyCell::new(|| match args {
24✔
NEW
60
            None => "".into(),
×
NEW
61
            Some(v) if v.is_empty() => "".into(),
×
NEW
62
            Some(v) => format!(" argv: {:?}", v),
×
NEW
63
        });
×
64

65
        for cmd_full in &self.cmd {
58✔
66
            if verbose > 0 {
34✔
NEW
67
                println!("laze: command: `{cmd_full}`{}", *argv_str);
×
68
            }
34✔
69

70
            if let Some(cmd) = cmd_full.strip_prefix(":") {
34✔
71
                let cmd = create_cmd_vec(cmd, args);
18✔
72

73
                self.execute_subtask(cmd, start_dir, verbose, all_tasks, parent_exports)
18✔
74
            } else {
75
                self.execute_shell_cmd(cmd_full, args, start_dir, verbose, parent_exports)
16✔
76
            }
77
            .with_context(|| format!("command `{cmd_full}`"))?;
34✔
78
        }
79
        Ok(())
24✔
80
    }
24✔
81

82
    fn execute_shell_cmd(
16✔
83
        &self,
16✔
84
        cmd: &str,
16✔
85
        args: Option<&Vec<&str>>,
16✔
86
        start_dir: &Path,
16✔
87
        verbose: u8,
16✔
88
        parent_exports: &Vector<VarExportSpec>,
16✔
89
    ) -> Result<(), Error> {
16✔
90
        use std::process::Command;
91
        let mut command = if cfg!(target_family = "windows") {
16✔
NEW
92
            let mut cmd = Command::new("cmd");
×
NEW
93
            cmd.arg("/C");
×
NEW
94
            cmd
×
95
        } else {
96
            let mut sh = Command::new("sh");
16✔
97
            sh.arg("-c");
16✔
98
            sh
16✔
99
        };
100

101
        if let Some(working_directory) = &self.workdir {
16✔
NEW
102
            // This includes support for absolute working directories through .join
×
NEW
103
            command.current_dir(start_dir.join(working_directory));
×
104
        } else {
16✔
105
            command.current_dir(start_dir);
16✔
106
        }
16✔
107

108
        // handle "export:" (export laze variables to task shell environment)
109
        for entry in parent_exports
16✔
110
            .into_iter()
16✔
111
            .chain(self.export.iter().flatten())
16✔
112
        {
113
            let VarExportSpec { variable, content } = entry;
16✔
114
            if let Some(val) = content {
16✔
115
                command.env(variable, val);
16✔
116
            }
16✔
117
        }
118

119
        // TODO: is this still needed?
120
        let cmd = cmd.replace("$$", "$");
16✔
121

122
        command.arg(cmd);
16✔
123

124
        if let Some(args) = args {
16✔
125
            if !args.is_empty() {
16✔
126
                command.arg("--");
7✔
127
                command.args(args);
7✔
128
            }
9✔
NEW
129
        }
×
130

131
        if verbose > 0 {
16✔
NEW
132
            let mut full_command = Vec::new();
×
NEW
133
            full_command.push(command.get_program().to_string_lossy());
×
NEW
134
            full_command.extend(command.get_args().map(OsStr::to_string_lossy));
×
UNCOV
135

×
NEW
136
            println!("laze: executing `{full_command:?}`");
×
137
        }
16✔
138

139
        if self.ignore_ctrl_c {
16✔
NEW
140
            EXIT_ON_SIGINT
×
NEW
141
                .get()
×
NEW
142
                .unwrap()
×
NEW
143
                .clone()
×
NEW
144
                .store(false, std::sync::atomic::Ordering::SeqCst);
×
145
        }
16✔
146
        // run command, wait for status
147
        let status = command.status().expect("executing command");
16✔
148

149
        if self.ignore_ctrl_c {
16✔
NEW
150
            EXIT_ON_SIGINT
×
NEW
151
                .get()
×
NEW
152
                .unwrap()
×
NEW
153
                .clone()
×
NEW
154
                .store(true, std::sync::atomic::Ordering::SeqCst);
×
155
        }
16✔
156

157
        status
16✔
158
            .success()
16✔
159
            .then_some(())
16✔
160
            .ok_or(TaskError::CmdExitError.into())
16✔
161
    }
16✔
162

163
    fn execute_subtask(
18✔
164
        &self,
18✔
165
        cmd: Vec<String>,
18✔
166
        start_dir: &Path,
18✔
167
        verbose: u8,
18✔
168
        all_tasks: &IndexMap<String, std::result::Result<Task, TaskError>>,
18✔
169
        parent_exports: &Vector<VarExportSpec>,
18✔
170
    ) -> Result<(), Error> {
18✔
171
        // turn cmd into proper `Vec<&str>` without the command name.
172
        let args = cmd.iter().skip(1).map(|s| s.as_str()).collect_vec();
40✔
173

174
        let task_name = &cmd[0];
18✔
175

176
        // resolve task name to task
177
        let other_task = all_tasks
18✔
178
            .get(task_name)
18✔
179
            .ok_or_else(|| TaskError::TaskNotFound {
18✔
NEW
180
                task_name: task_name.clone(),
×
NEW
181
            })?
×
182
            .as_ref()
18✔
183
            .map_err(|e| e.clone())
18✔
184
            .with_context(|| format!("task '{task_name}'"))?;
18✔
185

186
        let mut parent_exports = parent_exports.clone();
18✔
187
        if let Some(export) = self.export.as_ref() {
18✔
188
            parent_exports.append(export.clone());
3✔
189
        }
15✔
190

191
        other_task.execute(start_dir, Some(&args), verbose, all_tasks, &parent_exports)?;
18✔
192

193
        Ok(())
18✔
194
    }
18✔
195

196
    fn _with_env(&self, env: &im::HashMap<&String, String>, do_eval: bool) -> Result<Task, Error> {
44✔
197
        let expand = |s| {
68✔
198
            if do_eval {
68✔
199
                nested_env::expand_eval(s, env, nested_env::IfMissing::Empty)
34✔
200
            } else {
201
                nested_env::expand(s, env, nested_env::IfMissing::Ignore)
34✔
202
            }
203
        };
68✔
204

205
        Ok(Task {
206
            cmd: self
44✔
207
                .cmd
44✔
208
                .iter()
44✔
209
                .map(expand)
44✔
210
                .collect::<Result<Vec<String>, _>>()?,
44✔
211
            export: if do_eval {
44✔
212
                self.expand_export(env)
22✔
213
            } else {
214
                self.export.clone()
22✔
215
            },
216
            workdir: self.workdir.as_ref().map(expand).transpose()?,
44✔
217
            ..(*self).clone()
44✔
218
        })
219
    }
44✔
220

221
    /// This is called early when loading the yaml files.
222
    /// It will not evaluate expressions, and pass-through variables that are not
223
    /// found in `env`.
224
    pub fn with_env(&self, env: &im::HashMap<&String, String>) -> Result<Task, Error> {
22✔
225
        self._with_env(env, false)
22✔
226
    }
22✔
227

228
    /// This is called to generate the final task.
229
    /// It will evaluate expressions, and variables that are not
230
    /// found in `env` will be replaced with the empty string.
231
    pub fn with_env_eval(&self, env: &im::HashMap<&String, String>) -> Result<Task, Error> {
22✔
232
        self._with_env(env, true)
22✔
233
    }
22✔
234

235
    fn expand_export(&self, env: &im::HashMap<&String, String>) -> Option<Vector<VarExportSpec>> {
22✔
236
        self.export
22✔
237
            .as_ref()
22✔
238
            .map(|export| VarExportSpec::expand(export.iter(), env))
22✔
239
    }
22✔
240
}
241

242
fn create_cmd_vec(cmd: &str, args: Option<&Vec<&str>>) -> Vec<String> {
18✔
243
    let mut cmd = shell_words::split(cmd).unwrap();
18✔
244
    if let Some(args) = args {
18✔
245
        substitute_args(&mut cmd, args);
18✔
246
    }
18✔
247
    cmd
18✔
248
}
18✔
249

250
fn substitute_args(cmd: &mut Vec<String>, args: &Vec<&str>) {
18✔
251
    // This function deals with argument replacements.
252
    // \$1 -> first argument, \$<N> -> Nth argument,
253
    // \$* -> all arguments as one string,
254
    // \$@ -> all arguments as individual arguments.
255
    // .. also, braced equivalents (\${1}, \${*}, ...).
256
    //
257
    // This does not behave exactly like the shell, but tries to get as
258
    // close as possible.
259
    //
260
    // Differences so far:
261
    // - \$* / \${*} / \$@ / \${@} only work if they are indivdual arguments,
262
    //   not within another. So 'arg1 \$* arg2' works, '"some \$* arg1" arg2' won't.
263
    // - whereas in shell, '$*' and '$@' are equivalent and only '"$@"' keeps the individual args,
264
    //   laze uses star variant for single string, at variant for individual args.
265
    //   This is because `shell_words::split()` eats the double quotes.
266

267
    // lazily create the replacement for '\$*'
268
    let args_joined = std::cell::LazyCell::new(|| args.iter().join(" "));
18✔
269

270
    // These two create a helper `VariableMap` to be used by `substitute()`.
271
    let args_ = LocalVec::new(args);
18✔
272
    let variables = IgnoreMissing::new(&args_);
18✔
273

274
    // here we iterate the arguments, and:
275
    // 1. `\$@` / `\${@}` are replaced by multiple individual args.
276
    // 2. `\$*` / `\${*}` are replaced by the concatenated args
277
    // 3. all elements run through `substitute()`, substituting the numbered args.
278
    *cmd = cmd
18✔
279
        .iter()
18✔
280
        .flat_map(|arg| match arg.as_str() {
52✔
281
            "\"$@\"" | "\"${@}\"" | "$@" | "${@}" => args.clone(),
52✔
282
            _ => vec![arg.as_str()],
50✔
283
        })
52✔
284
        .map(|s| match s {
58✔
285
            "$*" | "${*}" | "$@" | "${@}" => args_joined.clone(),
58✔
286
            _ => substitute(s, &variables)
57✔
287
                .with_context(|| format!("substituting '{s}'"))
57✔
288
                .unwrap(),
57✔
289
        })
58✔
290
        .filter(|s| !s.is_empty())
58✔
291
        .collect::<Vec<String>>();
18✔
292
}
18✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc