1use 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#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SourceSet {
34 pub project_root: PathBuf,
35 pub files: Vec<PathBuf>,
36}
37
38impl SourceSet {
39 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 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 assert_eq!(names, vec!["aa.sv", "zz.sv", "hdr.svh"]);
186 }
187}