1use std::collections::BTreeSet;
11use std::fmt;
12use std::fs;
13use std::path::{Path, PathBuf};
14
15use crate::cond::Releases;
16use crate::merge::{self, Kind};
17
18#[derive(Debug, Clone)]
20pub struct Input {
21 pub minor: u32,
23 pub root: PathBuf,
25}
26
27#[derive(Debug, Clone)]
29pub struct Record {
30 pub path: String,
32 pub kind: Kind,
34 pub branches: usize,
36 pub guarded: bool,
38 pub releases: usize,
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct Report {
45 pub releases: Vec<String>,
47 pub records: Vec<Record>,
49 pub bytes_in: u64,
51 pub bytes_newest: u64,
54 pub bytes_out: u64,
56 pub problems: Vec<String>,
58}
59
60impl Report {
61 pub fn counted(&self, kind: Kind) -> usize {
63 self.records.iter().filter(|r| r.kind == kind).count()
64 }
65
66 pub fn busiest(&self, how_many: usize) -> Vec<&Record> {
68 let mut sorted: Vec<&Record> = self.records.iter().filter(|r| r.branches > 0).collect();
69 sorted.sort_by(|a, b| b.branches.cmp(&a.branches).then(a.path.cmp(&b.path)));
70 sorted.truncate(how_many);
71 sorted
72 }
73
74 pub fn listing(&self) -> String {
76 let mut out = String::new();
77 for record in &self.records {
78 let kind = match record.kind {
79 Kind::Same => "same",
80 Kind::Conditional => "merged",
81 Kind::PerRelease => "per-release",
82 };
83 let absent = if record.guarded { "guarded" } else { "everywhere" };
84 out.push_str(&format!(
85 "{}\t{kind}\t{}\t{absent}\t{}\n",
86 record.path, record.branches, record.releases
87 ));
88 }
89 out
90 }
91}
92
93impl fmt::Display for Report {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 writeln!(f, "headers: {} releases, {}", self.releases.len(), self.releases.join(" "))?;
96 writeln!(
97 f,
98 "headers: {} files, {} with no conditional, {} merged, {} per release, {} guarded",
99 self.records.len(),
100 self.counted(Kind::Same),
101 self.counted(Kind::Conditional),
102 self.counted(Kind::PerRelease),
103 self.records.iter().filter(|r| r.guarded).count(),
104 )?;
105 let branches: usize = self.records.iter().map(|r| r.branches).sum();
106 writeln!(f, "headers: {branches} conditionals written")?;
107 writeln!(
108 f,
109 "headers: {} KiB, against {} KiB for the newest install alone and {} KiB for all {}",
110 self.bytes_out / 1024,
111 self.bytes_newest / 1024,
112 self.bytes_in / 1024,
113 self.releases.len(),
114 )?;
115 for record in self.busiest(10) {
116 writeln!(f, "headers: {} has {} conditionals", record.path, record.branches)?;
117 }
118 Ok(())
119 }
120}
121
122pub fn merge_trees(inputs: &[Input], out: &Path) -> Result<Report, String> {
129 let releases = Releases::new(inputs.iter().map(|i| i.minor).collect())?;
130 if out.exists() && fs::read_dir(out).map(|mut d| d.next().is_some()).unwrap_or(false) {
131 return Err(format!("{} is not empty, and a merge writes a whole tree", out.display()));
132 }
133 let mut report = Report {
134 releases: (0..releases.count()).map(|n| releases.spelled(n)).collect(),
135 ..Report::default()
136 };
137
138 let mut paths: BTreeSet<String> = BTreeSet::new();
139 let mut found: Vec<BTreeSet<String>> = Vec::new();
140 for input in inputs {
141 let mut one = BTreeSet::new();
142 walk(&input.root, &input.root, &mut one)
143 .map_err(|why| format!("{}: {why}", input.root.display()))?;
144 paths.extend(one.iter().cloned());
145 found.push(one);
146 }
147 if paths.is_empty() {
148 return Err("none of the trees has a file in it".to_owned());
149 }
150
151 for path in &paths {
152 let mut texts: Vec<Option<String>> = Vec::with_capacity(inputs.len());
153 for (n, input) in inputs.iter().enumerate() {
154 if !found[n].contains(path) {
155 texts.push(None);
156 continue;
157 }
158 let whole = input.root.join(path);
159 let text =
160 fs::read_to_string(&whole).map_err(|why| format!("{}: {why}", whole.display()))?;
161 report.bytes_in += text.len() as u64;
162 if n + 1 == inputs.len() {
163 report.bytes_newest += text.len() as u64;
164 }
165 texts.push(Some(text));
166 }
167 let given: Vec<Option<&str>> = texts.iter().map(|t| t.as_deref()).collect();
168 let merged = merge::one(&releases, path, &given)?;
169 report.problems.extend(merged.problems.iter().cloned());
170 report.records.push(Record {
171 path: path.clone(),
172 kind: merged.kind,
173 branches: merged.branches,
174 guarded: merged.guarded,
175 releases: given.iter().filter(|t| t.is_some()).count(),
176 });
177 report.bytes_out += merged.text.len() as u64;
178 let whole = out.join(path);
179 if let Some(dir) = whole.parent() {
180 fs::create_dir_all(dir).map_err(|why| format!("{}: {why}", dir.display()))?;
181 }
182 fs::write(&whole, &merged.text).map_err(|why| format!("{}: {why}", whole.display()))?;
183 }
184 Ok(report)
185}
186
187fn walk(root: &Path, dir: &Path, out: &mut BTreeSet<String>) -> Result<(), String> {
193 let listing = fs::read_dir(dir).map_err(|why| format!("{}: {why}", dir.display()))?;
194 for entry in listing {
195 let entry = entry.map_err(|why| why.to_string())?;
196 let path = entry.path();
197 let kind = entry.file_type().map_err(|why| format!("{}: {why}", path.display()))?;
198 if kind.is_dir() {
199 walk(root, &path, out)?;
200 continue;
201 }
202 let relative = path
203 .strip_prefix(root)
204 .map_err(|_| format!("{} is not under {}", path.display(), root.display()))?;
205 let Some(name) = relative.to_str() else {
206 return Err(format!("{} is not a name this can write down", relative.display()));
207 };
208 out.insert(name.replace('\\', "/"));
209 }
210 Ok(())
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 fn scratch(name: &str) -> PathBuf {
219 let dir = std::env::temp_dir().join(format!("rucc-headers-{name}-{}", std::process::id()));
220 let _ = fs::remove_dir_all(&dir);
221 fs::create_dir_all(&dir).expect("a temporary directory");
222 dir
223 }
224
225 fn write(root: &Path, path: &str, text: &str) {
226 let whole = root.join(path);
227 fs::create_dir_all(whole.parent().expect("a parent")).expect("the directory");
228 fs::write(whole, text).expect("the file");
229 }
230
231 #[test]
232 fn two_trees_become_one_and_the_report_says_what_happened() {
233 let dir = scratch("two-trees");
234 let (old, new, out) = (dir.join("2.28"), dir.join("2.31"), dir.join("out"));
235 write(&old, "stdio.h", "int puts (const char *);\n");
236 write(&new, "stdio.h", "int puts (const char *);\nint newer (void);\n");
237 write(&old, "sys/gone.h", "int gone (void);\n");
238 write(&new, "sys/here.h", "int here (void);\n");
239 write(&old, "same.h", "int same (void);\n");
240 write(&new, "same.h", "int same (void);\n");
241
242 let inputs = vec![Input { minor: 28, root: old }, Input { minor: 31, root: new.clone() }];
243 let report = merge_trees(&inputs, &out).expect("a merge");
244 assert_eq!(report.problems, Vec::<String>::new());
245 assert_eq!(report.records.len(), 4);
246 assert_eq!(report.counted(Kind::Same), 3);
249 assert_eq!(report.records.iter().filter(|r| r.guarded).count(), 2);
250
251 assert_eq!(fs::read_to_string(out.join("same.h")).expect("written"), "int same (void);\n");
253 let here = fs::read_to_string(out.join("sys/here.h")).expect("written");
254 assert!(here.contains("#error"), "{here}");
255 assert!(here.contains("int here (void);"), "{here}");
256 assert_eq!(report.bytes_newest, newest_bytes(&new));
257 let _ = fs::remove_dir_all(&dir);
258 }
259
260 fn newest_bytes(root: &Path) -> u64 {
262 let mut paths = BTreeSet::new();
263 walk(root, root, &mut paths).expect("a walk");
264 paths.iter().map(|p| fs::metadata(root.join(p)).expect("there").len()).sum()
265 }
266
267 #[test]
268 fn a_tree_already_there_is_not_written_over() {
269 let dir = scratch("not-over");
270 let (old, new, out) = (dir.join("2.28"), dir.join("2.31"), dir.join("out"));
271 write(&old, "a.h", "int a (void);\n");
272 write(&new, "a.h", "int a (void);\n");
273 write(&out, "leftover.h", "from the last run\n");
274 let inputs = vec![Input { minor: 28, root: old }, Input { minor: 31, root: new }];
275 let why = merge_trees(&inputs, &out).expect_err("it is not empty");
276 assert!(why.contains("is not empty"), "{why}");
277 let _ = fs::remove_dir_all(&dir);
278 }
279
280 #[test]
281 fn one_release_is_not_a_merge() {
282 let dir = scratch("one-release");
283 let root = dir.join("2.28");
284 write(&root, "a.h", "int a (void);\n");
285 let why = merge_trees(&[Input { minor: 28, root }], &dir.join("out"))
286 .expect_err("one release is a copy");
287 assert!(why.contains("at least two releases"), "{why}");
288 let _ = fs::remove_dir_all(&dir);
289 }
290
291 #[test]
292 fn the_listing_has_one_line_per_file() {
293 let dir = scratch("listing");
294 let (old, new, out) = (dir.join("2.28"), dir.join("2.31"), dir.join("out"));
295 write(&old, "a.h", "int a (void);\n");
296 write(&new, "a.h", "int a (void);\nint b (void);\n");
297 let inputs = vec![Input { minor: 28, root: old }, Input { minor: 31, root: new }];
298 let report = merge_trees(&inputs, &out).expect("a merge");
299 assert_eq!(report.listing(), "a.h\tmerged\t1\teverywhere\t2\n");
300 assert!(format!("{report}").contains("1 files, 0 with no conditional, 1 merged"));
301 let _ = fs::remove_dir_all(&dir);
302 }
303}