summaryrefslogtreecommitdiffstats
path: root/Tools/build/smelly.py
diff options
context:
space:
mode:
Diffstat (limited to 'Tools/build/smelly.py')
-rwxr-xr-xTools/build/smelly.py173
1 files changed, 173 insertions, 0 deletions
diff --git a/Tools/build/smelly.py b/Tools/build/smelly.py
new file mode 100755
index 0000000..276a5ab
--- /dev/null
+++ b/Tools/build/smelly.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python
+# Script checking that all symbols exported by libpython start with Py or _Py
+
+import os.path
+import subprocess
+import sys
+import sysconfig
+
+
+ALLOWED_PREFIXES = ('Py', '_Py')
+if sys.platform == 'darwin':
+ ALLOWED_PREFIXES += ('__Py',)
+
+IGNORED_EXTENSION = "_ctypes_test"
+# Ignore constructor and destructor functions
+IGNORED_SYMBOLS = {'_init', '_fini'}
+
+
+def is_local_symbol_type(symtype):
+ # Ignore local symbols.
+
+ # If lowercase, the symbol is usually local; if uppercase, the symbol
+ # is global (external). There are however a few lowercase symbols that
+ # are shown for special global symbols ("u", "v" and "w").
+ if symtype.islower() and symtype not in "uvw":
+ return True
+
+ # Ignore the initialized data section (d and D) and the BSS data
+ # section. For example, ignore "__bss_start (type: B)"
+ # and "_edata (type: D)".
+ if symtype in "bBdD":
+ return True
+
+ return False
+
+
+def get_exported_symbols(library, dynamic=False):
+ print(f"Check that {library} only exports symbols starting with Py or _Py")
+
+ # Only look at dynamic symbols
+ args = ['nm', '--no-sort']
+ if dynamic:
+ args.append('--dynamic')
+ args.append(library)
+ print("+ %s" % ' '.join(args))
+ proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True)
+ if proc.returncode:
+ sys.stdout.write(proc.stdout)
+ sys.exit(proc.returncode)
+
+ stdout = proc.stdout.rstrip()
+ if not stdout:
+ raise Exception("command output is empty")
+ return stdout
+
+
+def get_smelly_symbols(stdout):
+ smelly_symbols = []
+ python_symbols = []
+ local_symbols = []
+
+ for line in stdout.splitlines():
+ # Split line '0000000000001b80 D PyTextIOWrapper_Type'
+ if not line:
+ continue
+
+ parts = line.split(maxsplit=2)
+ if len(parts) < 3:
+ continue
+
+ symtype = parts[1].strip()
+ symbol = parts[-1]
+ result = '%s (type: %s)' % (symbol, symtype)
+
+ if symbol.startswith(ALLOWED_PREFIXES):
+ python_symbols.append(result)
+ continue
+
+ if is_local_symbol_type(symtype):
+ local_symbols.append(result)
+ elif symbol in IGNORED_SYMBOLS:
+ local_symbols.append(result)
+ else:
+ smelly_symbols.append(result)
+
+ if local_symbols:
+ print(f"Ignore {len(local_symbols)} local symbols")
+ return smelly_symbols, python_symbols
+
+
+def check_library(library, dynamic=False):
+ nm_output = get_exported_symbols(library, dynamic)
+ smelly_symbols, python_symbols = get_smelly_symbols(nm_output)
+
+ if not smelly_symbols:
+ print(f"OK: no smelly symbol found ({len(python_symbols)} Python symbols)")
+ return 0
+
+ print()
+ smelly_symbols.sort()
+ for symbol in smelly_symbols:
+ print("Smelly symbol: %s" % symbol)
+
+ print()
+ print("ERROR: Found %s smelly symbols!" % len(smelly_symbols))
+ return len(smelly_symbols)
+
+
+def check_extensions():
+ print(__file__)
+ # This assumes pybuilddir.txt is in same directory as pyconfig.h.
+ # In the case of out-of-tree builds, we can't assume pybuilddir.txt is
+ # in the source folder.
+ config_dir = os.path.dirname(sysconfig.get_config_h_filename())
+ filename = os.path.join(config_dir, "pybuilddir.txt")
+ try:
+ with open(filename, encoding="utf-8") as fp:
+ pybuilddir = fp.readline()
+ except FileNotFoundError:
+ print(f"Cannot check extensions because {filename} does not exist")
+ return True
+
+ print(f"Check extension modules from {pybuilddir} directory")
+ builddir = os.path.join(config_dir, pybuilddir)
+ nsymbol = 0
+ for name in os.listdir(builddir):
+ if not name.endswith(".so"):
+ continue
+ if IGNORED_EXTENSION in name:
+ print()
+ print(f"Ignore extension: {name}")
+ continue
+
+ print()
+ filename = os.path.join(builddir, name)
+ nsymbol += check_library(filename, dynamic=True)
+
+ return nsymbol
+
+
+def main():
+ nsymbol = 0
+
+ # static library
+ LIBRARY = sysconfig.get_config_var('LIBRARY')
+ if not LIBRARY:
+ raise Exception("failed to get LIBRARY variable from sysconfig")
+ if os.path.exists(LIBRARY):
+ nsymbol += check_library(LIBRARY)
+
+ # dynamic library
+ LDLIBRARY = sysconfig.get_config_var('LDLIBRARY')
+ if not LDLIBRARY:
+ raise Exception("failed to get LDLIBRARY variable from sysconfig")
+ if LDLIBRARY != LIBRARY:
+ print()
+ nsymbol += check_library(LDLIBRARY, dynamic=True)
+
+ # Check extension modules like _ssl.cpython-310d-x86_64-linux-gnu.so
+ nsymbol += check_extensions()
+
+ if nsymbol:
+ print()
+ print(f"ERROR: Found {nsymbol} smelly symbols in total!")
+ sys.exit(1)
+
+ print()
+ print(f"OK: all exported symbols of all libraries "
+ f"are prefixed with {' or '.join(map(repr, ALLOWED_PREFIXES))}")
+
+
+if __name__ == "__main__":
+ main()