blob: aa953b2dfde8772f110b37b59de84055c8537069 [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
34
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]
60 yield symbol
61
62
63def check_library(library, abi_funcs, dynamic=False):
64 available_symbols = set(get_exported_symbols(library, dynamic))
65 missing_symbols = abi_funcs - available_symbols
66 if missing_symbols:
67 print(
68 f"Some symbols from the stable ABI are missing: {', '.join(missing_symbols)}"
69 )
70 return 1
71 return 0
72
73
74def generate_limited_api_symbols(args):
75 if hasattr(sys, "gettotalrefcount"):
76 print(
77 "Stable ABI symbols cannot be generated from a debug build", file=sys.stderr
78 )
79 sys.exit(1)
80 library = sysconfig.get_config_var("LIBRARY")
81 ldlibrary = sysconfig.get_config_var("LDLIBRARY")
82 if ldlibrary != library:
83 raise Exception("Limited ABI symbols can only be generated from a static build")
84 available_symbols = {
85 symbol for symbol in get_exported_symbols(library) if symbol.startswith("Py")
86 }
87
88 headers = [
89 file
90 for file in pathlib.Path("Include").glob("*.h")
91 if file.name not in EXCLUDED_HEADERS
92 ]
93 stable_data, stable_exported_data, stable_functions = get_limited_api_definitions(
94 headers
95 )
96 macros = get_limited_api_macros(headers)
97
98 stable_symbols = {
99 symbol
100 for symbol in (stable_functions | stable_exported_data | stable_data | macros)
101 if symbol.startswith("Py") and symbol in available_symbols
102 }
103 with open(args.output_file, "w") as output_file:
104 output_file.write(f"# File generated by 'make regen-limited-abi'\n")
105 output_file.write(
106 f"# This is NOT an authoritative list of stable ABI symbols\n"
107 )
108 for symbol in sorted(stable_symbols):
109 output_file.write(f"{symbol}\n")
110 sys.exit(0)
111
112
113def get_limited_api_macros(headers):
114 """Run the preprocesor over all the header files in "Include" setting
115 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter
116 and extracting all macro definitions (via adding -dM to the compiler arguments).
117 """
118
119 preprocesor_output_with_macros = subprocess.check_output(
120 sysconfig.get_config_var("CC").split()
121 + [
122 # Prevent the expansion of the exported macros so we can capture them later
123 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
124 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
125 "-I.",
126 "-I./Include",
127 "-dM",
128 "-E",
129 ]
130 + [str(file) for file in headers],
131 text=True,
132 stderr=subprocess.DEVNULL,
133 )
134
135 return {
136 target
137 for _, target in re.findall(
138 r"#define (\w+)\s*(?:\(.*?\))?\s+(\w+)", preprocesor_output_with_macros
139 )
140 }
141
142
143def get_limited_api_definitions(headers):
144 """Run the preprocesor over all the header files in "Include" setting
145 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter.
146
147 The limited API symbols will be extracted from the output of this command as it includes
148 the prototypes and definitions of all the exported symbols that are in the limited api.
149
150 This function does *NOT* extract the macros defined on the limited API
151 """
152 preprocesor_output = subprocess.check_output(
153 sysconfig.get_config_var("CC").split()
154 + [
155 # Prevent the expansion of the exported macros so we can capture them later
156 "-DPyAPI_FUNC=__PyAPI_FUNC",
157 "-DPyAPI_DATA=__PyAPI_DATA",
158 "-DEXPORT_DATA=__EXPORT_DATA",
159 "-D_Py_NO_RETURN=",
160 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
161 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
162 "-I.",
163 "-I./Include",
164 "-E",
165 ]
166 + [str(file) for file in headers],
167 text=True,
168 stderr=subprocess.DEVNULL,
169 )
170 stable_functions = set(
171 re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output)
172 )
173 stable_exported_data = set(
174 re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output)
175 )
176 stable_data = set(
177 re.findall(r"__PyAPI_DATA\(.*?\)\s*\(?(.*?)\)?\s*;", preprocesor_output)
178 )
179 return stable_data, stable_exported_data, stable_functions
180
181
182def check_symbols(parser_args):
183 with open(parser_args.stable_abi_file, "r") as filename:
184 abi_funcs = {
185 symbol
186 for symbol in filename.read().splitlines()
187 if symbol and not symbol.startswith("#")
188 }
189
190 ret = 0
191 # static library
192 LIBRARY = sysconfig.get_config_var("LIBRARY")
193 if not LIBRARY:
194 raise Exception("failed to get LIBRARY variable from sysconfig")
195 ret = check_library(LIBRARY, abi_funcs)
196
197 # dynamic library
198 LDLIBRARY = sysconfig.get_config_var("LDLIBRARY")
199 if not LDLIBRARY:
200 raise Exception("failed to get LDLIBRARY variable from sysconfig")
201 if LDLIBRARY != LIBRARY:
202 ret |= check_library(LDLIBRARY, abi_funcs, dynamic=True)
203
204 sys.exit(ret)
205
206
207def main():
208 parser = argparse.ArgumentParser(description="Process some integers.")
209 subparsers = parser.add_subparsers()
210 check_parser = subparsers.add_parser(
211 "check", help="Check the exported symbols against a given ABI file"
212 )
213 check_parser.add_argument(
214 "stable_abi_file", type=str, help="File with the stable abi functions"
215 )
216 check_parser.set_defaults(func=check_symbols)
217 generate_parser = subparsers.add_parser(
218 "generate",
219 help="Generate symbols from the header files and the exported symbols",
220 )
221 generate_parser.add_argument(
222 "output_file", type=str, help="File to dump the symbols to"
223 )
224 generate_parser.set_defaults(func=generate_limited_api_symbols)
225 args = parser.parse_args()
226 if "func" not in args:
227 parser.error("Either 'check' or 'generate' must be used")
228 sys.exit(1)
229
230 args.func(args)
231
232
233if __name__ == "__main__":
234 main()