Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Tools/build/check_extension_modules.py
12 views
1
"""Check extension modules
2
3
The script checks shared and built-in extension modules. It verifies that the
4
modules have been built and that they can be imported successfully. Missing
5
modules and failed imports are reported to the user. Shared extension
6
files are renamed on failed import.
7
8
Module information is parsed from several sources:
9
10
- core modules hard-coded in Modules/config.c.in
11
- Windows-specific modules that are hard-coded in PC/config.c
12
- MODULE_{name}_STATE entries in Makefile (provided through sysconfig)
13
- Various makesetup files:
14
- $(srcdir)/Modules/Setup
15
- Modules/Setup.[local|bootstrap|stdlib] files, which are generated
16
from $(srcdir)/Modules/Setup.*.in files
17
18
See --help for more information
19
"""
20
import argparse
21
import collections
22
import enum
23
import logging
24
import os
25
import pathlib
26
import re
27
import sys
28
import sysconfig
29
import warnings
30
31
from importlib._bootstrap import _load as bootstrap_load
32
from importlib.machinery import BuiltinImporter, ExtensionFileLoader, ModuleSpec
33
from importlib.util import spec_from_file_location, spec_from_loader
34
from typing import Iterable
35
36
SRC_DIR = pathlib.Path(__file__).parent.parent.parent
37
38
# core modules, hard-coded in Modules/config.h.in
39
CORE_MODULES = {
40
"_ast",
41
"_imp",
42
"_string",
43
"_tokenize",
44
"_warnings",
45
"builtins",
46
"gc",
47
"marshal",
48
"sys",
49
}
50
51
# Windows-only modules
52
WINDOWS_MODULES = {
53
"_overlapped",
54
"_testconsole",
55
"_winapi",
56
"msvcrt",
57
"nt",
58
"winreg",
59
"winsound",
60
}
61
62
63
logger = logging.getLogger(__name__)
64
65
parser = argparse.ArgumentParser(
66
prog="check_extension_modules",
67
description=__doc__,
68
formatter_class=argparse.RawDescriptionHelpFormatter,
69
)
70
71
parser.add_argument(
72
"--verbose",
73
action="store_true",
74
help="Verbose, report builtin, shared, and unavailable modules",
75
)
76
77
parser.add_argument(
78
"--debug",
79
action="store_true",
80
help="Enable debug logging",
81
)
82
83
parser.add_argument(
84
"--strict",
85
action=argparse.BooleanOptionalAction,
86
help=(
87
"Strict check, fail when a module is missing or fails to import"
88
"(default: no, unless env var PYTHONSTRICTEXTENSIONBUILD is set)"
89
),
90
default=bool(os.environ.get("PYTHONSTRICTEXTENSIONBUILD")),
91
)
92
93
parser.add_argument(
94
"--cross-compiling",
95
action=argparse.BooleanOptionalAction,
96
help=(
97
"Use cross-compiling checks "
98
"(default: no, unless env var _PYTHON_HOST_PLATFORM is set)."
99
),
100
default="_PYTHON_HOST_PLATFORM" in os.environ,
101
)
102
103
parser.add_argument(
104
"--list-module-names",
105
action="store_true",
106
help="Print a list of module names to stdout and exit",
107
)
108
109
110
class ModuleState(enum.Enum):
111
# Makefile state "yes"
112
BUILTIN = "builtin"
113
SHARED = "shared"
114
115
DISABLED = "disabled"
116
MISSING = "missing"
117
NA = "n/a"
118
# disabled by Setup / makesetup rule
119
DISABLED_SETUP = "disabled_setup"
120
121
def __bool__(self):
122
return self.value in {"builtin", "shared"}
123
124
125
ModuleInfo = collections.namedtuple("ModuleInfo", "name state")
126
127
128
class ModuleChecker:
129
pybuilddir_txt = "pybuilddir.txt"
130
131
setup_files = (
132
# see end of configure.ac
133
"Modules/Setup.local",
134
"Modules/Setup.stdlib",
135
"Modules/Setup.bootstrap",
136
SRC_DIR / "Modules/Setup",
137
)
138
139
def __init__(self, cross_compiling: bool = False, strict: bool = False):
140
self.cross_compiling = cross_compiling
141
self.strict_extensions_build = strict
142
self.ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")
143
self.platform = sysconfig.get_platform()
144
self.builddir = self.get_builddir()
145
self.modules = self.get_modules()
146
147
self.builtin_ok = []
148
self.shared_ok = []
149
self.failed_on_import = []
150
self.missing = []
151
self.disabled_configure = []
152
self.disabled_setup = []
153
self.notavailable = []
154
155
def check(self):
156
for modinfo in self.modules:
157
logger.debug("Checking '%s' (%s)", modinfo.name, self.get_location(modinfo))
158
if modinfo.state == ModuleState.DISABLED:
159
self.disabled_configure.append(modinfo)
160
elif modinfo.state == ModuleState.DISABLED_SETUP:
161
self.disabled_setup.append(modinfo)
162
elif modinfo.state == ModuleState.MISSING:
163
self.missing.append(modinfo)
164
elif modinfo.state == ModuleState.NA:
165
self.notavailable.append(modinfo)
166
else:
167
try:
168
if self.cross_compiling:
169
self.check_module_cross(modinfo)
170
else:
171
self.check_module_import(modinfo)
172
except (ImportError, FileNotFoundError):
173
self.rename_module(modinfo)
174
self.failed_on_import.append(modinfo)
175
else:
176
if modinfo.state == ModuleState.BUILTIN:
177
self.builtin_ok.append(modinfo)
178
else:
179
assert modinfo.state == ModuleState.SHARED
180
self.shared_ok.append(modinfo)
181
182
def summary(self, *, verbose: bool = False):
183
longest = max([len(e.name) for e in self.modules], default=0)
184
185
def print_three_column(modinfos: list[ModuleInfo]):
186
names = [modinfo.name for modinfo in modinfos]
187
names.sort(key=str.lower)
188
# guarantee zip() doesn't drop anything
189
while len(names) % 3:
190
names.append("")
191
for l, m, r in zip(names[::3], names[1::3], names[2::3]):
192
print("%-*s %-*s %-*s" % (longest, l, longest, m, longest, r))
193
194
if verbose and self.builtin_ok:
195
print("The following *built-in* modules have been successfully built:")
196
print_three_column(self.builtin_ok)
197
print()
198
199
if verbose and self.shared_ok:
200
print("The following *shared* modules have been successfully built:")
201
print_three_column(self.shared_ok)
202
print()
203
204
if self.disabled_configure:
205
print("The following modules are *disabled* in configure script:")
206
print_three_column(self.disabled_configure)
207
print()
208
209
if self.disabled_setup:
210
print("The following modules are *disabled* in Modules/Setup files:")
211
print_three_column(self.disabled_setup)
212
print()
213
214
if verbose and self.notavailable:
215
print(
216
f"The following modules are not available on platform '{self.platform}':"
217
)
218
print_three_column(self.notavailable)
219
print()
220
221
if self.missing:
222
print("The necessary bits to build these optional modules were not found:")
223
print_three_column(self.missing)
224
print("To find the necessary bits, look in configure.ac and config.log.")
225
print()
226
227
if self.failed_on_import:
228
print(
229
"Following modules built successfully "
230
"but were removed because they could not be imported:"
231
)
232
print_three_column(self.failed_on_import)
233
print()
234
235
if any(
236
modinfo.name == "_ssl" for modinfo in self.missing + self.failed_on_import
237
):
238
print("Could not build the ssl module!")
239
print("Python requires a OpenSSL 1.1.1 or newer")
240
if sysconfig.get_config_var("OPENSSL_LDFLAGS"):
241
print("Custom linker flags may require --with-openssl-rpath=auto")
242
print()
243
244
disabled = len(self.disabled_configure) + len(self.disabled_setup)
245
print(
246
f"Checked {len(self.modules)} modules ("
247
f"{len(self.builtin_ok)} built-in, "
248
f"{len(self.shared_ok)} shared, "
249
f"{len(self.notavailable)} n/a on {self.platform}, "
250
f"{disabled} disabled, "
251
f"{len(self.missing)} missing, "
252
f"{len(self.failed_on_import)} failed on import)"
253
)
254
255
def check_strict_build(self):
256
"""Fail if modules are missing and it's a strict build"""
257
if self.strict_extensions_build and (self.failed_on_import or self.missing):
258
raise RuntimeError("Failed to build some stdlib modules")
259
260
def list_module_names(self, *, all: bool = False) -> set:
261
names = {modinfo.name for modinfo in self.modules}
262
if all:
263
names.update(WINDOWS_MODULES)
264
return names
265
266
def get_builddir(self) -> pathlib.Path:
267
try:
268
with open(self.pybuilddir_txt, encoding="utf-8") as f:
269
builddir = f.read()
270
except FileNotFoundError:
271
logger.error("%s must be run from the top build directory", __file__)
272
raise
273
builddir = pathlib.Path(builddir)
274
logger.debug("%s: %s", self.pybuilddir_txt, builddir)
275
return builddir
276
277
def get_modules(self) -> list[ModuleInfo]:
278
"""Get module info from sysconfig and Modules/Setup* files"""
279
seen = set()
280
modules = []
281
# parsing order is important, first entry wins
282
for modinfo in self.get_core_modules():
283
modules.append(modinfo)
284
seen.add(modinfo.name)
285
for setup_file in self.setup_files:
286
for modinfo in self.parse_setup_file(setup_file):
287
if modinfo.name not in seen:
288
modules.append(modinfo)
289
seen.add(modinfo.name)
290
for modinfo in self.get_sysconfig_modules():
291
if modinfo.name not in seen:
292
modules.append(modinfo)
293
seen.add(modinfo.name)
294
logger.debug("Found %i modules in total", len(modules))
295
modules.sort()
296
return modules
297
298
def get_core_modules(self) -> Iterable[ModuleInfo]:
299
"""Get hard-coded core modules"""
300
for name in CORE_MODULES:
301
modinfo = ModuleInfo(name, ModuleState.BUILTIN)
302
logger.debug("Found core module %s", modinfo)
303
yield modinfo
304
305
def get_sysconfig_modules(self) -> Iterable[ModuleInfo]:
306
"""Get modules defined in Makefile through sysconfig
307
308
MODBUILT_NAMES: modules in *static* block
309
MODSHARED_NAMES: modules in *shared* block
310
MODDISABLED_NAMES: modules in *disabled* block
311
"""
312
moddisabled = set(sysconfig.get_config_var("MODDISABLED_NAMES").split())
313
if self.cross_compiling:
314
modbuiltin = set(sysconfig.get_config_var("MODBUILT_NAMES").split())
315
else:
316
modbuiltin = set(sys.builtin_module_names)
317
318
for key, value in sysconfig.get_config_vars().items():
319
if not key.startswith("MODULE_") or not key.endswith("_STATE"):
320
continue
321
if value not in {"yes", "disabled", "missing", "n/a"}:
322
raise ValueError(f"Unsupported value '{value}' for {key}")
323
324
modname = key[7:-6].lower()
325
if modname in moddisabled:
326
# Setup "*disabled*" rule
327
state = ModuleState.DISABLED_SETUP
328
elif value in {"disabled", "missing", "n/a"}:
329
state = ModuleState(value)
330
elif modname in modbuiltin:
331
assert value == "yes"
332
state = ModuleState.BUILTIN
333
else:
334
assert value == "yes"
335
state = ModuleState.SHARED
336
337
modinfo = ModuleInfo(modname, state)
338
logger.debug("Found %s in Makefile", modinfo)
339
yield modinfo
340
341
def parse_setup_file(self, setup_file: pathlib.Path) -> Iterable[ModuleInfo]:
342
"""Parse a Modules/Setup file"""
343
assign_var = re.compile(r"^\w+=") # EGG_SPAM=foo
344
# default to static module
345
state = ModuleState.BUILTIN
346
logger.debug("Parsing Setup file %s", setup_file)
347
with open(setup_file, encoding="utf-8") as f:
348
for line in f:
349
line = line.strip()
350
if not line or line.startswith("#") or assign_var.match(line):
351
continue
352
match line.split():
353
case ["*shared*"]:
354
state = ModuleState.SHARED
355
case ["*static*"]:
356
state = ModuleState.BUILTIN
357
case ["*disabled*"]:
358
state = ModuleState.DISABLED
359
case ["*noconfig*"]:
360
state = None
361
case [*items]:
362
if state == ModuleState.DISABLED:
363
# *disabled* can disable multiple modules per line
364
for item in items:
365
modinfo = ModuleInfo(item, state)
366
logger.debug("Found %s in %s", modinfo, setup_file)
367
yield modinfo
368
elif state in {ModuleState.SHARED, ModuleState.BUILTIN}:
369
# *shared* and *static*, first item is the name of the module.
370
modinfo = ModuleInfo(items[0], state)
371
logger.debug("Found %s in %s", modinfo, setup_file)
372
yield modinfo
373
374
def get_spec(self, modinfo: ModuleInfo) -> ModuleSpec:
375
"""Get ModuleSpec for builtin or extension module"""
376
if modinfo.state == ModuleState.SHARED:
377
location = os.fspath(self.get_location(modinfo))
378
loader = ExtensionFileLoader(modinfo.name, location)
379
return spec_from_file_location(modinfo.name, location, loader=loader)
380
elif modinfo.state == ModuleState.BUILTIN:
381
return spec_from_loader(modinfo.name, loader=BuiltinImporter)
382
else:
383
raise ValueError(modinfo)
384
385
def get_location(self, modinfo: ModuleInfo) -> pathlib.Path:
386
"""Get shared library location in build directory"""
387
if modinfo.state == ModuleState.SHARED:
388
return self.builddir / f"{modinfo.name}{self.ext_suffix}"
389
else:
390
return None
391
392
def _check_file(self, modinfo: ModuleInfo, spec: ModuleSpec):
393
"""Check that the module file is present and not empty"""
394
if spec.loader is BuiltinImporter:
395
return
396
try:
397
st = os.stat(spec.origin)
398
except FileNotFoundError:
399
logger.error("%s (%s) is missing", modinfo.name, spec.origin)
400
raise
401
if not st.st_size:
402
raise ImportError(f"{spec.origin} is an empty file")
403
404
def check_module_import(self, modinfo: ModuleInfo):
405
"""Attempt to import module and report errors"""
406
spec = self.get_spec(modinfo)
407
self._check_file(modinfo, spec)
408
try:
409
with warnings.catch_warnings():
410
# ignore deprecation warning from deprecated modules
411
warnings.simplefilter("ignore", DeprecationWarning)
412
bootstrap_load(spec)
413
except ImportError as e:
414
logger.error("%s failed to import: %s", modinfo.name, e)
415
raise
416
except Exception as e:
417
logger.exception("Importing extension '%s' failed!", modinfo.name)
418
raise
419
420
def check_module_cross(self, modinfo: ModuleInfo):
421
"""Sanity check for cross compiling"""
422
spec = self.get_spec(modinfo)
423
self._check_file(modinfo, spec)
424
425
def rename_module(self, modinfo: ModuleInfo) -> None:
426
"""Rename module file"""
427
if modinfo.state == ModuleState.BUILTIN:
428
logger.error("Cannot mark builtin module '%s' as failed!", modinfo.name)
429
return
430
431
failed_name = f"{modinfo.name}_failed{self.ext_suffix}"
432
builddir_path = self.get_location(modinfo)
433
if builddir_path.is_symlink():
434
symlink = builddir_path
435
module_path = builddir_path.resolve().relative_to(os.getcwd())
436
failed_path = module_path.parent / failed_name
437
else:
438
symlink = None
439
module_path = builddir_path
440
failed_path = self.builddir / failed_name
441
442
# remove old failed file
443
failed_path.unlink(missing_ok=True)
444
# remove symlink
445
if symlink is not None:
446
symlink.unlink(missing_ok=True)
447
# rename shared extension file
448
try:
449
module_path.rename(failed_path)
450
except FileNotFoundError:
451
logger.debug("Shared extension file '%s' does not exist.", module_path)
452
else:
453
logger.debug("Rename '%s' -> '%s'", module_path, failed_path)
454
455
456
def main():
457
args = parser.parse_args()
458
if args.debug:
459
args.verbose = True
460
logging.basicConfig(
461
level=logging.DEBUG if args.debug else logging.INFO,
462
format="[%(levelname)s] %(message)s",
463
)
464
465
checker = ModuleChecker(
466
cross_compiling=args.cross_compiling,
467
strict=args.strict,
468
)
469
if args.list_module_names:
470
names = checker.list_module_names(all=True)
471
for name in sorted(names):
472
print(name)
473
else:
474
checker.check()
475
checker.summary(verbose=args.verbose)
476
try:
477
checker.check_strict_build()
478
except RuntimeError as e:
479
parser.exit(1, f"\nError: {e}\n")
480
481
482
if __name__ == "__main__":
483
main()
484
485