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