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

kaspar030 / laze / 13356422699

16 Feb 2025 03:32PM UTC coverage: 82.395%. First build
13356422699

Pull #639

github

web-flow
Merge c1d6f4c79 into 0ac04e89f
Pull Request #639: feat: context `provides`/`provides_unique`

15 of 17 new or added lines in 1 file covered. (88.24%)

3571 of 4334 relevant lines covered (82.4%)

104.52 hits per line

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

93.45
/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
extern crate pathdiff;
8
extern crate serde_yaml;
9

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

17
use anyhow::{Context as _, Error, Result};
18
use camino::{Utf8Path, Utf8PathBuf};
19
use semver::Version;
20
use serde::{Deserialize, Deserializer};
21

22
use treestate::{FileState, TreeState};
23

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

31
mod import;
32
use import::ImportEntry;
33

34
pub type FileTreeState = TreeState<FileState, std::path::PathBuf>;
35

36
pub struct LoadStats {
37
    pub files: usize,
38
    pub parsing_time: Duration,
39
    pub stat_time: Duration,
40
}
41

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

51
fn default_none<T>() -> Option<T> {
2✔
52
    None
2✔
53
}
2✔
54

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

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

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

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

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

124
    Ok(v)
111✔
125
}
111✔
126

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

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

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

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

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

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

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

222
    pub help: Option<String>,
223

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

236
    #[serde(default = "default_as_false")]
237
    pub always: 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
            pool: yaml_rule.pool,
75✔
260
            description: yaml_rule.description,
75✔
261
            export: yaml_rule
75✔
262
                .export
75✔
263
                .map(|s| s.iter().map(|s| s.clone().into()).collect_vec()),
75✔
264
        }
75✔
265
    }
75✔
266
}
267

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

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

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

306
//     data.filename = Some(filename.clone());
307

308
//     Ok(data)
309
// }
310

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

232✔
318
    strings.retain(|x| !(x.get_name().starts_with('-') || removals.contains(&x.get_name()[..])));
232✔
319
}
232✔
320

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

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

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

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

349
    Ok(result)
54✔
350
}
55✔
351

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

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

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

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

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

42✔
400
    // yaml_datas holds all parsed yaml data
42✔
401
    let mut yaml_datas = Vec::new();
42✔
402

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

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

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

54✔
420
        let new_index_end = yaml_datas.len();
54✔
421

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

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

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

475
        let is_default = context_name.as_str() == "default";
60✔
476

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

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

525
        context_
59✔
526
            .env_early
59✔
527
            .insert("relpath".into(), EnvKey::Single(relpath));
59✔
528

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

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

544
        context_.apply_early_env()?;
59✔
545

546
        context_.defined_in = Some(filename.clone());
59✔
547

59✔
548
        // TODO(context-early-disables)
59✔
549
        context_.disable.clone_from(&context.disables);
59✔
550

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

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

579
        if let Some(disables) = context.disables.as_ref() {
59✔
580
            module.conflicts = Some(disables.clone());
2✔
581
        }
57✔
582

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

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

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

607
        Ok(module)
59✔
608
    }
60✔
609

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

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

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

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

647
        module
175✔
648
    }
175✔
649

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

116✔
668
        m.help.clone_from(&module.help);
116✔
669

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

727
        if let Some(conflicts) = &module.conflicts {
116✔
728
            m.add_conflicts(conflicts);
5✔
729
        }
111✔
730

731
        if let Some(provides) = &module.provides {
116✔
732
            m.add_provides(provides);
3✔
733
        }
113✔
734

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

743
        if module.notify_all {
116✔
744
            m.notify_all = true;
1✔
745
        }
115✔
746

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

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

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

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

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

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

816
        let relpath = m.relpath.as_ref().unwrap().clone();
116✔
817

116✔
818
        m.download.clone_from(&module.download);
116✔
819
        let srcdir = if let Some(download) = &m.download {
116✔
820
            let srcdir = download.srcdir(build_dir, &m);
2✔
821
            let tagfile = download.tagfile(&srcdir);
2✔
822

2✔
823
            m.add_build_dep_file(&tagfile);
2✔
824

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

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

837
        m.build.clone_from(&module.build);
116✔
838
        m.is_global_build_dep = module.is_global_build_dep;
116✔
839

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

846
        m.srcdir = module
116✔
847
            .srcdir
116✔
848
            .as_ref()
116✔
849
            .map_or(Some(srcdir), |s| Some(Utf8PathBuf::from(s)));
116✔
850

116✔
851
        // populate "early env"
116✔
852
        m.env_early
116✔
853
            .insert("relpath".into(), EnvKey::Single(relpath.to_string()));
116✔
854

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

116✔
867
        m.env_local.merge(&m.env_early);
116✔
868
        m.apply_early_env()?;
116✔
869

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

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

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

887
        Ok(m)
116✔
888
    }
116✔
889

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

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

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

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

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

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

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

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

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

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

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

1043
    contexts.merge_provides();
37✔
1044

37✔
1045
    let parsing_time = start.elapsed();
37✔
1046
    let start = Instant::now();
37✔
1047

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

37✔
1055
    let treestate = FileTreeState::new(filenames.iter());
37✔
1056
    let stat_time = start.elapsed();
37✔
1057

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

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