use crate::cond::{self, Releases};
use crate::diff;
use crate::norm;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Same,
Conditional,
PerRelease,
}
#[derive(Debug, Clone)]
pub struct Merged {
pub text: String,
pub kind: Kind,
pub guarded: bool,
pub branches: usize,
pub defined_the_macro: bool,
pub problems: Vec<String>,
}
pub const GUARD: &str = "#ifndef __GLIBC_MINOR__\n# error \"this is rucc's merged glibc header \
tree, in which the compiler defines __GLIBC_MINOR__ from the target; see \
spec/cross-compile/08-sysroots.md section 8.3\"\n#endif\n";
pub fn one(releases: &Releases, path: &str, texts: &[Option<&str>]) -> Result<Merged, String> {
assert_eq!(texts.len(), releases.count(), "one text per release, present or not");
for (n, text) in texts.iter().enumerate() {
if text.is_some_and(cond::carries_mark) {
return Err(format!(
"{path}: the {} copy contains {}, so it has been through a merge already and \
merging it again would read its conditionals as ours",
releases.spelled(n),
cond::MARK
));
}
}
let mut problems = Vec::new();
let patched: Vec<Option<(String, bool)>> = texts.iter().map(|t| t.map(patch)).collect();
let want: Vec<Option<&str>> =
patched.iter().map(|p| p.as_ref().map(|(text, _)| text.as_str())).collect();
let present: Vec<bool> = want.iter().map(Option::is_some).collect();
let have: Vec<usize> = (0..releases.count()).filter(|&n| present[n]).collect();
let (Some(&first), Some(&newest)) = (have.first(), have.last()) else {
return Err(format!("{path}: no release has it"));
};
let cut: Vec<Option<norm::Pieces>> = want.iter().map(|t| t.map(norm::pieces)).collect();
let pieces = |which: usize| cut[which].as_ref().expect("a release that has the file");
let mut spine: Vec<String> = pieces(first).keys().iter().map(|&k| k.to_owned()).collect();
let mut at: Vec<Vec<usize>> = vec![(0..spine.len()).collect()];
for &r in &have[1..] {
let theirs = pieces(r).keys();
let mine: Vec<&str> = spine.iter().map(String::as_str).collect();
let pairs = diff::aligned(&mine, &theirs);
let kept: Vec<String> = pairs.iter().map(|&(x, _)| spine[x].clone()).collect();
for row in &mut at {
*row = pairs.iter().map(|&(x, _)| row[x]).collect();
}
at.push(pairs.iter().map(|&(_, y)| y).collect());
spine = kept;
}
let slots = 2 * spine.len() + 1;
let by_slot: Vec<Vec<String>> = have
.iter()
.enumerate()
.map(|(j, &r)| {
let items = &pieces(r).items;
(0..slots)
.map(|slot| {
if slot % 2 == 1 {
return items[at[j][slot / 2]].text.clone();
}
let gap = slot / 2;
let from = if gap == 0 { 0 } else { at[j][gap - 1] + 1 };
let to = if gap == spine.len() { items.len() } else { at[j][gap] };
let mut text: String =
items[from..to].iter().map(|i| i.text.as_str()).collect();
if gap == spine.len() {
text.push_str(&pieces(r).tail);
}
text
})
.collect()
})
.collect();
let region = |lo: usize, hi: usize| grouped(&have, |j, _| by_slot[j][lo..=hi].concat());
let content: Vec<bool> =
(0..slots).map(|slot| by_slot.iter().any(|row| !row[slot].is_empty())).collect();
let whole_file =
|lo: usize, hi: usize| !content[..lo].contains(&true) && !content[hi + 1..].contains(&true);
let mut regions: Vec<(usize, usize)> = Vec::new();
let mut slot = 0;
while slot < slots {
let (mut lo, mut hi) = (slot, slot);
loop {
let groups = region(lo, hi);
let reach =
groups.iter().fold(Reach::default(), |all, (_, _, text)| all.with(&needs(text)));
if groups.len() == 1 || !reach.out_of_it() {
break;
}
let below = reach.below && hi + 1 < slots;
let above = reach.above && lo > 0;
if below {
hi += 1;
} else if above {
lo = regions.pop().expect("a region above to take back").0;
} else if hi + 1 < slots {
hi += 1;
} else if lo > 0 {
lo = regions.pop().expect("a region above to take back").0;
} else {
break;
}
}
regions.push((lo, hi));
slot = hi + 1;
}
let mut body = String::new();
let mut branches = 0;
let mut kind = Kind::Same;
for &(lo, hi) in ®ions {
let groups = region(lo, hi);
if let [(_, _, only)] = &groups[..] {
body.push_str(only);
continue;
}
let said: Vec<&(String, Vec<usize>, String)> =
groups.iter().filter(|(_, _, text)| !text.is_empty()).collect();
branches += 1;
kind = if whole_file(lo, hi) { Kind::PerRelease } else { Kind::Conditional };
for (n, (_, members, text)) in said.iter().enumerate() {
line_end(&mut body);
body.push_str(&cond::directive(
if n == 0 { "if" } else { "elif" },
Some(&condition(releases, members)),
));
body.push_str(text);
if !stands_alone(text) {
problems.push(format!(
"{path}: the copies for {} do not have balanced conditionals, so no branch \
around them is right",
spelled(releases, members)
));
}
}
line_end(&mut body);
body.push_str(&cond::directive("endif", None));
}
let guarded = have.len() != releases.count();
let text = if have.len() == releases.count() {
body
} else {
let mut text = cond::directive("if", Some(&condition(releases, &have)));
text.push_str(&body);
line_end(&mut text);
text.push_str(&cond::directive("else", None));
text.push_str(&format!(
"#error \"rucc: {path} is not a header of this glibc release; it is in {}\"\n",
spelled(releases, &have)
));
text.push_str(&cond::directive("endif", None));
text
};
for (n, each) in want.iter().enumerate() {
let Some(each) = each else { continue };
match cond::evaluate(&text, releases.minors()[n]) {
Ok(got) if norm::code(&got) == norm::code(each) => {}
Ok(got) => problems.push(format!(
"{path}: what this writes does not give the {} copy back, {}",
releases.spelled(n),
first_difference(&norm::code(&got), &norm::code(each))
)),
Err(why) => problems.push(format!(
"{path}: reading back what this writes for {} failed: {why}",
releases.spelled(n)
)),
}
}
if kind == Kind::Same && !guarded {
let same = want[newest].unwrap_or_default();
if text.trim_end_matches('\n') != same.trim_end_matches('\n') {
problems.push(format!(
"{path}: no conditional was needed and the text still is not the {} copy",
releases.spelled(newest)
));
}
}
Ok(Merged {
text,
kind,
guarded,
branches,
defined_the_macro: patched.iter().flatten().any(|(_, did)| *did),
problems,
})
}
fn grouped(
have: &[usize],
mut text: impl FnMut(usize, usize) -> String,
) -> Vec<(String, Vec<usize>, String)> {
let mut groups: Vec<(String, Vec<usize>, String)> = Vec::new();
for (j, &r) in have.iter().enumerate() {
let text = text(j, r);
let code = norm::code(&text);
match groups.iter_mut().find(|group| group.0 == code) {
Some(group) => {
group.1.push(r);
group.2 = text;
}
None => groups.push((code, vec![r], text)),
}
}
groups
}
fn condition(releases: &Releases, members: &[usize]) -> String {
let mut flags = vec![false; releases.count()];
for &m in members {
flags[m] = true;
}
releases.condition(&flags).unwrap_or_else(|| "1".to_owned())
}
fn spelled(releases: &Releases, members: &[usize]) -> String {
members.iter().map(|&m| releases.spelled(m)).collect::<Vec<_>>().join(" ")
}
fn stands_alone(text: &str) -> bool {
!needs(text).out_of_it()
}
#[derive(Debug, Default, Clone, Copy)]
struct Reach {
above: bool,
below: bool,
}
impl Reach {
fn with(self, other: &Reach) -> Self {
Self { above: self.above || other.above, below: self.below || other.below }
}
fn out_of_it(self) -> bool {
self.above || self.below
}
}
fn needs(text: &str) -> Reach {
let mut depth = 0i32;
let mut reach = Reach::default();
for line in norm::code(text).lines() {
let Some(rest) = line.trim_start().strip_prefix('#') else { continue };
let rest = rest.trim_start();
if rest.starts_with("if") {
depth += 1;
} else if rest.starts_with("endif") {
depth -= 1;
if depth < 0 {
reach.above = true;
depth = 0;
}
} else if (rest.starts_with("else") || rest.starts_with("elif")) && depth == 0 {
reach.above = true;
}
}
if depth > 0 {
reach.below = true;
}
reach
}
fn patch(text: &str) -> (String, bool) {
let cut = norm::pieces(text);
if !cut.items.iter().any(|item| defines_the_macro(&item.key)) {
return (text.to_owned(), false);
}
let mut out = String::with_capacity(text.len());
for item in &cut.items {
if defines_the_macro(&item.key) {
out.push_str(&item.text[..item.code_at]);
out.push_str(GUARD);
} else {
out.push_str(&item.text);
}
}
out.push_str(&cut.tail);
(out, true)
}
fn defines_the_macro(key: &str) -> bool {
let Some(rest) = key.strip_prefix('#') else { return false };
let Some(rest) = rest.trim_start().strip_prefix("define") else { return false };
let Some(rest) = rest.trim_start().strip_prefix(cond::MACRO) else { return false };
let value = rest.trim();
!value.is_empty() && value.bytes().all(|b| b.is_ascii_digit())
}
fn line_end(out: &mut String) {
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
}
fn first_difference(got: &str, want: &str) -> String {
for (n, (left, right)) in got.lines().zip(want.lines()).enumerate() {
if left != right {
return format!("at line {} of the code: {} against {}", n + 1, cut(left), cut(right));
}
}
format!("{} lines of code against {}", got.lines().count(), want.lines().count())
}
fn cut(line: &str) -> String {
let line = line.trim();
if line.chars().count() <= 60 {
return format!("`{line}`");
}
format!("`{}...`", line.chars().take(57).collect::<String>())
}
#[cfg(test)]
mod tests {
use super::*;
fn releases() -> Releases {
Releases::new(vec![28, 31, 34]).expect("ascending")
}
fn merged(texts: &[Option<&str>]) -> Merged {
let all = releases();
let out = one(&all, "sys/thing.h", texts).expect("a tree nobody merged before");
assert_eq!(out.problems, Vec::<String>::new());
for (n, want) in texts.iter().enumerate() {
if let Some(want) = want {
let got = cond::evaluate(&out.text, all.minors()[n]).expect("our own conditionals");
assert_eq!(norm::code(&got), norm::code(want), "the 2.{} copy", all.minors()[n]);
}
}
out
}
#[test]
fn three_copies_of_one_file_are_that_file() {
let text = "#ifndef _THING_H\n#define _THING_H 1\nint f (void);\n#endif\n";
let out = merged(&[Some(text), Some(text), Some(text)]);
assert_eq!(out.kind, Kind::Same);
assert_eq!(out.text, text);
assert_eq!(out.branches, 0);
assert!(!out.guarded);
}
#[test]
fn a_year_in_a_comment_is_not_worth_a_conditional() {
let old = "/* Copyright (C) 2018 FSF. */\nint f (void);\n";
let new = "/* Copyright (C) 2024 FSF. */\nint f (void);\n";
let out = merged(&[Some(old), Some(old), Some(new)]);
assert_eq!(out.kind, Kind::Same);
assert_eq!(out.text, new);
}
#[test]
fn a_declaration_added_in_the_newest_release_is_behind_a_conditional() {
let old = "int f (void);\n";
let new = "int f (void);\nint g (void);\n";
let out = merged(&[Some(old), Some(old), Some(new)]);
assert_eq!(out.kind, Kind::Conditional);
assert_eq!(out.branches, 1);
assert_eq!(
out.text,
"int f (void);\n#if __GLIBC_MINOR__ >= 34 /* rucc */\nint g (void);\n#endif /* rucc */\n"
);
}
#[test]
fn a_declaration_removed_in_the_newest_release_is_behind_one_too() {
let old = "int f (void);\nint gone (void);\n";
let new = "int f (void);\n";
let out = merged(&[Some(old), Some(old), Some(new)]);
assert_eq!(out.kind, Kind::Conditional);
assert!(out.text.contains("#if __GLIBC_MINOR__ < 34 /* rucc */"), "{}", out.text);
}
#[test]
fn a_constant_that_changed_value_is_one_conditional_with_two_branches() {
let out = merged(&[
Some("#define _STAT_VER 1\n"),
Some("#define _STAT_VER 1\n"),
Some("#define _STAT_VER 3\n"),
]);
assert_eq!(out.branches, 1);
assert_eq!(out.text.matches("#elif").count(), 1);
}
#[test]
fn a_header_that_arrives_later_says_so_for_the_releases_without_it() {
let out = merged(&[None, None, Some("int f (void);\n")]);
assert!(out.guarded);
assert!(out.text.starts_with("#if __GLIBC_MINOR__ >= 34 /* rucc */"), "{}", out.text);
assert!(
out.text.contains(
"#error \"rucc: sys/thing.h is not a header of this glibc \
release; it is in 2.34\""
),
"{}",
out.text
);
let gone = cond::evaluate(&out.text, 28).expect("ours");
assert!(gone.contains("#error"), "{gone}");
assert!(!gone.contains("int f (void);"), "{gone}");
}
#[test]
fn a_header_that_went_away_is_the_same_the_other_way_round() {
let out = merged(&[Some("int f (void);\n"), Some("int f (void);\n"), None]);
assert!(out.guarded);
assert!(out.text.starts_with("#if __GLIBC_MINOR__ < 34 /* rucc */"), "{}", out.text);
}
#[test]
fn a_region_inside_the_files_own_conditional_is_left_where_it_is() {
let old = "#ifdef __USE_GNU\nint f (void);\n#endif\n";
let new = "#ifdef __USE_GNU\nint f (void);\nint g (void);\n#endif\nint h (void);\n";
let out = merged(&[Some(old), Some(old), Some(new)]);
assert_eq!(out.kind, Kind::Conditional);
assert_eq!(out.branches, 2);
assert!(out.text.contains("#ifdef __USE_GNU\n"), "{}", out.text);
for release in [28, 34] {
let got = cond::evaluate(&out.text, release).expect("ours");
assert_eq!(got.matches("#ifdef __USE_GNU").count(), 1, "{got}");
assert_eq!(got.matches("#endif").count(), 1, "{got}");
}
}
#[test]
fn a_changed_condition_takes_its_block_with_it_and_not_the_file() {
let old = "int before (void);\n#ifdef A\nint f (void);\n#endif\nint after (void);\n";
let new = "int before (void);\n#if defined A || defined B\nint f (void);\n#endif\n\
int after (void);\n";
let out = merged(&[Some(old), Some(old), Some(new)]);
assert_eq!(out.kind, Kind::Conditional);
assert_eq!(out.branches, 1);
assert_eq!(out.text.matches("int before (void);").count(), 1, "{}", out.text);
assert_eq!(out.text.matches("int after (void);").count(), 1, "{}", out.text);
assert_eq!(out.text.matches("int f (void);").count(), 2, "{}", out.text);
}
#[test]
fn a_file_whose_conditionals_nest_differently_is_one_copy_per_release() {
let old = "#if A\nint f (void);\n#endif\n#if B\nint g (void);\n#endif\n";
let new = "#if A\nint f (void);\n#if B\nint g (void);\n#endif\n#endif\n";
let out = merged(&[Some(old), Some(old), Some(new)]);
assert_eq!(out.kind, Kind::PerRelease, "{}", out.text);
assert_eq!(out.branches, 1);
assert!(out.text.starts_with("#if __GLIBC_MINOR__ < 34 /* rucc */"), "{}", out.text);
assert_eq!(out.text.matches("int f (void);").count(), 2, "{}", out.text);
assert!(out.problems.is_empty(), "{:?}", out.problems);
}
#[test]
fn the_definition_of_the_version_macro_is_replaced_by_the_check_for_one() {
let all = releases();
let texts: Vec<String> = all
.minors()
.iter()
.map(|m| format!("#define __GLIBC__ 2\n#define\t__GLIBC_MINOR__\t{m}\nint f (void);\n"))
.collect();
let given: Vec<Option<&str>> = texts.iter().map(|t| Some(t.as_str())).collect();
let out = one(&all, "features.h", &given).expect("not merged before");
assert_eq!(out.problems, Vec::<String>::new());
assert!(out.defined_the_macro);
assert_eq!(out.kind, Kind::Same, "{}", out.text);
assert!(!out.text.contains("#define\t__GLIBC_MINOR__"), "{}", out.text);
assert!(out.text.contains("#define __GLIBC__ 2"), "{}", out.text);
assert!(out.text.contains("#ifndef __GLIBC_MINOR__"), "{}", out.text);
assert!(out.text.contains("# error"), "{}", out.text);
}
#[test]
fn a_comment_about_the_version_macro_is_left_alone() {
let text = "/* #define __GLIBC_MINOR__ 44 is what glibc does. */\nint f (void);\n";
let out = merged(&[Some(text), Some(text), Some(text)]);
assert!(!out.defined_the_macro);
assert_eq!(out.text, text);
}
#[test]
fn a_tree_that_has_been_merged_once_is_refused() {
let all = releases();
let text = format!("int f (void);\n{}", cond::directive("endif", None));
let why = one(&all, "sys/thing.h", &[Some(&text), Some(&text), Some(&text)])
.expect_err("it carries the marker");
assert!(why.contains("through a merge already"), "{why}");
}
#[test]
fn a_file_no_release_has_is_an_error_rather_than_an_empty_file() {
assert!(one(&releases(), "sys/thing.h", &[None, None, None]).is_err());
}
#[test]
fn a_continued_macro_that_changed_is_replaced_whole() {
let old = "#define F(a) \\\n ((a) + 1)\nint f (void);\n";
let new = "#define F(a) \\\n ((a) + 2)\nint f (void);\n";
let out = merged(&[Some(old), Some(old), Some(new)]);
assert_eq!(out.kind, Kind::Conditional);
for release in [28, 34] {
let got = cond::evaluate(&out.text, release).expect("ours");
assert_eq!(got.matches("#define F(a)").count(), 1, "{got}");
}
}
#[test]
fn a_file_with_no_trailing_newline_still_gets_whole_directives() {
let out = merged(&[Some("int f (void);"), Some("int f (void);"), Some("int g (void);")]);
for line in out.text.lines() {
assert!(!line.contains("#endif") || line.trim_start().starts_with('#'), "{line}");
}
}
#[test]
fn which_way_a_branch_reaches_out_of_itself() {
assert!(needs("#endif\n").above);
assert!(needs("#else\nint f (void);\n").above);
assert!(needs("#ifdef A\nint f (void);\n").below);
assert!(!needs("#ifdef A\nint f (void);\n#endif\n").out_of_it());
let both = needs("#endif\n#ifdef A\nint f (void);\n");
assert!(both.above && both.below);
}
#[test]
fn what_stands_alone_and_what_does_not() {
assert!(stands_alone("int f (void);\n"));
assert!(stands_alone("#ifdef A\nint f (void);\n#endif\n"));
assert!(!stands_alone("#endif\n"));
assert!(!stands_alone("#else\nint f (void);\n"));
assert!(!stands_alone("#ifdef A\nint f (void);\n"));
assert!(stands_alone("/* #endif */\nint f (void);\n"));
}
}