• 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

59.95
/src/main.rs
1
use core::sync::atomic::AtomicBool;
2
use std::env;
3
use std::str;
4
use std::sync::{Arc, OnceLock};
5

6
use anyhow::{anyhow, Context as _, Error, Result};
7
use camino::{Utf8Path, Utf8PathBuf};
8
use git_cache::GitCache;
9
use indexmap::IndexSet;
10
use itertools::Itertools;
11
use signal_hook::{consts::SIGINT, flag::register_conditional_shutdown};
12

13
#[global_allocator]
14
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
15

16
mod build;
17
mod cli;
18
mod data;
19
mod download;
20
mod generate;
21
mod insights;
22
mod model;
23
mod nested_env;
24
mod new;
25
mod ninja;
26
mod serde_bool_helpers;
27
mod subst_ext;
28
mod task_runner;
29
mod utils;
30

31
use model::{Context, ContextBag, Dependency, Module, Rule, Task, TaskError};
32

33
use generate::{get_ninja_build_file, BuildInfo, GenerateMode, GeneratorBuilder, Selector};
34
use nested_env::{Env, MergeOption};
35
use ninja::NinjaCmdBuilder;
36

37
pub static GIT_CACHE: OnceLock<GitCache> = OnceLock::new();
38

39
pub(crate) fn determine_project_root(start: &Utf8Path) -> Result<(Utf8PathBuf, Utf8PathBuf)> {
46✔
40
    let mut cwd = start.to_owned();
46✔
41

42
    loop {
43
        let mut tmp = cwd.clone();
55✔
44
        tmp.push("laze-project.yml");
55✔
45
        if tmp.exists() {
55✔
46
            return Ok((cwd, Utf8PathBuf::from("laze-project.yml")));
45✔
47
        }
10✔
48
        cwd = match cwd.parent() {
10✔
49
            Some(p) => Utf8PathBuf::from(p),
9✔
50
            None => return Err(anyhow!("cannot find laze-project.yml")),
1✔
51
        }
52
    }
53
}
46✔
54

55
fn ninja_run(
37✔
56
    ninja_buildfile: &Utf8Path,
37✔
57
    verbose: bool,
37✔
58
    targets: Option<Vec<Utf8PathBuf>>,
37✔
59
    jobs: Option<usize>,
37✔
60
    keep_going: Option<usize>,
37✔
61
) -> Result<i32, Error> {
37✔
62
    let mut ninja_cmd = NinjaCmdBuilder::default();
37✔
63

64
    ninja_cmd
37✔
65
        .verbose(verbose)
37✔
66
        .build_file(ninja_buildfile)
37✔
67
        .targets(targets);
37✔
68

69
    if let Some(jobs) = jobs {
37✔
70
        ninja_cmd.jobs(jobs);
1✔
71
    }
36✔
72

73
    if let Some(keep_going) = keep_going {
37✔
74
        ninja_cmd.keep_going(keep_going);
31✔
75
    }
31✔
76

77
    let ninja_cmd = ninja_cmd.build().unwrap();
37✔
78
    let ninja_binary = ninja_cmd.binary;
37✔
79
    let ninja_exit = ninja_cmd
37✔
80
        .run()
37✔
81
        .with_context(|| format!("launching ninja binary \"{}\"", ninja_binary))?;
37✔
82

83
    match ninja_exit.code() {
37✔
84
        Some(code) => match code {
37✔
85
            0 => Ok(code),
37✔
86
            _ => Err(anyhow!("ninja exited with code {code}")),
×
87
        },
88
        None => Err(anyhow!("ninja probably killed by signal")),
×
89
    }
90
}
37✔
91

92
fn main() {
46✔
93
    let result = try_main();
46✔
94
    match result {
46✔
95
        Err(e) => {
8✔
96
            if let Some(expr_err) = e.downcast_ref::<evalexpr::EvalexprError>() {
8✔
97
                // make expression errors more readable.
98
                // TODO: factor out
99
                eprintln!("laze: expression error: {expr_err}");
×
100
                eprintln!("laze: the error occured here:");
×
101
                let mut iter = e.chain().peekable();
×
102
                let mut i = 0;
×
103
                while let Some(next) = iter.next() {
×
104
                    if iter.peek().is_none() {
×
105
                        break;
×
106
                    }
×
107
                    eprintln!("{i:>5}: {next}");
×
108
                    i += 1;
×
109
                }
110
            } else {
8✔
111
                eprintln!("laze: error: {e:#}");
8✔
112
            }
8✔
113
            std::process::exit(1);
8✔
114
        }
115
        Ok(code) => std::process::exit(code),
38✔
116
    };
117
}
118

119
pub static EXIT_ON_SIGINT: OnceLock<Arc<AtomicBool>> = OnceLock::new();
120

121
fn try_main() -> Result<i32> {
46✔
122
    EXIT_ON_SIGINT.set(Arc::new(AtomicBool::new(true))).unwrap();
46✔
123
    register_conditional_shutdown(SIGINT, 130, EXIT_ON_SIGINT.get().unwrap().clone()).unwrap();
46✔
124

125
    clap_complete::env::CompleteEnv::with_factory(cli::clap).complete();
46✔
126

127
    let matches = cli::clap().get_matches();
46✔
128

129
    let git_cache_dir = Utf8PathBuf::from(&shellexpand::tilde(
46✔
130
        matches.get_one::<Utf8PathBuf>("git_cache_dir").unwrap(),
46✔
131
    ));
46✔
132

133
    GIT_CACHE
46✔
134
        .set(GitCache::new(git_cache_dir)?)
46✔
135
        .ok()
46✔
136
        .expect("creating git cache directory.");
46✔
137

138
    // handle project independent subcommands here
139
    match matches.subcommand() {
46✔
140
        Some(("new", matches)) => {
46✔
141
            new::from_matches(matches)?;
×
142
            return Ok(0);
×
143
        }
144
        Some(("completion", matches)) => {
46✔
145
            fn print_completions<G: clap_complete::Generator>(
×
146
                generator: G,
×
147
                cmd: &mut clap::Command,
×
148
            ) {
×
149
                clap_complete::generate(
×
150
                    generator,
×
151
                    cmd,
×
152
                    cmd.get_name().to_string(),
×
153
                    &mut std::io::stdout(),
×
154
                );
155
            }
×
156
            if let Some(generator) = matches
×
157
                .get_one::<clap_complete::Shell>("generator")
×
158
                .copied()
×
159
            {
×
160
                let mut cmd = cli::clap();
×
161
                eprintln!("Generating completion file for {}...", generator);
×
162
                print_completions(generator, &mut cmd);
×
163
            }
×
164
            return Ok(0);
×
165
        }
166
        Some(("manpages", matches)) => {
46✔
167
            fn create_manpage(cmd: clap::Command, outfile: &Utf8Path) -> Result<(), Error> {
×
168
                let man = clap_mangen::Man::new(cmd);
×
169
                let mut buffer: Vec<u8> = Default::default();
×
170
                man.render(&mut buffer)?;
×
171

172
                std::fs::write(outfile, buffer)?;
×
173
                Ok(())
×
174
            }
×
175
            let mut outpath: Utf8PathBuf =
×
176
                matches.get_one::<Utf8PathBuf>("outdir").unwrap().clone();
×
177
            let cmd = cli::clap();
×
178

179
            outpath.push("laze.1");
×
180
            create_manpage(cmd.clone(), &outpath)?;
×
181

182
            for subcommand in cmd.get_subcommands() {
×
183
                if subcommand.is_hide_set() {
×
184
                    continue;
×
185
                }
×
186
                let name = subcommand.get_name();
×
187
                outpath.pop();
×
188
                outpath.push(format!("laze-{name}.1"));
×
189
                create_manpage(subcommand.clone(), &outpath)?;
×
190
            }
191

192
            return Ok(0);
×
193
        }
194
        Some(("git-clone", matches)) => {
46✔
195
            let repository = matches.get_one::<String>("repository").unwrap();
×
196
            let target_path = matches.get_one::<Utf8PathBuf>("target_path").cloned();
×
197
            let wanted_commit = matches.get_one::<String>("commit");
×
198
            let sparse_paths = matches
×
199
                .get_many::<String>("sparse-add")
×
200
                .map(|v| v.into_iter().cloned().collect::<Vec<String>>());
×
201

202
            GIT_CACHE
×
203
                .get()
×
204
                .unwrap()
×
205
                .cloner()
×
206
                .commit(wanted_commit.cloned())
×
207
                .extra_clone_args_from_matches(matches)
×
208
                .repository_url(repository.clone())
×
209
                .sparse_paths(sparse_paths)
×
210
                .target_path(target_path)
×
211
                .update(matches.get_flag("update"))
×
212
                .do_clone()?;
×
213

214
            return Ok(0);
×
215
        }
216
        _ => (),
46✔
217
    }
218

219
    if let Some(dir) = matches.get_one::<Utf8PathBuf>("chdir") {
46✔
220
        env::set_current_dir(dir).context(format!("cannot change to directory \"{dir}\""))?;
×
221
    }
46✔
222

223
    let cwd = Utf8PathBuf::try_from(env::current_dir()?).expect("cwd not UTF8");
46✔
224

225
    let (project_root, project_file) = determine_project_root(&cwd)?;
46✔
226
    let start_relpath = pathdiff::diff_utf8_paths(&cwd, &project_root).unwrap();
45✔
227
    let start_relpath = if start_relpath.eq("") {
45✔
228
        ".".into()
45✔
229
    } else {
230
        start_relpath
×
231
    };
232

233
    println!(
45✔
234
        "laze: project root: {project_root} relpath: {start_relpath} project_file: {project_file}",
45✔
235
    );
236

237
    let global = matches.get_flag("global");
45✔
238
    env::set_current_dir(&project_root).context(format!("cannot change to \"{project_root}\""))?;
45✔
239

240
    let verbose = matches.get_count("verbose");
45✔
241

242
    match matches.subcommand() {
45✔
243
        Some(("build", build_matches)) => {
45✔
244
            let build_dir = build_matches.get_one::<Utf8PathBuf>("build-dir").unwrap();
45✔
245

246
            // collect builder names from args
247
            let builders = match build_matches.get_many::<String>("builders") {
45✔
248
                Some(values) => Selector::Some(values.cloned().collect::<IndexSet<String>>()),
1✔
249
                None => Selector::All,
44✔
250
            };
251

252
            // collect app names from args
253
            let apps = match build_matches.get_many::<String>("apps") {
45✔
254
                Some(values) => Selector::Some(values.cloned().collect::<IndexSet<String>>()),
2✔
255
                None => Selector::All,
43✔
256
            };
257

258
            let jobs = build_matches.get_one::<usize>("jobs").copied();
45✔
259
            let keep_going = build_matches.get_one::<usize>("keep_going").copied();
45✔
260

261
            let partitioner = build_matches
45✔
262
                .get_one::<task_partitioner::PartitionerBuilder>("partition")
45✔
263
                .map(|v| v.build());
45✔
264

265
            let info_outfile = build_matches.get_one::<Utf8PathBuf>("info-export");
45✔
266

267
            println!("laze: building {apps} for {builders}");
45✔
268

269
            // collect CLI selected/disabled modules
270
            let select = get_selects(build_matches);
45✔
271
            let disable = get_disables(build_matches);
45✔
272

273
            // collect CLI env overrides
274
            let cli_env = get_cli_vars(build_matches)?;
45✔
275

276
            let mode = match global {
45✔
277
                true => GenerateMode::Global,
41✔
278
                false => GenerateMode::Local(start_relpath.clone()),
4✔
279
            };
280

281
            let generator = GeneratorBuilder::default()
45✔
282
                .project_root(project_root.clone())
45✔
283
                .project_file(project_file)
45✔
284
                .build_dir(build_dir.clone())
45✔
285
                .mode(mode.clone())
45✔
286
                .builders(builders.clone())
45✔
287
                .apps(apps.clone())
45✔
288
                .select(select)
45✔
289
                .disable(disable)
45✔
290
                .cli_env(cli_env)
45✔
291
                .partitioner(partitioner.as_ref().map(|x| format!("{:?}", x)))
45✔
292
                .collect_insights(info_outfile.is_some())
45✔
293
                .disable_cache(info_outfile.is_some())
45✔
294
                .build()
45✔
295
                .unwrap();
45✔
296

297
            // arguments parsed, launch generation of ninja file(s)
298
            let builds = generator.execute(partitioner, verbose > 1)?;
45✔
299

300
            if let Some(info_outfile) = info_outfile {
38✔
301
                use std::fs::File;
302
                use std::io::BufWriter;
303
                let info_outfile = start_relpath.join(info_outfile);
1✔
304
                let insights = insights::Insights::from_builds(&builds.build_infos);
1✔
305
                let buffer =
1✔
306
                    BufWriter::new(File::create(&info_outfile).with_context(|| {
1✔
307
                        format!("creating info export file \"{info_outfile}\"")
×
308
                    })?);
×
309
                serde_json::to_writer_pretty(buffer, &insights)
1✔
310
                    .with_context(|| "exporting build info".to_string())?;
1✔
311
            }
37✔
312

313
            let ninja_build_file = get_ninja_build_file(build_dir, &mode);
38✔
314

315
            if build_matches.get_flag("compile-commands") {
38✔
316
                let mut compile_commands = project_root.clone();
1✔
317
                compile_commands.push("compile_commands.json");
1✔
318
                println!("laze: generating {compile_commands}");
1✔
319
                ninja::generate_compile_commands(&ninja_build_file, &compile_commands)?;
1✔
320
            }
37✔
321

322
            // collect (optional) task and it's arguments
323
            let task = collect_tasks(build_matches);
38✔
324

325
            // generation of ninja build file complete.
326
            // exit here if requested.
327
            if task.is_none() && build_matches.get_flag("generate-only") {
38✔
328
                return Ok(0);
1✔
329
            }
37✔
330

331
            if let Some((task, args)) = task {
37✔
332
                let builds: Vec<&BuildInfo> = builds
6✔
333
                    .build_infos
6✔
334
                    .iter()
6✔
335
                    .filter(|build_info| {
6✔
336
                        builders.selects(&build_info.builder)
6✔
337
                            && apps.selects(&build_info.binary)
6✔
338
                            && build_info.tasks.contains_key(task)
6✔
339
                    })
6✔
340
                    .collect();
6✔
341

342
                if !builds
6✔
343
                    .iter()
6✔
344
                    .any(|build_info| build_info.tasks.iter().any(|t| t.1.is_ok() && t.0 == task))
14✔
345
                {
346
                    let mut not_available = 0;
×
347
                    for b in builds {
×
348
                        for t in &b.tasks {
×
349
                            if t.1.is_err() && t.0 == task {
×
350
                                not_available += 1;
×
351
                                if verbose > 0 {
×
352
                                    eprintln!(
×
353
                                    "laze: warn: task \"{task}\" for binary \"{}\" on builder \"{}\": {}",
×
354
                                    b.binary,
×
355
                                    b.builder,
×
356
                                    t.1.as_ref().err().unwrap()
×
357
                                );
×
358
                                }
×
359
                            }
×
360
                        }
361
                    }
362

363
                    if not_available > 0 && verbose == 0 {
×
364
                        println!("laze hint: {not_available} target(s) not available, try `--verbose` to list why");
×
365
                    }
×
366
                    return Err(anyhow!("no matching target for task \"{}\" found.", task));
×
367
                }
6✔
368

369
                let multiple = build_matches.get_flag("multiple");
6✔
370

371
                if builds.len() > 1 && !multiple {
6✔
372
                    println!("laze: multiple task targets found:");
×
373
                    for build_info in builds {
×
374
                        eprintln!("{} {}", build_info.builder, build_info.binary);
×
375
                    }
×
376

377
                    // TODO: allow running tasks for multiple targets
378
                    return Err(anyhow!(
×
379
                        "please specify one of these builders, or -m/--multiple-tasks."
×
380
                    ));
×
381
                }
6✔
382

383
                let task_name = task;
6✔
384
                let mut targets = Vec::new();
6✔
385
                let mut ninja_targets = Vec::new();
6✔
386

387
                for build in builds {
12✔
388
                    let task = build.tasks.get(task).unwrap();
6✔
389
                    if let Ok(task) = task {
6✔
390
                        if task.build_app() {
6✔
391
                            let build_target = build.out.clone();
6✔
392
                            ninja_targets.push(build_target);
6✔
393
                        }
6✔
394
                        targets.push((build, task));
6✔
395
                    }
×
396
                }
397

398
                if !ninja_targets.is_empty() && !build_matches.get_flag("generate-only") {
6✔
399
                    let ninja_build_file = get_ninja_build_file(build_dir, &mode);
6✔
400
                    if ninja_run(
6✔
401
                        ninja_build_file.as_path(),
6✔
402
                        verbose > 0,
6✔
403
                        Some(ninja_targets),
6✔
404
                        jobs,
6✔
405
                        None, // have to fail on build error b/c no way of knowing *which* target
6✔
406
                              // failed
407
                    )? != 0
×
408
                    {
409
                        return Err(anyhow!("build error"));
×
410
                    };
6✔
411
                }
×
412

413
                let (results, errors) = task_runner::run_tasks(
6✔
414
                    task_name,
6✔
415
                    targets.iter(),
6✔
416
                    args.as_ref(),
6✔
417
                    verbose,
6✔
418
                    keep_going.unwrap(),
6✔
419
                    project_root.as_std_path(),
6✔
420
                )?;
×
421

422
                if errors > 0 {
6✔
423
                    if multiple {
×
424
                        // multiple tasks, more than zero errors. print them
425
                        println!("laze: the following tasks failed:");
×
426
                        for result in results.iter().filter(|r| r.result.is_err()) {
×
427
                            println!(
×
428
                                "laze: task \"{task_name}\" on app \"{}\" for builder \"{}\"",
×
429
                                result.build.binary, result.build.builder
×
430
                            );
×
431
                        }
×
432
                    } else {
433
                        // only one error. can't move out of first, cant clone, so print that here.
NEW
434
                        let (first, _rest) = results.split_first().unwrap();
×
NEW
435
                        if let Err(e) = &first.result {
×
NEW
436
                            eprintln!("laze: error: {e:#}");
×
NEW
437
                        }
×
438
                    }
439
                    return Ok(1);
×
440
                }
6✔
441
            } else {
442
                // build ninja target arguments, if necessary
443
                let targets: Option<Vec<Utf8PathBuf>> = if let Selector::All = builders {
31✔
444
                    if let Selector::All = apps {
31✔
445
                        None
31✔
446
                    } else {
447
                        // TODO: filter by app
448
                        None
×
449
                    }
450
                } else {
451
                    Some(
452
                        builds
×
453
                            .build_infos
×
454
                            .iter()
×
455
                            .filter_map(|build_info| {
×
456
                                (builders.selects(&build_info.builder)
×
457
                                    && apps.selects(&build_info.binary))
×
458
                                .then_some(build_info.out.clone())
×
459
                            })
×
460
                            .collect(),
×
461
                    )
462
                };
463

464
                ninja_run(
31✔
465
                    ninja_build_file.as_path(),
31✔
466
                    verbose > 0,
31✔
467
                    targets,
31✔
468
                    jobs,
31✔
469
                    keep_going,
31✔
470
                )?;
×
471
            }
472
        }
473
        Some(("clean", clean_matches)) => {
×
474
            let unused = clean_matches.get_flag("unused");
×
475
            let build_dir = clean_matches.get_one::<Utf8PathBuf>("build-dir").unwrap();
×
476
            let mode = match global {
×
477
                true => GenerateMode::Global,
×
478
                false => GenerateMode::Local(start_relpath),
×
479
            };
480
            let ninja_build_file = get_ninja_build_file(build_dir, &mode);
×
481
            let tool = match unused {
×
482
                true => "cleandead",
×
483
                false => "clean",
×
484
            };
485
            let clean_target: Option<Vec<Utf8PathBuf>> = Some(vec!["-t".into(), tool.into()]);
×
486
            ninja_run(
×
487
                ninja_build_file.as_path(),
×
488
                verbose > 0,
×
489
                clean_target,
×
490
                None,
×
491
                None,
×
492
            )?;
×
493
        }
494
        _ => {}
×
495
    };
496

497
    Ok(0)
37✔
498
}
46✔
499

500
fn collect_tasks(task_matches: &clap::ArgMatches) -> Option<(&str, Option<Vec<&str>>)> {
38✔
501
    match task_matches.subcommand() {
38✔
502
        Some((name, matches)) => {
6✔
503
            let args = matches
6✔
504
                .get_many::<std::ffi::OsString>("")
6✔
505
                .into_iter()
6✔
506
                .flatten()
6✔
507
                .map(|v| v.as_os_str().to_str().expect("task arg is invalid UTF8"))
6✔
508
                .collect::<Vec<_>>();
6✔
509
            Some((name, Some(args)))
6✔
510
        }
511
        _ => None,
32✔
512
    }
513
}
38✔
514

515
fn get_cli_vars(build_matches: &clap::ArgMatches) -> Result<Option<Env>, Error> {
45✔
516
    let cli_env = if let Some(entries) = build_matches.get_many::<String>("define") {
45✔
517
        let mut env = Env::new();
2✔
518

519
        for assignment in entries {
7✔
520
            env.assign_from_string(assignment)?;
5✔
521
        }
522

523
        Some(env)
2✔
524
    } else {
525
        None
43✔
526
    };
527
    Ok(cli_env)
45✔
528
}
45✔
529

530
fn get_disables(build_matches: &clap::ArgMatches) -> Option<Vec<String>> {
45✔
531
    let disable = build_matches
45✔
532
        .get_many::<String>("disable")
45✔
533
        .map(|vr| vr.cloned().collect_vec());
45✔
534
    disable
45✔
535
}
45✔
536

537
fn get_selects(build_matches: &clap::ArgMatches) -> Option<Vec<Dependency<String>>> {
45✔
538
    let select = build_matches.get_many::<String>("select");
45✔
539
    // convert CLI --select strings to Vec<Dependency>
540
    select.map(|vr| vr.map(crate::data::dependency_from_string).collect_vec())
45✔
541
}
45✔
542

543
#[cfg(test)]
544
mod test {
545
    #[test]
546
    fn test_clap() {
547
        crate::cli::clap().debug_assert();
548
    }
549
}
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