add scripts for tests
This commit is contained in:
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare one binary's required set string with a few provider libraries.
|
||||
|
||||
Usage:
|
||||
scripts/compare_binary_with_lib_sets.py [--bpp N] BINARY LIB.so [LIB.so ...]
|
||||
|
||||
For every library, this script compares:
|
||||
set(defined dynamic symbols from LIB) vs
|
||||
set(required dynamic symbols from BINARY that LIB provides)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(REPO_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
|
||||
from reimplement import set as rpmset # noqa: E402
|
||||
|
||||
|
||||
def run_nm(command: list[str]) -> str:
|
||||
proc = subprocess.run(command, text=True, capture_output=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"{' '.join(command)} failed with {proc.returncode}: {proc.stderr.strip()}")
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def normalize_required_symbol(symbol: str) -> str:
|
||||
"""Normalize nm's required `foo@VER` form to provider-like `foo@@VER`."""
|
||||
if "@@" in symbol or "@" not in symbol:
|
||||
return symbol
|
||||
name, version = symbol.split("@", 1)
|
||||
if not name or not version:
|
||||
return symbol
|
||||
return f"{name}@@{version}"
|
||||
|
||||
|
||||
def required_symbols(path: Path, normalize_versions: bool) -> set[str]:
|
||||
"""Return strong undefined dynamic symbols required by one ELF file."""
|
||||
output = run_nm(["nm", "--dynamic", "-u", str(path)])
|
||||
symbols: set[str] = set()
|
||||
for line in output.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 2 or parts[-2] != "U":
|
||||
continue
|
||||
symbol = parts[-1]
|
||||
symbols.add(normalize_required_symbol(symbol) if normalize_versions else symbol)
|
||||
return symbols
|
||||
|
||||
|
||||
def provided_symbols(path: Path) -> set[str]:
|
||||
"""Return defined dynamic symbols provided by one ELF shared library."""
|
||||
output = run_nm(["nm", "--dynamic", "-j", "-U", str(path)])
|
||||
return {line.strip() for line in output.splitlines() if line.strip()}
|
||||
|
||||
|
||||
def labels_to_set_string(labels: Iterable[str], bpp: int) -> str | None:
|
||||
item_set = rpmset.set_new()
|
||||
for label in sorted(set(labels)):
|
||||
rpmset.set_add(item_set, label)
|
||||
# set.py may print hash-collision warnings; keep stdout tabular.
|
||||
with contextlib.redirect_stdout(sys.stderr):
|
||||
return rpmset.set_fini(item_set, bpp)
|
||||
|
||||
|
||||
def compare_library(binary_required: set[str], library: Path, bpp: int) -> tuple[str, str, int, int, str, str]:
|
||||
provided = provided_symbols(library)
|
||||
required_from_library = binary_required.intersection(provided)
|
||||
provider_set = labels_to_set_string(provided, bpp)
|
||||
required_set = labels_to_set_string(required_from_library, bpp)
|
||||
|
||||
if not provided:
|
||||
return "no-provided-symbols", "", 0, 0, "-", "-"
|
||||
if not required_from_library:
|
||||
return "not-required", "", len(provided), 0, provider_set or "-", "-"
|
||||
|
||||
assert provider_set is not None
|
||||
assert required_set is not None
|
||||
cmp_result = rpmset.rpmsetcmp(provider_set, required_set)
|
||||
status = "compatible" if cmp_result in (0, 1) else "incompatible"
|
||||
return status, str(cmp_result), len(provided), len(required_from_library), provider_set, required_set
|
||||
|
||||
|
||||
def parse_args(argv: list[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Compare one binary against a few libraries using local set.py set strings.")
|
||||
parser.add_argument("binary", type=Path, help="ELF executable/shared object with required dynamic symbols")
|
||||
parser.add_argument("libraries", nargs="+", type=Path, help="provider shared libraries to compare against")
|
||||
parser.add_argument("--bpp", type=int, default=32, help="bits per hash used by local set.py")
|
||||
parser.add_argument(
|
||||
"--no-normalize-required-version",
|
||||
action="store_true",
|
||||
help="keep nm -u single-@ required symbols unchanged instead of converting foo@VER to foo@@VER",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
args = parse_args(argv)
|
||||
if shutil.which("nm") is None:
|
||||
print("missing required command: nm", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
required = required_symbols(args.binary, normalize_versions=not args.no_normalize_required_version)
|
||||
if not required:
|
||||
print(f"no strong dynamic required symbols found in {args.binary}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"binary\t{args.binary}")
|
||||
print(f"bpp\t{args.bpp}")
|
||||
print(f"required_symbols\t{len(required)}")
|
||||
print("status\tcmp\tlib\tprovided\trequired_from_lib\tprovider_set\trequired_set")
|
||||
|
||||
failed = False
|
||||
for library in args.libraries:
|
||||
try:
|
||||
status, cmp_result, provided_count, required_count, provider_set, required_set = compare_library(
|
||||
required, library, args.bpp
|
||||
)
|
||||
except RuntimeError as exc:
|
||||
print(f"error\t\t{library}\t0\t0\t-\t-", flush=True)
|
||||
print(exc, file=sys.stderr)
|
||||
failed = True
|
||||
continue
|
||||
|
||||
print(
|
||||
f"{status}\t{cmp_result}\t{library}\t{provided_count}\t{required_count}\t{provider_set}\t{required_set}"
|
||||
)
|
||||
failed = failed or status in {"incompatible", "no-provided-symbols"}
|
||||
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user