public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [tests/python] master: Add new test that monitors symbols required from dynamic compiled libs
@ 2026-07-08 10:18 Lumir Balhar
0 siblings, 0 replies; only message in thread
From: Lumir Balhar @ 2026-07-08 10:18 UTC (permalink / raw)
To: git-commits
A new commit has been pushed.
Repo : tests/python
Branch : master
Commit : b4af9ca880f132639f66279405874b2a6d94d6c8
Author : Lumir Balhar <lbalhar@redhat.com>
Date : 2026-07-01T12:17:03+02:00
Stats : +170/-0 in 3 file(s)
URL : https://src.fedoraproject.org/tests/python/c/b4af9ca880f132639f66279405874b2a6d94d6c8?branch=master
Log:
Add new test that monitors symbols required from dynamic compiled libs
---
diff --git a/required-symbols/check.sh b/required-symbols/check.sh
new file mode 100755
index 0000000..afce753
--- /dev/null
+++ b/required-symbols/check.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+# Compare required external symbols of compiled Python extension modules between
+# the PR build (pre-installed by Testing Farm) and the latest stable version
+# available in the distribution repositories.
+#
+# The PR build is already installed before this test runs (via the Testing Farm
+# artifact mechanism). We save its .so files, downgrade to the stable repo version,
+# collect symbols from both, and compare.
+#
+# Output:
+# + [module] symbol — PR build adds a new required symbol (potential issue possibly requiring update of Requires)
+# - [module] symbol — PR build drops a previously required symbol (generally OK)
+#
+# Exit codes:
+# 0 — no changes, or no stable version available to compare against (skipped)
+# 1 — symbol differences detected between PR build and stable release
+set -eo pipefail
+
+PYVER=${VERSION}
+SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
+
+# Directories containing compiled extension modules
+SCAN_DIRS=()
+for d in "/usr/lib64/python${PYVER}" "/usr/lib64/python${PYVER}t"; do
+ [ -d "$d" ] && SCAN_DIRS+=("$d")
+done
+
+if [ ${#SCAN_DIRS[@]} -eq 0 ]; then
+ echo "ERROR: No Python extension directories found for python${PYVER}" >&2
+ exit 1
+fi
+
+WORK_DIR=$(mktemp -d)
+trap 'rm -rf "$WORK_DIR"' EXIT
+
+echo "=== Collecting PR build symbols ==="
+python${PYVER} "$SCRIPT_DIR/collect_symbols.py" "${SCAN_DIRS[@]}" > "$WORK_DIR/new.json"
+
+# Downgrade to the latest stable version in the distribution repos.
+# If no older version is available, there is nothing to compare against → skip.
+echo ""
+echo "=== Available repos ==="
+dnf repolist --all
+
+echo ""
+echo "=== Downgrading to the latest stable version in repos ==="
+# Discover packages dynamically from the installed source RPM so we don't need
+# to hardcode subpackage names — works regardless of which subpackages were built.
+readarray -t PKGS < <(
+ rpm -qa --qf '%{SOURCERPM} %{NAME}\n' |
+ awk -v src="python${PYVER}" '$1 ~ "^" src "-[0-9]" { print $2 }' |
+ sort -u
+)
+echo "Packages to downgrade: ${PKGS[*]}"
+
+if ! dnf downgrade -y "${PKGS[@]}" 2>&1; then
+ echo "INFO: dnf downgrade failed or no stable version available; skipping comparison." >&2
+ exit 0
+fi
+
+echo ""
+echo "=== Collecting stable version symbols ==="
+python${PYVER} "$SCRIPT_DIR/collect_symbols.py" "${SCAN_DIRS[@]}" > "$WORK_DIR/old.json"
+
+echo ""
+echo "=== Comparing PR build against stable ==="
+python${PYVER} "$SCRIPT_DIR/compare_symbols.py" "$WORK_DIR/old.json" "$WORK_DIR/new.json" || COMPARE_RC=$?
+
+echo ""
+echo "=== Reverting package downgrade ==="
+dnf history undo last -y 2>&1
+
+exit ${COMPARE_RC:-0}
diff --git a/required-symbols/collect_symbols.py b/required-symbols/collect_symbols.py
new file mode 100755
index 0000000..9550a39
--- /dev/null
+++ b/required-symbols/collect_symbols.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+"""
+Collect undefined non-boring external symbols from compiled Python extension modules.
+
+Usage: collect_symbols.py <directory> [<directory>...]
+
+Scans all *.cpython-*.so files found recursively, unions symbols across all
+variants of each module (regular/debug/freethreading), and prints 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:
+ # Strip ABI tag (d=debug, t=freethreading, td=both) so variants of the same
+ # module merge together: _ssl.cpython-314td-x86_64-linux-gnu.so → _ssl
+ return re.sub(r"\.cpython-\d+[a-z]*-[^.]+\.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:]:
+ print(f"Scanning: {path_arg}", file=sys.stderr)
+ for so in sorted(Path(path_arg).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()
diff --git a/required-symbols/compare_symbols.py b/required-symbols/compare_symbols.py
new file mode 100755
index 0000000..2692993
--- /dev/null
+++ b/required-symbols/compare_symbols.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+"""
+Compare two symbol JSON files produced by collect_symbols.py.
+
+Usage: compare_symbols.py <old.json> <new.json>
+
+Output:
+ + [module] symbol — new build adds a required symbol the old build did not have
+ - [module] symbol — new build drops a required symbol the old build had
+
+Exit codes:
+ 0 — no differences
+ 1 — differences found
+"""
+
+import json
+import sys
+from pathlib import Path
+
+old = json.loads(Path(sys.argv[1]).read_text())
+new = json.loads(Path(sys.argv[2]).read_text())
+
+all_mods = sorted(set(old) | set(new))
+changes = []
+for mod in all_mods:
+ added = sorted(set(new.get(mod, [])) - set(old.get(mod, [])))
+ removed = sorted(set(old.get(mod, [])) - set(new.get(mod, [])))
+ if added or removed:
+ changes.append((mod, added, removed))
+
+if not changes:
+ print("OK: no symbol changes between stable and PR build")
+ sys.exit(0)
+
+print("Symbol changes detected (+ new requirement in PR build, - dropped by PR build):")
+for mod, added, removed in changes:
+ for sym in added:
+ print(f" + [{mod}] {sym}")
+ for sym in removed:
+ print(f" - [{mod}] {sym}")
+sys.exit(1)
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-07-08 10:18 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-08 10:18 [tests/python] master: Add new test that monitors symbols required from dynamic compiled libs Lumir Balhar
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox