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

kaspar030 / laze / 15307578317

28 May 2025 06:17PM UTC coverage: 80.217%. First build
15307578317

Pull #725

github

web-flow
Merge 140ee88e1 into 92a53b047
Pull Request #725: feat(tasks): implement tasks calling subtasks

134 of 175 new or added lines in 7 files covered. (76.57%)

3325 of 4145 relevant lines covered (80.22%)

100.75 hits per line

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

83.23
/src/model/task.rs
1
use std::ffi::OsStr;
2
use std::path::Path;
3

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

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

16
use super::shared::VarExportSpec;
17

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

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

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

50
    pub fn execute(
24✔
51
        &self,
24✔
52
        start_dir: &Path,
24✔
53
        args: Option<&Vec<&str>>,
24✔
54
        verbose: u8,
24✔
55
        all_tasks: &IndexMap<String, Result<Task, TaskError>>,
24✔
56
        parent_exports: &Vector<VarExportSpec>,
24✔
57
    ) -> Result<(), Error> {
24✔
58
        for cmd_full in &self.cmd {
58✔
59
            if verbose > 0 {
34✔
NEW
60
                println!("laze: command: '{cmd_full}'");
×
NEW
61
                if let Some(args) = args {
×
NEW
62
                    println!("laze:    args: {args:?}");
×
NEW
63
                }
×
64
            }
34✔
65

66
            if let Some(cmd) = cmd_full.strip_prefix(":") {
34✔
67
                let cmd = create_cmd_vec(cmd, args);
18✔
68

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

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

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

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

115
        // TODO: is this still needed?
116
        let cmd = cmd.replace("$$", "$");
16✔
117

118
        command.arg(cmd);
16✔
119

120
        if let Some(args) = args {
16✔
121
            command.arg("--");
16✔
122
            command.args(args);
16✔
123
        }
16✔
124

125
        if verbose > 0 {
16✔
NEW
126
            let command_with_args = command
×
NEW
127
                .get_args()
×
NEW
128
                .skip(1)
×
NEW
129
                .map(OsStr::to_string_lossy)
×
NEW
130
                .collect_vec();
×
131

×
NEW
132
            println!("laze: executing {command_with_args:?}");
×
133
        }
16✔
134

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

145
        if self.ignore_ctrl_c {
16✔
NEW
146
            EXIT_ON_SIGINT
×
NEW
147
                .get()
×
NEW
148
                .unwrap()
×
NEW
149
                .clone()
×
NEW
150
                .store(true, std::sync::atomic::Ordering::SeqCst);
×
151
        }
16✔
152

153
        status
16✔
154
            .success()
16✔
155
            .then_some(())
16✔
156
            .ok_or(TaskError::CmdExitError.into())
16✔
157
    }
16✔
158

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

170
        let task_name = &cmd[0];
18✔
171

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

182
        let mut parent_exports = parent_exports.clone();
18✔
183
        if let Some(export) = self.export.as_ref() {
18✔
184
            parent_exports.append(export.clone());
3✔
185
        }
15✔
186

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

189
        Ok(())
18✔
190
    }
18✔
191
    fn _with_env(&self, env: &im::HashMap<&String, String>, do_eval: bool) -> Result<Task, Error> {
44✔
192
        let expand = |s| {
68✔
193
            if do_eval {
68✔
194
                nested_env::expand_eval(s, env, nested_env::IfMissing::Empty)
34✔
195
            } else {
196
                nested_env::expand(s, env, nested_env::IfMissing::Ignore)
34✔
197
            }
198
        };
68✔
199

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

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

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

230
    fn expand_export(&self, env: &im::HashMap<&String, String>) -> Option<Vector<VarExportSpec>> {
22✔
231
        self.export
22✔
232
            .as_ref()
22✔
233
            .map(|export| VarExportSpec::expand(export.iter(), env))
22✔
234
    }
22✔
235
}
236

237
fn create_cmd_vec(cmd: &str, args: Option<&Vec<&str>>) -> Vec<String> {
18✔
238
    let mut cmd = shell_words::split(cmd).unwrap();
18✔
239
    if let Some(args) = args {
18✔
240
        substitute_args(&mut cmd, args);
18✔
241
    }
18✔
242
    cmd
18✔
243
}
18✔
244

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

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

265
    // These two create a helper `VariableMap` to be used by `substitute()`.
266
    let args_ = LocalVec::new(args);
18✔
267
    let variables = IgnoreMissing::new(&args_);
18✔
268

269
    // here we iterate the arguments, and:
270
    // 1. `\$@` / `\${@}` are replaced by multiple individual args.
271
    // 2. `\$*` / `\${*}` are replaced by the concatenated args
272
    // 3. all elements run through `substitute()`, substituting the numbered args.
273
    *cmd = cmd
18✔
274
        .iter()
18✔
275
        .flat_map(|arg| match arg.as_str() {
52✔
276
            "\"$@\"" | "\"${@}\"" | "$@" | "${@}" => args.clone(),
52✔
277
            _ => vec![arg.as_str()],
50✔
278
        })
52✔
279
        .map(|s| match s {
58✔
280
            "$*" | "${*}" | "$@" | "${@}" => args_joined.clone(),
58✔
281
            _ => substitute(s, &variables)
57✔
282
                .with_context(|| format!("substituting '{s}'"))
57✔
283
                .unwrap(),
57✔
284
        })
58✔
285
        .filter(|s| !s.is_empty())
58✔
286
        .collect::<Vec<String>>();
18✔
287
}
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

© 2026 Coveralls, Inc