Skip to main content

libc_abi_tools/provider/
build.rs

1//! Builds a release's startup objects and nonshared archives from recorded
2//! sources with the host's GCC, and verifies every object against the interface
3//! recorded from the container build. The same procedure as glibc-provider's
4//! `glibc-provider-replay`, which qualifies GCC versions in CI.
5//!
6//! Needs GCC and GNU binutils (`ar`, `readelf`).
7use crate::sha256;
8use std::{
9    collections::{BTreeMap, BTreeSet},
10    fs,
11    path::{Path, PathBuf},
12    process::Command,
13};
14
15/// GCC major versions qualified in CI for every release (glibc-provider's Replay
16/// workflow). Other versions are verified locally before use.
17pub const QUALIFIED_GCC: std::ops::RangeInclusive<u32> = 11..=16;
18
19/// One object behind a target, as recorded in `objects.tsv`.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Object {
22    /// The installed file the object goes into: a startup object such as
23    /// `crt1.o`, or an archive such as `libc_nonshared.a`.
24    pub target: String,
25    /// The object's position in the archive, for archive targets.
26    pub member: u32,
27    /// The object's path in the build tree.
28    pub object: String,
29    /// How glibc's build made it: `compile` (from a source file) or `relocatable`
30    /// (a relocatable link of other objects).
31    pub kind: String,
32    /// The directory, relative to the source tree, the command ran in (`.` for
33    /// the top).
34    pub directory: String,
35    /// The recorded GCC command line.
36    pub command: String,
37}
38
39/// Runs PROGRAM in DIR with search-path overrides removed and a fixed locale.
40pub(crate) fn run(program: &Path, args: &[String], dir: &Path) -> Result<String, String> {
41    let mut command = Command::new(program);
42    command.args(args).current_dir(dir).env("LC_ALL", "C");
43    for key in [
44        "CPATH",
45        "C_INCLUDE_PATH",
46        "CPLUS_INCLUDE_PATH",
47        "OBJC_INCLUDE_PATH",
48        "LIBRARY_PATH",
49        "GCC_EXEC_PREFIX",
50        "COMPILER_PATH",
51        "DEPENDENCIES_OUTPUT",
52        "SUNPRO_DEPENDENCIES",
53    ] {
54        command.env_remove(key);
55    }
56    let output = command
57        .output()
58        .map_err(|e| format!("cannot run {}: {e}", program.display()))?;
59    if !output.status.success() {
60        return Err(format!(
61            "{} failed in {}: {}",
62            program.display(),
63            dir.display(),
64            String::from_utf8_lossy(&output.stderr).trim()
65        ));
66    }
67    Ok(String::from_utf8_lossy(&output.stdout).into_owned())
68}
69
70/// The host tools a local build uses.
71#[derive(Clone, Debug)]
72pub struct Tools {
73    /// GCC, which compiles the objects and supplies `crtbegin.o`/`crtend.o`.
74    pub gcc: PathBuf,
75    /// GNU `ar`, which builds the nonshared archives.
76    pub ar: PathBuf,
77    /// GNU `readelf`, which reads the objects' interfaces for verification.
78    pub readelf: PathBuf,
79    /// The assembler GCC uses (`as`); its version is part of the content hash.
80    pub assembler: PathBuf,
81    /// The linker GCC uses (`ld`); its version is part of the content hash.
82    pub linker: PathBuf,
83}
84
85impl Default for Tools {
86    fn default() -> Self {
87        Self {
88            gcc: "gcc".into(),
89            ar: "ar".into(),
90            readelf: "readelf".into(),
91            assembler: "as".into(),
92            linker: "ld".into(),
93        }
94    }
95}
96
97impl Tools {
98    /// The first line of each tool's `--version`, which identifies the toolchain.
99    pub fn versions(&self) -> Result<Vec<(&'static str, String)>, String> {
100        let mut versions = Vec::new();
101        for (name, path) in [
102            ("gcc", &self.gcc),
103            ("as", &self.assembler),
104            ("ld", &self.linker),
105            ("ar", &self.ar),
106            ("readelf", &self.readelf),
107        ] {
108            let text = run(path, &["--version".into()], Path::new("."))?;
109            versions.push((name, text.lines().next().unwrap_or("").trim().to_string()));
110        }
111        if versions[0].1.to_ascii_lowercase().contains("clang") {
112            return Err(format!(
113                "{} is Clang; glibc's sources need GCC (Clang ignores attributes glibc \
114                 applies after a definition and would export hidden symbols)",
115                self.gcc.display()
116            ));
117        }
118        Ok(versions)
119    }
120
121    /// GCC's major version.
122    pub fn gcc_major(&self) -> Result<u32, String> {
123        let text = run(&self.gcc, &["-dumpversion".into()], Path::new("."))?;
124        text.trim()
125            .split('.')
126            .next()
127            .and_then(|m| m.parse().ok())
128            .ok_or_else(|| format!("cannot read GCC's version from {text:?}"))
129    }
130
131    /// The directory of the host GCC's `crtbegin.o` and `crtend.o`.
132    pub fn gcc_startup(&self) -> Result<PathBuf, String> {
133        let text = run(
134            &self.gcc,
135            &["-print-file-name=crtbegin.o".into()],
136            Path::new("."),
137        )?;
138        let path = PathBuf::from(text.trim());
139        if !path.is_absolute() || !path.is_file() {
140            return Err(format!("{} has no crtbegin.o", self.gcc.display()));
141        }
142        let dir = path.parent().ok_or("crtbegin.o has no directory")?;
143        if !dir.join("crtend.o").is_file() {
144            return Err(format!("{} lacks crtend.o", dir.display()));
145        }
146        fs::canonicalize(dir).map_err(|e| e.to_string())
147    }
148}
149
150/// Whether GCC accepts FLAG when assembling an empty file.
151fn accepts(tools: &Tools, flag: &str, scratch: &Path) -> bool {
152    let (source, object) = (scratch.join("probe.s"), scratch.join("probe.o"));
153    if fs::write(&source, "").is_err() {
154        return false;
155    }
156    let args = [
157        "-c".to_string(),
158        source.to_string_lossy().into_owned(),
159        "-o".into(),
160        object.to_string_lossy().into_owned(),
161        flag.into(),
162    ];
163    let accepted = run(&tools.gcc, &args, scratch).is_ok();
164    let _ = fs::remove_file(&source);
165    let _ = fs::remove_file(&object);
166    accepted
167}
168
169/// Flags that make the builder's implicit code-generation defaults explicit
170/// and switch off the host compiler's own (distribution) defaults, for whatever
171/// a recorded command does not already set. `builder` holds `release.tsv`'s
172/// `builder.*` values.
173pub fn defaults_flags(command: &str, builder: &BTreeMap<String, String>) -> Vec<String> {
174    let tokens: Vec<&str> = command.split_whitespace().collect();
175    let has = |prefixes: &[&str]| {
176        tokens
177            .iter()
178            .any(|t| prefixes.iter().any(|p| t.starts_with(p)))
179    };
180    let on = |key: &str| {
181        builder
182            .get(&format!("builder.{key}"))
183            .is_some_and(|v| v == "yes")
184    };
185    let mut flags = Vec::new();
186    if !has(&[
187        "-fPIC", "-fpic", "-fPIE", "-fpie", "-fno-pic", "-fno-PIC", "-fno-pie", "-fno-PIE",
188    ]) {
189        flags.push(if on("pie") { "-fPIE" } else { "-fno-pie" }.to_string());
190    }
191    if !has(&["-fstack-protector", "-fno-stack-protector"]) && !on("stack_protector") {
192        flags.push("-fno-stack-protector".into());
193    }
194    if !has(&["-fcf-protection"]) && !on("cet") {
195        flags.push("-fcf-protection=none".into());
196    }
197    if !on("fortify") {
198        flags.push("-U_FORTIFY_SOURCE".into());
199    }
200    if !has(&["-fstack-clash-protection", "-fno-stack-clash-protection"]) {
201        flags.push("-fno-stack-clash-protection".into());
202    }
203    flags
204}
205
206/// A recorded command for the host's GCC: `@SRC@`/`@BUILD@` replaced, dependency
207/// files dropped, EXTRA appended.
208pub fn translate(
209    command: &str,
210    gcc: &str,
211    src: &str,
212    build: &str,
213    extra: &[String],
214) -> Result<Vec<String>, String> {
215    let mut tokens: Vec<String> = command
216        .split_whitespace()
217        .map(|t| t.replace("@SRC@", src).replace("@BUILD@", build))
218        .collect();
219    if tokens.first().map(String::as_str) != Some("gcc") {
220        return Err(format!("recorded command does not run gcc: {command:?}"));
221    }
222    tokens[0] = gcc.into();
223    let mut out = Vec::new();
224    let mut skip = false;
225    for token in tokens {
226        if skip {
227            skip = false;
228            continue;
229        }
230        match token.as_str() {
231            "-MD" | "-MP" => {}
232            "-MF" | "-MT" => skip = true,
233            _ => out.push(token),
234        }
235    }
236    out.extend(extra.iter().cloned());
237    Ok(out)
238}
239
240/// What must match between two relocatable objects.
241#[derive(Clone, Debug, Default, PartialEq, Eq)]
242pub struct Interface {
243    /// Defined global/weak symbols: name, type, binding, visibility, section.
244    pub defined: BTreeSet<String>,
245    /// Undefined global/weak symbols: name, binding.
246    pub undefined: BTreeSet<String>,
247    /// Non-empty allocated sections (name, flags), except the stack note.
248    pub sections: BTreeSet<String>,
249    /// References outside debug sections: (section, form, symbol).
250    pub references: BTreeSet<(String, String, String)>,
251}
252
253/// The form of a relocation, merging encodings the linker resolves alike:
254/// newer assemblers emit `PLT32` for calls where older ones emitted `PC32`
255/// (binutils 2.31), and relaxable `GOTPCRELX` forms where older ones emitted
256/// `GOTPCREL` (binutils 2.26).
257fn relocation_form(kind: &str) -> String {
258    match kind {
259        "R_X86_64_PC32" | "R_X86_64_PLT32" => "pc-relative".into(),
260        "R_X86_64_GOTPCREL" | "R_X86_64_GOTPCRELX" | "R_X86_64_REX_GOTPCRELX" => "got".into(),
261        other => other.into(),
262    }
263}
264
265/// An object's interface and the SHA-256 of its allocated sections' names and
266/// bytes, read with `readelf`.
267pub fn interface(readelf: &Path, object: &Path) -> Result<(Interface, String), String> {
268    let path = object.to_string_lossy().to_string();
269    let here = Path::new(".");
270    let read = |flags: &[&str]| {
271        let mut args: Vec<String> = flags.iter().map(|f| f.to_string()).collect();
272        args.push(path.clone());
273        run(readelf, &args, here)
274    };
275    let mut names: BTreeMap<String, String> = BTreeMap::new();
276    let mut result = Interface::default();
277    let mut allocated = Vec::new();
278    for line in read(&["-W", "-S"])?.lines() {
279        let Some(rest) = line.trim_start().strip_prefix('[') else {
280            continue;
281        };
282        let Some((index, rest)) = rest.split_once(']') else {
283            continue;
284        };
285        let fields: Vec<&str> = rest.split_whitespace().collect();
286        if fields.len() < 7 || fields[0] == "Name" {
287            continue;
288        }
289        names.insert(index.trim().to_string(), fields[0].to_string());
290        // Name Type Address Off Size ES Flg Lk Inf Al
291        let flags = if fields.len() >= 10 { fields[6] } else { "" };
292        let empty = u64::from_str_radix(fields[4], 16).is_ok_and(|size| size == 0);
293        if flags.contains('A') && fields[0] != ".note.GNU-stack" && !empty {
294            result.sections.insert(format!("{} {}", fields[0], flags));
295            allocated.push(fields[0].to_string());
296        }
297    }
298    for line in read(&["-W", "-s"])?.lines() {
299        let f: Vec<&str> = line.split_whitespace().collect();
300        if f.len() < 8 || !f[0].ends_with(':') || f[0] == "Num:" {
301            continue;
302        }
303        let (kind, bind, visibility, index, name) = (f[3], f[4], f[5], f[6], f[7]);
304        if bind == "LOCAL" {
305            continue;
306        }
307        if index == "UND" {
308            // Older GNU as references the linker-defined GOT symbol.
309            if name != "_GLOBAL_OFFSET_TABLE_" {
310                result.undefined.insert(format!("{name} {bind}"));
311            }
312        } else {
313            let section = names.get(index).map_or(index, String::as_str);
314            result
315                .defined
316                .insert(format!("{name} {kind} {bind} {visibility} {section}"));
317        }
318    }
319    let mut section = String::new();
320    for line in read(&["-W", "-r"])?.lines() {
321        if let Some(rest) = line.strip_prefix("Relocation section '") {
322            section = rest.split('\'').next().unwrap_or("").to_string();
323            continue;
324        }
325        if section.contains(".debug") || section.is_empty() {
326            continue;
327        }
328        let f: Vec<&str> = line.split_whitespace().collect();
329        if f.len() < 3 || !f[2].starts_with("R_X86_64") {
330            continue;
331        }
332        let symbol = f.get(4).map_or("", |s| s.split('+').next().unwrap_or(s));
333        result
334            .references
335            .insert((section.clone(), relocation_form(f[2]), symbol.to_string()));
336    }
337    let mut bytes = Vec::new();
338    for name in allocated {
339        bytes.extend(name.as_bytes());
340        bytes.push(0);
341        bytes.extend(hex_dump_bytes(&read(&["-W", "-x", &name])?));
342    }
343    Ok((result, sha256::hex(&sha256::sha256(&bytes))))
344}
345
346/// The bytes of a `readelf -x` dump: each data line is `  0xADDRESS ` followed
347/// by up to four groups of eight hex digits in a fixed 35-column field, then
348/// the ASCII rendering.
349fn hex_dump_bytes(dump: &str) -> Vec<u8> {
350    let mut bytes = Vec::new();
351    for line in dump.lines() {
352        let Some(rest) = line.trim_start().strip_prefix("0x") else {
353            continue;
354        };
355        let Some((_, data)) = rest.split_once(' ') else {
356            continue;
357        };
358        let field: String = data
359            .chars()
360            .take(35)
361            .filter(|c| !c.is_whitespace())
362            .collect();
363        bytes.extend(
364            (0..field.len() / 2)
365                .filter_map(|i| u8::from_str_radix(field.get(2 * i..2 * i + 2)?, 16).ok()),
366        );
367    }
368    bytes
369}
370
371/// Reads `interfaces.tsv`: object -> (interface, allocated-bytes SHA-256).
372pub fn read_interfaces(text: &str) -> Result<BTreeMap<String, (Interface, String)>, String> {
373    let mut lines = text.lines();
374    if lines.next() != Some("object\tkind\tvalue") {
375        return Err("interfaces.tsv: unexpected header".into());
376    }
377    let mut map: BTreeMap<String, (Interface, String)> = BTreeMap::new();
378    for line in lines {
379        let [object, kind, value] = line.split('\t').collect::<Vec<_>>()[..] else {
380            return Err(format!("interfaces.tsv: malformed row {line:?}"));
381        };
382        let (interface, digest) = map.entry(object.to_string()).or_default();
383        let value = value.to_string();
384        match kind {
385            "defined" => drop(interface.defined.insert(value)),
386            "undefined" => drop(interface.undefined.insert(value)),
387            "section" => drop(interface.sections.insert(value)),
388            "reference" => {
389                let mut parts = value.splitn(3, ' ').map(String::from);
390                let (Some(s), Some(f), Some(n)) = (parts.next(), parts.next(), parts.next()) else {
391                    return Err(format!("interfaces.tsv: malformed reference {value}"));
392                };
393                interface.references.insert((s, f, n));
394            }
395            "bytes" => *digest = value,
396            _ => return Err(format!("interfaces.tsv: unknown kind {kind}")),
397        }
398    }
399    Ok(map)
400}
401
402/// How RECORDED (the container build) and BUILT differ; empty if they match.
403pub fn describe(recorded: &Interface, built: &Interface) -> String {
404    let diff = |name: &str, x: &BTreeSet<String>, y: &BTreeSet<String>| -> Option<String> {
405        let only_x: Vec<_> = x.difference(y).cloned().collect();
406        let only_y: Vec<_> = y.difference(x).cloned().collect();
407        (!only_x.is_empty() || !only_y.is_empty())
408            .then(|| format!("{name}: container only {only_x:?}, replay only {only_y:?}"))
409    };
410    // A reference the container made directly and the build makes through the
411    // GOT resolves to the same address; the GOT form is the more general one.
412    let only_a: BTreeSet<_> = recorded
413        .references
414        .difference(&built.references)
415        .cloned()
416        .collect();
417    let only_b: BTreeSet<_> = built
418        .references
419        .difference(&recorded.references)
420        .cloned()
421        .collect();
422    let container: BTreeSet<String> = only_a
423        .iter()
424        .filter(|r| {
425            !(r.1 == "pc-relative" && only_b.contains(&(r.0.clone(), "got".into(), r.2.clone())))
426        })
427        .map(|r| format!("{} {} {}", r.0, r.1, r.2))
428        .collect();
429    let replay: BTreeSet<String> = only_b
430        .iter()
431        .filter(|r| {
432            !(r.1 == "got" && only_a.contains(&(r.0.clone(), "pc-relative".into(), r.2.clone())))
433        })
434        .map(|r| format!("{} {} {}", r.0, r.1, r.2))
435        .collect();
436    [
437        diff("defined", &recorded.defined, &built.defined),
438        diff("undefined", &recorded.undefined, &built.undefined),
439        diff("sections", &recorded.sections, &built.sections),
440        diff("relocations", &container, &replay),
441    ]
442    .into_iter()
443    .flatten()
444    .collect::<Vec<_>>()
445    .join("; ")
446}
447
448/// What a release's recorded data supplies to a build.
449pub struct Recorded<'a> {
450    /// The glibc release, such as `2.28`.
451    pub release: &'a str,
452    /// `release.tsv` as key/value pairs.
453    pub values: BTreeMap<String, String>,
454    /// `objects.tsv`: every object to build.
455    pub objects: Vec<Object>,
456    /// `interfaces.tsv`.
457    pub interfaces: &'a str,
458    /// glibc-provider's `ACCEPTED_DIFFERENCES` table.
459    pub accepted: &'a str,
460}
461
462/// The verification of one built object.
463#[derive(Clone, Debug, PartialEq, Eq)]
464pub struct Verdict {
465    /// The object's path in the build tree.
466    pub object: String,
467    /// `identical-bytes`, `equivalent`, `accepted` or `differs`.
468    pub status: &'static str,
469    /// The differences from the container build (defined and undefined symbols,
470    /// sections, relocations); empty when there are none.
471    pub detail: String,
472}
473
474/// The result of [`build`].
475pub struct Built {
476    /// Each target (`crt1.o`, `libc_nonshared.a`, ...) and its path in the build tree.
477    pub targets: BTreeMap<String, PathBuf>,
478    /// One verdict per built object.
479    pub verdicts: Vec<Verdict>,
480}
481
482impl Built {
483    /// Whether every object matches the container build or differs only as reviewed.
484    pub fn verified(&self) -> bool {
485        self.verdicts.iter().all(|v| v.status != "differs")
486    }
487
488    /// Count per status, such as `identical-bytes: 12, equivalent: 3`.
489    pub fn summary(&self) -> String {
490        let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
491        for verdict in &self.verdicts {
492            *counts.entry(verdict.status).or_default() += 1;
493        }
494        counts
495            .iter()
496            .map(|(status, count)| format!("{status}: {count}"))
497            .collect::<Vec<_>>()
498            .join(", ")
499    }
500}
501
502/// Builds every object and target of RECORDED in INPUTS (written by
503/// glibc-provider-sources' `write_sources`: `src/`, `build/`, `uapi/`) and verifies
504/// each object against its recorded interface.
505pub fn build(recorded: &Recorded<'_>, inputs: &Path, tools: &Tools) -> Result<Built, String> {
506    let (src, build, uapi) = (
507        inputs.join("src"),
508        inputs.join("build"),
509        inputs.join("uapi"),
510    );
511    let interfaces = read_interfaces(recorded.interfaces)?;
512    if !recorded.values.contains_key("builder.pie") {
513        return Err("release.tsv lacks the builder defaults".into());
514    }
515    // Only the compiler's own headers and the recorded UAPI headers are visible.
516    let mut extra = vec!["-nostdinc".to_string()];
517    for dir in ["include", "include-fixed"] {
518        let path = run(
519            &tools.gcc,
520            &[format!("-print-file-name={dir}")],
521            Path::new("."),
522        )?;
523        extra.extend(["-isystem".to_string(), path.trim().to_string()]);
524    }
525    // Some distributions (Fedora) build GNU as to add an x86 "ISA used" note to
526    // every object; the builders' assemblers did not.
527    let used_note = "-Wa,-mx86-used-note=no".to_string();
528    if accepts(tools, &used_note, inputs) {
529        extra.push(used_note);
530    }
531    extra.extend(["-isystem".to_string(), uapi.to_string_lossy().into_owned()]);
532    // A relocatable link adds nothing of its own: no PIE, no build ID.
533    let link_extra = vec!["-no-pie".to_string(), "-Wl,--build-id=none".into()];
534    let (s, b) = (
535        src.to_string_lossy().into_owned(),
536        build.to_string_lossy().into_owned(),
537    );
538    let gcc = tools.gcc.to_string_lossy().into_owned();
539    let mut built = BTreeSet::new();
540    // Compile first, then relocatable links (their inputs must exist).
541    for pass in ["compile", "relocatable"] {
542        for row in recorded.objects.iter().filter(|r| r.kind == pass) {
543            if !built.insert(row.object.clone()) {
544                continue;
545            }
546            let cwd = if row.directory == "." {
547                src.clone()
548            } else {
549                src.join(&row.directory)
550            };
551            fs::create_dir_all(&cwd).map_err(|e| e.to_string())?;
552            if let Some(parent) = build.join(&row.object).parent() {
553                fs::create_dir_all(parent).map_err(|e| e.to_string())?;
554            }
555            let mut flags = if pass == "compile" {
556                extra.clone()
557            } else {
558                link_extra.clone()
559            };
560            if pass == "compile" {
561                flags.extend(defaults_flags(&row.command, &recorded.values));
562            }
563            let args = translate(&row.command, &gcc, &s, &b, &flags)?;
564            run(&tools.gcc, &args[1..], &cwd)
565                .map_err(|e| format!("glibc {}: {}: {e}", recorded.release, row.object))?;
566        }
567    }
568    let mut targets = BTreeMap::new();
569    let mut archives: BTreeMap<&str, Vec<(u32, &str)>> = BTreeMap::new();
570    for row in &recorded.objects {
571        if row.target.ends_with(".a") {
572            archives
573                .entry(&row.target)
574                .or_default()
575                .push((row.member, &row.object));
576        } else if Path::new(&row.object).file_name() == Some(row.target.as_ref()) {
577            targets.insert(row.target.clone(), build.join(&row.object));
578        }
579    }
580    for (archive, mut members) in archives {
581        members.sort();
582        members.dedup();
583        let path = build.join(archive);
584        let mut args = vec!["rcD".to_string(), path.to_string_lossy().into_owned()];
585        args.extend(
586            members
587                .iter()
588                .map(|(_, m)| build.join(m).to_string_lossy().into_owned()),
589        );
590        run(&tools.ar, &args, Path::new("."))?;
591        targets.insert(archive.to_string(), path);
592    }
593    for name in recorded
594        .objects
595        .iter()
596        .map(|o| o.target.as_str())
597        .collect::<BTreeSet<_>>()
598    {
599        if !targets.contains_key(name) {
600            return Err(format!(
601                "glibc {}: no object builds {name}",
602                recorded.release
603            ));
604        }
605    }
606    let mut verdicts = Vec::new();
607    for object in &built {
608        let (reference, reference_bytes) = interfaces
609            .get(object)
610            .ok_or_else(|| format!("glibc {}: interfaces.tsv lacks {object}", recorded.release))?;
611        let (actual, actual_bytes) = interface(&tools.readelf, &build.join(object))?;
612        let difference = describe(reference, &actual);
613        let reviewed = recorded.accepted.lines().skip(1).any(|line| {
614            let row: Vec<&str> = line.split('\t').collect();
615            row.len() >= 5
616                && row[0] == recorded.release
617                && row[1] == object
618                && matches!(row[2], "gcc" | "any")
619                && row[3] == difference
620        });
621        let status = if difference.is_empty() {
622            if *reference_bytes == actual_bytes {
623                "identical-bytes"
624            } else {
625                "equivalent"
626            }
627        } else if reviewed {
628            "accepted"
629        } else {
630            "differs"
631        };
632        verdicts.push(Verdict {
633            object: object.clone(),
634            status,
635            detail: difference,
636        });
637    }
638    Ok(Built { targets, verdicts })
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    #[test]
646    fn compares_references_not_encodings() {
647        let refs = |items: &[(&str, &str)]| Interface {
648            references: items
649                .iter()
650                .map(|(kind, sym)| (".rela.text".into(), relocation_form(kind), sym.to_string()))
651                .collect(),
652            ..Interface::default()
653        };
654        let old = refs(&[("R_X86_64_GOTPCREL", "g"), ("R_X86_64_PC32", "f")]);
655        let new = refs(&[("R_X86_64_REX_GOTPCRELX", "g"), ("R_X86_64_PLT32", "f")]);
656        assert_eq!(describe(&old, &new), "");
657        // A direct reference made through the GOT instead is the general form.
658        let direct = refs(&[("R_X86_64_PC32", "h")]);
659        let through_got = refs(&[("R_X86_64_GOTPCRELX", "h")]);
660        assert_eq!(describe(&direct, &through_got), "");
661        // The reverse is not accepted, nor is a missing reference.
662        assert_ne!(describe(&through_got, &direct), "");
663        assert_ne!(describe(&direct, &refs(&[])), "");
664    }
665
666    #[test]
667    fn reads_hex_dumps_without_the_ascii_column() {
668        let dump = "\nHex dump of section '.rodata.cst4':\n  0x00000000 01000200 deadbeef 31ed4989 ab       ........1.I..\n  0x00000010 cafe                                ..\n";
669        assert_eq!(
670            hex_dump_bytes(dump),
671            [1, 0, 2, 0, 0xde, 0xad, 0xbe, 0xef, 0x31, 0xed, 0x49, 0x89, 0xab, 0xca, 0xfe]
672        );
673    }
674
675    #[test]
676    fn reads_recorded_interfaces() {
677        let text = "object\tkind\tvalue\ncsu/start.o\tdefined\t_start FUNC GLOBAL DEFAULT .text\ncsu/start.o\treference\t.rela.text got __libc_start_main\ncsu/start.o\tbytes\tab\n";
678        let read = read_interfaces(text).unwrap();
679        let (interface, digest) = &read["csu/start.o"];
680        assert_eq!(digest, "ab");
681        assert!(interface.references.contains(&(
682            ".rela.text".into(),
683            "got".into(),
684            "__libc_start_main".into()
685        )));
686        assert!(read_interfaces("object\tkind\tvalue\nx\tother\ty\n").is_err());
687        assert!(read_interfaces("bad header\n").is_err());
688    }
689
690    #[test]
691    fn makes_builder_defaults_explicit_only_where_unset() {
692        let pie: BTreeMap<String, String> = [("builder.pie".to_string(), "yes".to_string())].into();
693        let none = BTreeMap::new();
694        assert!(defaults_flags("gcc x.c -c", &pie).contains(&"-fPIE".to_string()));
695        assert!(defaults_flags("gcc x.c -c", &none).contains(&"-fno-pie".to_string()));
696        let pic = defaults_flags("gcc x.c -c -fPIC -fno-stack-protector", &none);
697        assert!(!pic
698            .iter()
699            .any(|f| f.contains("pie") || f.contains("stack-protector")));
700        assert!(pic.contains(&"-U_FORTIFY_SOURCE".to_string()));
701    }
702
703    #[test]
704    fn translates_recorded_commands() {
705        let args = translate(
706            "gcc ../x.S -c -I@BUILD@/csu -o @BUILD@/csu/x.o -MD -MP -MF @BUILD@/csu/x.o.dt -MT @BUILD@/csu/x.o",
707            "/usr/bin/gcc-14",
708            "/o/src",
709            "/o/build",
710            &["-nostdinc".into()],
711        )
712        .unwrap();
713        assert_eq!(
714            args,
715            [
716                "/usr/bin/gcc-14",
717                "../x.S",
718                "-c",
719                "-I/o/build/csu",
720                "-o",
721                "/o/build/csu/x.o",
722                "-nostdinc"
723            ]
724        );
725        assert!(translate("cc x.c", "gcc", "/s", "/b", &[]).is_err());
726    }
727}