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