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

kaspar030 / laze / 14314362609

07 Apr 2025 04:23PM UTC coverage: 80.862% (-0.4%) from 81.237%
14314362609

push

github

web-flow
build(deps): bump indexmap from 2.8.0 to 2.9.0 (#698)

3583 of 4431 relevant lines covered (80.86%)

102.83 hits per line

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

93.27
/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>
63✔
41
where
63✔
42
    T: Deserialize<'de>,
63✔
43
    D: Deserializer<'de>,
63✔
44
{
63✔
45
    Deserialize::deserialize(deserializer).map(Some)
63✔
46
}
63✔
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
{
1✔
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>
111✔
107
where
111✔
108
    D: Deserializer<'de>,
111✔
109
{
111✔
110
    let v = Option::<String>::deserialize(deserializer)?;
111✔
111

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

121
    Ok(v)
111✔
122
}
111✔
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
    rules: Option<Vec<YamlRule>>,
136
    var_options: Option<im::HashMap<String, MergeOption>>,
137
    tasks: Option<HashMap<String, YamlTask>>,
138
    #[serde(default = "default_as_false", alias = "buildable")]
139
    is_builder: bool,
140
    #[serde(rename = "meta")]
141
    _meta: Option<Value>,
142
}
143

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

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

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

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

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

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

219
    pub help: Option<String>,
220

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

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

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

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

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

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

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

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

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

309
//     data.filename = Some(filename.clone());
310

311
//     Ok(data)
312
// }
313

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

232✔
321
    strings.retain(|x| !(x.get_name().starts_with('-') || removals.contains(&x.get_name()[..])));
232✔
322
}
232✔
323

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

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

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

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

352
    Ok(result)
54✔
353
}
55✔
354

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

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

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

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

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

42✔
403
    // yaml_datas holds all parsed yaml data
42✔
404
    let mut yaml_datas = Vec::new();
42✔
405

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

42✔
413
    let mut filenames_pos = 0;
42✔
414
    while filenames_pos < filenames.len() {
96✔
415
        let include = filenames.get_index(filenames_pos).unwrap();
55✔
416
        let filename = include.filename.clone();
55✔
417
        let new_index_start = yaml_datas.len();
55✔
418

55✔
419
        // load all yaml documents from filename, append to yaml_datas
55✔
420
        yaml_datas.append(&mut load_all(include, new_index_start)?);
55✔
421
        filenames_pos += 1;
54✔
422

54✔
423
        let new_index_end = yaml_datas.len();
54✔
424

425
        // iterate over newly added documents
426
        for new in yaml_datas[new_index_start..new_index_end].iter() {
56✔
427
            if let Some(subdirs) = &new.subdirs {
56✔
428
                let relpath = filename.parent().unwrap().to_path_buf();
6✔
429

430
                // collect subdirs, add do filenames list
431
                for subdir in subdirs {
12✔
432
                    let sub_file = Utf8Path::new(&relpath).join(subdir).join("laze.yml");
6✔
433
                    filenames.insert(FileInclude::new(
6✔
434
                        sub_file,
6✔
435
                        new.doc_idx,
6✔
436
                        new.import_root.clone(),
6✔
437
                    ));
6✔
438
                }
6✔
439
            }
50✔
440
            if let Some(imports) = &new.imports {
56✔
441
                for import in imports {
10✔
442
                    // TODO: `import.handle()` does the actual git checkout (or whatever
443
                    // import action), so probably better handling of any errors is
444
                    // in order.
445
                    filenames.insert(FileInclude::new_import(
6✔
446
                        import.handle(build_dir)?,
6✔
447
                        new.doc_idx,
6✔
448
                    ));
449
                }
450
            }
52✔
451
            if let Some(includes) = &new.includes {
56✔
452
                let relpath = filename.parent().unwrap().to_path_buf();
1✔
453
                for filename in includes {
2✔
454
                    let filepath = Utf8Path::new(&relpath).join(filename);
1✔
455
                    filenames.insert(FileInclude::new(
1✔
456
                        filepath,
1✔
457
                        new.doc_idx,
1✔
458
                        new.import_root.clone(),
1✔
459
                    ));
1✔
460
                }
1✔
461
            }
55✔
462
        }
463
    }
464

465
    fn convert_context(
60✔
466
        context: &YamlContext,
60✔
467
        contexts: &mut ContextBag,
60✔
468
        is_builder: bool,
60✔
469
        filename: &Utf8PathBuf,
60✔
470
        import_root: &Option<ImportRoot>,
60✔
471
    ) -> Result<Module, Error> {
60✔
472
        let context_name = &context.name;
60✔
473
        let context_parent = match &context.parent {
60✔
474
            Some(x) => x.clone(),
10✔
475
            None => "default".to_string(),
50✔
476
        };
477

478
        let is_default = context_name.as_str() == "default";
60✔
479

480
        // println!(
481
        //     "{} {} parent {}",
482
        //     match is_builder {
483
        //         true => "builder",
484
        //         false => "context",
485
        //     },
486
        //     context_name,
487
        //     context_parent,
488
        // );
489
        let context_ = contexts
60✔
490
            .add_context_or_builder(
60✔
491
                Context::new(
60✔
492
                    context_name.clone(),
60✔
493
                    if is_default {
60✔
494
                        None
16✔
495
                    } else {
496
                        Some(context_parent.clone())
44✔
497
                    },
498
                ),
499
                is_builder,
60✔
500
            )
60✔
501
            .with_context(|| format!("{:?}: adding context \"{}\"", &filename, &context_name))?;
60✔
502

503
        context_.help.clone_from(&context.help);
59✔
504
        context_.env.clone_from(&context.env);
59✔
505
        if let Some(rules) = &context.rules {
59✔
506
            context_.rules = Some(IndexMap::new());
36✔
507
            for rule in rules {
111✔
508
                let mut rule: Rule = rule.clone().into();
75✔
509
                rule.context = Some(context_name.clone());
75✔
510
                context_
75✔
511
                    .rules
75✔
512
                    .as_mut()
75✔
513
                    .unwrap()
75✔
514
                    .insert(rule.name.clone(), rule);
75✔
515
            }
75✔
516
        }
23✔
517
        context_.var_options.clone_from(&context.var_options);
59✔
518
        // populate "early env"
519
        let relpath = {
59✔
520
            let relpath = filename.parent().unwrap().as_str();
59✔
521
            if relpath.is_empty() {
59✔
522
                ".".to_string()
56✔
523
            } else {
524
                relpath.to_string()
3✔
525
            }
526
        };
527

528
        context_
59✔
529
            .env_early
59✔
530
            .insert("relpath".into(), EnvKey::Single(relpath));
59✔
531

59✔
532
        context_.env_early.insert(
59✔
533
            "root".into(),
59✔
534
            EnvKey::Single(match import_root {
59✔
535
                Some(import_root) => import_root.path().to_string(),
3✔
536
                None => ".".into(),
56✔
537
            }),
538
        );
539

540
        if let Some(tasks) = &context.tasks {
59✔
541
            context_.tasks = Some(
5✔
542
                convert_tasks(tasks, &context_.env_early)
5✔
543
                    .with_context(|| format!("{:?} context \"{}\"", &filename, context.name))?,
5✔
544
            )
545
        }
54✔
546

547
        context_.apply_early_env()?;
59✔
548

549
        context_.defined_in = Some(filename.clone());
59✔
550

59✔
551
        // TODO(context-early-disables)
59✔
552
        context_.disable.clone_from(&context.disables);
59✔
553

59✔
554
        // Each Context has an associated module.
59✔
555
        // This holds:
59✔
556
        // - selects
59✔
557
        // - disables
59✔
558
        // - provides
59✔
559
        // - provides_unique
59✔
560
        // TODO:
59✔
561
        // - env (in global env)
59✔
562
        // - rules
59✔
563
        // - tasks
59✔
564
        let module_name = Some(context_.module_name());
59✔
565
        let mut module = init_module(
59✔
566
            &module_name,
59✔
567
            Some(context_name),
59✔
568
            false,
59✔
569
            filename,
59✔
570
            import_root,
59✔
571
            None,
59✔
572
        );
573

574
        // collect context level "select:"
575
        if let Some(selects) = &context.selects {
59✔
576
            for dep_name in selects {
3✔
577
                // println!("- {}", dep_name);
2✔
578
                module.selects.push(dependency_from_string(dep_name));
2✔
579
            }
2✔
580
        }
58✔
581

582
        if let Some(disables) = context.disables.as_ref() {
59✔
583
            module.conflicts = Some(disables.clone());
2✔
584
        }
57✔
585

586
        if let Some(provides) = context.provides.as_ref() {
59✔
587
            module.provides = Some(provides.clone());
2✔
588
        }
57✔
589

590
        if let Some(provides_unique) = context.provides_unique.as_ref() {
59✔
591
            if let Some(provides) = module.provides.as_mut() {
4✔
592
                provides.extend(provides_unique.iter().cloned());
×
593
            } else {
4✔
594
                module.provides = Some(provides_unique.clone());
4✔
595
            }
4✔
596
            if let Some(conflicts) = module.conflicts.as_mut() {
4✔
597
                conflicts.extend(provides_unique.iter().cloned());
×
598
            } else {
4✔
599
                module.conflicts = Some(provides_unique.clone());
4✔
600
            }
4✔
601
        }
55✔
602

603
        // make context module depend on its parent's context module
604
        if !is_default {
59✔
605
            module
43✔
606
                .selects
43✔
607
                .push(Dependency::Hard(Context::module_name_for(&context_parent)));
43✔
608
        }
43✔
609

610
        Ok(module)
59✔
611
    }
60✔
612

613
    fn init_module(
175✔
614
        name: &Option<String>,
175✔
615
        context: Option<&String>,
175✔
616
        is_binary: bool,
175✔
617
        filename: &Utf8Path,
175✔
618
        import_root: &Option<ImportRoot>,
175✔
619
        defaults: Option<&Module>,
175✔
620
    ) -> Module {
175✔
621
        let relpath = filename.parent().unwrap();
175✔
622

623
        let name = match name {
175✔
624
            Some(name) => name.clone(),
171✔
625
            None => if let Some(import_root) = import_root {
4✔
626
                filename
×
627
                    .parent()
×
628
                    .unwrap()
×
629
                    .strip_prefix(import_root.path())
×
630
                    .unwrap()
×
631
            } else {
632
                relpath
4✔
633
            }
634
            .to_string(),
4✔
635
        };
636

637
        let mut module = match defaults {
175✔
638
            Some(defaults) => Module::from(defaults, name, context.cloned()),
5✔
639
            None => Module::new(name, context.cloned()),
170✔
640
        };
641

642
        module.is_binary = is_binary;
175✔
643
        module.defined_in = Some(filename.to_path_buf());
175✔
644
        module.relpath = Some(if relpath.eq("") {
175✔
645
            Utf8PathBuf::from(".")
160✔
646
        } else {
647
            Utf8PathBuf::from(&relpath)
15✔
648
        });
649

650
        module
175✔
651
    }
175✔
652

653
    fn convert_module(
116✔
654
        module: &YamlModule,
116✔
655
        context: Option<&String>,
116✔
656
        is_binary: bool,
116✔
657
        filename: &Utf8Path,
116✔
658
        import_root: &Option<ImportRoot>,
116✔
659
        defaults: Option<&Module>,
116✔
660
        build_dir: &Utf8Path,
116✔
661
    ) -> Result<Module, Error> {
116✔
662
        let mut m = init_module(
116✔
663
            &module.name,
116✔
664
            context,
116✔
665
            is_binary,
116✔
666
            filename,
116✔
667
            import_root,
116✔
668
            defaults,
116✔
669
        );
116✔
670

116✔
671
        m.help.clone_from(&module.help);
116✔
672

673
        // convert module dependencies
674
        // "selects" means "module will be part of the build"
675
        // "uses" means "if module is part of the build, transitively import its exported env vars"
676
        // "depends" means both select and use a module
677
        // a build configuration fails if a selected or depended on module is not
678
        // available.
679
        // "conflicts" will make any module with the specified name unavailable
680
        // in the binary's build context, if the module specifying a conflict
681
        // is part of the dependency tree
682
        //
683
        if let Some(selects) = &module.selects {
116✔
684
            // println!("selects:");
685
            for dep_spec in selects {
36✔
686
                match dep_spec {
21✔
687
                    StringOrMapVecString::String(dep_name) => {
21✔
688
                        m.selects.push(dependency_from_string(dep_name));
21✔
689
                    }
21✔
690
                    StringOrMapVecString::Map(dep_map) => {
×
691
                        for (k, v) in dep_map {
×
692
                            for dep_name in v {
×
693
                                m.selects.push(dependency_from_string_if(dep_name, k));
×
694
                            }
×
695
                        }
696
                    }
697
                }
698
            }
699
        }
101✔
700
        if let Some(uses) = &module.uses {
116✔
701
            // println!("uses:");
702
            for dep_name in uses {
16✔
703
                // println!("- {}", dep_name);
8✔
704
                m.imports.push(dependency_from_string(dep_name));
8✔
705
            }
8✔
706
        }
108✔
707
        if let Some(depends) = &module.depends {
116✔
708
            // println!("depends:");
709
            for dep_spec in depends {
75✔
710
                match dep_spec {
42✔
711
                    StringOrMapVecString::String(dep_name) => {
38✔
712
                        // println!("- {}", dep_name);
38✔
713
                        m.selects.push(dependency_from_string(dep_name));
38✔
714
                        m.imports.push(dependency_from_string(dep_name));
38✔
715
                    }
38✔
716
                    StringOrMapVecString::Map(dep_map) => {
4✔
717
                        for (k, v) in dep_map {
8✔
718
                            // println!("- {}:", k);
719
                            for dep_name in v {
8✔
720
                                // println!("  - {}", dep_name);
4✔
721
                                m.selects.push(dependency_from_string_if(dep_name, k));
4✔
722
                                m.imports.push(dependency_from_string_if(dep_name, k));
4✔
723
                            }
4✔
724
                        }
725
                    }
726
                }
727
            }
728
        }
83✔
729

730
        if let Some(conflicts) = &module.conflicts {
116✔
731
            m.add_conflicts(conflicts);
5✔
732
        }
111✔
733

734
        if let Some(provides) = &module.provides {
116✔
735
            m.add_provides(provides);
3✔
736
        }
113✔
737

738
        if let Some(provides_unique) = &module.provides_unique {
116✔
739
            // a "uniquely provided module" requires to be the only provider
×
740
            // for that module. think `provides_unique: [ libc ]`.
×
741
            // practically, it means adding to both "provides" and "conflicts"
×
742
            m.add_conflicts(provides_unique);
×
743
            m.add_provides(provides_unique);
×
744
        }
116✔
745

746
        if module.notify_all {
116✔
747
            m.notify_all = true;
1✔
748
        }
115✔
749

750
        // if a module name starts with "-", remove it from the list, also the
751
        // same name without "-".
752
        // this allows adding e.g., a dependency in "default: ...", but removing
753
        // it later. add/remove/add won't work, though.
754
        process_removes(&mut m.selects);
116✔
755
        process_removes(&mut m.imports);
116✔
756

757
        // copy over environment
758
        if let Some(env) = &module.env {
116✔
759
            if let Some(local) = &env.local {
53✔
760
                m.env_local.merge(local);
15✔
761
            }
38✔
762
            if let Some(export) = &env.export {
53✔
763
                m.env_export.merge(export);
36✔
764
            }
36✔
765
            if let Some(global) = &env.global {
53✔
766
                m.env_global.merge(global);
9✔
767
            }
44✔
768
        }
63✔
769

770
        if let Some(sources) = &module.sources {
116✔
771
            let mut sources_optional = IndexMap::new();
69✔
772
            for source in sources {
144✔
773
                match source {
75✔
774
                    StringOrMapVecString::String(source) => m.sources.push(source.clone()),
72✔
775
                    StringOrMapVecString::Map(source) => {
3✔
776
                        // collect optional sources into sources_optional
777
                        for (k, v) in source {
6✔
778
                            let list: &mut Vec<String> = sources_optional.entry(k).or_default();
3✔
779
                            for entry in v {
6✔
780
                                list.push(entry.clone());
3✔
781
                            }
3✔
782
                        }
783
                    }
784
                }
785
            }
786

787
            // if there are optional sources, merge them into the module's
788
            // optional sources map
789
            if !sources_optional.is_empty() {
69✔
790
                if m.sources_optional.is_none() {
3✔
791
                    m.sources_optional = Some(IndexMap::new());
3✔
792
                }
3✔
793
                let m_sources_optional = m.sources_optional.as_mut().unwrap();
3✔
794
                for (k, v) in sources_optional {
6✔
795
                    let list = m_sources_optional.entry(k.clone()).or_default();
3✔
796
                    for entry in v {
6✔
797
                        list.push(entry.clone());
3✔
798
                    }
3✔
799
                }
800
            }
66✔
801
        }
47✔
802

803
        if let Some(defaults_blocklist) = &mut m.blocklist {
116✔
804
            if let Some(module_blocklist) = &module.blocklist {
×
805
                defaults_blocklist.append(&mut (module_blocklist.clone()));
×
806
            }
×
807
        } else {
116✔
808
            m.blocklist.clone_from(&module.blocklist);
116✔
809
        }
116✔
810

811
        if let Some(defaults_allowlist) = &mut m.allowlist {
116✔
812
            if let Some(module_allowlist) = &module.allowlist {
×
813
                defaults_allowlist.append(&mut (module_allowlist.clone()));
×
814
            }
×
815
        } else {
116✔
816
            m.allowlist.clone_from(&module.allowlist);
116✔
817
        }
116✔
818

819
        let relpath = m.relpath.as_ref().unwrap().clone();
116✔
820

116✔
821
        m.download.clone_from(&module.download);
116✔
822
        let srcdir = if let Some(download) = &m.download {
116✔
823
            let srcdir = download.srcdir(build_dir, &m);
2✔
824
            let tagfile = download.tagfile(&srcdir);
2✔
825

2✔
826
            m.add_build_dep_file(&tagfile);
2✔
827

2✔
828
            // if a module has downloaded files, always consider it to be a
2✔
829
            // build dependency, as all dependees / users might include e.g.,
2✔
830
            // downloaded headers.
2✔
831
            m.is_build_dep = true;
2✔
832

2✔
833
            srcdir
2✔
834
        } else if relpath != "." {
114✔
835
            relpath.clone()
12✔
836
        } else {
837
            "".into()
102✔
838
        };
839

840
        m.build.clone_from(&module.build);
116✔
841
        m.is_global_build_dep = module.is_global_build_dep;
116✔
842

116✔
843
        if m.download.is_none() {
116✔
844
            // if a module has downloaded source, it is already a build dependency
114✔
845
            // for dependees / users. Otherwise, do what the user thinks.
114✔
846
            m.is_build_dep = module.is_build_dep;
114✔
847
        }
114✔
848

849
        m.srcdir = module
116✔
850
            .srcdir
116✔
851
            .as_ref()
116✔
852
            .map_or(Some(srcdir), |s| Some(Utf8PathBuf::from(s)));
116✔
853

854
        // populate "early env"
855
        m.env_early
116✔
856
            .insert("relpath".into(), EnvKey::Single(relpath.to_string()));
116✔
857

116✔
858
        m.env_early.insert(
116✔
859
            "root".into(),
116✔
860
            EnvKey::Single(match import_root {
116✔
861
                Some(import_root) => import_root.path().to_string(),
4✔
862
                None => ".".into(),
112✔
863
            }),
864
        );
865
        m.env_early.insert(
116✔
866
            "srcdir".into(),
116✔
867
            EnvKey::Single(m.srcdir.as_ref().unwrap().as_path().to_string()),
116✔
868
        );
116✔
869

116✔
870
        m.env_local.merge(&m.env_early);
116✔
871
        m.apply_early_env()?;
116✔
872

873
        // handle module tasks
874
        if let Some(tasks) = &module.tasks {
116✔
875
            m.tasks = convert_tasks(tasks, &m.env_early)
×
876
                .with_context(|| format!("{:?} module \"{}\"", &filename, m.name))?;
×
877

878
            // This makes the module provide_unique a marker module `::task::<task-name>`
879
            // for each task it defines, enabling the dependency resolver to sort
880
            // out duplicates.
881
            m.add_provides(tasks.keys().map(|name| format!("::task::{name}")));
×
882
            m.add_conflicts(tasks.keys().map(|name| format!("::task::{name}")));
×
883
        }
116✔
884

885
        if is_binary {
116✔
886
            m.env_global
61✔
887
                .insert("appdir".into(), EnvKey::Single(relpath.to_string()));
61✔
888
        }
61✔
889

890
        Ok(m)
116✔
891
    }
116✔
892

893
    // collect and convert contexts
894
    // this needs to be done before collecting modules, as that requires
895
    // contexts to be finalized.
896
    let mut context_modules = Vec::new();
41✔
897
    for data in &yaml_datas {
96✔
898
        for (list, is_builder) in [(&data.contexts, false), (&data.builders, true)].iter() {
111✔
899
            if let Some(context_list) = list {
111✔
900
                for context in context_list {
103✔
901
                    let module = convert_context(
60✔
902
                        context,
60✔
903
                        &mut contexts,
60✔
904
                        *is_builder | context.is_builder,
60✔
905
                        data.filename.as_ref().unwrap(),
60✔
906
                        &data.import_root,
60✔
907
                    )?;
1✔
908
                    context_modules.push(module);
59✔
909
                }
910
            }
67✔
911
        }
912
    }
913

914
    // after this, there's a default context, context relationships and envs have been set up.
915
    // modules can now be processed.
916
    contexts.finalize()?;
40✔
917

918
    // add the associated modules to their respective contexts
919
    for module in context_modules.drain(..) {
57✔
920
        contexts.add_module(module)?;
57✔
921
    }
922

923
    // for context in &contexts.contexts {
924
    //     if let Some(env) = &context.env {
925
    //         println!("context {} env:", context.name);
926
    //         dbg!(env);
927
    //     }
928
    // }
929
    let mut subdir_module_defaults_map = HashMap::new();
39✔
930
    let mut subdir_app_defaults_map = HashMap::new();
39✔
931

932
    fn get_defaults(
108✔
933
        data: &YamlFile,
108✔
934
        defaults_map: &HashMap<usize, Module>,
108✔
935
        key: &str,
108✔
936
        is_binary: bool,
108✔
937
        build_dir: &Utf8Path,
108✔
938
    ) -> Option<Module> {
108✔
939
        // this function determines the module or app defaults for a given YamlFile
940

941
        // determine inherited "defaults: module: ..."
942
        let subdir_defaults: Option<&Module> = if let Some(included_by) = &data.included_by {
108✔
943
            defaults_map.get(included_by)
26✔
944
        } else {
945
            None
82✔
946
        };
947

948
        // determine "defaults: module: ..." from yaml document
949
        let mut module_defaults = if let Some(defaults) = &data.defaults {
108✔
950
            if let Some(module_defaults) = defaults.get(key) {
4✔
951
                let context = &module_defaults
2✔
952
                    .context
2✔
953
                    .as_ref()
2✔
954
                    .map(|context| match context {
2✔
955
                        StringOrVecString::List(_) => {
956
                            panic!("module defaults with context _list_")
×
957
                        }
958
                        StringOrVecString::Single(context) => context,
1✔
959
                    });
1✔
960

961
                Some(
2✔
962
                    convert_module(
2✔
963
                        module_defaults,
2✔
964
                        *context,
2✔
965
                        is_binary,
2✔
966
                        data.filename.as_ref().unwrap(),
2✔
967
                        &data.import_root,
2✔
968
                        subdir_defaults,
2✔
969
                        build_dir,
2✔
970
                    )
2✔
971
                    .unwrap(),
2✔
972
                )
2✔
973
            } else {
974
                None
2✔
975
            }
976
        } else {
977
            None
104✔
978
        };
979
        if module_defaults.is_none() {
108✔
980
            if let Some(subdir_defaults) = subdir_defaults {
106✔
981
                module_defaults = Some(subdir_defaults.clone());
2✔
982
            }
104✔
983
        }
2✔
984
        module_defaults
108✔
985
    }
108✔
986

987
    for data in &yaml_datas {
91✔
988
        let module_defaults = get_defaults(
54✔
989
            data,
54✔
990
            &subdir_module_defaults_map,
54✔
991
            "module",
54✔
992
            false,
54✔
993
            build_dir,
54✔
994
        );
54✔
995
        let app_defaults = get_defaults(data, &subdir_app_defaults_map, "app", true, build_dir);
54✔
996

54✔
997
        if data.subdirs.is_some() {
54✔
998
            if let Some(module_defaults) = &module_defaults {
6✔
999
                subdir_module_defaults_map.insert(data.doc_idx.unwrap(), module_defaults.clone());
2✔
1000
            }
4✔
1001
            if let Some(app_defaults) = &app_defaults {
6✔
1002
                subdir_app_defaults_map.insert(data.doc_idx.unwrap(), app_defaults.clone());
×
1003
            }
6✔
1004
        }
48✔
1005

1006
        for (list, is_binary) in [(&data.modules, false), (&data.apps, true)].iter() {
106✔
1007
            if let Some(module_list) = list {
106✔
1008
                if let Some(module_list) = module_list {
63✔
1009
                    for module in module_list {
170✔
1010
                        for context in module.get_contexts() {
112✔
1011
                            contexts.add_module(convert_module(
112✔
1012
                                module,
112✔
1013
                                context,
112✔
1014
                                *is_binary,
112✔
1015
                                data.filename.as_ref().unwrap(),
112✔
1016
                                &data.import_root,
112✔
1017
                                if *is_binary {
112✔
1018
                                    app_defaults.as_ref()
59✔
1019
                                } else {
1020
                                    module_defaults.as_ref()
53✔
1021
                                },
1022
                                build_dir,
112✔
1023
                            )?)?;
2✔
1024
                        }
1025
                    }
1026
                } else if *is_binary {
2✔
1027
                    // if an app list is empty, add a default entry.
1028
                    // this allows a convenient file only containing "app:"
1029
                    let module = YamlModule::default_binary();
2✔
1030
                    for context in module.get_contexts() {
2✔
1031
                        contexts.add_module(convert_module(
2✔
1032
                            &module,
2✔
1033
                            context,
2✔
1034
                            *is_binary,
2✔
1035
                            data.filename.as_ref().unwrap(),
2✔
1036
                            &data.import_root,
2✔
1037
                            app_defaults.as_ref(),
2✔
1038
                            build_dir,
2✔
1039
                        )?)?;
×
1040
                    }
1041
                }
×
1042
            }
43✔
1043
        }
1044
    }
1045

1046
    contexts.merge_provides();
37✔
1047

37✔
1048
    let parsing_time = start.elapsed();
37✔
1049
    let start = Instant::now();
37✔
1050

37✔
1051
    // convert Utf8PathBufs to PathBufs
37✔
1052
    // TODO: make treestate support camino Utf8PathBuf
37✔
1053
    let filenames = filenames
37✔
1054
        .drain(..)
37✔
1055
        .map(|include| include.filename.into_std_path_buf())
50✔
1056
        .collect_vec();
37✔
1057

37✔
1058
    let treestate = FileTreeState::new(filenames.iter());
37✔
1059
    let stat_time = start.elapsed();
37✔
1060

37✔
1061
    let stats = LoadStats {
37✔
1062
        parsing_time,
37✔
1063
        stat_time,
37✔
1064
        files: filenames.len(),
37✔
1065
    };
37✔
1066
    Ok((contexts, treestate, stats))
37✔
1067
}
42✔
1068

1069
fn convert_tasks(
5✔
1070
    tasks: &HashMap<String, YamlTask>,
5✔
1071
    env: &Env,
5✔
1072
) -> Result<HashMap<String, Task>, Error> {
5✔
1073
    let flattened_env = env.flatten()?;
5✔
1074
    tasks
5✔
1075
        .iter()
5✔
1076
        .map(|(name, task)| {
10✔
1077
            let task = Task::from(task.clone());
10✔
1078
            let task = task.with_env(&flattened_env);
10✔
1079
            task.map(|task| (name.clone(), task))
10✔
1080
                .with_context(|| format!("task \"{}\"", name.clone()))
10✔
1081
        })
10✔
1082
        .collect::<Result<_, _>>()
5✔
1083
}
5✔
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