public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
From: Lumir Balhar <lbalhar@redhat.com>
To: git-commits@fedoraproject.org
Subject: [tests/python] master: Add new test that monitors symbols required from dynamic compiled libs
Date: Wed, 08 Jul 2026 10:18:34 GMT	[thread overview]
Message-ID: <178350591483.1.3884475593673895432.tests-python-b4af9ca880f1@fedoraproject.org> (raw)

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)

                 reply	other threads:[~2026-07-08 10:18 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=178350591483.1.3884475593673895432.tests-python-b4af9ca880f1@fedoraproject.org \
    --to=lbalhar@redhat.com \
    --cc=git-commits@fedoraproject.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox