• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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