blob: 1690cfce1727d7c7f582b373344832e39b6382ca [file] [log] [blame]
Pablo Galindo85f1ded2020-12-04 22:05:58 +00001#!/usr/bin/env python
2
3import argparse
4import glob
Victor Stinner801bb0b2021-02-17 11:14:42 +01005import os.path
Pablo Galindo85f1ded2020-12-04 22:05:58 +00006import pathlib
Victor Stinner801bb0b2021-02-17 11:14:42 +01007import re
Pablo Galindo85f1ded2020-12-04 22:05:58 +00008import subprocess
9import sys
10import sysconfig
11
12EXCLUDED_HEADERS = {
13 "bytes_methods.h",
14 "cellobject.h",
15 "classobject.h",
16 "code.h",
17 "compile.h",
18 "datetime.h",
19 "dtoa.h",
20 "frameobject.h",
21 "funcobject.h",
22 "genobject.h",
23 "longintrepr.h",
24 "parsetok.h",
Pablo Galindo85f1ded2020-12-04 22:05:58 +000025 "pyatomic.h",
Pablo Galindo85f1ded2020-12-04 22:05:58 +000026 "pytime.h",
Pablo Galindo85f1ded2020-12-04 22:05:58 +000027 "token.h",
28 "ucnhash.h",
29}
30
Pablo Galindo09114112020-12-15 18:16:13 +000031MACOS = (sys.platform == "darwin")
Pablo Galindo85f1ded2020-12-04 22:05:58 +000032
33def get_exported_symbols(library, dynamic=False):
34 # Only look at dynamic symbols
35 args = ["nm", "--no-sort"]
36 if dynamic:
37 args.append("--dynamic")
38 args.append(library)
39 proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True)
40 if proc.returncode:
41 sys.stdout.write(proc.stdout)
42 sys.exit(proc.returncode)
43
44 stdout = proc.stdout.rstrip()
45 if not stdout:
46 raise Exception("command output is empty")
47
48 for line in stdout.splitlines():
49 # Split line '0000000000001b80 D PyTextIOWrapper_Type'
50 if not line:
51 continue
52
53 parts = line.split(maxsplit=2)
54 if len(parts) < 3:
55 continue
56
57 symbol = parts[-1]
Pablo Galindo09114112020-12-15 18:16:13 +000058 if MACOS and symbol.startswith("_"):
59 yield symbol[1:]
60 else:
61 yield symbol
Pablo Galindo85f1ded2020-12-04 22:05:58 +000062
63
Pablo Galindo79c18492020-12-04 23:19:21 +000064def check_library(stable_abi_file, library, abi_funcs, dynamic=False):
Pablo Galindo85f1ded2020-12-04 22:05:58 +000065 available_symbols = set(get_exported_symbols(library, dynamic))
66 missing_symbols = abi_funcs - available_symbols
67 if missing_symbols:
Pablo Galindo79c18492020-12-04 23:19:21 +000068 raise Exception(
69 f"""\
70Some symbols from the limited API are missing: {', '.join(missing_symbols)}
71
72This error means that there are some missing symbols among the ones exported
73in the Python library ("libpythonx.x.a" or "libpythonx.x.so"). This normally
74means that some symbol, function implementation or a prototype, belonging to
75a symbol in the limited API has been deleted or is missing.
76
77Check if this was a mistake and if not, update the file containing the limited
78API symbols. This file is located at:
79
80{stable_abi_file}
81
82You can read more about the limited API and its contracts at:
83
84https://docs.python.org/3/c-api/stable.html
85
86And in PEP 384:
87
88https://www.python.org/dev/peps/pep-0384/
89"""
Pablo Galindo85f1ded2020-12-04 22:05:58 +000090 )
Pablo Galindo85f1ded2020-12-04 22:05:58 +000091
92
93def generate_limited_api_symbols(args):
Pablo Galindo85f1ded2020-12-04 22:05:58 +000094 library = sysconfig.get_config_var("LIBRARY")
95 ldlibrary = sysconfig.get_config_var("LDLIBRARY")
96 if ldlibrary != library:
97 raise Exception("Limited ABI symbols can only be generated from a static build")
98 available_symbols = {
99 symbol for symbol in get_exported_symbols(library) if symbol.startswith("Py")
100 }
101
102 headers = [
103 file
104 for file in pathlib.Path("Include").glob("*.h")
105 if file.name not in EXCLUDED_HEADERS
106 ]
107 stable_data, stable_exported_data, stable_functions = get_limited_api_definitions(
108 headers
109 )
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000110
111 stable_symbols = {
112 symbol
Victor Stinner61092a92021-04-01 14:13:42 +0200113 for symbol in (stable_functions | stable_exported_data | stable_data)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000114 if symbol.startswith("Py") and symbol in available_symbols
115 }
116 with open(args.output_file, "w") as output_file:
117 output_file.write(f"# File generated by 'make regen-limited-abi'\n")
118 output_file.write(
119 f"# This is NOT an authoritative list of stable ABI symbols\n"
120 )
121 for symbol in sorted(stable_symbols):
122 output_file.write(f"{symbol}\n")
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000123
124
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000125def get_limited_api_definitions(headers):
126 """Run the preprocesor over all the header files in "Include" setting
127 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter.
128
129 The limited API symbols will be extracted from the output of this command as it includes
130 the prototypes and definitions of all the exported symbols that are in the limited api.
131
132 This function does *NOT* extract the macros defined on the limited API
133 """
134 preprocesor_output = subprocess.check_output(
135 sysconfig.get_config_var("CC").split()
136 + [
137 # Prevent the expansion of the exported macros so we can capture them later
138 "-DPyAPI_FUNC=__PyAPI_FUNC",
139 "-DPyAPI_DATA=__PyAPI_DATA",
140 "-DEXPORT_DATA=__EXPORT_DATA",
141 "-D_Py_NO_RETURN=",
142 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
143 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
144 "-I.",
145 "-I./Include",
146 "-E",
147 ]
148 + [str(file) for file in headers],
149 text=True,
150 stderr=subprocess.DEVNULL,
151 )
152 stable_functions = set(
153 re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output)
154 )
155 stable_exported_data = set(
156 re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output)
157 )
158 stable_data = set(
159 re.findall(r"__PyAPI_DATA\(.*?\)\s*\(?(.*?)\)?\s*;", preprocesor_output)
160 )
161 return stable_data, stable_exported_data, stable_functions
162
163
164def check_symbols(parser_args):
165 with open(parser_args.stable_abi_file, "r") as filename:
166 abi_funcs = {
167 symbol
168 for symbol in filename.read().splitlines()
169 if symbol and not symbol.startswith("#")
170 }
171
Pablo Galindo79c18492020-12-04 23:19:21 +0000172 try:
173 # static library
174 LIBRARY = sysconfig.get_config_var("LIBRARY")
175 if not LIBRARY:
176 raise Exception("failed to get LIBRARY variable from sysconfig")
Victor Stinner801bb0b2021-02-17 11:14:42 +0100177 if os.path.exists(LIBRARY):
178 check_library(parser_args.stable_abi_file, LIBRARY, abi_funcs)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000179
Pablo Galindo79c18492020-12-04 23:19:21 +0000180 # dynamic library
181 LDLIBRARY = sysconfig.get_config_var("LDLIBRARY")
182 if not LDLIBRARY:
183 raise Exception("failed to get LDLIBRARY variable from sysconfig")
184 if LDLIBRARY != LIBRARY:
185 check_library(
186 parser_args.stable_abi_file, LDLIBRARY, abi_funcs, dynamic=True
187 )
188 except Exception as e:
189 print(e, file=sys.stderr)
190 sys.exit(1)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000191
192
193def main():
194 parser = argparse.ArgumentParser(description="Process some integers.")
195 subparsers = parser.add_subparsers()
196 check_parser = subparsers.add_parser(
197 "check", help="Check the exported symbols against a given ABI file"
198 )
199 check_parser.add_argument(
200 "stable_abi_file", type=str, help="File with the stable abi functions"
201 )
202 check_parser.set_defaults(func=check_symbols)
203 generate_parser = subparsers.add_parser(
204 "generate",
205 help="Generate symbols from the header files and the exported symbols",
206 )
207 generate_parser.add_argument(
208 "output_file", type=str, help="File to dump the symbols to"
209 )
210 generate_parser.set_defaults(func=generate_limited_api_symbols)
211 args = parser.parse_args()
212 if "func" not in args:
213 parser.error("Either 'check' or 'generate' must be used")
214 sys.exit(1)
215
216 args.func(args)
217
218
219if __name__ == "__main__":
220 main()