blob: b3a46f985e0a2da75b6bf32c49d4815cd3c1afd1 [file] [log] [blame]
Pablo Galindo85f1ded2020-12-04 22:05:58 +00001#!/usr/bin/env python
2
3import argparse
4import glob
5import re
6import pathlib
7import subprocess
8import sys
9import sysconfig
10
11EXCLUDED_HEADERS = {
12 "bytes_methods.h",
13 "cellobject.h",
14 "classobject.h",
15 "code.h",
16 "compile.h",
17 "datetime.h",
18 "dtoa.h",
19 "frameobject.h",
20 "funcobject.h",
21 "genobject.h",
22 "longintrepr.h",
23 "parsetok.h",
24 "pyarena.h",
25 "pyatomic.h",
26 "pyctype.h",
27 "pydebug.h",
28 "pytime.h",
29 "symtable.h",
30 "token.h",
31 "ucnhash.h",
32}
33
34
35def get_exported_symbols(library, dynamic=False):
36 # Only look at dynamic symbols
37 args = ["nm", "--no-sort"]
38 if dynamic:
39 args.append("--dynamic")
40 args.append(library)
41 proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True)
42 if proc.returncode:
43 sys.stdout.write(proc.stdout)
44 sys.exit(proc.returncode)
45
46 stdout = proc.stdout.rstrip()
47 if not stdout:
48 raise Exception("command output is empty")
49
50 for line in stdout.splitlines():
51 # Split line '0000000000001b80 D PyTextIOWrapper_Type'
52 if not line:
53 continue
54
55 parts = line.split(maxsplit=2)
56 if len(parts) < 3:
57 continue
58
59 symbol = parts[-1]
60 yield symbol
61
62
Pablo Galindo79c18492020-12-04 23:19:21 +000063def check_library(stable_abi_file, library, abi_funcs, dynamic=False):
Pablo Galindo85f1ded2020-12-04 22:05:58 +000064 available_symbols = set(get_exported_symbols(library, dynamic))
65 missing_symbols = abi_funcs - available_symbols
66 if missing_symbols:
Pablo Galindo79c18492020-12-04 23:19:21 +000067 raise Exception(
68 f"""\
69Some symbols from the limited API are missing: {', '.join(missing_symbols)}
70
71This error means that there are some missing symbols among the ones exported
72in the Python library ("libpythonx.x.a" or "libpythonx.x.so"). This normally
73means that some symbol, function implementation or a prototype, belonging to
74a symbol in the limited API has been deleted or is missing.
75
76Check if this was a mistake and if not, update the file containing the limited
77API symbols. This file is located at:
78
79{stable_abi_file}
80
81You can read more about the limited API and its contracts at:
82
83https://docs.python.org/3/c-api/stable.html
84
85And in PEP 384:
86
87https://www.python.org/dev/peps/pep-0384/
88"""
Pablo Galindo85f1ded2020-12-04 22:05:58 +000089 )
Pablo Galindo85f1ded2020-12-04 22:05:58 +000090
91
92def generate_limited_api_symbols(args):
93 if hasattr(sys, "gettotalrefcount"):
94 print(
95 "Stable ABI symbols cannot be generated from a debug build", file=sys.stderr
96 )
97 sys.exit(1)
98 library = sysconfig.get_config_var("LIBRARY")
99 ldlibrary = sysconfig.get_config_var("LDLIBRARY")
100 if ldlibrary != library:
101 raise Exception("Limited ABI symbols can only be generated from a static build")
102 available_symbols = {
103 symbol for symbol in get_exported_symbols(library) if symbol.startswith("Py")
104 }
105
106 headers = [
107 file
108 for file in pathlib.Path("Include").glob("*.h")
109 if file.name not in EXCLUDED_HEADERS
110 ]
111 stable_data, stable_exported_data, stable_functions = get_limited_api_definitions(
112 headers
113 )
114 macros = get_limited_api_macros(headers)
115
116 stable_symbols = {
117 symbol
118 for symbol in (stable_functions | stable_exported_data | stable_data | macros)
119 if symbol.startswith("Py") and symbol in available_symbols
120 }
121 with open(args.output_file, "w") as output_file:
122 output_file.write(f"# File generated by 'make regen-limited-abi'\n")
123 output_file.write(
124 f"# This is NOT an authoritative list of stable ABI symbols\n"
125 )
126 for symbol in sorted(stable_symbols):
127 output_file.write(f"{symbol}\n")
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000128
129
130def get_limited_api_macros(headers):
131 """Run the preprocesor over all the header files in "Include" setting
132 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter
133 and extracting all macro definitions (via adding -dM to the compiler arguments).
134 """
135
136 preprocesor_output_with_macros = subprocess.check_output(
137 sysconfig.get_config_var("CC").split()
138 + [
139 # Prevent the expansion of the exported macros so we can capture them later
140 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
141 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
142 "-I.",
143 "-I./Include",
144 "-dM",
145 "-E",
146 ]
147 + [str(file) for file in headers],
148 text=True,
149 stderr=subprocess.DEVNULL,
150 )
151
152 return {
153 target
154 for _, target in re.findall(
155 r"#define (\w+)\s*(?:\(.*?\))?\s+(\w+)", preprocesor_output_with_macros
156 )
157 }
158
159
160def get_limited_api_definitions(headers):
161 """Run the preprocesor over all the header files in "Include" setting
162 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter.
163
164 The limited API symbols will be extracted from the output of this command as it includes
165 the prototypes and definitions of all the exported symbols that are in the limited api.
166
167 This function does *NOT* extract the macros defined on the limited API
168 """
169 preprocesor_output = subprocess.check_output(
170 sysconfig.get_config_var("CC").split()
171 + [
172 # Prevent the expansion of the exported macros so we can capture them later
173 "-DPyAPI_FUNC=__PyAPI_FUNC",
174 "-DPyAPI_DATA=__PyAPI_DATA",
175 "-DEXPORT_DATA=__EXPORT_DATA",
176 "-D_Py_NO_RETURN=",
177 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
178 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
179 "-I.",
180 "-I./Include",
181 "-E",
182 ]
183 + [str(file) for file in headers],
184 text=True,
185 stderr=subprocess.DEVNULL,
186 )
187 stable_functions = set(
188 re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output)
189 )
190 stable_exported_data = set(
191 re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output)
192 )
193 stable_data = set(
194 re.findall(r"__PyAPI_DATA\(.*?\)\s*\(?(.*?)\)?\s*;", preprocesor_output)
195 )
196 return stable_data, stable_exported_data, stable_functions
197
198
199def check_symbols(parser_args):
200 with open(parser_args.stable_abi_file, "r") as filename:
201 abi_funcs = {
202 symbol
203 for symbol in filename.read().splitlines()
204 if symbol and not symbol.startswith("#")
205 }
206
Pablo Galindo79c18492020-12-04 23:19:21 +0000207 try:
208 # static library
209 LIBRARY = sysconfig.get_config_var("LIBRARY")
210 if not LIBRARY:
211 raise Exception("failed to get LIBRARY variable from sysconfig")
212 check_library(parser_args.stable_abi_file, LIBRARY, abi_funcs)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000213
Pablo Galindo79c18492020-12-04 23:19:21 +0000214 # dynamic library
215 LDLIBRARY = sysconfig.get_config_var("LDLIBRARY")
216 if not LDLIBRARY:
217 raise Exception("failed to get LDLIBRARY variable from sysconfig")
218 if LDLIBRARY != LIBRARY:
219 check_library(
220 parser_args.stable_abi_file, LDLIBRARY, abi_funcs, dynamic=True
221 )
222 except Exception as e:
223 print(e, file=sys.stderr)
224 sys.exit(1)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000225
226
227def main():
228 parser = argparse.ArgumentParser(description="Process some integers.")
229 subparsers = parser.add_subparsers()
230 check_parser = subparsers.add_parser(
231 "check", help="Check the exported symbols against a given ABI file"
232 )
233 check_parser.add_argument(
234 "stable_abi_file", type=str, help="File with the stable abi functions"
235 )
236 check_parser.set_defaults(func=check_symbols)
237 generate_parser = subparsers.add_parser(
238 "generate",
239 help="Generate symbols from the header files and the exported symbols",
240 )
241 generate_parser.add_argument(
242 "output_file", type=str, help="File to dump the symbols to"
243 )
244 generate_parser.set_defaults(func=generate_limited_api_symbols)
245 args = parser.parse_args()
246 if "func" not in args:
247 parser.error("Either 'check' or 'generate' must be used")
248 sys.exit(1)
249
250 args.func(args)
251
252
253if __name__ == "__main__":
254 main()