Skip to main content

rucc_headers/
tree.rs

1//! Every file of every release, merged into one tree, with the numbers that come out of doing it.
2//!
3//! The per file work is in `merge.rs`. What is here is the part that makes it a tree: the union of
4//! the paths, the order they are done in, the writing, and the count of what happened. The count is
5//! the point as much as the tree is, because `spec/cross-compile/08-sysroots.md` section 8.3 names
6//! 210 files that change somewhere between 2.28 and 2.44 and this is what turns that survey into a
7//! statement about an artifact: how many files needed a conditional, how many could not be cut
8//! finer than the whole file, and what the tree weighs against the installs it came from.
9
10use 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/// One installed header tree and the release it is.
19#[derive(Debug, Clone)]
20pub struct Input {
21    /// The glibc minor version, so 28 for 2.28.
22    pub minor: u32,
23    /// The directory its headers are under, which is the one `usr/include` ends at.
24    pub root: PathBuf,
25}
26
27/// What one file became.
28#[derive(Debug, Clone)]
29pub struct Record {
30    /// Where it is in the tree.
31    pub path: String,
32    /// What had to be done to it.
33    pub kind: Kind,
34    /// How many conditionals of ours are in it.
35    pub branches: usize,
36    /// Whether some release does not have it.
37    pub guarded: bool,
38    /// How many releases have it.
39    pub releases: usize,
40}
41
42/// What a merge of a whole tree did.
43#[derive(Debug, Clone, Default)]
44pub struct Report {
45    /// How the releases are spelled, for the first line of the report.
46    pub releases: Vec<String>,
47    /// One entry per file in the merged tree.
48    pub records: Vec<Record>,
49    /// What the installs weigh, added up.
50    pub bytes_in: u64,
51    /// What the newest install weighs on its own, which is the honest thing to compare against:
52    /// shipping one release is the alternative to merging.
53    pub bytes_newest: u64,
54    /// What the merged tree weighs.
55    pub bytes_out: u64,
56    /// Everything that is wrong, which has to be empty for the tree to be worth shipping.
57    pub problems: Vec<String>,
58}
59
60impl Report {
61    /// How many files ended up each way.
62    pub fn counted(&self, kind: Kind) -> usize {
63        self.records.iter().filter(|r| r.kind == kind).count()
64    }
65
66    /// The files with the most conditionals in them, worst first.
67    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    /// The record as one line per file, for a reviewer who wants to see the whole list.
75    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
122/// Merges every input tree into one at `out`.
123///
124/// The output directory has to be empty or absent, because writing a merged tree over a tree that
125/// is already there leaves whatever the last run wrote and nothing says which file came from which
126/// run. The producer gets a fresh directory and renames it into place, the same way
127/// `rucc_driver::install` does with a sysroot.
128pub 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
187/// Every file under `dir`, named relative to `root`, with forward slashes.
188///
189/// Sorted, because the order files are merged in is the order the report lists them in and a report
190/// that depends on what order a directory happens to be read in is a report two people cannot
191/// compare.
192fn 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    /// A directory under the temporary directory, named after the test that wanted it.
218    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        // Three files needed no conditional: the one nobody changed and the two only one release
247        // has, which are wrapped in a guard and are otherwise a copy.
248        assert_eq!(report.counted(Kind::Same), 3);
249        assert_eq!(report.records.iter().filter(|r| r.guarded).count(), 2);
250
251        // Every file of both trees is in the output, and the one nobody changed is a copy.
252        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    /// What one install weighs, which the report has to agree with.
261    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}