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

kaspar030 / laze / 19943262596

04 Dec 2025 08:41PM UTC coverage: 80.01% (-0.2%) from 80.256%
19943262596

push

github

web-flow
feat: introduce `requires` (#820)

75 of 116 new or added lines in 6 files covered. (64.66%)

1 existing line in 1 file now uncovered.

3366 of 4207 relevant lines covered (80.01%)

97.54 hits per line

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

92.59
/src/data.rs
1
//! This module deals with converting laze .yml files into the format that
2
//! the generate module needs.
3
//!
4
//! This is intentionally separate from the main generate types in order to be a
5
//! bit more flexible on changes to the format.
6

7
use indexmap::{IndexMap, IndexSet};
8
use itertools::Itertools;
9
use serde_yaml::Value;
10
use std::collections::{HashMap, HashSet};
11
use std::fs::read_to_string;
12
use std::time::{Duration, Instant};
13

14
use anyhow::{Context as _, Error, Result};
15
use camino::{Utf8Path, Utf8PathBuf};
16
use semver::Version;
17
use serde::{Deserialize, Deserializer, Serialize};
18

19
use treestate::{FileState, TreeState};
20

21
use super::download::Download;
22
use super::model::CustomBuild;
23
use super::nested_env::{Env, EnvKey, MergeOption};
24
use super::{Context, ContextBag, Dependency, Module, Rule, Task};
25
use crate::serde_bool_helpers::{default_as_false, default_as_true};
26
use crate::utils::{StringOrMapString, StringOrMapVecString};
27

28
mod import;
29
use import::ImportEntry;
30

31
pub type FileTreeState = TreeState<FileState, std::path::PathBuf>;
32

33
pub struct LoadStats {
34
    pub files: usize,
35
    pub parsing_time: Duration,
36
    pub stat_time: Duration,
37
}
38

39
// Any value that is present is considered Some value, including null.
40
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
65✔
41
where
65✔
42
    T: Deserialize<'de>,
65✔
43
    D: Deserializer<'de>,
65✔
44
{
45
    Deserialize::deserialize(deserializer).map(Some)
65✔
46
}
65✔
47

48
fn default_none<T>() -> Option<T> {
2✔
49
    None
2✔
50
}
2✔
51

52
fn deserialize_version_checked<'de, D>(deserializer: D) -> Result<Option<Version>, D::Error>
1✔
53
where
1✔
54
    //    T: Deserialize<'de>,
1✔
55
    D: Deserializer<'de>,
1✔
56
{
57
    use serde::de;
58

59
    let version: Option<String> = Deserialize::deserialize(deserializer)?;
1✔
60
    if let Some(version) = &version {
1✔
61
        if let Ok(version) = Version::parse(version) {
1✔
62
            let my_version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
1✔
63
            if version > my_version {
1✔
64
                return Err(de::Error::custom(format!(
1✔
65
                    "laze_required_version >= {version}, but this is laze {my_version}"
1✔
66
                )));
1✔
67
            }
×
68
            Ok(Some(version))
×
69
        } else {
70
            Err(de::Error::custom(format!(
×
71
                "error parsing \"{version}\" as semver version string"
×
72
            )))
×
73
        }
74
    } else {
75
        Ok(None)
×
76
    }
77
}
1✔
78

79
#[derive(Debug, Serialize, Deserialize)]
80
#[serde(deny_unknown_fields)]
81
struct YamlFile {
82
    contexts: Option<Vec<YamlContext>>,
83
    builders: Option<Vec<YamlContext>>,
84
    #[serde(default, deserialize_with = "deserialize_some")]
85
    modules: Option<Option<Vec<YamlModule>>>,
86
    #[serde(default, deserialize_with = "deserialize_some")]
87
    apps: Option<Option<Vec<YamlModule>>>,
88
    imports: Option<Vec<ImportEntry>>,
89
    includes: Option<Vec<String>>,
90
    subdirs: Option<Vec<String>>,
91
    defaults: Option<HashMap<String, YamlModule>>,
92
    #[serde(default, deserialize_with = "deserialize_version_checked")]
93
    laze_required_version: Option<Version>,
94
    #[serde(skip)]
95
    filename: Option<Utf8PathBuf>,
96
    #[serde(skip)]
97
    doc_idx: Option<usize>,
98
    #[serde(skip)]
99
    included_by: Option<usize>,
100
    #[serde(skip)]
101
    import_root: Option<ImportRoot>,
102
    #[serde(rename = "meta")]
103
    _meta: Option<Value>,
104
}
105

106
fn check_module_name<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
113✔
107
where
113✔
108
    D: Deserializer<'de>,
113✔
109
{
110
    let v = Option::<String>::deserialize(deserializer)?;
113✔
111

112
    if let Some(v) = v.as_ref() {
113✔
113
        if v.starts_with("context::") {
113✔
114
            return Err(serde::de::Error::invalid_value(
×
115
                serde::de::Unexpected::Str(v),
×
116
                &"a string not starting with \"context::\"",
×
117
            ));
×
118
        }
113✔
119
    }
×
120

121
    Ok(v)
113✔
122
}
113✔
123

124
#[derive(Debug, Serialize, Deserialize)]
125
#[serde(deny_unknown_fields)]
126
struct YamlContext {
127
    name: String,
128
    parent: Option<String>,
129
    help: Option<String>,
130
    env: Option<Env>,
131
    selects: Option<Vec<String>>,
132
    disables: Option<Vec<String>>,
133
    provides: Option<Vec<String>>,
134
    provides_unique: Option<Vec<String>>,
135
    requires: Option<Vec<String>>,
136
    rules: Option<Vec<YamlRule>>,
137
    var_options: Option<im::HashMap<String, MergeOption>>,
138
    tasks: Option<HashMap<String, YamlTask>>,
139
    #[serde(default = "default_as_false", alias = "buildable")]
140
    is_builder: bool,
141
    #[serde(rename = "meta")]
142
    _meta: Option<Value>,
143
}
144

145
#[derive(Debug, Serialize, Deserialize)]
146
#[serde(untagged)]
147
enum StringOrVecString {
148
    Single(String),
149
    List(Vec<String>),
150
}
151

152
#[derive(Default, Debug, Serialize, Deserialize)]
153
#[serde(deny_unknown_fields)]
154
struct YamlModule {
155
    #[serde(default = "default_none", deserialize_with = "check_module_name")]
156
    name: Option<String>,
157
    context: Option<StringOrVecString>,
158
    help: Option<String>,
159
    depends: Option<Vec<StringOrMapVecString>>,
160
    selects: Option<Vec<StringOrMapVecString>>,
161
    uses: Option<Vec<String>>,
162
    provides: Option<Vec<String>>,
163
    provides_unique: Option<Vec<String>>,
164
    #[serde(alias = "disables")]
165
    conflicts: Option<Vec<String>>,
166
    requires: Option<Vec<String>>,
167
    #[serde(default = "default_as_false")]
168
    notify_all: bool,
169
    sources: Option<Vec<StringOrMapVecString>>,
170
    tasks: Option<HashMap<String, YamlTask>>,
171
    build: Option<CustomBuild>,
172
    env: Option<YamlModuleEnv>,
173
    blocklist: Option<Vec<String>>,
174
    allowlist: Option<Vec<String>>,
175
    download: Option<Download>,
176
    srcdir: Option<Utf8PathBuf>,
177
    #[serde(default = "default_as_false")]
178
    is_build_dep: bool,
179
    #[serde(default = "default_as_false")]
180
    is_global_build_dep: bool,
181
    #[serde(skip)]
182
    _is_binary: bool,
183
    #[serde(rename = "meta")]
184
    _meta: Option<Value>,
185
}
186

187
impl YamlModule {
188
    fn default_binary() -> YamlModule {
2✔
189
        YamlModule {
2✔
190
            _is_binary: true,
2✔
191
            ..Self::default()
2✔
192
        }
2✔
193
    }
2✔
194

195
    fn get_contexts(&self) -> Vec<Option<&String>> {
115✔
196
        if let Some(contexts) = &self.context {
115✔
197
            match contexts {
14✔
198
                StringOrVecString::Single(single) => vec![Some(single)],
13✔
199
                StringOrVecString::List(list) => list.iter().map(Some).collect_vec(),
1✔
200
            }
201
        } else {
202
            vec![None]
101✔
203
        }
204
    }
115✔
205
}
206

207
#[derive(Debug, Serialize, Deserialize)]
208
#[serde(deny_unknown_fields)]
209
struct YamlModuleEnv {
210
    local: Option<Env>,
211
    export: Option<Env>,
212
    global: Option<Env>,
213
}
214

215
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
216
#[serde(deny_unknown_fields)]
217
pub struct YamlRule {
218
    pub name: String,
219
    pub cmd: String,
220

221
    pub help: Option<String>,
222

223
    #[serde(rename = "in")]
224
    pub in_: Option<String>,
225
    pub out: Option<String>,
226
    pub context: Option<String>,
227
    pub options: Option<HashMap<String, String>>,
228
    pub gcc_deps: Option<String>,
229
    pub rspfile: Option<String>,
230
    pub rspfile_content: Option<String>,
231
    pub pool: Option<String>,
232
    pub description: Option<String>,
233
    pub export: Option<Vec<StringOrMapString>>,
234

235
    #[serde(default = "default_as_false")]
236
    pub always: bool,
237

238
    #[serde(default = "default_as_true", alias = "sharable")]
239
    pub shareable: bool,
240

241
    #[serde(rename = "meta")]
242
    _meta: Option<Value>,
243
}
244

245
impl From<YamlRule> for Rule {
246
    //TODO: use deserialize_with as only the export field needs special handling
247
    fn from(yaml_rule: YamlRule) -> Self {
78✔
248
        Rule {
249
            always: yaml_rule.always,
78✔
250
            cmd: yaml_rule.cmd,
78✔
251
            context: yaml_rule.context,
78✔
252

253
            name: yaml_rule.name,
78✔
254
            help: yaml_rule.help,
78✔
255
            in_: yaml_rule.in_,
78✔
256
            out: yaml_rule.out,
78✔
257
            options: yaml_rule.options,
78✔
258
            gcc_deps: yaml_rule.gcc_deps,
78✔
259
            rspfile: yaml_rule.rspfile,
78✔
260
            rspfile_content: yaml_rule.rspfile_content,
78✔
261
            shareable: yaml_rule.shareable,
78✔
262
            pool: yaml_rule.pool,
78✔
263
            description: yaml_rule.description,
78✔
264
            export: yaml_rule
78✔
265
                .export
78✔
266
                .map(|s| s.iter().map(|s| s.clone().into()).collect_vec()),
78✔
267
        }
268
    }
78✔
269
}
270

271
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
272
#[serde(deny_unknown_fields)]
273
pub struct YamlTask {
274
    pub cmd: Vec<String>,
275
    pub help: Option<String>,
276
    pub required_vars: Option<Vec<String>>,
277
    pub required_modules: Option<Vec<String>>,
278
    pub export: Option<Vec<StringOrMapString>>,
279
    #[serde(default = "default_as_true")]
280
    pub build: bool,
281
    #[serde(default = "default_as_false")]
282
    pub ignore_ctrl_c: bool,
283
    pub workdir: Option<String>,
284
    #[serde(rename = "meta")]
285
    _meta: Option<Value>,
286
}
287

288
impl From<YamlTask> for Task {
289
    fn from(yaml_task: YamlTask) -> Self {
22✔
290
        Task {
291
            cmd: yaml_task.cmd,
22✔
292
            help: yaml_task.help,
22✔
293
            required_vars: yaml_task.required_vars,
22✔
294
            required_modules: yaml_task.required_modules,
22✔
295
            export: yaml_task
22✔
296
                .export
22✔
297
                .map(|s| s.iter().map(|s| s.clone().into()).collect()),
22✔
298
            build: yaml_task.build,
22✔
299
            ignore_ctrl_c: yaml_task.ignore_ctrl_c,
22✔
300
            workdir: yaml_task.workdir,
22✔
301
        }
302
    }
22✔
303
}
304

305
// fn load_one<'a>(filename: &Utf8PathBuf) -> Result<YamlFile> {
306
//     let file = read_to_string(filename).unwrap();
307
//     let docs: Vec<&str> = file.split("\n---\n").collect();
308
//     let mut data: YamlFile = serde_yaml::from_str(&docs[0])
309
//         .with_context(|| format!("while parsing {}", filename.display()))?;
310

311
//     data.filename = Some(filename.clone());
312

313
//     Ok(data)
314
// }
315

316
fn process_removes(strings: &mut Vec<Dependency<String>>) {
236✔
317
    let removals = strings
236✔
318
        .iter()
236✔
319
        .filter(|x| x.get_name().starts_with('-'))
236✔
320
        .map(|x| x.get_name()[1..].to_string())
236✔
321
        .collect::<HashSet<_>>();
236✔
322

323
    strings.retain(|x| !(x.get_name().starts_with('-') || removals.contains(&x.get_name()[..])));
236✔
324
}
236✔
325

326
pub fn dependency_from_string(dep_name: &String) -> Dependency<String> {
107✔
327
    match dep_name.as_bytes()[0] {
107✔
328
        b'?' => Dependency::Soft(dep_name[1..].to_string()),
9✔
329
        _ => Dependency::Hard(dep_name.clone()),
98✔
330
    }
331
}
107✔
332

333
pub fn dependency_from_string_if(dep_name: &String, other: &str) -> Dependency<String> {
8✔
334
    match dep_name.as_bytes()[0] {
8✔
335
        b'?' => Dependency::IfThenSoft(other.to_string(), dep_name[1..].to_string()),
×
336
        _ => Dependency::IfThenHard(other.to_string(), dep_name.clone()),
8✔
337
    }
338
}
8✔
339

340
fn load_all(file_include: &FileInclude, index_start: usize) -> Result<Vec<YamlFile>> {
57✔
341
    let filename = &file_include.filename;
57✔
342
    let file = read_to_string(filename).with_context(|| format!("{:?}", filename))?;
57✔
343

344
    let mut result = Vec::new();
57✔
345
    for (n, doc) in serde_yaml::Deserializer::from_str(&file).enumerate() {
59✔
346
        let mut parsed = YamlFile::deserialize(doc).with_context(|| filename.clone())?;
59✔
347
        parsed.filename = Some(filename.clone());
58✔
348
        parsed.doc_idx = Some(index_start + n);
58✔
349
        parsed.included_by = file_include.included_by_doc_idx;
58✔
350
        parsed.import_root.clone_from(&file_include.import_root);
58✔
351
        result.push(parsed);
58✔
352
    }
353

354
    Ok(result)
56✔
355
}
57✔
356

357
#[derive(Hash, Debug, PartialEq, Eq, Clone)]
358
struct ImportRoot(Utf8PathBuf);
359
impl ImportRoot {
360
    fn path(&self) -> &Utf8Path {
7✔
361
        self.0.as_path()
7✔
362
    }
7✔
363
}
364

365
#[derive(Hash, Debug, PartialEq, Eq)]
366
struct FileInclude {
367
    filename: Utf8PathBuf,
368
    included_by_doc_idx: Option<usize>,
369
    import_root: Option<ImportRoot>,
370
}
371

372
impl FileInclude {
373
    fn new(
51✔
374
        filename: Utf8PathBuf,
51✔
375
        included_by_doc_idx: Option<usize>,
51✔
376
        import_root: Option<ImportRoot>,
51✔
377
    ) -> Self {
51✔
378
        FileInclude {
51✔
379
            filename,
51✔
380
            included_by_doc_idx,
51✔
381
            import_root,
51✔
382
        }
51✔
383
    }
51✔
384

385
    fn new_import(filename: Utf8PathBuf, included_by_doc_idx: Option<usize>) -> Self {
6✔
386
        // TODO: (opt) Cow import_root?
387
        let import_root = Some(ImportRoot(Utf8PathBuf::from(
6✔
388
            filename.parent().as_ref().unwrap(),
6✔
389
        )));
6✔
390
        FileInclude {
6✔
391
            filename,
6✔
392
            included_by_doc_idx,
6✔
393
            import_root,
6✔
394
        }
6✔
395
    }
6✔
396
}
397

398
pub fn load(
44✔
399
    filename: &Utf8Path,
44✔
400
    build_dir: &Utf8Path,
44✔
401
) -> Result<(ContextBag, FileTreeState, LoadStats)> {
44✔
402
    let mut contexts = ContextBag::new();
44✔
403
    let start = Instant::now();
44✔
404

405
    // yaml_datas holds all parsed yaml data
406
    let mut yaml_datas = Vec::new();
44✔
407

408
    // filenames contains all filenames so far included.
409
    // when reading files, any "subdir" will be converted to "subdir/laze.yml", then added to the
410
    // set.
411
    // using an IndexSet so files can only be added once
412
    let mut filenames: IndexSet<FileInclude> = IndexSet::new();
44✔
413
    let main_file = Utf8PathBuf::from(filename);
44✔
414

415
    let mut local_file = main_file.clone();
44✔
416
    local_file.set_file_name("laze-local.yml");
44✔
417

418
    filenames.insert(FileInclude::new(main_file, None, None));
44✔
419

420
    if local_file.is_file() {
44✔
421
        filenames.insert(FileInclude::new(local_file, None, None));
×
422
    }
44✔
423

424
    let mut filenames_pos = 0;
44✔
425
    while filenames_pos < filenames.len() {
100✔
426
        let include = filenames.get_index(filenames_pos).unwrap();
57✔
427
        let filename = include.filename.clone();
57✔
428
        let new_index_start = yaml_datas.len();
57✔
429

430
        // load all yaml documents from filename, append to yaml_datas
431
        yaml_datas.append(&mut load_all(include, new_index_start)?);
57✔
432
        filenames_pos += 1;
56✔
433

434
        let new_index_end = yaml_datas.len();
56✔
435

436
        // iterate over newly added documents
437
        for new in yaml_datas[new_index_start..new_index_end].iter() {
58✔
438
            if let Some(subdirs) = &new.subdirs {
58✔
439
                let relpath = filename.parent().unwrap().to_path_buf();
6✔
440

441
                // collect subdirs, add do filenames list
442
                for subdir in subdirs {
6✔
443
                    let sub_file = Utf8Path::new(&relpath).join(subdir).join("laze.yml");
6✔
444
                    filenames.insert(FileInclude::new(
6✔
445
                        sub_file,
6✔
446
                        new.doc_idx,
6✔
447
                        new.import_root.clone(),
6✔
448
                    ));
6✔
449
                }
6✔
450
            }
52✔
451
            if let Some(imports) = &new.imports {
58✔
452
                for import in imports {
6✔
453
                    // TODO: `import.handle()` does the actual git checkout (or whatever
454
                    // import action), so probably better handling of any errors is
455
                    // in order.
456
                    filenames.insert(FileInclude::new_import(
6✔
457
                        import.handle(build_dir)?,
6✔
458
                        new.doc_idx,
6✔
459
                    ));
460
                }
461
            }
54✔
462
            if let Some(includes) = &new.includes {
58✔
463
                let relpath = filename.parent().unwrap().to_path_buf();
1✔
464
                for filename in includes {
1✔
465
                    let filepath = Utf8Path::new(&relpath).join(filename);
1✔
466
                    filenames.insert(FileInclude::new(
1✔
467
                        filepath,
1✔
468
                        new.doc_idx,
1✔
469
                        new.import_root.clone(),
1✔
470
                    ));
1✔
471
                }
1✔
472
            }
57✔
473
        }
474
    }
475

476
    fn convert_context(
62✔
477
        context: &YamlContext,
62✔
478
        contexts: &mut ContextBag,
62✔
479
        is_builder: bool,
62✔
480
        filename: &Utf8PathBuf,
62✔
481
        import_root: &Option<ImportRoot>,
62✔
482
    ) -> Result<Module, Error> {
62✔
483
        let context_name = &context.name;
62✔
484
        let context_parent = match &context.parent {
62✔
485
            Some(x) => x.clone(),
10✔
486
            None => "default".to_string(),
52✔
487
        };
488

489
        let is_default = context_name.as_str() == "default";
62✔
490

491
        // println!(
492
        //     "{} {} parent {}",
493
        //     match is_builder {
494
        //         true => "builder",
495
        //         false => "context",
496
        //     },
497
        //     context_name,
498
        //     context_parent,
499
        // );
500
        let context_ = contexts
62✔
501
            .add_context_or_builder(
62✔
502
                Context::new(
62✔
503
                    context_name.clone(),
62✔
504
                    if is_default {
62✔
505
                        None
17✔
506
                    } else {
507
                        Some(context_parent.clone())
45✔
508
                    },
509
                ),
510
                is_builder,
62✔
511
            )
512
            .with_context(|| format!("{:?}: adding context \"{}\"", &filename, &context_name))?;
62✔
513

514
        context_.help.clone_from(&context.help);
61✔
515
        context_.env.clone_from(&context.env);
61✔
516
        if let Some(rules) = &context.rules {
61✔
517
            context_.rules = Some(IndexMap::new());
38✔
518
            for rule in rules {
78✔
519
                let mut rule: Rule = rule.clone().into();
78✔
520
                rule.context = Some(context_name.clone());
78✔
521
                context_
78✔
522
                    .rules
78✔
523
                    .as_mut()
78✔
524
                    .unwrap()
78✔
525
                    .insert(rule.name.clone(), rule);
78✔
526
            }
78✔
527
        }
23✔
528
        context_.var_options.clone_from(&context.var_options);
61✔
529
        // populate "early env"
530
        let relpath = {
61✔
531
            let relpath = filename.parent().unwrap().as_str();
61✔
532
            if relpath.is_empty() {
61✔
533
                ".".to_string()
58✔
534
            } else {
535
                relpath.to_string()
3✔
536
            }
537
        };
538

539
        context_
61✔
540
            .env_early
61✔
541
            .insert("relpath".into(), EnvKey::Single(relpath));
61✔
542

543
        context_.env_early.insert(
61✔
544
            "root".into(),
61✔
545
            EnvKey::Single(match import_root {
61✔
546
                Some(import_root) => import_root.path().to_string(),
3✔
547
                None => ".".into(),
58✔
548
            }),
549
        );
550

551
        if let Some(tasks) = &context.tasks {
61✔
552
            context_.tasks = Some(
6✔
553
                convert_tasks(tasks, &context_.env_early)
6✔
554
                    .with_context(|| format!("{:?} context \"{}\"", &filename, context.name))?,
6✔
555
            )
556
        }
55✔
557

558
        context_.apply_early_env()?;
61✔
559

560
        context_.defined_in = Some(filename.clone());
61✔
561

562
        // TODO(context-early-disables)
563
        context_.disable.clone_from(&context.disables);
61✔
564

565
        // Each Context has an associated module.
566
        // This holds:
567
        // - selects
568
        // - disables
569
        // - provides
570
        // - provides_unique
571
        // TODO:
572
        // - env (in global env)
573
        // - rules
574
        // - tasks
575
        let module_name = Some(context_.module_name());
61✔
576
        let mut module = init_module(
61✔
577
            &module_name,
61✔
578
            Some(context_name),
61✔
579
            false,
580
            filename,
61✔
581
            import_root,
61✔
582
            None,
61✔
583
        );
584

585
        // collect context level "select:"
586
        if let Some(selects) = &context.selects {
61✔
587
            for dep_name in selects {
2✔
588
                // println!("- {}", dep_name);
2✔
589
                module.selects.push(dependency_from_string(dep_name));
2✔
590
            }
2✔
591
        }
60✔
592

593
        if let Some(disables) = context.disables.as_ref() {
61✔
594
            module.conflicts = Some(disables.clone());
2✔
595
        }
59✔
596

597
        if let Some(provides) = context.provides.as_ref() {
61✔
598
            module.provides = Some(provides.clone());
2✔
599
        }
59✔
600

601
        if let Some(requires) = context.requires.as_ref() {
61✔
NEW
602
            module.requires = Some(requires.clone());
×
603
        }
61✔
604

605
        if let Some(provides_unique) = context.provides_unique.as_ref() {
61✔
606
            if let Some(provides) = module.provides.as_mut() {
4✔
607
                provides.extend(provides_unique.iter().cloned());
×
608
            } else {
4✔
609
                module.provides = Some(provides_unique.clone());
4✔
610
            }
4✔
611
            if let Some(conflicts) = module.conflicts.as_mut() {
4✔
612
                conflicts.extend(provides_unique.iter().cloned());
×
613
            } else {
4✔
614
                module.conflicts = Some(provides_unique.clone());
4✔
615
            }
4✔
616
        }
57✔
617

618
        // make context module depend on its parent's context module
619
        if !is_default {
61✔
620
            module
44✔
621
                .selects
44✔
622
                .push(Dependency::Hard(Context::module_name_for(&context_parent)));
44✔
623
        }
44✔
624

625
        Ok(module)
61✔
626
    }
62✔
627

628
    fn init_module(
179✔
629
        name: &Option<String>,
179✔
630
        context: Option<&String>,
179✔
631
        is_binary: bool,
179✔
632
        filename: &Utf8Path,
179✔
633
        import_root: &Option<ImportRoot>,
179✔
634
        defaults: Option<&Module>,
179✔
635
    ) -> Module {
179✔
636
        let relpath = filename.parent().unwrap();
179✔
637

638
        let name = match name {
179✔
639
            Some(name) => name.clone(),
175✔
640
            None => if let Some(import_root) = import_root {
4✔
641
                filename
×
642
                    .parent()
×
643
                    .unwrap()
×
644
                    .strip_prefix(import_root.path())
×
645
                    .unwrap()
×
646
            } else {
647
                relpath
4✔
648
            }
649
            .to_string(),
4✔
650
        };
651

652
        let mut module = match defaults {
179✔
653
            Some(defaults) => Module::from(defaults, name, context.cloned()),
5✔
654
            None => Module::new(name, context.cloned()),
174✔
655
        };
656

657
        module.is_binary = is_binary;
179✔
658
        module.defined_in = Some(filename.to_path_buf());
179✔
659
        module.relpath = Some(if relpath.eq("") {
179✔
660
            Utf8PathBuf::from(".")
164✔
661
        } else {
662
            Utf8PathBuf::from(&relpath)
15✔
663
        });
664

665
        module
179✔
666
    }
179✔
667

668
    fn convert_module(
118✔
669
        module: &YamlModule,
118✔
670
        context: Option<&String>,
118✔
671
        is_binary: bool,
118✔
672
        filename: &Utf8Path,
118✔
673
        import_root: &Option<ImportRoot>,
118✔
674
        defaults: Option<&Module>,
118✔
675
        build_dir: &Utf8Path,
118✔
676
    ) -> Result<Module, Error> {
118✔
677
        let mut m = init_module(
118✔
678
            &module.name,
118✔
679
            context,
118✔
680
            is_binary,
118✔
681
            filename,
118✔
682
            import_root,
118✔
683
            defaults,
118✔
684
        );
685

686
        m.help.clone_from(&module.help);
118✔
687

688
        // convert module dependencies
689
        // "selects" means "module will be part of the build"
690
        // "uses" means "if module is part of the build, transitively import its exported env vars"
691
        // "depends" means both select and use a module
692
        // a build configuration fails if a selected or depended on module is not
693
        // available.
694
        // "conflicts" will make any module with the specified name unavailable
695
        // in the binary's build context, if the module specifying a conflict
696
        // is part of the dependency tree
697
        //
698
        if let Some(selects) = &module.selects {
118✔
699
            // println!("selects:");
700
            for dep_spec in selects {
21✔
701
                match dep_spec {
21✔
702
                    StringOrMapVecString::String(dep_name) => {
21✔
703
                        m.selects.push(dependency_from_string(dep_name));
21✔
704
                    }
21✔
705
                    StringOrMapVecString::Map(dep_map) => {
×
706
                        for (k, v) in dep_map {
×
707
                            for dep_name in v {
×
708
                                m.selects.push(dependency_from_string_if(dep_name, k));
×
709
                            }
×
710
                        }
711
                    }
712
                }
713
            }
714
        }
103✔
715
        if let Some(uses) = &module.uses {
118✔
716
            // println!("uses:");
717
            for dep_name in uses {
8✔
718
                // println!("- {}", dep_name);
8✔
719
                m.imports.push(dependency_from_string(dep_name));
8✔
720
            }
8✔
721
        }
110✔
722
        if let Some(depends) = &module.depends {
118✔
723
            // println!("depends:");
724
            for dep_spec in depends {
42✔
725
                match dep_spec {
42✔
726
                    StringOrMapVecString::String(dep_name) => {
38✔
727
                        // println!("- {}", dep_name);
38✔
728
                        m.selects.push(dependency_from_string(dep_name));
38✔
729
                        m.imports.push(dependency_from_string(dep_name));
38✔
730
                    }
38✔
731
                    StringOrMapVecString::Map(dep_map) => {
4✔
732
                        for (k, v) in dep_map {
4✔
733
                            // println!("- {}:", k);
734
                            for dep_name in v {
4✔
735
                                // println!("  - {}", dep_name);
4✔
736
                                m.selects.push(dependency_from_string_if(dep_name, k));
4✔
737
                                m.imports.push(dependency_from_string_if(dep_name, k));
4✔
738
                            }
4✔
739
                        }
740
                    }
741
                }
742
            }
743
        }
85✔
744

745
        if let Some(conflicts) = &module.conflicts {
118✔
746
            m.add_conflicts(conflicts);
5✔
747
        }
113✔
748

749
        if let Some(provides) = &module.provides {
118✔
750
            m.add_provides(provides);
3✔
751
        }
115✔
752

753
        if let Some(provides_unique) = &module.provides_unique {
118✔
754
            // a "uniquely provided module" requires to be the only provider
×
755
            // for that module. think `provides_unique: [ libc ]`.
×
756
            // practically, it means adding to both "provides" and "conflicts"
×
757
            m.add_conflicts(provides_unique);
×
758
            m.add_provides(provides_unique);
×
759
        }
118✔
760

761
        if let Some(requires) = &module.requires {
118✔
NEW
762
            m.add_requires(requires);
×
763
        }
118✔
764

765
        if module.notify_all {
118✔
766
            m.notify_all = true;
1✔
767
        }
117✔
768

769
        // if a module name starts with "-", remove it from the list, also the
770
        // same name without "-".
771
        // this allows adding e.g., a dependency in "default: ...", but removing
772
        // it later. add/remove/add won't work, though.
773
        process_removes(&mut m.selects);
118✔
774
        process_removes(&mut m.imports);
118✔
775

776
        // copy over environment
777
        if let Some(env) = &module.env {
118✔
778
            if let Some(local) = &env.local {
53✔
779
                m.env_local.merge(local);
15✔
780
            }
38✔
781
            if let Some(export) = &env.export {
53✔
782
                m.env_export.merge(export);
36✔
783
            }
36✔
784
            if let Some(global) = &env.global {
53✔
785
                m.env_global.merge(global);
9✔
786
            }
44✔
787
        }
65✔
788

789
        if let Some(sources) = &module.sources {
118✔
790
            let mut sources_optional = IndexMap::new();
70✔
791
            for source in sources {
76✔
792
                match source {
76✔
793
                    StringOrMapVecString::String(source) => m.sources.push(source.clone()),
73✔
794
                    StringOrMapVecString::Map(source) => {
3✔
795
                        // collect optional sources into sources_optional
796
                        for (k, v) in source {
3✔
797
                            let list: &mut Vec<String> = sources_optional.entry(k).or_default();
3✔
798
                            for entry in v {
3✔
799
                                list.push(entry.clone());
3✔
800
                            }
3✔
801
                        }
802
                    }
803
                }
804
            }
805

806
            // if there are optional sources, merge them into the module's
807
            // optional sources map
808
            if !sources_optional.is_empty() {
70✔
809
                if m.sources_optional.is_none() {
3✔
810
                    m.sources_optional = Some(IndexMap::new());
3✔
811
                }
3✔
812
                let m_sources_optional = m.sources_optional.as_mut().unwrap();
3✔
813
                for (k, v) in sources_optional {
3✔
814
                    let list = m_sources_optional.entry(k.clone()).or_default();
3✔
815
                    for entry in v {
3✔
816
                        list.push(entry.clone());
3✔
817
                    }
3✔
818
                }
819
            }
67✔
820
        }
48✔
821

822
        if let Some(defaults_blocklist) = &mut m.blocklist {
118✔
823
            if let Some(module_blocklist) = &module.blocklist {
×
824
                defaults_blocklist.append(&mut (module_blocklist.clone()));
×
825
            }
×
826
        } else {
118✔
827
            m.blocklist.clone_from(&module.blocklist);
118✔
828
        }
118✔
829

830
        if let Some(defaults_allowlist) = &mut m.allowlist {
118✔
831
            if let Some(module_allowlist) = &module.allowlist {
×
832
                defaults_allowlist.append(&mut (module_allowlist.clone()));
×
833
            }
×
834
        } else {
118✔
835
            m.allowlist.clone_from(&module.allowlist);
118✔
836
        }
118✔
837

838
        let relpath = m.relpath.as_ref().unwrap().clone();
118✔
839

840
        m.download.clone_from(&module.download);
118✔
841
        let srcdir = if let Some(download) = &m.download {
118✔
842
            let srcdir = download.srcdir(build_dir, &m);
2✔
843
            let tagfile = download.tagfile(&srcdir);
2✔
844

845
            m.add_build_dep_file(&tagfile);
2✔
846

847
            // if a module has downloaded files, always consider it to be a
848
            // build dependency, as all dependees / users might include e.g.,
849
            // downloaded headers.
850
            m.is_build_dep = true;
2✔
851

852
            srcdir
2✔
853
        } else if relpath != "." {
116✔
854
            relpath.clone()
12✔
855
        } else {
856
            "".into()
104✔
857
        };
858

859
        m.build.clone_from(&module.build);
118✔
860
        m.is_global_build_dep = module.is_global_build_dep;
118✔
861

862
        if m.download.is_none() {
118✔
863
            // if a module has downloaded source, it is already a build dependency
116✔
864
            // for dependees / users. Otherwise, do what the user thinks.
116✔
865
            m.is_build_dep = module.is_build_dep;
116✔
866
        }
116✔
867

868
        m.srcdir = module
118✔
869
            .srcdir
118✔
870
            .as_ref()
118✔
871
            .map_or(Some(srcdir), |s| Some(Utf8PathBuf::from(s)));
118✔
872

873
        // populate "early env"
874
        m.env_early
118✔
875
            .insert("relpath".into(), EnvKey::Single(relpath.to_string()));
118✔
876

877
        m.env_early.insert(
118✔
878
            "root".into(),
118✔
879
            EnvKey::Single(match import_root {
118✔
880
                Some(import_root) => import_root.path().to_string(),
4✔
881
                None => ".".into(),
114✔
882
            }),
883
        );
884
        m.env_early.insert(
118✔
885
            "srcdir".into(),
118✔
886
            EnvKey::Single(m.srcdir.as_ref().unwrap().as_path().to_string()),
118✔
887
        );
888

889
        m.env_local.merge(&m.env_early);
118✔
890
        m.apply_early_env()?;
118✔
891

892
        // handle module tasks
893
        if let Some(tasks) = &module.tasks {
118✔
894
            m.tasks = convert_tasks(tasks, &m.env_early)
×
895
                .with_context(|| format!("{:?} module \"{}\"", &filename, m.name))?;
×
896

897
            // This makes the module provide_unique a marker module `::task::<task-name>`
898
            // for each task it defines, enabling the dependency resolver to sort
899
            // out duplicates.
900
            m.add_provides(tasks.keys().map(|name| format!("::task::{name}")));
×
901
            m.add_conflicts(tasks.keys().map(|name| format!("::task::{name}")));
×
902
        }
118✔
903

904
        if is_binary {
118✔
905
            m.env_global
63✔
906
                .insert("appdir".into(), EnvKey::Single(relpath.to_string()));
63✔
907
        }
63✔
908

909
        Ok(m)
118✔
910
    }
118✔
911

912
    // collect and convert contexts
913
    // this needs to be done before collecting modules, as that requires
914
    // contexts to be finalized.
915
    let mut context_modules = Vec::new();
43✔
916
    for data in &yaml_datas {
58✔
917
        for (list, is_builder) in [(&data.contexts, false), (&data.builders, true)].iter() {
115✔
918
            if let Some(context_list) = list {
115✔
919
                for context in context_list {
62✔
920
                    let module = convert_context(
62✔
921
                        context,
62✔
922
                        &mut contexts,
62✔
923
                        *is_builder | context.is_builder,
62✔
924
                        data.filename.as_ref().unwrap(),
62✔
925
                        &data.import_root,
62✔
926
                    )?;
1✔
927
                    context_modules.push(module);
61✔
928
                }
929
            }
69✔
930
        }
931
    }
932

933
    // after this, there's a default context, context relationships and envs have been set up.
934
    // modules can now be processed.
935
    contexts.finalize()?;
42✔
936

937
    // add the associated modules to their respective contexts
938
    for module in context_modules.drain(..) {
59✔
939
        contexts.add_module(module)?;
59✔
940
    }
941

942
    // for context in &contexts.contexts {
943
    //     if let Some(env) = &context.env {
944
    //         println!("context {} env:", context.name);
945
    //         dbg!(env);
946
    //     }
947
    // }
948
    let mut subdir_module_defaults_map = HashMap::new();
41✔
949
    let mut subdir_app_defaults_map = HashMap::new();
41✔
950

951
    fn get_defaults(
112✔
952
        data: &YamlFile,
112✔
953
        defaults_map: &HashMap<usize, Module>,
112✔
954
        key: &str,
112✔
955
        is_binary: bool,
112✔
956
        build_dir: &Utf8Path,
112✔
957
    ) -> Option<Module> {
112✔
958
        // this function determines the module or app defaults for a given YamlFile
959

960
        // determine inherited "defaults: module: ..."
961
        let subdir_defaults: Option<&Module> = if let Some(included_by) = &data.included_by {
112✔
962
            defaults_map.get(included_by)
26✔
963
        } else {
964
            None
86✔
965
        };
966

967
        // determine "defaults: module: ..." from yaml document
968
        let mut module_defaults = if let Some(defaults) = &data.defaults {
112✔
969
            if let Some(module_defaults) = defaults.get(key) {
4✔
970
                let context = &module_defaults
2✔
971
                    .context
2✔
972
                    .as_ref()
2✔
973
                    .map(|context| match context {
2✔
974
                        StringOrVecString::List(_) => {
975
                            panic!("module defaults with context _list_")
×
976
                        }
977
                        StringOrVecString::Single(context) => context,
1✔
978
                    });
1✔
979

980
                Some(
2✔
981
                    convert_module(
2✔
982
                        module_defaults,
2✔
983
                        *context,
2✔
984
                        is_binary,
2✔
985
                        data.filename.as_ref().unwrap(),
2✔
986
                        &data.import_root,
2✔
987
                        subdir_defaults,
2✔
988
                        build_dir,
2✔
989
                    )
2✔
990
                    .unwrap(),
2✔
991
                )
2✔
992
            } else {
993
                None
2✔
994
            }
995
        } else {
996
            None
108✔
997
        };
998
        if module_defaults.is_none() {
112✔
999
            if let Some(subdir_defaults) = subdir_defaults {
110✔
1000
                module_defaults = Some(subdir_defaults.clone());
2✔
1001
            }
108✔
1002
        }
2✔
1003
        module_defaults
112✔
1004
    }
112✔
1005

1006
    for data in &yaml_datas {
56✔
1007
        let module_defaults = get_defaults(
56✔
1008
            data,
56✔
1009
            &subdir_module_defaults_map,
56✔
1010
            "module",
56✔
1011
            false,
1012
            build_dir,
56✔
1013
        );
1014
        let app_defaults = get_defaults(data, &subdir_app_defaults_map, "app", true, build_dir);
56✔
1015

1016
        if data.subdirs.is_some() {
56✔
1017
            if let Some(module_defaults) = &module_defaults {
6✔
1018
                subdir_module_defaults_map.insert(data.doc_idx.unwrap(), module_defaults.clone());
2✔
1019
            }
4✔
1020
            if let Some(app_defaults) = &app_defaults {
6✔
1021
                subdir_app_defaults_map.insert(data.doc_idx.unwrap(), app_defaults.clone());
×
1022
            }
6✔
1023
        }
50✔
1024

1025
        for (list, is_binary) in [(&data.modules, false), (&data.apps, true)].iter() {
110✔
1026
            if let Some(module_list) = list {
110✔
1027
                if let Some(module_list) = module_list {
65✔
1028
                    for module in module_list {
113✔
1029
                        for context in module.get_contexts() {
114✔
1030
                            contexts.add_module(convert_module(
114✔
1031
                                module,
114✔
1032
                                context,
114✔
1033
                                *is_binary,
114✔
1034
                                data.filename.as_ref().unwrap(),
114✔
1035
                                &data.import_root,
114✔
1036
                                if *is_binary {
114✔
1037
                                    app_defaults.as_ref()
61✔
1038
                                } else {
1039
                                    module_defaults.as_ref()
53✔
1040
                                },
1041
                                build_dir,
114✔
1042
                            )?)?;
2✔
1043
                        }
1044
                    }
1045
                } else if *is_binary {
2✔
1046
                    // if an app list is empty, add a default entry.
1047
                    // this allows a convenient file only containing "app:"
1048
                    let module = YamlModule::default_binary();
2✔
1049
                    for context in module.get_contexts() {
2✔
1050
                        contexts.add_module(convert_module(
2✔
1051
                            &module,
2✔
1052
                            context,
2✔
1053
                            *is_binary,
2✔
1054
                            data.filename.as_ref().unwrap(),
2✔
1055
                            &data.import_root,
2✔
1056
                            app_defaults.as_ref(),
2✔
1057
                            build_dir,
2✔
1058
                        )?)?;
×
1059
                    }
1060
                }
×
1061
            }
45✔
1062
        }
1063
    }
1064

1065
    contexts.merge_provides();
39✔
1066

1067
    let parsing_time = start.elapsed();
39✔
1068
    let start = Instant::now();
39✔
1069

1070
    // convert Utf8PathBufs to PathBufs
1071
    // TODO: make treestate support camino Utf8PathBuf
1072
    let filenames = filenames
39✔
1073
        .drain(..)
39✔
1074
        .map(|include| include.filename.into_std_path_buf())
52✔
1075
        .collect_vec();
39✔
1076

1077
    let treestate = FileTreeState::new(filenames.iter());
39✔
1078
    let stat_time = start.elapsed();
39✔
1079

1080
    let stats = LoadStats {
39✔
1081
        parsing_time,
39✔
1082
        stat_time,
39✔
1083
        files: filenames.len(),
39✔
1084
    };
39✔
1085
    Ok((contexts, treestate, stats))
39✔
1086
}
44✔
1087

1088
fn convert_tasks(
6✔
1089
    tasks: &HashMap<String, YamlTask>,
6✔
1090
    env: &Env,
6✔
1091
) -> Result<HashMap<String, Task>, Error> {
6✔
1092
    let flattened_env = env.flatten()?;
6✔
1093
    tasks
6✔
1094
        .iter()
6✔
1095
        .map(|(name, task)| {
22✔
1096
            let task = Task::from(task.clone());
22✔
1097
            let task = task.with_env(&flattened_env);
22✔
1098
            task.map(|task| (name.clone(), task))
22✔
1099
                .with_context(|| format!("task \"{}\"", name.clone()))
22✔
1100
        })
22✔
1101
        .collect::<Result<_, _>>()
6✔
1102
}
6✔
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