1#!/usr/bin/env python 2# Script checking that all symbols exported by libpython start with Py or _Py 3 4import os.path 5import subprocess 6import sys 7import sysconfig 8 9 10ALLOWED_PREFIXES = ('Py', '_Py') 11if sys.platform == 'darwin': 12 ALLOWED_PREFIXES += ('__Py',) 13 14IGNORED_EXTENSION = "_ctypes_test" 15# Ignore constructor and destructor functions 16IGNORED_SYMBOLS = {'_init', '_fini'} 17 18 19def is_local_symbol_type(symtype): 20 # Ignore local symbols. 21 22 # If lowercase, the symbol is usually local; if uppercase, the symbol 23 # is global (external). There are however a few lowercase symbols that 24 # are shown for special global symbols ("u", "v" and "w"). 25 if symtype.islower() and symtype not in "uvw": 26 return True 27 28 # Ignore the initialized data section (d and D) and the BSS data 29 # section. For example, ignore "__bss_start (type: B)" 30 # and "_edata (type: D)". 31 if symtype in "bBdD": 32 return True 33 34 return False 35 36 37def get_exported_symbols(library, dynamic=False): 38 print(f"Check that {library} only exports symbols starting with Py or _Py") 39 40 # Only look at dynamic symbols 41 args = ['nm', '--no-sort'] 42 if dynamic: 43 args.append('--dynamic') 44 args.append(library) 45 print("+ %s" % ' '.join(args)) 46 proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True) 47 if proc.returncode: 48 sys.stdout.write(proc.stdout) 49 sys.exit(proc.returncode) 50 51 stdout = proc.stdout.rstrip() 52 if not stdout: 53 raise Exception("command output is empty") 54 return stdout 55 56 57def get_smelly_symbols(stdout): 58 smelly_symbols = [] 59 python_symbols = [] 60 local_symbols = [] 61 62 for line in stdout.splitlines(): 63 # Split line '0000000000001b80 D PyTextIOWrapper_Type' 64 if not line: 65 continue 66 67 parts = line.split(maxsplit=2) 68 if len(parts) < 3: 69 continue 70 71 symtype = parts[1].strip() 72 symbol = parts[-1] 73 result = '%s (type: %s)' % (symbol, symtype) 74 75 if symbol.startswith(ALLOWED_PREFIXES): 76 python_symbols.append(result) 77 continue 78 79 if is_local_symbol_type(symtype): 80 local_symbols.append(result) 81 elif symbol in IGNORED_SYMBOLS: 82 local_symbols.append(result) 83 else: 84 smelly_symbols.append(result) 85 86 if local_symbols: 87 print(f"Ignore {len(local_symbols)} local symbols") 88 return smelly_symbols, python_symbols 89 90 91def check_library(library, dynamic=False): 92 nm_output = get_exported_symbols(library, dynamic) 93 smelly_symbols, python_symbols = get_smelly_symbols(nm_output) 94 95 if not smelly_symbols: 96 print(f"OK: no smelly symbol found ({len(python_symbols)} Python symbols)") 97 return 0 98 99 print() 100 smelly_symbols.sort() 101 for symbol in smelly_symbols: 102 print("Smelly symbol: %s" % symbol) 103 104 print() 105 print("ERROR: Found %s smelly symbols!" % len(smelly_symbols)) 106 return len(smelly_symbols) 107 108 109def check_extensions(): 110 print(__file__) 111 # This assumes pybuilddir.txt is in same directory as pyconfig.h. 112 # In the case of out-of-tree builds, we can't assume pybuilddir.txt is 113 # in the source folder. 114 config_dir = os.path.dirname(sysconfig.get_config_h_filename()) 115 filename = os.path.join(config_dir, "pybuilddir.txt") 116 try: 117 with open(filename, encoding="utf-8") as fp: 118 pybuilddir = fp.readline() 119 except FileNotFoundError: 120 print(f"Cannot check extensions because {filename} does not exist") 121 return True 122 123 print(f"Check extension modules from {pybuilddir} directory") 124 builddir = os.path.join(config_dir, pybuilddir) 125 nsymbol = 0 126 for name in os.listdir(builddir): 127 if not name.endswith(".so"): 128 continue 129 if IGNORED_EXTENSION in name: 130 print() 131 print(f"Ignore extension: {name}") 132 continue 133 134 print() 135 filename = os.path.join(builddir, name) 136 nsymbol += check_library(filename, dynamic=True) 137 138 return nsymbol 139 140 141def main(): 142 nsymbol = 0 143 144 # static library 145 LIBRARY = sysconfig.get_config_var('LIBRARY') 146 if not LIBRARY: 147 raise Exception("failed to get LIBRARY variable from sysconfig") 148 if os.path.exists(LIBRARY): 149 nsymbol += check_library(LIBRARY) 150 151 # dynamic library 152 LDLIBRARY = sysconfig.get_config_var('LDLIBRARY') 153 if not LDLIBRARY: 154 raise Exception("failed to get LDLIBRARY variable from sysconfig") 155 if LDLIBRARY != LIBRARY: 156 print() 157 nsymbol += check_library(LDLIBRARY, dynamic=True) 158 159 # Check extension modules like _ssl.cpython-310d-x86_64-linux-gnu.so 160 nsymbol += check_extensions() 161 162 if nsymbol: 163 print() 164 print(f"ERROR: Found {nsymbol} smelly symbols in total!") 165 sys.exit(1) 166 167 print() 168 print(f"OK: all exported symbols of all libraries " 169 f"are prefixed with {' or '.join(map(repr, ALLOWED_PREFIXES))}") 170 171 172if __name__ == "__main__": 173 main() 174