(root)/
Python-3.12.0/
Tools/
build/
stable_abi.py
       1  """Check the stable ABI manifest or generate files from it
       2  
       3  By default, the tool only checks existing files/libraries.
       4  Pass --generate to recreate auto-generated files instead.
       5  
       6  For actions that take a FILENAME, the filename can be left out to use a default
       7  (relative to the manifest file, as they appear in the CPython codebase).
       8  """
       9  
      10  from functools import partial
      11  from pathlib import Path
      12  import dataclasses
      13  import subprocess
      14  import sysconfig
      15  import argparse
      16  import textwrap
      17  import tomllib
      18  import difflib
      19  import pprint
      20  import sys
      21  import os
      22  import os.path
      23  import io
      24  import re
      25  import csv
      26  
      27  SCRIPT_NAME = 'Tools/build/stable_abi.py'
      28  MISSING = object()
      29  
      30  EXCLUDED_HEADERS = {
      31      "bytes_methods.h",
      32      "cellobject.h",
      33      "classobject.h",
      34      "code.h",
      35      "compile.h",
      36      "datetime.h",
      37      "dtoa.h",
      38      "frameobject.h",
      39      "genobject.h",
      40      "longintrepr.h",
      41      "parsetok.h",
      42      "pyatomic.h",
      43      "pytime.h",
      44      "token.h",
      45      "ucnhash.h",
      46  }
      47  MACOS = (sys.platform == "darwin")
      48  UNIXY = MACOS or (sys.platform == "linux")  # XXX should this be "not Windows"?
      49  
      50  
      51  # The stable ABI manifest (Misc/stable_abi.toml) exists only to fill the
      52  # following dataclasses.
      53  # Feel free to change its syntax (and the `parse_manifest` function)
      54  # to better serve that purpose (while keeping it human-readable).
      55  
      56  class ESC[4;38;5;81mManifest:
      57      """Collection of `ABIItem`s forming the stable ABI/limited API."""
      58      def __init__(self):
      59          self.contents = dict()
      60  
      61      def add(self, item):
      62          if item.name in self.contents:
      63              # We assume that stable ABI items do not share names,
      64              # even if they're different kinds (e.g. function vs. macro).
      65              raise ValueError(f'duplicate ABI item {item.name}')
      66          self.contents[item.name] = item
      67  
      68      def select(self, kinds, *, include_abi_only=True, ifdef=None):
      69          """Yield selected items of the manifest
      70  
      71          kinds: set of requested kinds, e.g. {'function', 'macro'}
      72          include_abi_only: if True (default), include all items of the
      73              stable ABI.
      74              If False, include only items from the limited API
      75              (i.e. items people should use today)
      76          ifdef: set of feature macros (e.g. {'HAVE_FORK', 'MS_WINDOWS'}).
      77              If None (default), items are not filtered by this. (This is
      78              different from the empty set, which filters out all such
      79              conditional items.)
      80          """
      81          for name, item in sorted(self.contents.items()):
      82              if item.kind not in kinds:
      83                  continue
      84              if item.abi_only and not include_abi_only:
      85                  continue
      86              if (ifdef is not None
      87                      and item.ifdef is not None
      88                      and item.ifdef not in ifdef):
      89                  continue
      90              yield item
      91  
      92      def dump(self):
      93          """Yield lines to recreate the manifest file (sans comments/newlines)"""
      94          for item in self.contents.values():
      95              fields = dataclasses.fields(item)
      96              yield f"[{item.kind}.{item.name}]"
      97              for field in fields:
      98                  if field.name in {'name', 'value', 'kind'}:
      99                      continue
     100                  value = getattr(item, field.name)
     101                  if value == field.default:
     102                      pass
     103                  elif value is True:
     104                      yield f"    {field.name} = true"
     105                  elif value:
     106                      yield f"    {field.name} = {value!r}"
     107  
     108  
     109  itemclasses = {}
     110  def itemclass(kind):
     111      """Register the decorated class in `itemclasses`"""
     112      def decorator(cls):
     113          itemclasses[kind] = cls
     114          return cls
     115      return decorator
     116  
     117  @itemclass('function')
     118  @itemclass('macro')
     119  @itemclass('data')
     120  @itemclass('const')
     121  @itemclass('typedef')
     122  @dataclasses.dataclass
     123  class ESC[4;38;5;81mABIItem:
     124      """Information on one item (function, macro, struct, etc.)"""
     125  
     126      name: str
     127      kind: str
     128      added: str = None
     129      abi_only: bool = False
     130      ifdef: str = None
     131  
     132  @itemclass('feature_macro')
     133  @dataclasses.dataclass(kw_only=True)
     134  class ESC[4;38;5;81mFeatureMacro(ESC[4;38;5;149mABIItem):
     135      name: str
     136      doc: str
     137      windows: bool = False
     138      abi_only: bool = True
     139  
     140  @itemclass('struct')
     141  @dataclasses.dataclass(kw_only=True)
     142  class ESC[4;38;5;81mStruct(ESC[4;38;5;149mABIItem):
     143      struct_abi_kind: str
     144      members: list = None
     145  
     146  
     147  def parse_manifest(file):
     148      """Parse the given file (iterable of lines) to a Manifest"""
     149  
     150      manifest = Manifest()
     151  
     152      data = tomllib.load(file)
     153  
     154      for kind, itemclass in itemclasses.items():
     155          for name, item_data in data[kind].items():
     156              try:
     157                  item = itemclass(name=name, kind=kind, **item_data)
     158                  manifest.add(item)
     159              except BaseException as exc:
     160                  exc.add_note(f'in {kind} {name}')
     161                  raise
     162  
     163      return manifest
     164  
     165  # The tool can run individual "actions".
     166  # Most actions are "generators", which generate a single file from the
     167  # manifest. (Checking works by generating a temp file & comparing.)
     168  # Other actions, like "--unixy-check", don't work on a single file.
     169  
     170  generators = []
     171  def generator(var_name, default_path):
     172      """Decorates a file generator: function that writes to a file"""
     173      def _decorator(func):
     174          func.var_name = var_name
     175          func.arg_name = '--' + var_name.replace('_', '-')
     176          func.default_path = default_path
     177          generators.append(func)
     178          return func
     179      return _decorator
     180  
     181  
     182  @generator("python3dll", 'PC/python3dll.c')
     183  def gen_python3dll(manifest, args, outfile):
     184      """Generate/check the source for the Windows stable ABI library"""
     185      write = partial(print, file=outfile)
     186      content = f"""\
     187          /* Re-export stable Python ABI */
     188  
     189          /* Generated by {SCRIPT_NAME} */
     190      """
     191      content += r"""
     192          #ifdef _M_IX86
     193          #define DECORATE "_"
     194          #else
     195          #define DECORATE
     196          #endif
     197  
     198          #define EXPORT_FUNC(name) \
     199              __pragma(comment(linker, "/EXPORT:" DECORATE #name "=" PYTHON_DLL_NAME "." #name))
     200          #define EXPORT_DATA(name) \
     201              __pragma(comment(linker, "/EXPORT:" DECORATE #name "=" PYTHON_DLL_NAME "." #name ",DATA"))
     202      """
     203      write(textwrap.dedent(content))
     204  
     205      def sort_key(item):
     206          return item.name.lower()
     207  
     208      windows_feature_macros = {
     209          item.name for item in manifest.select({'feature_macro'}) if item.windows
     210      }
     211      for item in sorted(
     212              manifest.select(
     213                  {'function'},
     214                  include_abi_only=True,
     215                  ifdef=windows_feature_macros),
     216              key=sort_key):
     217          write(f'EXPORT_FUNC({item.name})')
     218  
     219      write()
     220  
     221      for item in sorted(
     222              manifest.select(
     223                  {'data'},
     224                  include_abi_only=True,
     225                  ifdef=windows_feature_macros),
     226              key=sort_key):
     227          write(f'EXPORT_DATA({item.name})')
     228  
     229  REST_ROLES = {
     230      'function': 'function',
     231      'data': 'var',
     232      'struct': 'type',
     233      'macro': 'macro',
     234      # 'const': 'const',  # all undocumented
     235      'typedef': 'type',
     236  }
     237  
     238  @generator("doc_list", 'Doc/data/stable_abi.dat')
     239  def gen_doc_annotations(manifest, args, outfile):
     240      """Generate/check the stable ABI list for documentation annotations"""
     241      writer = csv.DictWriter(
     242          outfile,
     243          ['role', 'name', 'added', 'ifdef_note', 'struct_abi_kind'],
     244          lineterminator='\n')
     245      writer.writeheader()
     246      for item in manifest.select(REST_ROLES.keys(), include_abi_only=False):
     247          if item.ifdef:
     248              ifdef_note = manifest.contents[item.ifdef].doc
     249          else:
     250              ifdef_note = None
     251          row = {
     252              'role': REST_ROLES[item.kind],
     253              'name': item.name,
     254              'added': item.added,
     255              'ifdef_note': ifdef_note}
     256          rows = [row]
     257          if item.kind == 'struct':
     258              row['struct_abi_kind'] = item.struct_abi_kind
     259              for member_name in item.members or ():
     260                  rows.append({
     261                      'role': 'member',
     262                      'name': f'{item.name}.{member_name}',
     263                      'added': item.added})
     264          writer.writerows(rows)
     265  
     266  @generator("ctypes_test", 'Lib/test/test_stable_abi_ctypes.py')
     267  def gen_ctypes_test(manifest, args, outfile):
     268      """Generate/check the ctypes-based test for exported symbols"""
     269      write = partial(print, file=outfile)
     270      write(textwrap.dedent(f'''\
     271          # Generated by {SCRIPT_NAME}
     272  
     273          """Test that all symbols of the Stable ABI are accessible using ctypes
     274          """
     275  
     276          import sys
     277          import unittest
     278          from test.support.import_helper import import_module
     279          from _testcapi import get_feature_macros
     280  
     281          feature_macros = get_feature_macros()
     282          ctypes_test = import_module('ctypes')
     283  
     284          class TestStableABIAvailability(unittest.TestCase):
     285              def test_available_symbols(self):
     286  
     287                  for symbol_name in SYMBOL_NAMES:
     288                      with self.subTest(symbol_name):
     289                          ctypes_test.pythonapi[symbol_name]
     290  
     291              def test_feature_macros(self):
     292                  self.assertEqual(
     293                      set(get_feature_macros()), EXPECTED_FEATURE_MACROS)
     294  
     295              # The feature macros for Windows are used in creating the DLL
     296              # definition, so they must be known on all platforms.
     297              # If we are on Windows, we check that the hardcoded data matches
     298              # the reality.
     299              @unittest.skipIf(sys.platform != "win32", "Windows specific test")
     300              def test_windows_feature_macros(self):
     301                  for name, value in WINDOWS_FEATURE_MACROS.items():
     302                      if value != 'maybe':
     303                          with self.subTest(name):
     304                              self.assertEqual(feature_macros[name], value)
     305  
     306          SYMBOL_NAMES = (
     307      '''))
     308      items = manifest.select(
     309          {'function', 'data'},
     310          include_abi_only=True,
     311      )
     312      optional_items = {}
     313      for item in items:
     314          if item.name in (
     315                  # Some symbols aren't exported on all platforms.
     316                  # This is a bug: https://bugs.python.org/issue44133
     317                  'PyModule_Create2', 'PyModule_FromDefAndSpec2',
     318              ):
     319              continue
     320          if item.ifdef:
     321              optional_items.setdefault(item.ifdef, []).append(item.name)
     322          else:
     323              write(f'    "{item.name}",')
     324      write(")")
     325      for ifdef, names in optional_items.items():
     326          write(f"if feature_macros[{ifdef!r}]:")
     327          write(f"    SYMBOL_NAMES += (")
     328          for name in names:
     329              write(f"        {name!r},")
     330          write("    )")
     331      write("")
     332      feature_macros = list(manifest.select({'feature_macro'}))
     333      feature_names = sorted(m.name for m in feature_macros)
     334      write(f"EXPECTED_FEATURE_MACROS = set({pprint.pformat(feature_names)})")
     335  
     336      windows_feature_macros = {m.name: m.windows for m in feature_macros}
     337      write(f"WINDOWS_FEATURE_MACROS = {pprint.pformat(windows_feature_macros)}")
     338  
     339  
     340  @generator("testcapi_feature_macros", 'Modules/_testcapi_feature_macros.inc')
     341  def gen_testcapi_feature_macros(manifest, args, outfile):
     342      """Generate/check the stable ABI list for documentation annotations"""
     343      write = partial(print, file=outfile)
     344      write(f'// Generated by {SCRIPT_NAME}')
     345      write()
     346      write('// Add an entry in dict `result` for each Stable ABI feature macro.')
     347      write()
     348      for macro in manifest.select({'feature_macro'}):
     349          name = macro.name
     350          write(f'#ifdef {name}')
     351          write(f'    res = PyDict_SetItemString(result, "{name}", Py_True);')
     352          write('#else')
     353          write(f'    res = PyDict_SetItemString(result, "{name}", Py_False);')
     354          write('#endif')
     355          write('if (res) {')
     356          write('    Py_DECREF(result); return NULL;')
     357          write('}')
     358          write()
     359  
     360  
     361  def generate_or_check(manifest, args, path, func):
     362      """Generate/check a file with a single generator
     363  
     364      Return True if successful; False if a comparison failed.
     365      """
     366  
     367      outfile = io.StringIO()
     368      func(manifest, args, outfile)
     369      generated = outfile.getvalue()
     370      existing = path.read_text()
     371  
     372      if generated != existing:
     373          if args.generate:
     374              path.write_text(generated)
     375          else:
     376              print(f'File {path} differs from expected!')
     377              diff = difflib.unified_diff(
     378                  generated.splitlines(), existing.splitlines(),
     379                  str(path), '<expected>',
     380                  lineterm='',
     381              )
     382              for line in diff:
     383                  print(line)
     384              return False
     385      return True
     386  
     387  
     388  def do_unixy_check(manifest, args):
     389      """Check headers & library using "Unixy" tools (GCC/clang, binutils)"""
     390      okay = True
     391  
     392      # Get all macros first: we'll need feature macros like HAVE_FORK and
     393      # MS_WINDOWS for everything else
     394      present_macros = gcc_get_limited_api_macros(['Include/Python.h'])
     395      feature_macros = set(m.name for m in manifest.select({'feature_macro'}))
     396      feature_macros &= present_macros
     397  
     398      # Check that we have all needed macros
     399      expected_macros = set(
     400          item.name for item in manifest.select({'macro'})
     401      )
     402      missing_macros = expected_macros - present_macros
     403      okay &= _report_unexpected_items(
     404          missing_macros,
     405          'Some macros from are not defined from "Include/Python.h"'
     406          + 'with Py_LIMITED_API:')
     407  
     408      expected_symbols = set(item.name for item in manifest.select(
     409          {'function', 'data'}, include_abi_only=True, ifdef=feature_macros,
     410      ))
     411  
     412      # Check the static library (*.a)
     413      LIBRARY = sysconfig.get_config_var("LIBRARY")
     414      if not LIBRARY:
     415          raise Exception("failed to get LIBRARY variable from sysconfig")
     416      if os.path.exists(LIBRARY):
     417          okay &= binutils_check_library(
     418              manifest, LIBRARY, expected_symbols, dynamic=False)
     419  
     420      # Check the dynamic library (*.so)
     421      LDLIBRARY = sysconfig.get_config_var("LDLIBRARY")
     422      if not LDLIBRARY:
     423          raise Exception("failed to get LDLIBRARY variable from sysconfig")
     424      okay &= binutils_check_library(
     425              manifest, LDLIBRARY, expected_symbols, dynamic=False)
     426  
     427      # Check definitions in the header files
     428      expected_defs = set(item.name for item in manifest.select(
     429          {'function', 'data'}, include_abi_only=False, ifdef=feature_macros,
     430      ))
     431      found_defs = gcc_get_limited_api_definitions(['Include/Python.h'])
     432      missing_defs = expected_defs - found_defs
     433      okay &= _report_unexpected_items(
     434          missing_defs,
     435          'Some expected declarations were not declared in '
     436          + '"Include/Python.h" with Py_LIMITED_API:')
     437  
     438      # Some Limited API macros are defined in terms of private symbols.
     439      # These are not part of Limited API (even though they're defined with
     440      # Py_LIMITED_API). They must be part of the Stable ABI, though.
     441      private_symbols = {n for n in expected_symbols if n.startswith('_')}
     442      extra_defs = found_defs - expected_defs - private_symbols
     443      okay &= _report_unexpected_items(
     444          extra_defs,
     445          'Some extra declarations were found in "Include/Python.h" '
     446          + 'with Py_LIMITED_API:')
     447  
     448      return okay
     449  
     450  
     451  def _report_unexpected_items(items, msg):
     452      """If there are any `items`, report them using "msg" and return false"""
     453      if items:
     454          print(msg, file=sys.stderr)
     455          for item in sorted(items):
     456              print(' -', item, file=sys.stderr)
     457          return False
     458      return True
     459  
     460  
     461  def binutils_get_exported_symbols(library, dynamic=False):
     462      """Retrieve exported symbols using the nm(1) tool from binutils"""
     463      # Only look at dynamic symbols
     464      args = ["nm", "--no-sort"]
     465      if dynamic:
     466          args.append("--dynamic")
     467      args.append(library)
     468      proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True)
     469      if proc.returncode:
     470          sys.stdout.write(proc.stdout)
     471          sys.exit(proc.returncode)
     472  
     473      stdout = proc.stdout.rstrip()
     474      if not stdout:
     475          raise Exception("command output is empty")
     476  
     477      for line in stdout.splitlines():
     478          # Split line '0000000000001b80 D PyTextIOWrapper_Type'
     479          if not line:
     480              continue
     481  
     482          parts = line.split(maxsplit=2)
     483          if len(parts) < 3:
     484              continue
     485  
     486          symbol = parts[-1]
     487          if MACOS and symbol.startswith("_"):
     488              yield symbol[1:]
     489          else:
     490              yield symbol
     491  
     492  
     493  def binutils_check_library(manifest, library, expected_symbols, dynamic):
     494      """Check that library exports all expected_symbols"""
     495      available_symbols = set(binutils_get_exported_symbols(library, dynamic))
     496      missing_symbols = expected_symbols - available_symbols
     497      if missing_symbols:
     498          print(textwrap.dedent(f"""\
     499              Some symbols from the limited API are missing from {library}:
     500                  {', '.join(missing_symbols)}
     501  
     502              This error means that there are some missing symbols among the
     503              ones exported in the library.
     504              This normally means that some symbol, function implementation or
     505              a prototype belonging to a symbol in the limited API has been
     506              deleted or is missing.
     507          """), file=sys.stderr)
     508          return False
     509      return True
     510  
     511  
     512  def gcc_get_limited_api_macros(headers):
     513      """Get all limited API macros from headers.
     514  
     515      Runs the preprocessor over all the header files in "Include" setting
     516      "-DPy_LIMITED_API" to the correct value for the running version of the
     517      interpreter and extracting all macro definitions (via adding -dM to the
     518      compiler arguments).
     519  
     520      Requires Python built with a GCC-compatible compiler. (clang might work)
     521      """
     522  
     523      api_hexversion = sys.version_info.major << 24 | sys.version_info.minor << 16
     524  
     525      preprocesor_output_with_macros = subprocess.check_output(
     526          sysconfig.get_config_var("CC").split()
     527          + [
     528              # Prevent the expansion of the exported macros so we can
     529              # capture them later
     530              "-DSIZEOF_WCHAR_T=4",  # The actual value is not important
     531              f"-DPy_LIMITED_API={api_hexversion}",
     532              "-I.",
     533              "-I./Include",
     534              "-dM",
     535              "-E",
     536          ]
     537          + [str(file) for file in headers],
     538          text=True,
     539      )
     540  
     541      return {
     542          target
     543          for target in re.findall(
     544              r"#define (\w+)", preprocesor_output_with_macros
     545          )
     546      }
     547  
     548  
     549  def gcc_get_limited_api_definitions(headers):
     550      """Get all limited API definitions from headers.
     551  
     552      Run the preprocessor over all the header files in "Include" setting
     553      "-DPy_LIMITED_API" to the correct value for the running version of the
     554      interpreter.
     555  
     556      The limited API symbols will be extracted from the output of this command
     557      as it includes the prototypes and definitions of all the exported symbols
     558      that are in the limited api.
     559  
     560      This function does *NOT* extract the macros defined on the limited API
     561  
     562      Requires Python built with a GCC-compatible compiler. (clang might work)
     563      """
     564      api_hexversion = sys.version_info.major << 24 | sys.version_info.minor << 16
     565      preprocesor_output = subprocess.check_output(
     566          sysconfig.get_config_var("CC").split()
     567          + [
     568              # Prevent the expansion of the exported macros so we can capture
     569              # them later
     570              "-DPyAPI_FUNC=__PyAPI_FUNC",
     571              "-DPyAPI_DATA=__PyAPI_DATA",
     572              "-DEXPORT_DATA=__EXPORT_DATA",
     573              "-D_Py_NO_RETURN=",
     574              "-DSIZEOF_WCHAR_T=4",  # The actual value is not important
     575              f"-DPy_LIMITED_API={api_hexversion}",
     576              "-I.",
     577              "-I./Include",
     578              "-E",
     579          ]
     580          + [str(file) for file in headers],
     581          text=True,
     582          stderr=subprocess.DEVNULL,
     583      )
     584      stable_functions = set(
     585          re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output)
     586      )
     587      stable_exported_data = set(
     588          re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output)
     589      )
     590      stable_data = set(
     591          re.findall(r"__PyAPI_DATA\(.*?\)[\s\*\(]*([^);]*)\)?.*;", preprocesor_output)
     592      )
     593      return stable_data | stable_exported_data | stable_functions
     594  
     595  def check_private_names(manifest):
     596      """Ensure limited API doesn't contain private names
     597  
     598      Names prefixed by an underscore are private by definition.
     599      """
     600      for name, item in manifest.contents.items():
     601          if name.startswith('_') and not item.abi_only:
     602              raise ValueError(
     603                  f'`{name}` is private (underscore-prefixed) and should be '
     604                  + 'removed from the stable ABI list or or marked `abi_only`')
     605  
     606  def check_dump(manifest, filename):
     607      """Check that manifest.dump() corresponds to the data.
     608  
     609      Mainly useful when debugging this script.
     610      """
     611      dumped = tomllib.loads('\n'.join(manifest.dump()))
     612      with filename.open('rb') as file:
     613          from_file = tomllib.load(file)
     614      if dumped != from_file:
     615          print(f'Dump differs from loaded data!', file=sys.stderr)
     616          diff = difflib.unified_diff(
     617              pprint.pformat(dumped).splitlines(),
     618              pprint.pformat(from_file).splitlines(),
     619              '<dumped>', str(filename),
     620              lineterm='',
     621          )
     622          for line in diff:
     623              print(line, file=sys.stderr)
     624          return False
     625      else:
     626          return True
     627  
     628  def main():
     629      parser = argparse.ArgumentParser(
     630          description=__doc__,
     631          formatter_class=argparse.RawDescriptionHelpFormatter,
     632      )
     633      parser.add_argument(
     634          "file", type=Path, metavar='FILE',
     635          help="file with the stable abi manifest",
     636      )
     637      parser.add_argument(
     638          "--generate", action='store_true',
     639          help="generate file(s), rather than just checking them",
     640      )
     641      parser.add_argument(
     642          "--generate-all", action='store_true',
     643          help="as --generate, but generate all file(s) using default filenames."
     644              + " (unlike --all, does not run any extra checks)",
     645      )
     646      parser.add_argument(
     647          "-a", "--all", action='store_true',
     648          help="run all available checks using default filenames",
     649      )
     650      parser.add_argument(
     651          "-l", "--list", action='store_true',
     652          help="list available generators and their default filenames; then exit",
     653      )
     654      parser.add_argument(
     655          "--dump", action='store_true',
     656          help="dump the manifest contents (used for debugging the parser)",
     657      )
     658  
     659      actions_group = parser.add_argument_group('actions')
     660      for gen in generators:
     661          actions_group.add_argument(
     662              gen.arg_name, dest=gen.var_name,
     663              type=str, nargs="?", default=MISSING,
     664              metavar='FILENAME',
     665              help=gen.__doc__,
     666          )
     667      actions_group.add_argument(
     668          '--unixy-check', action='store_true',
     669          help=do_unixy_check.__doc__,
     670      )
     671      args = parser.parse_args()
     672  
     673      base_path = args.file.parent.parent
     674  
     675      if args.list:
     676          for gen in generators:
     677              print(f'{gen.arg_name}: {base_path / gen.default_path}')
     678          sys.exit(0)
     679  
     680      run_all_generators = args.generate_all
     681  
     682      if args.generate_all:
     683          args.generate = True
     684  
     685      if args.all:
     686          run_all_generators = True
     687          if UNIXY:
     688              args.unixy_check = True
     689  
     690      try:
     691          file = args.file.open('rb')
     692      except FileNotFoundError as err:
     693          if args.file.suffix == '.txt':
     694              # Provide a better error message
     695              suggestion = args.file.with_suffix('.toml')
     696              raise FileNotFoundError(
     697                  f'{args.file} not found. Did you mean {suggestion} ?') from err
     698          raise
     699      with file:
     700          manifest = parse_manifest(file)
     701  
     702      check_private_names(manifest)
     703  
     704      # Remember results of all actions (as booleans).
     705      # At the end we'll check that at least one action was run,
     706      # and also fail if any are false.
     707      results = {}
     708  
     709      if args.dump:
     710          for line in manifest.dump():
     711              print(line)
     712          results['dump'] = check_dump(manifest, args.file)
     713  
     714      for gen in generators:
     715          filename = getattr(args, gen.var_name)
     716          if filename is None or (run_all_generators and filename is MISSING):
     717              filename = base_path / gen.default_path
     718          elif filename is MISSING:
     719              continue
     720  
     721          results[gen.var_name] = generate_or_check(manifest, args, filename, gen)
     722  
     723      if args.unixy_check:
     724          results['unixy_check'] = do_unixy_check(manifest, args)
     725  
     726      if not results:
     727          if args.generate:
     728              parser.error('No file specified. Use --help for usage.')
     729          parser.error('No check specified. Use --help for usage.')
     730  
     731      failed_results = [name for name, result in results.items() if not result]
     732  
     733      if failed_results:
     734          raise Exception(f"""
     735          These checks related to the stable ABI did not succeed:
     736              {', '.join(failed_results)}
     737  
     738          If you see diffs in the output, files derived from the stable
     739          ABI manifest the were not regenerated.
     740          Run `make regen-limited-abi` to fix this.
     741  
     742          Otherwise, see the error(s) above.
     743  
     744          The stable ABI manifest is at: {args.file}
     745          Note that there is a process to follow when modifying it.
     746  
     747          You can read more about the limited API and its contracts at:
     748  
     749          https://docs.python.org/3/c-api/stable.html
     750  
     751          And in PEP 384:
     752  
     753          https://peps.python.org/pep-0384/
     754          """)
     755  
     756  
     757  if __name__ == "__main__":
     758      main()