Files
ARSV/scripts/check_arch_set_impl.py
T
2026-07-10 02:52:05 +03:00

173 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Check reimplement/set.py against ELF libraries installed on this Arch Linux system.
This is the local-system analogue of check_alt_set_impl.py. It treats shared
libraries as providers (defined dynamic symbols) and executables/shared objects
as requirers (undefined dynamic symbols). Required labels are split by provider
library before comparing, so each library is checked only against symbols that
it actually exports.
"""
from __future__ import annotations
import argparse
import os
import shutil
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
import check_alt_set_impl as compat # noqa: E402
DEFAULT_PROVIDER_DIRS = [Path("/usr/lib")]
DEFAULT_REQUIRER_DIRS = [Path("/usr/bin"), Path("/usr/lib")]
def parse_path_list(values: list[str] | None, defaults: list[Path]) -> list[Path]:
paths: list[Path] = []
for value in values or []:
paths.extend(Path(part) for part in value.split(",") if part)
return paths or defaults
def is_shared_library_path(path: Path) -> bool:
name = path.name
return ".debug" not in name and name.startswith("lib") and ".so" in name
def iter_files(paths: list[Path], recursive: bool) -> list[Path]:
files: list[Path] = []
seen: set[Path] = set()
for root in paths:
if root.is_file():
candidates = [root]
elif recursive:
candidates = (path for path in root.rglob("*") if path.is_file())
else:
candidates = (path for path in root.iterdir() if path.is_file()) if root.is_dir() else []
for path in candidates:
try:
resolved = path.resolve()
except OSError:
continue
if resolved in seen:
continue
seen.add(resolved)
files.append(path)
return sorted(files)
def executable_or_library(path: Path) -> bool:
return is_shared_library_path(path) or os.access(path, os.X_OK)
def build_local_provider_sets(paths: list[Path], bpp: int, recursive: bool, max_files: int) -> list[compat.LabelSet]:
provider_files = [path for path in iter_files(paths, recursive) if is_shared_library_path(path)][:max_files]
sets: list[compat.LabelSet] = []
for path in provider_files:
labels = compat.nm_symbols(path, "provided")
if labels:
sets.append(compat.generate_label_set("provided", str(path), labels, bpp, package="arch-local"))
return sets
def build_local_requirer_sets(
paths: list[Path],
bpp: int,
recursive: bool,
max_files: int,
normalize_versions: bool,
) -> list[compat.LabelSet]:
requirer_files = [path for path in iter_files(paths, recursive) if executable_or_library(path)][:max_files]
sets: list[compat.LabelSet] = []
for path in requirer_files:
labels = compat.nm_symbols(path, "required")
if normalize_versions:
labels = [compat.normalize_required_symbol(label) for label in labels]
if labels:
sets.append(compat.generate_label_set("required", str(path), labels, bpp, package="arch-local"))
return sets
def print_unmatched_required_labels(provider_sets: list[compat.LabelSet], requirer_sets: list[compat.LabelSet], limit: int) -> None:
provided = {label for provider in provider_sets for label in provider.labels}
rows: list[tuple[str, int, list[str]]] = []
for requirer in requirer_sets:
missing = sorted(set(requirer.labels).difference(provided))
if missing:
rows.append((requirer.member, len(missing), missing[:limit]))
print("\n# required labels not exported by scanned provider libraries")
print("requirer\tmissing_labels\texamples")
for member, count, examples in rows[:limit]:
print(f"{member}\t{count}\t{', '.join(examples)}")
if len(rows) > limit:
print(f"... {len(rows) - limit} more requirer files with unmatched labels")
def parse_args(argv: list[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate Provided/Required set strings from local Arch Linux ELF files and compare them with reimplement/set.py."
)
parser.add_argument("--provider-dir", action="append", help="directory/file containing provider libraries; repeat or comma-separate")
parser.add_argument("--requirer-dir", action="append", help="directory/file containing requirer ELF files; repeat or comma-separate")
parser.add_argument("--bpp", type=int, default=32, help="bits per hash used by local set.py")
parser.add_argument("--max-provider-files", type=int, default=256, help="max provider libraries to inspect")
parser.add_argument("--max-requirer-files", type=int, default=256, help="max requirer files to inspect")
parser.add_argument("--recursive", action="store_true", help="scan directories recursively")
parser.add_argument("--all-pairs", action="store_true", help="compare every provider set with every full requirer set")
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",
)
parser.add_argument("--unmatched-limit", type=int, default=20, help="max unmatched-label rows/examples to print")
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
provider_paths = parse_path_list(args.provider_dir, DEFAULT_PROVIDER_DIRS)
requirer_paths = parse_path_list(args.requirer_dir, DEFAULT_REQUIRER_DIRS)
provider_sets = build_local_provider_sets(provider_paths, args.bpp, args.recursive, args.max_provider_files)
requirer_sets = build_local_requirer_sets(
requirer_paths,
args.bpp,
args.recursive,
args.max_requirer_files,
normalize_versions=not args.no_normalize_required_version,
)
print("system: arch-local")
print(f"bpp: {args.bpp}")
print(f"provider paths: {', '.join(str(path) for path in provider_paths)}")
print(f"requirer paths: {', '.join(str(path) for path in requirer_paths)}")
print(f"required symbol version normalization: {not args.no_normalize_required_version}")
compat.print_label_sets("generated Provided sets", provider_sets)
compat.print_label_sets("generated Required sets", requirer_sets)
if not provider_sets or not requirer_sets:
print("\nNo comparable sets generated.", file=sys.stderr)
return 1
if args.all_pairs:
results = [compat.compare_label_sets(provider, requirer) for provider in provider_sets for requirer in requirer_sets]
else:
results = compat.build_dependency_results(provider_sets, requirer_sets, args.bpp)
compat.print_results(results)
print_unmatched_required_labels(provider_sets, requirer_sets, args.unmatched_limit)
return 0 if results and all(result.status == "compatible" for result in results) else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))