blob: 0f9e36547e765d4db17875d577645756b8c646d8 [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):
94 if hasattr(sys, "gettotalrefcount"):
95 print(
96 "Stable ABI symbols cannot be generated from a debug build", file=sys.stderr
97 )
98 sys.exit(1)
99 library = sysconfig.get_config_var("LIBRARY")
100 ldlibrary = sysconfig.get_config_var("LDLIBRARY")
101 if ldlibrary != library:
102 raise Exception("Limited ABI symbols can only be generated from a static build")
103 available_symbols = {
104 symbol for symbol in get_exported_symbols(library) if symbol.startswith("Py")
105 }
106
107 headers = [
108 file
109 for file in pathlib.Path("Include").glob("*.h")
110 if file.name not in EXCLUDED_HEADERS
111 ]
112 stable_data, stable_exported_data, stable_functions = get_limited_api_definitions(
113 headers
114 )
115 macros = get_limited_api_macros(headers)
116
117 stable_symbols = {
118 symbol
119 for symbol in (stable_functions | stable_exported_data | stable_data | macros)
120 if symbol.startswith("Py") and symbol in available_symbols
121 }
122 with open(args.output_file, "w") as output_file:
123 output_file.write(f"# File generated by 'make regen-limited-abi'\n")
124 output_file.write(
125 f"# This is NOT an authoritative list of stable ABI symbols\n"
126 )
127 for symbol in sorted(stable_symbols):
128 output_file.write(f"{symbol}\n")
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000129
130
131def get_limited_api_macros(headers):
132 """Run the preprocesor over all the header files in "Include" setting
133 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter
134 and extracting all macro definitions (via adding -dM to the compiler arguments).
135 """
136
137 preprocesor_output_with_macros = subprocess.check_output(
138 sysconfig.get_config_var("CC").split()
139 + [
140 # Prevent the expansion of the exported macros so we can capture them later
141 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
142 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
143 "-I.",
144 "-I./Include",
145 "-dM",
146 "-E",
147 ]
148 + [str(file) for file in headers],
149 text=True,
150 stderr=subprocess.DEVNULL,
151 )
152
153 return {
154 target
155 for _, target in re.findall(
156 r"#define (\w+)\s*(?:\(.*?\))?\s+(\w+)", preprocesor_output_with_macros
157 )
158 }
159
160
161def get_limited_api_definitions(headers):
162 """Run the preprocesor over all the header files in "Include" setting
163 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter.
164
165 The limited API symbols will be extracted from the output of this command as it includes
166 the prototypes and definitions of all the exported symbols that are in the limited api.
167
168 This function does *NOT* extract the macros defined on the limited API
169 """
170 preprocesor_output = subprocess.check_output(
171 sysconfig.get_config_var("CC").split()
172 + [
173 # Prevent the expansion of the exported macros so we can capture them later
174 "-DPyAPI_FUNC=__PyAPI_FUNC",
175 "-DPyAPI_DATA=__PyAPI_DATA",
176 "-DEXPORT_DATA=__EXPORT_DATA",
177 "-D_Py_NO_RETURN=",
178 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
179 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
180 "-I.",
181 "-I./Include",
182 "-E",
183 ]
184 + [str(file) for file in headers],
185 text=True,
186 stderr=subprocess.DEVNULL,
187 )
188 stable_functions = set(
189 re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output)
190 )
191 stable_exported_data = set(
192 re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output)
193 )
194 stable_data = set(
195 re.findall(r"__PyAPI_DATA\(.*?\)\s*\(?(.*?)\)?\s*;", preprocesor_output)
196 )
197 return stable_data, stable_exported_data, stable_functions
198
199
200def check_symbols(parser_args):
201 with open(parser_args.stable_abi_file, "r") as filename:
202 abi_funcs = {
203 symbol
204 for symbol in filename.read().splitlines()
205 if symbol and not symbol.startswith("#")
206 }
207
Pablo Galindo79c18492020-12-04 23:19:21 +0000208 try:
209 # static library
210 LIBRARY = sysconfig.get_config_var("LIBRARY")
211 if not LIBRARY:
212 raise Exception("failed to get LIBRARY variable from sysconfig")
Victor Stinner801bb0b2021-02-17 11:14:42 +0100213 if os.path.exists(LIBRARY):
214 check_library(parser_args.stable_abi_file, LIBRARY, abi_funcs)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000215
Pablo Galindo79c18492020-12-04 23:19:21 +0000216 # dynamic library
217 LDLIBRARY = sysconfig.get_config_var("LDLIBRARY")
218 if not LDLIBRARY:
219 raise Exception("failed to get LDLIBRARY variable from sysconfig")
220 if LDLIBRARY != LIBRARY:
221 check_library(
222 parser_args.stable_abi_file, LDLIBRARY, abi_funcs, dynamic=True
223 )
224 except Exception as e:
225 print(e, file=sys.stderr)
226 sys.exit(1)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000227
228
229def main():
230 parser = argparse.ArgumentParser(description="Process some integers.")
231 subparsers = parser.add_subparsers()
232 check_parser = subparsers.add_parser(
233 "check", help="Check the exported symbols against a given ABI file"
234 )
235 check_parser.add_argument(
236 "stable_abi_file", type=str, help="File with the stable abi functions"
237 )
238 check_parser.set_defaults(func=check_symbols)
239 generate_parser = subparsers.add_parser(
240 "generate",
241 help="Generate symbols from the header files and the exported symbols",
242 )
243 generate_parser.add_argument(
244 "output_file", type=str, help="File to dump the symbols to"
245 )
246 generate_parser.set_defaults(func=generate_limited_api_symbols)
247 args = parser.parse_args()
248 if "func" not in args:
249 parser.error("Either 'check' or 'generate' must be used")
250 sys.exit(1)
251
252 args.func(args)
253
254
255if __name__ == "__main__":
256 main()