Skip to main content

kiln_build/
source_set.rs

1//! Resolve manifest globs into a concrete list of source files.
2
3use std::path::{Path, PathBuf};
4
5use thiserror::Error;
6
7use kiln_core::Manifest;
8
9#[derive(Debug, Error)]
10pub enum SourceSetError {
11    #[error("invalid source glob `{glob}`: {source}")]
12    InvalidGlob {
13        glob: String,
14        #[source]
15        source: glob::PatternError,
16    },
17
18    #[error("error walking source glob `{glob}`: {source}")]
19    WalkGlob {
20        glob: String,
21        #[source]
22        source: glob::GlobError,
23    },
24
25    #[error("no source files matched the configured globs in `{root}`")]
26    NoSources { root: PathBuf },
27}
28
29/// Resolved source files from a [`Manifest`]. Paths are absolute and
30/// deduplicated; order is stable: the order globs appear in the manifest,
31/// then alphabetical within each glob.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SourceSet {
34    pub project_root: PathBuf,
35    pub files: Vec<PathBuf>,
36}
37
38impl SourceSet {
39    /// Resolve `manifest.design.sources` plus every
40    /// `[vendor.<name>] sim_models` and `stubs` glob against
41    /// `project_root`. Vendor sources are appended in vendor-name order
42    /// after all design sources, with cross-set deduplication.
43    pub fn resolve(project_root: &Path, manifest: &Manifest) -> Result<Self, SourceSetError> {
44        let mut files: Vec<PathBuf> = Vec::new();
45        let mut seen: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::new();
46
47        let mut all_globs: Vec<String> = manifest.design.sources.clone();
48        for vendor in manifest.vendor.values() {
49            all_globs.extend(vendor.sim_models.iter().cloned());
50            all_globs.extend(vendor.stubs.iter().cloned());
51        }
52
53        for raw_glob in &all_globs {
54            let pattern = if Path::new(raw_glob).is_absolute() {
55                raw_glob.clone()
56            } else {
57                project_root.join(raw_glob).to_string_lossy().into_owned()
58            };
59            let entries = glob::glob(&pattern).map_err(|source| SourceSetError::InvalidGlob {
60                glob: raw_glob.clone(),
61                source,
62            })?;
63            let mut matched: Vec<PathBuf> = Vec::new();
64            for entry in entries {
65                let path = entry.map_err(|source| SourceSetError::WalkGlob {
66                    glob: raw_glob.clone(),
67                    source,
68                })?;
69                if path.is_file() {
70                    let canonical = path.canonicalize().unwrap_or(path);
71                    if seen.insert(canonical.clone()) {
72                        matched.push(canonical);
73                    }
74                }
75            }
76            matched.sort();
77            files.extend(matched);
78        }
79
80        if files.is_empty() {
81            return Err(SourceSetError::NoSources {
82                root: project_root.to_path_buf(),
83            });
84        }
85
86        Ok(SourceSet {
87            project_root: project_root.to_path_buf(),
88            files,
89        })
90    }
91
92    /// Returns the resolved files. Equivalent to `&self.files`.
93    pub fn files(&self) -> &[PathBuf] {
94        &self.files
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn manifest(sources: &[&str]) -> Manifest {
103        let mut sources_block = String::from("sources = [\n");
104        for s in sources {
105            sources_block.push_str(&format!("    \"{s}\",\n"));
106        }
107        sources_block.push(']');
108        let toml_text = format!(
109            r#"
110            [package]
111            name = "demo"
112            version = "0.1.0"
113
114            [design]
115            top = "demo"
116            {sources_block}
117            "#
118        );
119        toml_text.parse::<Manifest>().unwrap()
120    }
121
122    #[test]
123    fn resolves_simple_glob() {
124        let tmp = tempfile::tempdir().unwrap();
125        let src = tmp.path().join("src");
126        std::fs::create_dir_all(&src).unwrap();
127        std::fs::write(src.join("a.sv"), "module a; endmodule").unwrap();
128        std::fs::write(src.join("b.sv"), "module b; endmodule").unwrap();
129        let m = manifest(&["src/**/*.sv"]);
130        let set = SourceSet::resolve(tmp.path(), &m).unwrap();
131        assert_eq!(set.files.len(), 2);
132        assert!(set.files.iter().all(|p| p.is_absolute()));
133        let names: Vec<_> = set
134            .files
135            .iter()
136            .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
137            .collect();
138        assert_eq!(names, vec!["a.sv", "b.sv"]);
139    }
140
141    #[test]
142    fn dedupes_overlapping_globs() {
143        let tmp = tempfile::tempdir().unwrap();
144        std::fs::create_dir_all(tmp.path().join("src")).unwrap();
145        std::fs::write(tmp.path().join("src/x.sv"), "module x; endmodule").unwrap();
146        std::fs::write(tmp.path().join("src/y.svh"), "// header").unwrap();
147        let m = manifest(&["src/**/*.sv", "src/**/*.svh", "src/**/*.sv"]);
148        let set = SourceSet::resolve(tmp.path(), &m).unwrap();
149        assert_eq!(set.files.len(), 2, "duplicates should be filtered");
150    }
151
152    #[test]
153    fn empty_match_errors() {
154        let tmp = tempfile::tempdir().unwrap();
155        std::fs::create_dir_all(tmp.path().join("src")).unwrap();
156        let m = manifest(&["src/**/*.sv"]);
157        let err = SourceSet::resolve(tmp.path(), &m).unwrap_err();
158        assert!(matches!(err, SourceSetError::NoSources { .. }));
159    }
160
161    #[test]
162    fn invalid_glob_pattern_errors() {
163        let tmp = tempfile::tempdir().unwrap();
164        let m = manifest(&["src/**/*.sv[", "src/**/*.svh"]);
165        let err = SourceSet::resolve(tmp.path(), &m).unwrap_err();
166        assert!(matches!(err, SourceSetError::InvalidGlob { .. }));
167    }
168
169    #[test]
170    fn order_is_glob_order_then_alphabetical() {
171        let tmp = tempfile::tempdir().unwrap();
172        let inc = tmp.path().join("src/inc");
173        std::fs::create_dir_all(&inc).unwrap();
174        std::fs::write(tmp.path().join("src/zz.sv"), "").unwrap();
175        std::fs::write(tmp.path().join("src/aa.sv"), "").unwrap();
176        std::fs::write(inc.join("hdr.svh"), "").unwrap();
177        let m = manifest(&["src/**/*.sv", "src/**/*.svh"]);
178        let set = SourceSet::resolve(tmp.path(), &m).unwrap();
179        let names: Vec<_> = set
180            .files
181            .iter()
182            .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
183            .collect();
184        // sv files first (alphabetical within glob), then svh.
185        assert_eq!(names, vec!["aa.sv", "zz.sv", "hdr.svh"]);
186    }
187}