blob: 14fcf2f56a207344605c4de854478c4135613c5b [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 )
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000115
116 stable_symbols = {
117 symbol
Victor Stinner61092a92021-04-01 14:13:42 +0200118 for symbol in (stable_functions | stable_exported_data | stable_data)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000119 if symbol.startswith("Py") and symbol in available_symbols
120 }
121 with open(args.output_file, "w") as output_file:
122 output_file.write(f"# File generated by 'make regen-limited-abi'\n")
123 output_file.write(
124 f"# This is NOT an authoritative list of stable ABI symbols\n"
125 )
126 for symbol in sorted(stable_symbols):
127 output_file.write(f"{symbol}\n")
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000128
129
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000130def get_limited_api_definitions(headers):
131 """Run the preprocesor over all the header files in "Include" setting
132 "-DPy_LIMITED_API" to the correct value for the running version of the interpreter.
133
134 The limited API symbols will be extracted from the output of this command as it includes
135 the prototypes and definitions of all the exported symbols that are in the limited api.
136
137 This function does *NOT* extract the macros defined on the limited API
138 """
139 preprocesor_output = 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 "-DPyAPI_FUNC=__PyAPI_FUNC",
144 "-DPyAPI_DATA=__PyAPI_DATA",
145 "-DEXPORT_DATA=__EXPORT_DATA",
146 "-D_Py_NO_RETURN=",
147 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
148 f"-DPy_LIMITED_API={sys.version_info.major << 24 | sys.version_info.minor << 16}",
149 "-I.",
150 "-I./Include",
151 "-E",
152 ]
153 + [str(file) for file in headers],
154 text=True,
155 stderr=subprocess.DEVNULL,
156 )
157 stable_functions = set(
158 re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output)
159 )
160 stable_exported_data = set(
161 re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output)
162 )
163 stable_data = set(
164 re.findall(r"__PyAPI_DATA\(.*?\)\s*\(?(.*?)\)?\s*;", preprocesor_output)
165 )
166 return stable_data, stable_exported_data, stable_functions
167
168
169def check_symbols(parser_args):
170 with open(parser_args.stable_abi_file, "r") as filename:
171 abi_funcs = {
172 symbol
173 for symbol in filename.read().splitlines()
174 if symbol and not symbol.startswith("#")
175 }
176
Pablo Galindo79c18492020-12-04 23:19:21 +0000177 try:
178 # static library
179 LIBRARY = sysconfig.get_config_var("LIBRARY")
180 if not LIBRARY:
181 raise Exception("failed to get LIBRARY variable from sysconfig")
Victor Stinner801bb0b2021-02-17 11:14:42 +0100182 if os.path.exists(LIBRARY):
183 check_library(parser_args.stable_abi_file, LIBRARY, abi_funcs)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000184
Pablo Galindo79c18492020-12-04 23:19:21 +0000185 # dynamic library
186 LDLIBRARY = sysconfig.get_config_var("LDLIBRARY")
187 if not LDLIBRARY:
188 raise Exception("failed to get LDLIBRARY variable from sysconfig")
189 if LDLIBRARY != LIBRARY:
190 check_library(
191 parser_args.stable_abi_file, LDLIBRARY, abi_funcs, dynamic=True
192 )
193 except Exception as e:
194 print(e, file=sys.stderr)
195 sys.exit(1)
Pablo Galindo85f1ded2020-12-04 22:05:58 +0000196
197
198def main():
199 parser = argparse.ArgumentParser(description="Process some integers.")
200 subparsers = parser.add_subparsers()
201 check_parser = subparsers.add_parser(
202 "check", help="Check the exported symbols against a given ABI file"
203 )
204 check_parser.add_argument(
205 "stable_abi_file", type=str, help="File with the stable abi functions"
206 )
207 check_parser.set_defaults(func=check_symbols)
208 generate_parser = subparsers.add_parser(
209 "generate",
210 help="Generate symbols from the header files and the exported symbols",
211 )
212 generate_parser.add_argument(
213 "output_file", type=str, help="File to dump the symbols to"
214 )
215 generate_parser.set_defaults(func=generate_limited_api_symbols)
216 args = parser.parse_args()
217 if "func" not in args:
218 parser.error("Either 'check' or 'generate' must be used")
219 sys.exit(1)
220
221 args.func(args)
222
223
224if __name__ == "__main__":
225 main()