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