(root)/
Python-3.11.7/
Tools/
scripts/
smelly.py
       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()