17db96d56Sopenharmony_ci"""Check the stable ABI manifest or generate files from it 27db96d56Sopenharmony_ci 37db96d56Sopenharmony_ciBy default, the tool only checks existing files/libraries. 47db96d56Sopenharmony_ciPass --generate to recreate auto-generated files instead. 57db96d56Sopenharmony_ci 67db96d56Sopenharmony_ciFor actions that take a FILENAME, the filename can be left out to use a default 77db96d56Sopenharmony_ci(relative to the manifest file, as they appear in the CPython codebase). 87db96d56Sopenharmony_ci""" 97db96d56Sopenharmony_ci 107db96d56Sopenharmony_cifrom functools import partial 117db96d56Sopenharmony_cifrom pathlib import Path 127db96d56Sopenharmony_ciimport dataclasses 137db96d56Sopenharmony_ciimport subprocess 147db96d56Sopenharmony_ciimport sysconfig 157db96d56Sopenharmony_ciimport argparse 167db96d56Sopenharmony_ciimport textwrap 177db96d56Sopenharmony_ciimport tomllib 187db96d56Sopenharmony_ciimport difflib 197db96d56Sopenharmony_ciimport shutil 207db96d56Sopenharmony_ciimport pprint 217db96d56Sopenharmony_ciimport sys 227db96d56Sopenharmony_ciimport os 237db96d56Sopenharmony_ciimport os.path 247db96d56Sopenharmony_ciimport io 257db96d56Sopenharmony_ciimport re 267db96d56Sopenharmony_ciimport csv 277db96d56Sopenharmony_ci 287db96d56Sopenharmony_ciMISSING = object() 297db96d56Sopenharmony_ci 307db96d56Sopenharmony_ciEXCLUDED_HEADERS = { 317db96d56Sopenharmony_ci "bytes_methods.h", 327db96d56Sopenharmony_ci "cellobject.h", 337db96d56Sopenharmony_ci "classobject.h", 347db96d56Sopenharmony_ci "code.h", 357db96d56Sopenharmony_ci "compile.h", 367db96d56Sopenharmony_ci "datetime.h", 377db96d56Sopenharmony_ci "dtoa.h", 387db96d56Sopenharmony_ci "frameobject.h", 397db96d56Sopenharmony_ci "genobject.h", 407db96d56Sopenharmony_ci "longintrepr.h", 417db96d56Sopenharmony_ci "parsetok.h", 427db96d56Sopenharmony_ci "pyatomic.h", 437db96d56Sopenharmony_ci "pytime.h", 447db96d56Sopenharmony_ci "token.h", 457db96d56Sopenharmony_ci "ucnhash.h", 467db96d56Sopenharmony_ci} 477db96d56Sopenharmony_ciMACOS = (sys.platform == "darwin") 487db96d56Sopenharmony_ciUNIXY = MACOS or (sys.platform == "linux") # XXX should this be "not Windows"? 497db96d56Sopenharmony_ci 507db96d56Sopenharmony_ci 517db96d56Sopenharmony_ci# The stable ABI manifest (Misc/stable_abi.toml) exists only to fill the 527db96d56Sopenharmony_ci# following dataclasses. 537db96d56Sopenharmony_ci# Feel free to change its syntax (and the `parse_manifest` function) 547db96d56Sopenharmony_ci# to better serve that purpose (while keeping it human-readable). 557db96d56Sopenharmony_ci 567db96d56Sopenharmony_ciclass Manifest: 577db96d56Sopenharmony_ci """Collection of `ABIItem`s forming the stable ABI/limited API.""" 587db96d56Sopenharmony_ci def __init__(self): 597db96d56Sopenharmony_ci self.contents = dict() 607db96d56Sopenharmony_ci 617db96d56Sopenharmony_ci def add(self, item): 627db96d56Sopenharmony_ci if item.name in self.contents: 637db96d56Sopenharmony_ci # We assume that stable ABI items do not share names, 647db96d56Sopenharmony_ci # even if they're different kinds (e.g. function vs. macro). 657db96d56Sopenharmony_ci raise ValueError(f'duplicate ABI item {item.name}') 667db96d56Sopenharmony_ci self.contents[item.name] = item 677db96d56Sopenharmony_ci 687db96d56Sopenharmony_ci def select(self, kinds, *, include_abi_only=True, ifdef=None): 697db96d56Sopenharmony_ci """Yield selected items of the manifest 707db96d56Sopenharmony_ci 717db96d56Sopenharmony_ci kinds: set of requested kinds, e.g. {'function', 'macro'} 727db96d56Sopenharmony_ci include_abi_only: if True (default), include all items of the 737db96d56Sopenharmony_ci stable ABI. 747db96d56Sopenharmony_ci If False, include only items from the limited API 757db96d56Sopenharmony_ci (i.e. items people should use today) 767db96d56Sopenharmony_ci ifdef: set of feature macros (e.g. {'HAVE_FORK', 'MS_WINDOWS'}). 777db96d56Sopenharmony_ci If None (default), items are not filtered by this. (This is 787db96d56Sopenharmony_ci different from the empty set, which filters out all such 797db96d56Sopenharmony_ci conditional items.) 807db96d56Sopenharmony_ci """ 817db96d56Sopenharmony_ci for name, item in sorted(self.contents.items()): 827db96d56Sopenharmony_ci if item.kind not in kinds: 837db96d56Sopenharmony_ci continue 847db96d56Sopenharmony_ci if item.abi_only and not include_abi_only: 857db96d56Sopenharmony_ci continue 867db96d56Sopenharmony_ci if (ifdef is not None 877db96d56Sopenharmony_ci and item.ifdef is not None 887db96d56Sopenharmony_ci and item.ifdef not in ifdef): 897db96d56Sopenharmony_ci continue 907db96d56Sopenharmony_ci yield item 917db96d56Sopenharmony_ci 927db96d56Sopenharmony_ci def dump(self): 937db96d56Sopenharmony_ci """Yield lines to recreate the manifest file (sans comments/newlines)""" 947db96d56Sopenharmony_ci for item in self.contents.values(): 957db96d56Sopenharmony_ci fields = dataclasses.fields(item) 967db96d56Sopenharmony_ci yield f"[{item.kind}.{item.name}]" 977db96d56Sopenharmony_ci for field in fields: 987db96d56Sopenharmony_ci if field.name in {'name', 'value', 'kind'}: 997db96d56Sopenharmony_ci continue 1007db96d56Sopenharmony_ci value = getattr(item, field.name) 1017db96d56Sopenharmony_ci if value == field.default: 1027db96d56Sopenharmony_ci pass 1037db96d56Sopenharmony_ci elif value is True: 1047db96d56Sopenharmony_ci yield f" {field.name} = true" 1057db96d56Sopenharmony_ci elif value: 1067db96d56Sopenharmony_ci yield f" {field.name} = {value!r}" 1077db96d56Sopenharmony_ci 1087db96d56Sopenharmony_ci 1097db96d56Sopenharmony_ciitemclasses = {} 1107db96d56Sopenharmony_cidef itemclass(kind): 1117db96d56Sopenharmony_ci """Register the decorated class in `itemclasses`""" 1127db96d56Sopenharmony_ci def decorator(cls): 1137db96d56Sopenharmony_ci itemclasses[kind] = cls 1147db96d56Sopenharmony_ci return cls 1157db96d56Sopenharmony_ci return decorator 1167db96d56Sopenharmony_ci 1177db96d56Sopenharmony_ci@itemclass('function') 1187db96d56Sopenharmony_ci@itemclass('macro') 1197db96d56Sopenharmony_ci@itemclass('data') 1207db96d56Sopenharmony_ci@itemclass('const') 1217db96d56Sopenharmony_ci@itemclass('typedef') 1227db96d56Sopenharmony_ci@dataclasses.dataclass 1237db96d56Sopenharmony_ciclass ABIItem: 1247db96d56Sopenharmony_ci """Information on one item (function, macro, struct, etc.)""" 1257db96d56Sopenharmony_ci 1267db96d56Sopenharmony_ci name: str 1277db96d56Sopenharmony_ci kind: str 1287db96d56Sopenharmony_ci added: str = None 1297db96d56Sopenharmony_ci abi_only: bool = False 1307db96d56Sopenharmony_ci ifdef: str = None 1317db96d56Sopenharmony_ci 1327db96d56Sopenharmony_ci@itemclass('feature_macro') 1337db96d56Sopenharmony_ci@dataclasses.dataclass(kw_only=True) 1347db96d56Sopenharmony_ciclass FeatureMacro(ABIItem): 1357db96d56Sopenharmony_ci name: str 1367db96d56Sopenharmony_ci doc: str 1377db96d56Sopenharmony_ci windows: bool = False 1387db96d56Sopenharmony_ci abi_only: bool = True 1397db96d56Sopenharmony_ci 1407db96d56Sopenharmony_ci@itemclass('struct') 1417db96d56Sopenharmony_ci@dataclasses.dataclass(kw_only=True) 1427db96d56Sopenharmony_ciclass Struct(ABIItem): 1437db96d56Sopenharmony_ci struct_abi_kind: str 1447db96d56Sopenharmony_ci members: list = None 1457db96d56Sopenharmony_ci 1467db96d56Sopenharmony_ci 1477db96d56Sopenharmony_cidef parse_manifest(file): 1487db96d56Sopenharmony_ci """Parse the given file (iterable of lines) to a Manifest""" 1497db96d56Sopenharmony_ci 1507db96d56Sopenharmony_ci manifest = Manifest() 1517db96d56Sopenharmony_ci 1527db96d56Sopenharmony_ci data = tomllib.load(file) 1537db96d56Sopenharmony_ci 1547db96d56Sopenharmony_ci for kind, itemclass in itemclasses.items(): 1557db96d56Sopenharmony_ci for name, item_data in data[kind].items(): 1567db96d56Sopenharmony_ci try: 1577db96d56Sopenharmony_ci item = itemclass(name=name, kind=kind, **item_data) 1587db96d56Sopenharmony_ci manifest.add(item) 1597db96d56Sopenharmony_ci except BaseException as exc: 1607db96d56Sopenharmony_ci exc.add_note(f'in {kind} {name}') 1617db96d56Sopenharmony_ci raise 1627db96d56Sopenharmony_ci 1637db96d56Sopenharmony_ci return manifest 1647db96d56Sopenharmony_ci 1657db96d56Sopenharmony_ci# The tool can run individual "actions". 1667db96d56Sopenharmony_ci# Most actions are "generators", which generate a single file from the 1677db96d56Sopenharmony_ci# manifest. (Checking works by generating a temp file & comparing.) 1687db96d56Sopenharmony_ci# Other actions, like "--unixy-check", don't work on a single file. 1697db96d56Sopenharmony_ci 1707db96d56Sopenharmony_cigenerators = [] 1717db96d56Sopenharmony_cidef generator(var_name, default_path): 1727db96d56Sopenharmony_ci """Decorates a file generator: function that writes to a file""" 1737db96d56Sopenharmony_ci def _decorator(func): 1747db96d56Sopenharmony_ci func.var_name = var_name 1757db96d56Sopenharmony_ci func.arg_name = '--' + var_name.replace('_', '-') 1767db96d56Sopenharmony_ci func.default_path = default_path 1777db96d56Sopenharmony_ci generators.append(func) 1787db96d56Sopenharmony_ci return func 1797db96d56Sopenharmony_ci return _decorator 1807db96d56Sopenharmony_ci 1817db96d56Sopenharmony_ci 1827db96d56Sopenharmony_ci@generator("python3dll", 'PC/python3dll.c') 1837db96d56Sopenharmony_cidef gen_python3dll(manifest, args, outfile): 1847db96d56Sopenharmony_ci """Generate/check the source for the Windows stable ABI library""" 1857db96d56Sopenharmony_ci write = partial(print, file=outfile) 1867db96d56Sopenharmony_ci write(textwrap.dedent(r""" 1877db96d56Sopenharmony_ci /* Re-export stable Python ABI */ 1887db96d56Sopenharmony_ci 1897db96d56Sopenharmony_ci /* Generated by Tools/scripts/stable_abi.py */ 1907db96d56Sopenharmony_ci 1917db96d56Sopenharmony_ci #ifdef _M_IX86 1927db96d56Sopenharmony_ci #define DECORATE "_" 1937db96d56Sopenharmony_ci #else 1947db96d56Sopenharmony_ci #define DECORATE 1957db96d56Sopenharmony_ci #endif 1967db96d56Sopenharmony_ci 1977db96d56Sopenharmony_ci #define EXPORT_FUNC(name) \ 1987db96d56Sopenharmony_ci __pragma(comment(linker, "/EXPORT:" DECORATE #name "=" PYTHON_DLL_NAME "." #name)) 1997db96d56Sopenharmony_ci #define EXPORT_DATA(name) \ 2007db96d56Sopenharmony_ci __pragma(comment(linker, "/EXPORT:" DECORATE #name "=" PYTHON_DLL_NAME "." #name ",DATA")) 2017db96d56Sopenharmony_ci """)) 2027db96d56Sopenharmony_ci 2037db96d56Sopenharmony_ci def sort_key(item): 2047db96d56Sopenharmony_ci return item.name.lower() 2057db96d56Sopenharmony_ci 2067db96d56Sopenharmony_ci windows_feature_macros = { 2077db96d56Sopenharmony_ci item.name for item in manifest.select({'feature_macro'}) if item.windows 2087db96d56Sopenharmony_ci } 2097db96d56Sopenharmony_ci for item in sorted( 2107db96d56Sopenharmony_ci manifest.select( 2117db96d56Sopenharmony_ci {'function'}, 2127db96d56Sopenharmony_ci include_abi_only=True, 2137db96d56Sopenharmony_ci ifdef=windows_feature_macros), 2147db96d56Sopenharmony_ci key=sort_key): 2157db96d56Sopenharmony_ci write(f'EXPORT_FUNC({item.name})') 2167db96d56Sopenharmony_ci 2177db96d56Sopenharmony_ci write() 2187db96d56Sopenharmony_ci 2197db96d56Sopenharmony_ci for item in sorted( 2207db96d56Sopenharmony_ci manifest.select( 2217db96d56Sopenharmony_ci {'data'}, 2227db96d56Sopenharmony_ci include_abi_only=True, 2237db96d56Sopenharmony_ci ifdef=windows_feature_macros), 2247db96d56Sopenharmony_ci key=sort_key): 2257db96d56Sopenharmony_ci write(f'EXPORT_DATA({item.name})') 2267db96d56Sopenharmony_ci 2277db96d56Sopenharmony_ciREST_ROLES = { 2287db96d56Sopenharmony_ci 'function': 'function', 2297db96d56Sopenharmony_ci 'data': 'var', 2307db96d56Sopenharmony_ci 'struct': 'type', 2317db96d56Sopenharmony_ci 'macro': 'macro', 2327db96d56Sopenharmony_ci # 'const': 'const', # all undocumented 2337db96d56Sopenharmony_ci 'typedef': 'type', 2347db96d56Sopenharmony_ci} 2357db96d56Sopenharmony_ci 2367db96d56Sopenharmony_ci@generator("doc_list", 'Doc/data/stable_abi.dat') 2377db96d56Sopenharmony_cidef gen_doc_annotations(manifest, args, outfile): 2387db96d56Sopenharmony_ci """Generate/check the stable ABI list for documentation annotations""" 2397db96d56Sopenharmony_ci writer = csv.DictWriter( 2407db96d56Sopenharmony_ci outfile, 2417db96d56Sopenharmony_ci ['role', 'name', 'added', 'ifdef_note', 'struct_abi_kind'], 2427db96d56Sopenharmony_ci lineterminator='\n') 2437db96d56Sopenharmony_ci writer.writeheader() 2447db96d56Sopenharmony_ci for item in manifest.select(REST_ROLES.keys(), include_abi_only=False): 2457db96d56Sopenharmony_ci if item.ifdef: 2467db96d56Sopenharmony_ci ifdef_note = manifest.contents[item.ifdef].doc 2477db96d56Sopenharmony_ci else: 2487db96d56Sopenharmony_ci ifdef_note = None 2497db96d56Sopenharmony_ci row = { 2507db96d56Sopenharmony_ci 'role': REST_ROLES[item.kind], 2517db96d56Sopenharmony_ci 'name': item.name, 2527db96d56Sopenharmony_ci 'added': item.added, 2537db96d56Sopenharmony_ci 'ifdef_note': ifdef_note} 2547db96d56Sopenharmony_ci rows = [row] 2557db96d56Sopenharmony_ci if item.kind == 'struct': 2567db96d56Sopenharmony_ci row['struct_abi_kind'] = item.struct_abi_kind 2577db96d56Sopenharmony_ci for member_name in item.members or (): 2587db96d56Sopenharmony_ci rows.append({ 2597db96d56Sopenharmony_ci 'role': 'member', 2607db96d56Sopenharmony_ci 'name': f'{item.name}.{member_name}', 2617db96d56Sopenharmony_ci 'added': item.added}) 2627db96d56Sopenharmony_ci writer.writerows(rows) 2637db96d56Sopenharmony_ci 2647db96d56Sopenharmony_ci@generator("ctypes_test", 'Lib/test/test_stable_abi_ctypes.py') 2657db96d56Sopenharmony_cidef gen_ctypes_test(manifest, args, outfile): 2667db96d56Sopenharmony_ci """Generate/check the ctypes-based test for exported symbols""" 2677db96d56Sopenharmony_ci write = partial(print, file=outfile) 2687db96d56Sopenharmony_ci write(textwrap.dedent(''' 2697db96d56Sopenharmony_ci # Generated by Tools/scripts/stable_abi.py 2707db96d56Sopenharmony_ci 2717db96d56Sopenharmony_ci """Test that all symbols of the Stable ABI are accessible using ctypes 2727db96d56Sopenharmony_ci """ 2737db96d56Sopenharmony_ci 2747db96d56Sopenharmony_ci import sys 2757db96d56Sopenharmony_ci import unittest 2767db96d56Sopenharmony_ci from test.support.import_helper import import_module 2777db96d56Sopenharmony_ci from _testcapi import get_feature_macros 2787db96d56Sopenharmony_ci 2797db96d56Sopenharmony_ci feature_macros = get_feature_macros() 2807db96d56Sopenharmony_ci ctypes_test = import_module('ctypes') 2817db96d56Sopenharmony_ci 2827db96d56Sopenharmony_ci class TestStableABIAvailability(unittest.TestCase): 2837db96d56Sopenharmony_ci def test_available_symbols(self): 2847db96d56Sopenharmony_ci 2857db96d56Sopenharmony_ci for symbol_name in SYMBOL_NAMES: 2867db96d56Sopenharmony_ci with self.subTest(symbol_name): 2877db96d56Sopenharmony_ci ctypes_test.pythonapi[symbol_name] 2887db96d56Sopenharmony_ci 2897db96d56Sopenharmony_ci def test_feature_macros(self): 2907db96d56Sopenharmony_ci self.assertEqual( 2917db96d56Sopenharmony_ci set(get_feature_macros()), EXPECTED_FEATURE_MACROS) 2927db96d56Sopenharmony_ci 2937db96d56Sopenharmony_ci # The feature macros for Windows are used in creating the DLL 2947db96d56Sopenharmony_ci # definition, so they must be known on all platforms. 2957db96d56Sopenharmony_ci # If we are on Windows, we check that the hardcoded data matches 2967db96d56Sopenharmony_ci # the reality. 2977db96d56Sopenharmony_ci @unittest.skipIf(sys.platform != "win32", "Windows specific test") 2987db96d56Sopenharmony_ci def test_windows_feature_macros(self): 2997db96d56Sopenharmony_ci for name, value in WINDOWS_FEATURE_MACROS.items(): 3007db96d56Sopenharmony_ci if value != 'maybe': 3017db96d56Sopenharmony_ci with self.subTest(name): 3027db96d56Sopenharmony_ci self.assertEqual(feature_macros[name], value) 3037db96d56Sopenharmony_ci 3047db96d56Sopenharmony_ci SYMBOL_NAMES = ( 3057db96d56Sopenharmony_ci ''')) 3067db96d56Sopenharmony_ci items = manifest.select( 3077db96d56Sopenharmony_ci {'function', 'data'}, 3087db96d56Sopenharmony_ci include_abi_only=True, 3097db96d56Sopenharmony_ci ) 3107db96d56Sopenharmony_ci optional_items = {} 3117db96d56Sopenharmony_ci for item in items: 3127db96d56Sopenharmony_ci if item.name in ( 3137db96d56Sopenharmony_ci # Some symbols aren't exported on all platforms. 3147db96d56Sopenharmony_ci # This is a bug: https://bugs.python.org/issue44133 3157db96d56Sopenharmony_ci 'PyModule_Create2', 'PyModule_FromDefAndSpec2', 3167db96d56Sopenharmony_ci ): 3177db96d56Sopenharmony_ci continue 3187db96d56Sopenharmony_ci if item.ifdef: 3197db96d56Sopenharmony_ci optional_items.setdefault(item.ifdef, []).append(item.name) 3207db96d56Sopenharmony_ci else: 3217db96d56Sopenharmony_ci write(f' "{item.name}",') 3227db96d56Sopenharmony_ci write(")") 3237db96d56Sopenharmony_ci for ifdef, names in optional_items.items(): 3247db96d56Sopenharmony_ci write(f"if feature_macros[{ifdef!r}]:") 3257db96d56Sopenharmony_ci write(f" SYMBOL_NAMES += (") 3267db96d56Sopenharmony_ci for name in names: 3277db96d56Sopenharmony_ci write(f" {name!r},") 3287db96d56Sopenharmony_ci write(" )") 3297db96d56Sopenharmony_ci write("") 3307db96d56Sopenharmony_ci feature_macros = list(manifest.select({'feature_macro'})) 3317db96d56Sopenharmony_ci feature_names = sorted(m.name for m in feature_macros) 3327db96d56Sopenharmony_ci write(f"EXPECTED_FEATURE_MACROS = set({pprint.pformat(feature_names)})") 3337db96d56Sopenharmony_ci 3347db96d56Sopenharmony_ci windows_feature_macros = {m.name: m.windows for m in feature_macros} 3357db96d56Sopenharmony_ci write(f"WINDOWS_FEATURE_MACROS = {pprint.pformat(windows_feature_macros)}") 3367db96d56Sopenharmony_ci 3377db96d56Sopenharmony_ci 3387db96d56Sopenharmony_ci@generator("testcapi_feature_macros", 'Modules/_testcapi_feature_macros.inc') 3397db96d56Sopenharmony_cidef gen_testcapi_feature_macros(manifest, args, outfile): 3407db96d56Sopenharmony_ci """Generate/check the stable ABI list for documentation annotations""" 3417db96d56Sopenharmony_ci write = partial(print, file=outfile) 3427db96d56Sopenharmony_ci write('// Generated by Tools/scripts/stable_abi.py') 3437db96d56Sopenharmony_ci write() 3447db96d56Sopenharmony_ci write('// Add an entry in dict `result` for each Stable ABI feature macro.') 3457db96d56Sopenharmony_ci write() 3467db96d56Sopenharmony_ci for macro in manifest.select({'feature_macro'}): 3477db96d56Sopenharmony_ci name = macro.name 3487db96d56Sopenharmony_ci write(f'#ifdef {name}') 3497db96d56Sopenharmony_ci write(f' res = PyDict_SetItemString(result, "{name}", Py_True);') 3507db96d56Sopenharmony_ci write('#else') 3517db96d56Sopenharmony_ci write(f' res = PyDict_SetItemString(result, "{name}", Py_False);') 3527db96d56Sopenharmony_ci write('#endif') 3537db96d56Sopenharmony_ci write('if (res) {') 3547db96d56Sopenharmony_ci write(' Py_DECREF(result); return NULL;') 3557db96d56Sopenharmony_ci write('}') 3567db96d56Sopenharmony_ci write() 3577db96d56Sopenharmony_ci 3587db96d56Sopenharmony_ci 3597db96d56Sopenharmony_cidef generate_or_check(manifest, args, path, func): 3607db96d56Sopenharmony_ci """Generate/check a file with a single generator 3617db96d56Sopenharmony_ci 3627db96d56Sopenharmony_ci Return True if successful; False if a comparison failed. 3637db96d56Sopenharmony_ci """ 3647db96d56Sopenharmony_ci 3657db96d56Sopenharmony_ci outfile = io.StringIO() 3667db96d56Sopenharmony_ci func(manifest, args, outfile) 3677db96d56Sopenharmony_ci generated = outfile.getvalue() 3687db96d56Sopenharmony_ci existing = path.read_text() 3697db96d56Sopenharmony_ci 3707db96d56Sopenharmony_ci if generated != existing: 3717db96d56Sopenharmony_ci if args.generate: 3727db96d56Sopenharmony_ci path.write_text(generated) 3737db96d56Sopenharmony_ci else: 3747db96d56Sopenharmony_ci print(f'File {path} differs from expected!') 3757db96d56Sopenharmony_ci diff = difflib.unified_diff( 3767db96d56Sopenharmony_ci generated.splitlines(), existing.splitlines(), 3777db96d56Sopenharmony_ci str(path), '<expected>', 3787db96d56Sopenharmony_ci lineterm='', 3797db96d56Sopenharmony_ci ) 3807db96d56Sopenharmony_ci for line in diff: 3817db96d56Sopenharmony_ci print(line) 3827db96d56Sopenharmony_ci return False 3837db96d56Sopenharmony_ci return True 3847db96d56Sopenharmony_ci 3857db96d56Sopenharmony_ci 3867db96d56Sopenharmony_cidef do_unixy_check(manifest, args): 3877db96d56Sopenharmony_ci """Check headers & library using "Unixy" tools (GCC/clang, binutils)""" 3887db96d56Sopenharmony_ci okay = True 3897db96d56Sopenharmony_ci 3907db96d56Sopenharmony_ci # Get all macros first: we'll need feature macros like HAVE_FORK and 3917db96d56Sopenharmony_ci # MS_WINDOWS for everything else 3927db96d56Sopenharmony_ci present_macros = gcc_get_limited_api_macros(['Include/Python.h']) 3937db96d56Sopenharmony_ci feature_macros = set(m.name for m in manifest.select({'feature_macro'})) 3947db96d56Sopenharmony_ci feature_macros &= present_macros 3957db96d56Sopenharmony_ci 3967db96d56Sopenharmony_ci # Check that we have all needed macros 3977db96d56Sopenharmony_ci expected_macros = set( 3987db96d56Sopenharmony_ci item.name for item in manifest.select({'macro'}) 3997db96d56Sopenharmony_ci ) 4007db96d56Sopenharmony_ci missing_macros = expected_macros - present_macros 4017db96d56Sopenharmony_ci okay &= _report_unexpected_items( 4027db96d56Sopenharmony_ci missing_macros, 4037db96d56Sopenharmony_ci 'Some macros from are not defined from "Include/Python.h"' 4047db96d56Sopenharmony_ci + 'with Py_LIMITED_API:') 4057db96d56Sopenharmony_ci 4067db96d56Sopenharmony_ci expected_symbols = set(item.name for item in manifest.select( 4077db96d56Sopenharmony_ci {'function', 'data'}, include_abi_only=True, ifdef=feature_macros, 4087db96d56Sopenharmony_ci )) 4097db96d56Sopenharmony_ci 4107db96d56Sopenharmony_ci # Check the static library (*.a) 4117db96d56Sopenharmony_ci LIBRARY = sysconfig.get_config_var("LIBRARY") 4127db96d56Sopenharmony_ci if not LIBRARY: 4137db96d56Sopenharmony_ci raise Exception("failed to get LIBRARY variable from sysconfig") 4147db96d56Sopenharmony_ci if os.path.exists(LIBRARY): 4157db96d56Sopenharmony_ci okay &= binutils_check_library( 4167db96d56Sopenharmony_ci manifest, LIBRARY, expected_symbols, dynamic=False) 4177db96d56Sopenharmony_ci 4187db96d56Sopenharmony_ci # Check the dynamic library (*.so) 4197db96d56Sopenharmony_ci LDLIBRARY = sysconfig.get_config_var("LDLIBRARY") 4207db96d56Sopenharmony_ci if not LDLIBRARY: 4217db96d56Sopenharmony_ci raise Exception("failed to get LDLIBRARY variable from sysconfig") 4227db96d56Sopenharmony_ci okay &= binutils_check_library( 4237db96d56Sopenharmony_ci manifest, LDLIBRARY, expected_symbols, dynamic=False) 4247db96d56Sopenharmony_ci 4257db96d56Sopenharmony_ci # Check definitions in the header files 4267db96d56Sopenharmony_ci expected_defs = set(item.name for item in manifest.select( 4277db96d56Sopenharmony_ci {'function', 'data'}, include_abi_only=False, ifdef=feature_macros, 4287db96d56Sopenharmony_ci )) 4297db96d56Sopenharmony_ci found_defs = gcc_get_limited_api_definitions(['Include/Python.h']) 4307db96d56Sopenharmony_ci missing_defs = expected_defs - found_defs 4317db96d56Sopenharmony_ci okay &= _report_unexpected_items( 4327db96d56Sopenharmony_ci missing_defs, 4337db96d56Sopenharmony_ci 'Some expected declarations were not declared in ' 4347db96d56Sopenharmony_ci + '"Include/Python.h" with Py_LIMITED_API:') 4357db96d56Sopenharmony_ci 4367db96d56Sopenharmony_ci # Some Limited API macros are defined in terms of private symbols. 4377db96d56Sopenharmony_ci # These are not part of Limited API (even though they're defined with 4387db96d56Sopenharmony_ci # Py_LIMITED_API). They must be part of the Stable ABI, though. 4397db96d56Sopenharmony_ci private_symbols = {n for n in expected_symbols if n.startswith('_')} 4407db96d56Sopenharmony_ci extra_defs = found_defs - expected_defs - private_symbols 4417db96d56Sopenharmony_ci okay &= _report_unexpected_items( 4427db96d56Sopenharmony_ci extra_defs, 4437db96d56Sopenharmony_ci 'Some extra declarations were found in "Include/Python.h" ' 4447db96d56Sopenharmony_ci + 'with Py_LIMITED_API:') 4457db96d56Sopenharmony_ci 4467db96d56Sopenharmony_ci return okay 4477db96d56Sopenharmony_ci 4487db96d56Sopenharmony_ci 4497db96d56Sopenharmony_cidef _report_unexpected_items(items, msg): 4507db96d56Sopenharmony_ci """If there are any `items`, report them using "msg" and return false""" 4517db96d56Sopenharmony_ci if items: 4527db96d56Sopenharmony_ci print(msg, file=sys.stderr) 4537db96d56Sopenharmony_ci for item in sorted(items): 4547db96d56Sopenharmony_ci print(' -', item, file=sys.stderr) 4557db96d56Sopenharmony_ci return False 4567db96d56Sopenharmony_ci return True 4577db96d56Sopenharmony_ci 4587db96d56Sopenharmony_ci 4597db96d56Sopenharmony_cidef binutils_get_exported_symbols(library, dynamic=False): 4607db96d56Sopenharmony_ci """Retrieve exported symbols using the nm(1) tool from binutils""" 4617db96d56Sopenharmony_ci # Only look at dynamic symbols 4627db96d56Sopenharmony_ci args = ["nm", "--no-sort"] 4637db96d56Sopenharmony_ci if dynamic: 4647db96d56Sopenharmony_ci args.append("--dynamic") 4657db96d56Sopenharmony_ci args.append(library) 4667db96d56Sopenharmony_ci proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True) 4677db96d56Sopenharmony_ci if proc.returncode: 4687db96d56Sopenharmony_ci sys.stdout.write(proc.stdout) 4697db96d56Sopenharmony_ci sys.exit(proc.returncode) 4707db96d56Sopenharmony_ci 4717db96d56Sopenharmony_ci stdout = proc.stdout.rstrip() 4727db96d56Sopenharmony_ci if not stdout: 4737db96d56Sopenharmony_ci raise Exception("command output is empty") 4747db96d56Sopenharmony_ci 4757db96d56Sopenharmony_ci for line in stdout.splitlines(): 4767db96d56Sopenharmony_ci # Split line '0000000000001b80 D PyTextIOWrapper_Type' 4777db96d56Sopenharmony_ci if not line: 4787db96d56Sopenharmony_ci continue 4797db96d56Sopenharmony_ci 4807db96d56Sopenharmony_ci parts = line.split(maxsplit=2) 4817db96d56Sopenharmony_ci if len(parts) < 3: 4827db96d56Sopenharmony_ci continue 4837db96d56Sopenharmony_ci 4847db96d56Sopenharmony_ci symbol = parts[-1] 4857db96d56Sopenharmony_ci if MACOS and symbol.startswith("_"): 4867db96d56Sopenharmony_ci yield symbol[1:] 4877db96d56Sopenharmony_ci else: 4887db96d56Sopenharmony_ci yield symbol 4897db96d56Sopenharmony_ci 4907db96d56Sopenharmony_ci 4917db96d56Sopenharmony_cidef binutils_check_library(manifest, library, expected_symbols, dynamic): 4927db96d56Sopenharmony_ci """Check that library exports all expected_symbols""" 4937db96d56Sopenharmony_ci available_symbols = set(binutils_get_exported_symbols(library, dynamic)) 4947db96d56Sopenharmony_ci missing_symbols = expected_symbols - available_symbols 4957db96d56Sopenharmony_ci if missing_symbols: 4967db96d56Sopenharmony_ci print(textwrap.dedent(f"""\ 4977db96d56Sopenharmony_ci Some symbols from the limited API are missing from {library}: 4987db96d56Sopenharmony_ci {', '.join(missing_symbols)} 4997db96d56Sopenharmony_ci 5007db96d56Sopenharmony_ci This error means that there are some missing symbols among the 5017db96d56Sopenharmony_ci ones exported in the library. 5027db96d56Sopenharmony_ci This normally means that some symbol, function implementation or 5037db96d56Sopenharmony_ci a prototype belonging to a symbol in the limited API has been 5047db96d56Sopenharmony_ci deleted or is missing. 5057db96d56Sopenharmony_ci """), file=sys.stderr) 5067db96d56Sopenharmony_ci return False 5077db96d56Sopenharmony_ci return True 5087db96d56Sopenharmony_ci 5097db96d56Sopenharmony_ci 5107db96d56Sopenharmony_cidef gcc_get_limited_api_macros(headers): 5117db96d56Sopenharmony_ci """Get all limited API macros from headers. 5127db96d56Sopenharmony_ci 5137db96d56Sopenharmony_ci Runs the preprocessor over all the header files in "Include" setting 5147db96d56Sopenharmony_ci "-DPy_LIMITED_API" to the correct value for the running version of the 5157db96d56Sopenharmony_ci interpreter and extracting all macro definitions (via adding -dM to the 5167db96d56Sopenharmony_ci compiler arguments). 5177db96d56Sopenharmony_ci 5187db96d56Sopenharmony_ci Requires Python built with a GCC-compatible compiler. (clang might work) 5197db96d56Sopenharmony_ci """ 5207db96d56Sopenharmony_ci 5217db96d56Sopenharmony_ci api_hexversion = sys.version_info.major << 24 | sys.version_info.minor << 16 5227db96d56Sopenharmony_ci 5237db96d56Sopenharmony_ci preprocesor_output_with_macros = subprocess.check_output( 5247db96d56Sopenharmony_ci sysconfig.get_config_var("CC").split() 5257db96d56Sopenharmony_ci + [ 5267db96d56Sopenharmony_ci # Prevent the expansion of the exported macros so we can 5277db96d56Sopenharmony_ci # capture them later 5287db96d56Sopenharmony_ci "-DSIZEOF_WCHAR_T=4", # The actual value is not important 5297db96d56Sopenharmony_ci f"-DPy_LIMITED_API={api_hexversion}", 5307db96d56Sopenharmony_ci "-I.", 5317db96d56Sopenharmony_ci "-I./Include", 5327db96d56Sopenharmony_ci "-dM", 5337db96d56Sopenharmony_ci "-E", 5347db96d56Sopenharmony_ci ] 5357db96d56Sopenharmony_ci + [str(file) for file in headers], 5367db96d56Sopenharmony_ci text=True, 5377db96d56Sopenharmony_ci ) 5387db96d56Sopenharmony_ci 5397db96d56Sopenharmony_ci return { 5407db96d56Sopenharmony_ci target 5417db96d56Sopenharmony_ci for target in re.findall( 5427db96d56Sopenharmony_ci r"#define (\w+)", preprocesor_output_with_macros 5437db96d56Sopenharmony_ci ) 5447db96d56Sopenharmony_ci } 5457db96d56Sopenharmony_ci 5467db96d56Sopenharmony_ci 5477db96d56Sopenharmony_cidef gcc_get_limited_api_definitions(headers): 5487db96d56Sopenharmony_ci """Get all limited API definitions from headers. 5497db96d56Sopenharmony_ci 5507db96d56Sopenharmony_ci Run the preprocessor over all the header files in "Include" setting 5517db96d56Sopenharmony_ci "-DPy_LIMITED_API" to the correct value for the running version of the 5527db96d56Sopenharmony_ci interpreter. 5537db96d56Sopenharmony_ci 5547db96d56Sopenharmony_ci The limited API symbols will be extracted from the output of this command 5557db96d56Sopenharmony_ci as it includes the prototypes and definitions of all the exported symbols 5567db96d56Sopenharmony_ci that are in the limited api. 5577db96d56Sopenharmony_ci 5587db96d56Sopenharmony_ci This function does *NOT* extract the macros defined on the limited API 5597db96d56Sopenharmony_ci 5607db96d56Sopenharmony_ci Requires Python built with a GCC-compatible compiler. (clang might work) 5617db96d56Sopenharmony_ci """ 5627db96d56Sopenharmony_ci api_hexversion = sys.version_info.major << 24 | sys.version_info.minor << 16 5637db96d56Sopenharmony_ci preprocesor_output = subprocess.check_output( 5647db96d56Sopenharmony_ci sysconfig.get_config_var("CC").split() 5657db96d56Sopenharmony_ci + [ 5667db96d56Sopenharmony_ci # Prevent the expansion of the exported macros so we can capture 5677db96d56Sopenharmony_ci # them later 5687db96d56Sopenharmony_ci "-DPyAPI_FUNC=__PyAPI_FUNC", 5697db96d56Sopenharmony_ci "-DPyAPI_DATA=__PyAPI_DATA", 5707db96d56Sopenharmony_ci "-DEXPORT_DATA=__EXPORT_DATA", 5717db96d56Sopenharmony_ci "-D_Py_NO_RETURN=", 5727db96d56Sopenharmony_ci "-DSIZEOF_WCHAR_T=4", # The actual value is not important 5737db96d56Sopenharmony_ci f"-DPy_LIMITED_API={api_hexversion}", 5747db96d56Sopenharmony_ci "-I.", 5757db96d56Sopenharmony_ci "-I./Include", 5767db96d56Sopenharmony_ci "-E", 5777db96d56Sopenharmony_ci ] 5787db96d56Sopenharmony_ci + [str(file) for file in headers], 5797db96d56Sopenharmony_ci text=True, 5807db96d56Sopenharmony_ci stderr=subprocess.DEVNULL, 5817db96d56Sopenharmony_ci ) 5827db96d56Sopenharmony_ci stable_functions = set( 5837db96d56Sopenharmony_ci re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output) 5847db96d56Sopenharmony_ci ) 5857db96d56Sopenharmony_ci stable_exported_data = set( 5867db96d56Sopenharmony_ci re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output) 5877db96d56Sopenharmony_ci ) 5887db96d56Sopenharmony_ci stable_data = set( 5897db96d56Sopenharmony_ci re.findall(r"__PyAPI_DATA\(.*?\)[\s\*\(]*([^);]*)\)?.*;", preprocesor_output) 5907db96d56Sopenharmony_ci ) 5917db96d56Sopenharmony_ci return stable_data | stable_exported_data | stable_functions 5927db96d56Sopenharmony_ci 5937db96d56Sopenharmony_cidef check_private_names(manifest): 5947db96d56Sopenharmony_ci """Ensure limited API doesn't contain private names 5957db96d56Sopenharmony_ci 5967db96d56Sopenharmony_ci Names prefixed by an underscore are private by definition. 5977db96d56Sopenharmony_ci """ 5987db96d56Sopenharmony_ci for name, item in manifest.contents.items(): 5997db96d56Sopenharmony_ci if name.startswith('_') and not item.abi_only: 6007db96d56Sopenharmony_ci raise ValueError( 6017db96d56Sopenharmony_ci f'`{name}` is private (underscore-prefixed) and should be ' 6027db96d56Sopenharmony_ci + 'removed from the stable ABI list or or marked `abi_only`') 6037db96d56Sopenharmony_ci 6047db96d56Sopenharmony_cidef check_dump(manifest, filename): 6057db96d56Sopenharmony_ci """Check that manifest.dump() corresponds to the data. 6067db96d56Sopenharmony_ci 6077db96d56Sopenharmony_ci Mainly useful when debugging this script. 6087db96d56Sopenharmony_ci """ 6097db96d56Sopenharmony_ci dumped = tomllib.loads('\n'.join(manifest.dump())) 6107db96d56Sopenharmony_ci with filename.open('rb') as file: 6117db96d56Sopenharmony_ci from_file = tomllib.load(file) 6127db96d56Sopenharmony_ci if dumped != from_file: 6137db96d56Sopenharmony_ci print(f'Dump differs from loaded data!', file=sys.stderr) 6147db96d56Sopenharmony_ci diff = difflib.unified_diff( 6157db96d56Sopenharmony_ci pprint.pformat(dumped).splitlines(), 6167db96d56Sopenharmony_ci pprint.pformat(from_file).splitlines(), 6177db96d56Sopenharmony_ci '<dumped>', str(filename), 6187db96d56Sopenharmony_ci lineterm='', 6197db96d56Sopenharmony_ci ) 6207db96d56Sopenharmony_ci for line in diff: 6217db96d56Sopenharmony_ci print(line, file=sys.stderr) 6227db96d56Sopenharmony_ci return False 6237db96d56Sopenharmony_ci else: 6247db96d56Sopenharmony_ci return True 6257db96d56Sopenharmony_ci 6267db96d56Sopenharmony_cidef main(): 6277db96d56Sopenharmony_ci parser = argparse.ArgumentParser( 6287db96d56Sopenharmony_ci description=__doc__, 6297db96d56Sopenharmony_ci formatter_class=argparse.RawDescriptionHelpFormatter, 6307db96d56Sopenharmony_ci ) 6317db96d56Sopenharmony_ci parser.add_argument( 6327db96d56Sopenharmony_ci "file", type=Path, metavar='FILE', 6337db96d56Sopenharmony_ci help="file with the stable abi manifest", 6347db96d56Sopenharmony_ci ) 6357db96d56Sopenharmony_ci parser.add_argument( 6367db96d56Sopenharmony_ci "--generate", action='store_true', 6377db96d56Sopenharmony_ci help="generate file(s), rather than just checking them", 6387db96d56Sopenharmony_ci ) 6397db96d56Sopenharmony_ci parser.add_argument( 6407db96d56Sopenharmony_ci "--generate-all", action='store_true', 6417db96d56Sopenharmony_ci help="as --generate, but generate all file(s) using default filenames." 6427db96d56Sopenharmony_ci + " (unlike --all, does not run any extra checks)", 6437db96d56Sopenharmony_ci ) 6447db96d56Sopenharmony_ci parser.add_argument( 6457db96d56Sopenharmony_ci "-a", "--all", action='store_true', 6467db96d56Sopenharmony_ci help="run all available checks using default filenames", 6477db96d56Sopenharmony_ci ) 6487db96d56Sopenharmony_ci parser.add_argument( 6497db96d56Sopenharmony_ci "-l", "--list", action='store_true', 6507db96d56Sopenharmony_ci help="list available generators and their default filenames; then exit", 6517db96d56Sopenharmony_ci ) 6527db96d56Sopenharmony_ci parser.add_argument( 6537db96d56Sopenharmony_ci "--dump", action='store_true', 6547db96d56Sopenharmony_ci help="dump the manifest contents (used for debugging the parser)", 6557db96d56Sopenharmony_ci ) 6567db96d56Sopenharmony_ci 6577db96d56Sopenharmony_ci actions_group = parser.add_argument_group('actions') 6587db96d56Sopenharmony_ci for gen in generators: 6597db96d56Sopenharmony_ci actions_group.add_argument( 6607db96d56Sopenharmony_ci gen.arg_name, dest=gen.var_name, 6617db96d56Sopenharmony_ci type=str, nargs="?", default=MISSING, 6627db96d56Sopenharmony_ci metavar='FILENAME', 6637db96d56Sopenharmony_ci help=gen.__doc__, 6647db96d56Sopenharmony_ci ) 6657db96d56Sopenharmony_ci actions_group.add_argument( 6667db96d56Sopenharmony_ci '--unixy-check', action='store_true', 6677db96d56Sopenharmony_ci help=do_unixy_check.__doc__, 6687db96d56Sopenharmony_ci ) 6697db96d56Sopenharmony_ci args = parser.parse_args() 6707db96d56Sopenharmony_ci 6717db96d56Sopenharmony_ci base_path = args.file.parent.parent 6727db96d56Sopenharmony_ci 6737db96d56Sopenharmony_ci if args.list: 6747db96d56Sopenharmony_ci for gen in generators: 6757db96d56Sopenharmony_ci print(f'{gen.arg_name}: {base_path / gen.default_path}') 6767db96d56Sopenharmony_ci sys.exit(0) 6777db96d56Sopenharmony_ci 6787db96d56Sopenharmony_ci run_all_generators = args.generate_all 6797db96d56Sopenharmony_ci 6807db96d56Sopenharmony_ci if args.generate_all: 6817db96d56Sopenharmony_ci args.generate = True 6827db96d56Sopenharmony_ci 6837db96d56Sopenharmony_ci if args.all: 6847db96d56Sopenharmony_ci run_all_generators = True 6857db96d56Sopenharmony_ci args.unixy_check = True 6867db96d56Sopenharmony_ci 6877db96d56Sopenharmony_ci try: 6887db96d56Sopenharmony_ci file = args.file.open('rb') 6897db96d56Sopenharmony_ci except FileNotFoundError as err: 6907db96d56Sopenharmony_ci if args.file.suffix == '.txt': 6917db96d56Sopenharmony_ci # Provide a better error message 6927db96d56Sopenharmony_ci suggestion = args.file.with_suffix('.toml') 6937db96d56Sopenharmony_ci raise FileNotFoundError( 6947db96d56Sopenharmony_ci f'{args.file} not found. Did you mean {suggestion} ?') from err 6957db96d56Sopenharmony_ci raise 6967db96d56Sopenharmony_ci with file: 6977db96d56Sopenharmony_ci manifest = parse_manifest(file) 6987db96d56Sopenharmony_ci 6997db96d56Sopenharmony_ci check_private_names(manifest) 7007db96d56Sopenharmony_ci 7017db96d56Sopenharmony_ci # Remember results of all actions (as booleans). 7027db96d56Sopenharmony_ci # At the end we'll check that at least one action was run, 7037db96d56Sopenharmony_ci # and also fail if any are false. 7047db96d56Sopenharmony_ci results = {} 7057db96d56Sopenharmony_ci 7067db96d56Sopenharmony_ci if args.dump: 7077db96d56Sopenharmony_ci for line in manifest.dump(): 7087db96d56Sopenharmony_ci print(line) 7097db96d56Sopenharmony_ci results['dump'] = check_dump(manifest, args.file) 7107db96d56Sopenharmony_ci 7117db96d56Sopenharmony_ci for gen in generators: 7127db96d56Sopenharmony_ci filename = getattr(args, gen.var_name) 7137db96d56Sopenharmony_ci if filename is None or (run_all_generators and filename is MISSING): 7147db96d56Sopenharmony_ci filename = base_path / gen.default_path 7157db96d56Sopenharmony_ci elif filename is MISSING: 7167db96d56Sopenharmony_ci continue 7177db96d56Sopenharmony_ci 7187db96d56Sopenharmony_ci results[gen.var_name] = generate_or_check(manifest, args, filename, gen) 7197db96d56Sopenharmony_ci 7207db96d56Sopenharmony_ci if args.unixy_check: 7217db96d56Sopenharmony_ci results['unixy_check'] = do_unixy_check(manifest, args) 7227db96d56Sopenharmony_ci 7237db96d56Sopenharmony_ci if not results: 7247db96d56Sopenharmony_ci if args.generate: 7257db96d56Sopenharmony_ci parser.error('No file specified. Use --help for usage.') 7267db96d56Sopenharmony_ci parser.error('No check specified. Use --help for usage.') 7277db96d56Sopenharmony_ci 7287db96d56Sopenharmony_ci failed_results = [name for name, result in results.items() if not result] 7297db96d56Sopenharmony_ci 7307db96d56Sopenharmony_ci if failed_results: 7317db96d56Sopenharmony_ci raise Exception(f""" 7327db96d56Sopenharmony_ci These checks related to the stable ABI did not succeed: 7337db96d56Sopenharmony_ci {', '.join(failed_results)} 7347db96d56Sopenharmony_ci 7357db96d56Sopenharmony_ci If you see diffs in the output, files derived from the stable 7367db96d56Sopenharmony_ci ABI manifest the were not regenerated. 7377db96d56Sopenharmony_ci Run `make regen-limited-abi` to fix this. 7387db96d56Sopenharmony_ci 7397db96d56Sopenharmony_ci Otherwise, see the error(s) above. 7407db96d56Sopenharmony_ci 7417db96d56Sopenharmony_ci The stable ABI manifest is at: {args.file} 7427db96d56Sopenharmony_ci Note that there is a process to follow when modifying it. 7437db96d56Sopenharmony_ci 7447db96d56Sopenharmony_ci You can read more about the limited API and its contracts at: 7457db96d56Sopenharmony_ci 7467db96d56Sopenharmony_ci https://docs.python.org/3/c-api/stable.html 7477db96d56Sopenharmony_ci 7487db96d56Sopenharmony_ci And in PEP 384: 7497db96d56Sopenharmony_ci 7507db96d56Sopenharmony_ci https://peps.python.org/pep-0384/ 7517db96d56Sopenharmony_ci """) 7527db96d56Sopenharmony_ci 7537db96d56Sopenharmony_ci 7547db96d56Sopenharmony_ciif __name__ == "__main__": 7557db96d56Sopenharmony_ci main() 756