blob: 44f426e096adcf5bb294ebc36120c7d6575f282b [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 "pydebug.h",
27 "pytime.h",
28 "symtable.h",
29 "token.h",
30 "ucnhash.h",
31}
32
Pablo Galindo09114112020-12-15 18:16:13 +000033MACOS = (sys.platform == "darwin")
Pablo Galindo85f1ded2020-12-04 22:05:58 +000034
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]
Pablo Galindo09114112020-12-15 18:16:13 +000060 if MACOS and symbol.startswith("_"):
61 yield symbol[1:]
62 else:
63 yield symbol
Pablo Galindo85f1ded2020-12-04 22:05:58 +000064
65
Pablo Galindo79c18492020-12-04 23:19:21 +000066def check_library(stable_abi_file, library, abi_funcs, dynamic=False):
Pablo Galindo85f1ded2020-12-04 22:05:58 +000067 available_symbols = set(get_exported_symbols(library, dynamic))
68 missing_symbols = abi_funcs - available_symbols
69 if missing_symbols:
Pablo Galindo79c18492020-12-04 23:19:21 +000070 raise Exception(
71 f"""\
72Some symbols from the limited API are missing: {', '.join(missing_symbols)}
73
74This error means that there are some missing symbols among the ones exported
75in the Python library ("libpythonx.x.a" or "libpythonx.x.so"). This normally
76means that some symbol, function implementation or a prototype, belonging to
77a symbol in the limited API has been deleted or is missing.
78
79Check if this was a mistake and if not, update the file containing the limited
80API symbols. This file is located at:
81
82{stable_abi_file}
83
84You can read more about the limited API and its contracts at:
85
86https://docs.python.org/3/c-api/stable.html
87
88And in PEP 384:
89
90https://www.python.org/dev/peps/pep-0384/
91"""
Pablo Galindo85f1ded2020-12-04 22:05:58 +000092 )
Pablo Galindo85f1ded2020-12-04 22:05:58 +000093
94
95def generate_limited_api_symbols(args):
96 if hasattr(sys, "gettotalrefcount"):
97 print(
98 "Stable ABI symbols cannot be generated from a debug build", file=sys.stderr
99 )
100 sys.exit(1)
101 library = sysconfig.get_config_var("LIBRARY")
102 ldlibrary = sysconfig.get_config_var("LDLIBRARY")
103 if ldlibrary != library:
104 raise Exception("Limited ABI symbols can only be generated from a static build")
105 available_symbols = {
106 symbol for symbol in get_exported_symbols(library) if symbol.startswith("Py")
107 }
108
109 headers = [
110 file
111 for file in pathlib.Path("Include").glob("*.h")
112 if file.name not in EXCLUDED_HEADERS
113 ]
114 stable_data, stable_exported_data, stable_functions = get_limited_api_definitions(
115 headers
116 )
117 macros = get_limited_api_macros(headers)
118
119 stable_symbols = {
120 symbol
121 for symbol in (stable_functions | stable_exported_data | stable_data | macros)
122 if symbol.startswith("Py") and symbol in available_symbols
123 }
124 with open(args.output_file, "w") as output_file:
125 output_file.write(f"# File generated by 'make regen-limited-abi'\n")
126 output_file.write(
127 f"# This is NOT an authoritative list of stable ABI symbols\n"
128 )
129 for symbol in sorted(stable_symbols):
130 output_file.write(f"{symbol}\n")
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000131
132
133def get_limited_api_macros(headers):
134 """Run the preprocesor over all the header files in "Include" setting
135 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter
136 and extracting all macro definitions (via adding -dM to the compiler arguments).
137 """
138
139 preprocesor_output_with_macros = subprocess.check_output(
140 sysconfig.get_config_var("CC").split()
141 + [
142 # Prevent the expansion of the exported macros so we can capture them later
143 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
144 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
145 "-I.",
146 "-I./Include",
147 "-dM",
148 "-E",
149 ]
150 + [str(file) for file in headers],
151 text=True,
152 stderr=subprocess.DEVNULL,
153 )
154
155 return {
156 target
157 for _, target in re.findall(
158 r"#define (\w+)\s*(?:\(.*?\))?\s+(\w+)", preprocesor_output_with_macros
159 )
160 }
161
162
163def get_limited_api_definitions(headers):
164 """Run the preprocesor over all the header files in "Include" setting
165 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter.
166
167 The limited API symbols will be extracted from the output of this command as it includes
168 the prototypes and definitions of all the exported symbols that are in the limited api.
169
170 This function does *NOT* extract the macros defined on the limited API
171 """
172 preprocesor_output = subprocess.check_output(
173 sysconfig.get_config_var("CC").split()
174 + [
175 # Prevent the expansion of the exported macros so we can capture them later
176 "-DPyAPI_FUNC=__PyAPI_FUNC",
177 "-DPyAPI_DATA=__PyAPI_DATA",
178 "-DEXPORT_DATA=__EXPORT_DATA",
179 "-D_Py_NO_RETURN=",
180 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
181 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
182 "-I.",
183 "-I./Include",
184 "-E",
185 ]
186 + [str(file) for file in headers],
187 text=True,
188 stderr=subprocess.DEVNULL,
189 )
190 stable_functions = set(
191 re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output)
192 )
193 stable_exported_data = set(
194 re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output)
195 )
196 stable_data = set(
197 re.findall(r"__PyAPI_DATA\(.*?\)\s*\(?(.*?)\)?\s*;", preprocesor_output)
198 )
199 return stable_data, stable_exported_data, stable_functions
200
201
202def check_symbols(parser_args):
203 with open(parser_args.stable_abi_file, "r") as filename:
204 abi_funcs = {
205 symbol
206 for symbol in filename.read().splitlines()
207 if symbol and not symbol.startswith("#")
208 }
209
Pablo Galindo79c18492020-12-04 23:19:21 +0000210 try:
211 # static library
212 LIBRARY = sysconfig.get_config_var("LIBRARY")
213 if not LIBRARY:
214 raise Exception("failed to get LIBRARY variable from sysconfig")
Victor Stinner801bb0b2021-02-17 11:14:42 +0100215 if os.path.exists(LIBRARY):
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()