1 #!/usr/bin/env python
2 # Script checking that all symbols exported by libpython start with Py or _Py
3
4 import os.path
5 import subprocess
6 import sys
7 import sysconfig
8
9
10 ALLOWED_PREFIXES = ('Py', '_Py')
11 if sys.platform == 'darwin':
12 ALLOWED_PREFIXES += ('__Py',)
13
14 IGNORED_EXTENSION = "_ctypes_test"
15 # Ignore constructor and destructor functions
16 IGNORED_SYMBOLS = {'_init', '_fini'}
17
18
19 def 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
37 def 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
57 def 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
91 def 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
109 def 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
141 def 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
172 if __name__ == "__main__":
173 main()