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

kaspar030 / laze / 21909131430

11 Feb 2026 02:30PM UTC coverage: 79.674% (+0.5%) from 79.216%
21909131430

push

github

web-flow
build(deps): bump tempfile from 3.24.0 to 3.25.0 (#852)

3371 of 4231 relevant lines covered (79.67%)

97.22 hits per line

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

86.34
/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 log::debug;
10
use serde::{Deserialize, Serialize};
11
use thiserror::Error;
12

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

18
use super::shared::VarExportSpec;
19

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

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

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

52
    pub fn execute(
24✔
53
        &self,
24✔
54
        start_dir: &Path,
24✔
55
        args: Option<&Vec<&str>>,
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✔
60
            None => "".into(),
×
61
            Some(v) if v.is_empty() => "".into(),
×
62
            Some(v) => format!(" argv: {:?}", v),
×
63
        });
×
64

65
        for cmd_full in &self.cmd {
34✔
66
            debug!("laze: command: `{cmd_full}`{}", *argv_str);
34✔
67

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

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

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

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

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

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

119
        command.arg(cmd);
16✔
120

121
        if let Some(args) = args {
16✔
122
            if !args.is_empty() {
16✔
123
                command.arg("--");
7✔
124
                command.args(args);
7✔
125
            }
9✔
126
        }
×
127

128
        let mut full_command = Vec::new();
16✔
129
        full_command.push(command.get_program().to_string_lossy());
16✔
130
        full_command.extend(command.get_args().map(OsStr::to_string_lossy));
16✔
131

132
        debug!("laze: executing `{full_command:?}`");
16✔
133

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

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

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

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

168
        let task_name = &cmd[0];
18✔
169

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

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

185
        other_task.execute(start_dir, Some(&args), all_tasks, &parent_exports)?;
18✔
186

187
        Ok(())
18✔
188
    }
18✔
189

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

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

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

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

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

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

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

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

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

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