| 
									
										
										
										
											2022-07-05 09:25:45 +02:00
										 |  |  | """Check extension modules
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The script checks shared and built-in extension modules. It verifies that the | 
					
						
							|  |  |  | modules have been built and that they can be imported successfully. Missing | 
					
						
							|  |  |  | modules and failed imports are reported to the user. Shared extension | 
					
						
							|  |  |  | files are renamed on failed import. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Module information is parsed from several sources: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | - core modules hard-coded in Modules/config.c.in | 
					
						
							|  |  |  | - Windows-specific modules that are hard-coded in PC/config.c | 
					
						
							|  |  |  | - MODULE_{name}_STATE entries in Makefile (provided through sysconfig) | 
					
						
							|  |  |  | - Various makesetup files: | 
					
						
							|  |  |  |   - $(srcdir)/Modules/Setup | 
					
						
							|  |  |  |   - Modules/Setup.[local|bootstrap|stdlib] files, which are generated | 
					
						
							|  |  |  |     from $(srcdir)/Modules/Setup.*.in files | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | See --help for more information | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | import argparse | 
					
						
							|  |  |  | import collections | 
					
						
							|  |  |  | import enum | 
					
						
							|  |  |  | import logging | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import pathlib | 
					
						
							|  |  |  | import re | 
					
						
							|  |  |  | import sys | 
					
						
							|  |  |  | import sysconfig | 
					
						
							|  |  |  | import warnings | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | from importlib._bootstrap import _load as bootstrap_load | 
					
						
							|  |  |  | from importlib.machinery import BuiltinImporter, ExtensionFileLoader, ModuleSpec | 
					
						
							|  |  |  | from importlib.util import spec_from_file_location, spec_from_loader | 
					
						
							|  |  |  | from typing import Iterable | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | SRC_DIR = pathlib.Path(__file__).parent.parent.parent | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # core modules, hard-coded in Modules/config.h.in | 
					
						
							|  |  |  | CORE_MODULES = { | 
					
						
							|  |  |  |     "_ast", | 
					
						
							|  |  |  |     "_imp", | 
					
						
							|  |  |  |     "_string", | 
					
						
							|  |  |  |     "_tokenize", | 
					
						
							|  |  |  |     "_warnings", | 
					
						
							|  |  |  |     "builtins", | 
					
						
							|  |  |  |     "gc", | 
					
						
							|  |  |  |     "marshal", | 
					
						
							|  |  |  |     "sys", | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | # Windows-only modules | 
					
						
							|  |  |  | WINDOWS_MODULES = { | 
					
						
							|  |  |  |     "_overlapped", | 
					
						
							|  |  |  |     "_testconsole", | 
					
						
							|  |  |  |     "_winapi", | 
					
						
							|  |  |  |     "msvcrt", | 
					
						
							|  |  |  |     "nt", | 
					
						
							|  |  |  |     "winreg", | 
					
						
							|  |  |  |     "winsound", | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | logger = logging.getLogger(__name__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | parser = argparse.ArgumentParser( | 
					
						
							|  |  |  |     prog="check_extension_modules", | 
					
						
							|  |  |  |     description=__doc__, | 
					
						
							|  |  |  |     formatter_class=argparse.RawDescriptionHelpFormatter, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | parser.add_argument( | 
					
						
							|  |  |  |     "--verbose", | 
					
						
							|  |  |  |     action="store_true", | 
					
						
							|  |  |  |     help="Verbose, report builtin, shared, and unavailable modules", | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | parser.add_argument( | 
					
						
							|  |  |  |     "--debug", | 
					
						
							|  |  |  |     action="store_true", | 
					
						
							|  |  |  |     help="Enable debug logging", | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | parser.add_argument( | 
					
						
							|  |  |  |     "--strict", | 
					
						
							|  |  |  |     action=argparse.BooleanOptionalAction, | 
					
						
							|  |  |  |     help=( | 
					
						
							|  |  |  |         "Strict check, fail when a module is missing or fails to import" | 
					
						
							|  |  |  |         "(default: no, unless env var PYTHONSTRICTEXTENSIONBUILD is set)" | 
					
						
							|  |  |  |     ), | 
					
						
							|  |  |  |     default=bool(os.environ.get("PYTHONSTRICTEXTENSIONBUILD")), | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | parser.add_argument( | 
					
						
							|  |  |  |     "--cross-compiling", | 
					
						
							|  |  |  |     action=argparse.BooleanOptionalAction, | 
					
						
							|  |  |  |     help=( | 
					
						
							|  |  |  |         "Use cross-compiling checks " | 
					
						
							|  |  |  |         "(default: no, unless env var _PYTHON_HOST_PLATFORM is set)." | 
					
						
							|  |  |  |     ), | 
					
						
							|  |  |  |     default="_PYTHON_HOST_PLATFORM" in os.environ, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | parser.add_argument( | 
					
						
							|  |  |  |     "--list-module-names", | 
					
						
							|  |  |  |     action="store_true", | 
					
						
							|  |  |  |     help="Print a list of module names to stdout and exit", | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ModuleState(enum.Enum): | 
					
						
							|  |  |  |     # Makefile state "yes" | 
					
						
							|  |  |  |     BUILTIN = "builtin" | 
					
						
							|  |  |  |     SHARED = "shared" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     DISABLED = "disabled" | 
					
						
							|  |  |  |     MISSING = "missing" | 
					
						
							|  |  |  |     NA = "n/a" | 
					
						
							|  |  |  |     # disabled by Setup / makesetup rule | 
					
						
							|  |  |  |     DISABLED_SETUP = "disabled_setup" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __bool__(self): | 
					
						
							|  |  |  |         return self.value in {"builtin", "shared"} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ModuleInfo = collections.namedtuple("ModuleInfo", "name state") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ModuleChecker: | 
					
						
							|  |  |  |     pybuilddir_txt = "pybuilddir.txt" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     setup_files = ( | 
					
						
							| 
									
										
										
										
											2022-07-14 09:51:49 +02:00
										 |  |  |         # see end of configure.ac | 
					
						
							| 
									
										
										
										
											2022-07-05 09:25:45 +02:00
										 |  |  |         "Modules/Setup.local", | 
					
						
							|  |  |  |         "Modules/Setup.stdlib", | 
					
						
							| 
									
										
										
										
											2022-07-14 09:51:49 +02:00
										 |  |  |         "Modules/Setup.bootstrap", | 
					
						
							|  |  |  |         SRC_DIR / "Modules/Setup", | 
					
						
							| 
									
										
										
										
											2022-07-05 09:25:45 +02:00
										 |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, cross_compiling: bool = False, strict: bool = False): | 
					
						
							|  |  |  |         self.cross_compiling = cross_compiling | 
					
						
							|  |  |  |         self.strict_extensions_build = strict | 
					
						
							|  |  |  |         self.ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") | 
					
						
							|  |  |  |         self.platform = sysconfig.get_platform() | 
					
						
							|  |  |  |         self.builddir = self.get_builddir() | 
					
						
							|  |  |  |         self.modules = self.get_modules() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         self.builtin_ok = [] | 
					
						
							|  |  |  |         self.shared_ok = [] | 
					
						
							|  |  |  |         self.failed_on_import = [] | 
					
						
							|  |  |  |         self.missing = [] | 
					
						
							|  |  |  |         self.disabled_configure = [] | 
					
						
							|  |  |  |         self.disabled_setup = [] | 
					
						
							|  |  |  |         self.notavailable = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def check(self): | 
					
						
							|  |  |  |         for modinfo in self.modules: | 
					
						
							|  |  |  |             logger.debug("Checking '%s' (%s)", modinfo.name, self.get_location(modinfo)) | 
					
						
							|  |  |  |             if modinfo.state == ModuleState.DISABLED: | 
					
						
							|  |  |  |                 self.disabled_configure.append(modinfo) | 
					
						
							|  |  |  |             elif modinfo.state == ModuleState.DISABLED_SETUP: | 
					
						
							|  |  |  |                 self.disabled_setup.append(modinfo) | 
					
						
							|  |  |  |             elif modinfo.state == ModuleState.MISSING: | 
					
						
							|  |  |  |                 self.missing.append(modinfo) | 
					
						
							|  |  |  |             elif modinfo.state == ModuleState.NA: | 
					
						
							|  |  |  |                 self.notavailable.append(modinfo) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 try: | 
					
						
							|  |  |  |                     if self.cross_compiling: | 
					
						
							|  |  |  |                         self.check_module_cross(modinfo) | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         self.check_module_import(modinfo) | 
					
						
							|  |  |  |                 except (ImportError, FileNotFoundError): | 
					
						
							|  |  |  |                     self.rename_module(modinfo) | 
					
						
							|  |  |  |                     self.failed_on_import.append(modinfo) | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     if modinfo.state == ModuleState.BUILTIN: | 
					
						
							|  |  |  |                         self.builtin_ok.append(modinfo) | 
					
						
							|  |  |  |                     else: | 
					
						
							|  |  |  |                         assert modinfo.state == ModuleState.SHARED | 
					
						
							|  |  |  |                         self.shared_ok.append(modinfo) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def summary(self, *, verbose: bool = False): | 
					
						
							|  |  |  |         longest = max([len(e.name) for e in self.modules], default=0) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         def print_three_column(modinfos: list[ModuleInfo]): | 
					
						
							|  |  |  |             names = [modinfo.name for modinfo in modinfos] | 
					
						
							|  |  |  |             names.sort(key=str.lower) | 
					
						
							|  |  |  |             # guarantee zip() doesn't drop anything | 
					
						
							|  |  |  |             while len(names) % 3: | 
					
						
							|  |  |  |                 names.append("") | 
					
						
							|  |  |  |             for l, m, r in zip(names[::3], names[1::3], names[2::3]): | 
					
						
							|  |  |  |                 print("%-*s   %-*s   %-*s" % (longest, l, longest, m, longest, r)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if verbose and self.builtin_ok: | 
					
						
							|  |  |  |             print("The following *built-in* modules have been successfully built:") | 
					
						
							|  |  |  |             print_three_column(self.builtin_ok) | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if verbose and self.shared_ok: | 
					
						
							|  |  |  |             print("The following *shared* modules have been successfully built:") | 
					
						
							|  |  |  |             print_three_column(self.shared_ok) | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.disabled_configure: | 
					
						
							|  |  |  |             print("The following modules are *disabled* in configure script:") | 
					
						
							|  |  |  |             print_three_column(self.disabled_configure) | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.disabled_setup: | 
					
						
							|  |  |  |             print("The following modules are *disabled* in Modules/Setup files:") | 
					
						
							|  |  |  |             print_three_column(self.disabled_setup) | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if verbose and self.notavailable: | 
					
						
							|  |  |  |             print( | 
					
						
							|  |  |  |                 f"The following modules are not available on platform '{self.platform}':" | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             print_three_column(self.notavailable) | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.missing: | 
					
						
							|  |  |  |             print("The necessary bits to build these optional modules were not found:") | 
					
						
							|  |  |  |             print_three_column(self.missing) | 
					
						
							|  |  |  |             print("To find the necessary bits, look in configure.ac and config.log.") | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if self.failed_on_import: | 
					
						
							|  |  |  |             print( | 
					
						
							|  |  |  |                 "Following modules built successfully " | 
					
						
							|  |  |  |                 "but were removed because they could not be imported:" | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             print_three_column(self.failed_on_import) | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if any( | 
					
						
							|  |  |  |             modinfo.name == "_ssl" for modinfo in self.missing + self.failed_on_import | 
					
						
							|  |  |  |         ): | 
					
						
							|  |  |  |             print("Could not build the ssl module!") | 
					
						
							|  |  |  |             print("Python requires a OpenSSL 1.1.1 or newer") | 
					
						
							|  |  |  |             if sysconfig.get_config_var("OPENSSL_LDFLAGS"): | 
					
						
							|  |  |  |                 print("Custom linker flags may require --with-openssl-rpath=auto") | 
					
						
							|  |  |  |             print() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         disabled = len(self.disabled_configure) + len(self.disabled_setup) | 
					
						
							|  |  |  |         print( | 
					
						
							|  |  |  |             f"Checked {len(self.modules)} modules (" | 
					
						
							|  |  |  |             f"{len(self.builtin_ok)} built-in, " | 
					
						
							|  |  |  |             f"{len(self.shared_ok)} shared, " | 
					
						
							|  |  |  |             f"{len(self.notavailable)} n/a on {self.platform}, " | 
					
						
							|  |  |  |             f"{disabled} disabled, " | 
					
						
							|  |  |  |             f"{len(self.missing)} missing, " | 
					
						
							|  |  |  |             f"{len(self.failed_on_import)} failed on import)" | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def check_strict_build(self): | 
					
						
							|  |  |  |         """Fail if modules are missing and it's a strict build""" | 
					
						
							|  |  |  |         if self.strict_extensions_build and (self.failed_on_import or self.missing): | 
					
						
							|  |  |  |             raise RuntimeError("Failed to build some stdlib modules") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def list_module_names(self, *, all: bool = False) -> set: | 
					
						
							|  |  |  |         names = {modinfo.name for modinfo in self.modules} | 
					
						
							|  |  |  |         if all: | 
					
						
							|  |  |  |             names.update(WINDOWS_MODULES) | 
					
						
							|  |  |  |         return names | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_builddir(self) -> pathlib.Path: | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             with open(self.pybuilddir_txt, encoding="utf-8") as f: | 
					
						
							|  |  |  |                 builddir = f.read() | 
					
						
							|  |  |  |         except FileNotFoundError: | 
					
						
							|  |  |  |             logger.error("%s must be run from the top build directory", __file__) | 
					
						
							|  |  |  |             raise | 
					
						
							|  |  |  |         builddir = pathlib.Path(builddir) | 
					
						
							|  |  |  |         logger.debug("%s: %s", self.pybuilddir_txt, builddir) | 
					
						
							|  |  |  |         return builddir | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_modules(self) -> list[ModuleInfo]: | 
					
						
							|  |  |  |         """Get module info from sysconfig and Modules/Setup* files""" | 
					
						
							|  |  |  |         seen = set() | 
					
						
							|  |  |  |         modules = [] | 
					
						
							|  |  |  |         # parsing order is important, first entry wins | 
					
						
							|  |  |  |         for modinfo in self.get_core_modules(): | 
					
						
							|  |  |  |             modules.append(modinfo) | 
					
						
							|  |  |  |             seen.add(modinfo.name) | 
					
						
							|  |  |  |         for setup_file in self.setup_files: | 
					
						
							|  |  |  |             for modinfo in self.parse_setup_file(setup_file): | 
					
						
							|  |  |  |                 if modinfo.name not in seen: | 
					
						
							|  |  |  |                     modules.append(modinfo) | 
					
						
							|  |  |  |                     seen.add(modinfo.name) | 
					
						
							|  |  |  |         for modinfo in self.get_sysconfig_modules(): | 
					
						
							|  |  |  |             if modinfo.name not in seen: | 
					
						
							|  |  |  |                 modules.append(modinfo) | 
					
						
							|  |  |  |                 seen.add(modinfo.name) | 
					
						
							|  |  |  |         logger.debug("Found %i modules in total", len(modules)) | 
					
						
							|  |  |  |         modules.sort() | 
					
						
							|  |  |  |         return modules | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_core_modules(self) -> Iterable[ModuleInfo]: | 
					
						
							|  |  |  |         """Get hard-coded core modules""" | 
					
						
							|  |  |  |         for name in CORE_MODULES: | 
					
						
							|  |  |  |             modinfo = ModuleInfo(name, ModuleState.BUILTIN) | 
					
						
							|  |  |  |             logger.debug("Found core module %s", modinfo) | 
					
						
							|  |  |  |             yield modinfo | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_sysconfig_modules(self) -> Iterable[ModuleInfo]: | 
					
						
							|  |  |  |         """Get modules defined in Makefile through sysconfig
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         MODBUILT_NAMES: modules in *static* block | 
					
						
							|  |  |  |         MODSHARED_NAMES: modules in *shared* block | 
					
						
							|  |  |  |         MODDISABLED_NAMES: modules in *disabled* block | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         moddisabled = set(sysconfig.get_config_var("MODDISABLED_NAMES").split()) | 
					
						
							|  |  |  |         if self.cross_compiling: | 
					
						
							|  |  |  |             modbuiltin = set(sysconfig.get_config_var("MODBUILT_NAMES").split()) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             modbuiltin = set(sys.builtin_module_names) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for key, value in sysconfig.get_config_vars().items(): | 
					
						
							|  |  |  |             if not key.startswith("MODULE_") or not key.endswith("_STATE"): | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             if value not in {"yes", "disabled", "missing", "n/a"}: | 
					
						
							|  |  |  |                 raise ValueError(f"Unsupported value '{value}' for {key}") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             modname = key[7:-6].lower() | 
					
						
							|  |  |  |             if modname in moddisabled: | 
					
						
							|  |  |  |                 # Setup "*disabled*" rule | 
					
						
							|  |  |  |                 state = ModuleState.DISABLED_SETUP | 
					
						
							|  |  |  |             elif value in {"disabled", "missing", "n/a"}: | 
					
						
							|  |  |  |                 state = ModuleState(value) | 
					
						
							|  |  |  |             elif modname in modbuiltin: | 
					
						
							|  |  |  |                 assert value == "yes" | 
					
						
							|  |  |  |                 state = ModuleState.BUILTIN | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 assert value == "yes" | 
					
						
							|  |  |  |                 state = ModuleState.SHARED | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             modinfo = ModuleInfo(modname, state) | 
					
						
							|  |  |  |             logger.debug("Found %s in Makefile", modinfo) | 
					
						
							|  |  |  |             yield modinfo | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def parse_setup_file(self, setup_file: pathlib.Path) -> Iterable[ModuleInfo]: | 
					
						
							|  |  |  |         """Parse a Modules/Setup file""" | 
					
						
							|  |  |  |         assign_var = re.compile(r"^\w+=")  # EGG_SPAM=foo | 
					
						
							|  |  |  |         # default to static module | 
					
						
							|  |  |  |         state = ModuleState.BUILTIN | 
					
						
							|  |  |  |         logger.debug("Parsing Setup file %s", setup_file) | 
					
						
							|  |  |  |         with open(setup_file, encoding="utf-8") as f: | 
					
						
							|  |  |  |             for line in f: | 
					
						
							|  |  |  |                 line = line.strip() | 
					
						
							|  |  |  |                 if not line or line.startswith("#") or assign_var.match(line): | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |                 match line.split(): | 
					
						
							|  |  |  |                     case ["*shared*"]: | 
					
						
							|  |  |  |                         state = ModuleState.SHARED | 
					
						
							|  |  |  |                     case ["*static*"]: | 
					
						
							|  |  |  |                         state = ModuleState.BUILTIN | 
					
						
							|  |  |  |                     case ["*disabled*"]: | 
					
						
							|  |  |  |                         state = ModuleState.DISABLED | 
					
						
							|  |  |  |                     case ["*noconfig*"]: | 
					
						
							|  |  |  |                         state = None | 
					
						
							|  |  |  |                     case [*items]: | 
					
						
							|  |  |  |                         if state == ModuleState.DISABLED: | 
					
						
							|  |  |  |                             # *disabled* can disable multiple modules per line | 
					
						
							|  |  |  |                             for item in items: | 
					
						
							|  |  |  |                                 modinfo = ModuleInfo(item, state) | 
					
						
							|  |  |  |                                 logger.debug("Found %s in %s", modinfo, setup_file) | 
					
						
							|  |  |  |                                 yield modinfo | 
					
						
							|  |  |  |                         elif state in {ModuleState.SHARED, ModuleState.BUILTIN}: | 
					
						
							|  |  |  |                             # *shared* and *static*, first item is the name of the module. | 
					
						
							|  |  |  |                             modinfo = ModuleInfo(items[0], state) | 
					
						
							|  |  |  |                             logger.debug("Found %s in %s", modinfo, setup_file) | 
					
						
							|  |  |  |                             yield modinfo | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_spec(self, modinfo: ModuleInfo) -> ModuleSpec: | 
					
						
							|  |  |  |         """Get ModuleSpec for builtin or extension module""" | 
					
						
							|  |  |  |         if modinfo.state == ModuleState.SHARED: | 
					
						
							|  |  |  |             location = os.fspath(self.get_location(modinfo)) | 
					
						
							|  |  |  |             loader = ExtensionFileLoader(modinfo.name, location) | 
					
						
							|  |  |  |             return spec_from_file_location(modinfo.name, location, loader=loader) | 
					
						
							|  |  |  |         elif modinfo.state == ModuleState.BUILTIN: | 
					
						
							|  |  |  |             return spec_from_loader(modinfo.name, loader=BuiltinImporter) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             raise ValueError(modinfo) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_location(self, modinfo: ModuleInfo) -> pathlib.Path: | 
					
						
							|  |  |  |         """Get shared library location in build directory""" | 
					
						
							|  |  |  |         if modinfo.state == ModuleState.SHARED: | 
					
						
							|  |  |  |             return self.builddir / f"{modinfo.name}{self.ext_suffix}" | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _check_file(self, modinfo: ModuleInfo, spec: ModuleSpec): | 
					
						
							|  |  |  |         """Check that the module file is present and not empty""" | 
					
						
							|  |  |  |         if spec.loader is BuiltinImporter: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             st = os.stat(spec.origin) | 
					
						
							|  |  |  |         except FileNotFoundError: | 
					
						
							|  |  |  |             logger.error("%s (%s) is missing", modinfo.name, spec.origin) | 
					
						
							|  |  |  |             raise | 
					
						
							|  |  |  |         if not st.st_size: | 
					
						
							|  |  |  |             raise ImportError(f"{spec.origin} is an empty file") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def check_module_import(self, modinfo: ModuleInfo): | 
					
						
							|  |  |  |         """Attempt to import module and report errors""" | 
					
						
							|  |  |  |         spec = self.get_spec(modinfo) | 
					
						
							|  |  |  |         self._check_file(modinfo, spec) | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             with warnings.catch_warnings(): | 
					
						
							|  |  |  |                 # ignore deprecation warning from deprecated modules | 
					
						
							|  |  |  |                 warnings.simplefilter("ignore", DeprecationWarning) | 
					
						
							|  |  |  |                 bootstrap_load(spec) | 
					
						
							|  |  |  |         except ImportError as e: | 
					
						
							|  |  |  |             logger.error("%s failed to import: %s", modinfo.name, e) | 
					
						
							|  |  |  |             raise | 
					
						
							|  |  |  |         except Exception as e: | 
					
						
							|  |  |  |             logger.exception("Importing extension '%s' failed!", modinfo.name) | 
					
						
							|  |  |  |             raise | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def check_module_cross(self, modinfo: ModuleInfo): | 
					
						
							|  |  |  |         """Sanity check for cross compiling""" | 
					
						
							|  |  |  |         spec = self.get_spec(modinfo) | 
					
						
							|  |  |  |         self._check_file(modinfo, spec) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def rename_module(self, modinfo: ModuleInfo) -> None: | 
					
						
							|  |  |  |         """Rename module file""" | 
					
						
							|  |  |  |         if modinfo.state == ModuleState.BUILTIN: | 
					
						
							|  |  |  |             logger.error("Cannot mark builtin module '%s' as failed!", modinfo.name) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         failed_name = f"{modinfo.name}_failed{self.ext_suffix}" | 
					
						
							|  |  |  |         builddir_path = self.get_location(modinfo) | 
					
						
							|  |  |  |         if builddir_path.is_symlink(): | 
					
						
							|  |  |  |             symlink = builddir_path | 
					
						
							|  |  |  |             module_path = builddir_path.resolve().relative_to(os.getcwd()) | 
					
						
							|  |  |  |             failed_path = module_path.parent / failed_name | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             symlink = None | 
					
						
							|  |  |  |             module_path = builddir_path | 
					
						
							|  |  |  |             failed_path = self.builddir / failed_name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # remove old failed file | 
					
						
							|  |  |  |         failed_path.unlink(missing_ok=True) | 
					
						
							|  |  |  |         # remove symlink | 
					
						
							|  |  |  |         if symlink is not None: | 
					
						
							|  |  |  |             symlink.unlink(missing_ok=True) | 
					
						
							|  |  |  |         # rename shared extension file | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             module_path.rename(failed_path) | 
					
						
							|  |  |  |         except FileNotFoundError: | 
					
						
							|  |  |  |             logger.debug("Shared extension file '%s' does not exist.", module_path) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             logger.debug("Rename '%s' -> '%s'", module_path, failed_path) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def main(): | 
					
						
							|  |  |  |     args = parser.parse_args() | 
					
						
							|  |  |  |     if args.debug: | 
					
						
							|  |  |  |         args.verbose = True | 
					
						
							|  |  |  |     logging.basicConfig( | 
					
						
							|  |  |  |         level=logging.DEBUG if args.debug else logging.INFO, | 
					
						
							|  |  |  |         format="[%(levelname)s] %(message)s", | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     checker = ModuleChecker( | 
					
						
							|  |  |  |         cross_compiling=args.cross_compiling, | 
					
						
							|  |  |  |         strict=args.strict, | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     if args.list_module_names: | 
					
						
							|  |  |  |         names = checker.list_module_names(all=True) | 
					
						
							|  |  |  |         for name in sorted(names): | 
					
						
							|  |  |  |             print(name) | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         checker.check() | 
					
						
							|  |  |  |         checker.summary(verbose=args.verbose) | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             checker.check_strict_build() | 
					
						
							|  |  |  |         except RuntimeError as e: | 
					
						
							|  |  |  |             parser.exit(1, f"\nError: {e}\n") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if __name__ == "__main__": | 
					
						
							|  |  |  |     main() |