67 lines
2.2 KiB
Python
Executable file
67 lines
2.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Collect undefined non-boring external symbols from compiled Python extension modules
|
|
and libpython shared libraries.
|
|
|
|
Usage: collect_symbols.py <path> [<path>...]
|
|
|
|
Each path may be a directory (scanned recursively for *.cpython-*.so) or a file
|
|
(scanned directly; intended for libpython*.so.* passed explicitly from the caller).
|
|
Symbols are unioned across all variants of each module (regular/debug/freethreading),
|
|
and the result is printed as JSON to stdout.
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
BORING = re.compile(
|
|
r"@GLIBC_" # glibc versioned symbols (stable by definition)
|
|
r"|@GCC_" # GCC built-ins
|
|
r"|^_?Py[A-Za-z_]" # Python C API (resolved from libpython at runtime)
|
|
r"|^__" # C runtime internals
|
|
r"|^_ITM_" # Intel transactional memory
|
|
)
|
|
|
|
|
|
def module_name(so: Path) -> str:
|
|
# _ssl.cpython-314td-x86_64-linux-gnu.so → _ssl
|
|
if m := re.match(r"(.+?)\.cpython-", so.name):
|
|
return m.group(1)
|
|
# libpython3.14.so.1.0 → libpython3.14
|
|
return re.sub(r"\.so.*$", "", so.name)
|
|
|
|
|
|
def external_symbols(so: Path) -> list[str]:
|
|
result = subprocess.run(["nm", "-D", str(so)], capture_output=True, text=True)
|
|
return sorted(
|
|
parts[-1]
|
|
for line in result.stdout.splitlines()
|
|
if len(parts := line.split()) >= 2
|
|
and parts[-2] == "U"
|
|
and not BORING.search(parts[-1])
|
|
)
|
|
|
|
|
|
modules: dict[str, set[str]] = {}
|
|
for path_arg in sys.argv[1:]:
|
|
p = Path(path_arg)
|
|
if p.is_file():
|
|
print(f"Scanning: {p.name}", file=sys.stderr)
|
|
if syms := external_symbols(p):
|
|
modules.setdefault(module_name(p), set()).update(syms)
|
|
else:
|
|
print(f"Scanning: {path_arg}", file=sys.stderr)
|
|
for so in sorted(p.rglob("*.cpython-*.so")):
|
|
if so.is_symlink():
|
|
continue
|
|
if syms := external_symbols(so):
|
|
modules.setdefault(module_name(so), set()).update(syms)
|
|
|
|
total_symbols = sum(len(v) for v in modules.values())
|
|
print(f"Found {len(modules)} modules, {total_symbols} tracked symbols", file=sys.stderr)
|
|
|
|
json.dump({k: sorted(v) for k, v in sorted(modules.items())}, sys.stdout, indent=2)
|
|
print()
|