libc-abi-tools 0.1.0

Rust tools for libc ABI metadata, starting with glibc
Documentation
use libc_abi_tools::{elf, glibc, matrix::Matrix, version::Version};
use std::{
    ffi::OsString,
    io::{self, Write},
    path::PathBuf,
};

const HELP: &str = "libc-abi - Rust libc ABI metadata tools (glibc first)

USAGE
  libc-abi glibc snapshot [--data DIR] --release VERSION [--symbol NAME]
  libc-abi glibc collect --source GLIBC_CHECKOUT --output DIR
  libc-abi glibc export-data --output DIR
  libc-abi glibc elf --input SHARED_OBJECT
  libc-abi glibc matrix [--data DIR] [--symbol NAME]
  libc-abi glibc diff [--data DIR] --from VERSION --to VERSION [--symbol NAME]
  libc-abi glibc changes [--data DIR]
  libc-abi glibc expect [--data DIR] --release VERSION --library NAME --symbol NAME --version VERSION
  libc-abi --version

The glibc ABI lists come from the GNU C Library's release tags through the
glibc-abilists crate (LGPL-2.1-or-later), compiled in by default. --data DIR
reads them from a directory instead; DIR contains version directories such as
2.17/ and 2.22/ (the layout of glibc-abilists' data/ and of export-data).
Built without the bundled-glibc-data feature, --data is required.

snapshot writes stable TSV for one exact x86_64 Linux glibc release.
export-data writes the bundled ABI lists into a new directory.
collect requires a clean checkout at an exact glibc release tag; no overwrites.
elf writes defined dynamic-symbol evidence from ELF64 x86_64, including SONAME.
An ELF file does not establish its source release or semantic alias relationships.
Target releases start at 2.17; older symbol versions are retained.
matrix compares all available snapshots; first observation is not introduction.
expect prints accept/reject/unknown for exact library/symbol/version membership.
Missing release/library evidence is unknown, not a rejection or fallback.
No stub libraries, sysroots, headers, or runtime implementations are generated.
";

fn run(args: Vec<OsString>) -> Result<(), String> {
    if args.is_empty() || matches!(args[0].to_str(), Some("--help" | "-h" | "help")) {
        print!("{HELP}");
        return Ok(());
    }
    if args.len() == 1 && args[0] == "--version" {
        println!("libc-abi {}", env!("CARGO_PKG_VERSION"));
        return Ok(());
    }
    if args.len() < 2 || args[0] != "glibc" {
        return Err("expected glibc snapshot, glibc collect, or glibc elf; see --help".into());
    }
    let command = args[1].to_str().ok_or("command must be UTF-8")?;
    let allowed: &[&str] = match command {
        "snapshot" => &["--data", "--release", "--symbol"],
        "collect" => &["--source", "--output"],
        "export-data" => &["--output"],
        "elf" => &["--input"],
        "matrix" => &["--data", "--symbol"],
        "diff" => &["--data", "--from", "--to", "--symbol"],
        "changes" => &["--data"],
        "expect" => &["--data", "--release", "--library", "--symbol", "--version"],
        _ => return Err(format!("unknown glibc command {command:?}")),
    };
    let mut options = std::collections::BTreeMap::new();
    let mut i = 2;
    while i < args.len() {
        if args[i] == "--help" {
            print!("{HELP}");
            return Ok(());
        }
        let flag = args[i].to_str().ok_or("option must be UTF-8")?;
        if !allowed.contains(&flag) {
            return Err(format!("unknown option {flag:?}"));
        }
        let value = args
            .get(i + 1)
            .ok_or_else(|| format!("{flag} needs a value"))?;
        if value.is_empty() || value.to_str().is_some_and(|s| s.starts_with("--")) {
            return Err(format!("{flag} needs a nonempty value"));
        }
        if options.insert(flag, value).is_some() {
            return Err(format!("duplicate option {flag}"));
        }
        i += 2;
    }
    let required = |flag| {
        options
            .get(flag)
            .copied()
            .ok_or_else(|| format!("{flag} is required"))
    };
    match command {
        "matrix" | "diff" | "changes" | "expect" => {
            let matrix = match options.get("--data") {
                Some(dir) => Matrix::load(&PathBuf::from(dir))?,
                None => bundled_matrix()?,
            };
            let value = |flag| {
                required(flag)?
                    .to_str()
                    .ok_or_else(|| format!("{flag} must be UTF-8"))
            };
            let symbol = options
                .get("--symbol")
                .map(|v| v.to_str().ok_or("symbol must be UTF-8"))
                .transpose()?;
            let text = match command {
                "matrix" => matrix.to_tsv(symbol),
                "diff" => {
                    matrix.diff_tsv(value("--from")?.parse()?, value("--to")?.parse()?, symbol)?
                }
                "changes" => matrix.changes_tsv(),
                _ => format!(
                    "{}\n",
                    matrix
                        .expect(
                            value("--release")?.parse()?,
                            value("--library")?,
                            value("--symbol")?,
                            value("--version")?.parse()?
                        )
                        .label()
                ),
            };
            if let Err(error) = io::stdout().lock().write_all(text.as_bytes()) {
                if error.kind() != io::ErrorKind::BrokenPipe {
                    return Err(error.to_string());
                }
            }
        }
        "elf" => {
            let text = elf::Metadata::load(&PathBuf::from(required("--input")?))?.to_tsv();
            match io::stdout().lock().write_all(text.as_bytes()) {
                Err(error) if error.kind() != io::ErrorKind::BrokenPipe => {
                    return Err(error.to_string())
                }
                _ => {}
            }
        }
        "snapshot" => {
            let release: Version = required("--release")?
                .to_str()
                .ok_or("release must be UTF-8")?
                .parse()?;
            let symbol = options
                .get("--symbol")
                .map(|s| s.to_str().ok_or("symbol must be UTF-8"))
                .transpose()?;
            let snapshot = match options.get("--data") {
                Some(dir) => glibc::Snapshot::load(&PathBuf::from(dir), release)?,
                None => bundled_snapshot(release)?,
            };
            let text = snapshot.to_tsv(symbol);
            match io::stdout().lock().write_all(text.as_bytes()) {
                Err(error) if error.kind() != io::ErrorKind::BrokenPipe => {
                    return Err(error.to_string())
                }
                _ => {}
            }
        }
        "collect" => {
            let destination = glibc::collect(
                &PathBuf::from(required("--source")?),
                &PathBuf::from(required("--output")?),
            )?;
            println!("Collected {}", destination.display());
        }
        "export-data" => export_data(&PathBuf::from(required("--output")?))?,
        _ => unreachable!(),
    }
    Ok(())
}

#[cfg(feature = "bundled-glibc-data")]
fn bundled_matrix() -> Result<Matrix, String> {
    Matrix::bundled()
}

#[cfg(feature = "bundled-glibc-data")]
fn bundled_snapshot(release: Version) -> Result<glibc::Snapshot, String> {
    glibc::Snapshot::bundled(release)
}

#[cfg(feature = "bundled-glibc-data")]
fn export_data(output: &std::path::Path) -> Result<(), String> {
    let count = glibc::export_bundled(output)?;
    println!("Exported {count} glibc ABI lists to {}", output.display());
    Ok(())
}

#[cfg(not(feature = "bundled-glibc-data"))]
const NOT_BUNDLED: &str =
    "built without the bundled-glibc-data feature; pass --data DIR (see --help)";

#[cfg(not(feature = "bundled-glibc-data"))]
fn bundled_matrix() -> Result<Matrix, String> {
    Err(NOT_BUNDLED.into())
}

#[cfg(not(feature = "bundled-glibc-data"))]
fn bundled_snapshot(_: Version) -> Result<glibc::Snapshot, String> {
    Err(NOT_BUNDLED.into())
}

#[cfg(not(feature = "bundled-glibc-data"))]
fn export_data(_: &std::path::Path) -> Result<(), String> {
    Err(NOT_BUNDLED.into())
}

fn main() {
    if let Err(error) = run(std::env::args_os().skip(1).collect()) {
        eprintln!("libc-abi: {error}");
        std::process::exit(1);
    }
}