mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 21:51:50 +00:00 
			
		
		
		
	gh-93939: Add script to check extension modules (#94545)
Add script ``Tools/scripts/check_modules.py`` to check and validate builtin and shared extension modules. The script also handles ``Modules/Setup`` and will eventually replace ``setup.py``. Co-authored-by: Victor Stinner <vstinner@python.org> Co-authored-by: Erlend Egeberg Aasland <erlend.aasland@protonmail.com>
This commit is contained in:
		
							parent
							
								
									fd76eb547d
								
							
						
					
					
						commit
						7bd67d1d88
					
				
					 4 changed files with 504 additions and 41 deletions
				
			
		|  | @ -918,6 +918,9 @@ oldsharedmods: $(SHAREDMODS) pybuilddir.txt | ||||||
| 		fi; \ | 		fi; \ | ||||||
| 	done | 	done | ||||||
| 
 | 
 | ||||||
|  | checksharedmods: oldsharedmods sharedmods $(PYTHON_FOR_BUILD_DEPS) | ||||||
|  | 	@$(RUNSHARED) $(PYTHON_FOR_BUILD) $(srcdir)/Tools/scripts/check_extension_modules.py | ||||||
|  | 
 | ||||||
| Modules/Setup.local: | Modules/Setup.local: | ||||||
| 	@# Create empty Setup.local when file was deleted by user | 	@# Create empty Setup.local when file was deleted by user | ||||||
| 	echo "# Edit this file for local setup changes" > $@ | 	echo "# Edit this file for local setup changes" > $@ | ||||||
|  | @ -2531,7 +2534,8 @@ update-config: | ||||||
| Python/thread.o: @THREADHEADERS@ $(srcdir)/Python/condvar.h | Python/thread.o: @THREADHEADERS@ $(srcdir)/Python/condvar.h | ||||||
| 
 | 
 | ||||||
| # Declare targets that aren't real files | # Declare targets that aren't real files | ||||||
| .PHONY: all build_all build_wasm sharedmods check-clean-src oldsharedmods test quicktest | .PHONY: all build_all build_wasm sharedmods check-clean-src | ||||||
|  | .PHONY: oldsharedmods checksharedmods test quicktest | ||||||
| .PHONY: install altinstall oldsharedinstall bininstall altbininstall | .PHONY: install altinstall oldsharedinstall bininstall altbininstall | ||||||
| .PHONY: maninstall libinstall inclinstall libainstall sharedinstall | .PHONY: maninstall libinstall inclinstall libainstall sharedinstall | ||||||
| .PHONY: frameworkinstall frameworkinstallframework frameworkinstallstructure | .PHONY: frameworkinstall frameworkinstallframework frameworkinstallstructure | ||||||
|  |  | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | Add script ``Tools/scripts/check_modules.py`` to check and validate builtin | ||||||
|  | and shared extension modules. The script also handles ``Modules/Setup`` and | ||||||
|  | will eventually replace ``setup.py``. | ||||||
							
								
								
									
										489
									
								
								Tools/scripts/check_extension_modules.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										489
									
								
								Tools/scripts/check_extension_modules.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,489 @@ | ||||||
|  | """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 = { | ||||||
|  |     "_msi", | ||||||
|  |     "_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 = ( | ||||||
|  |         SRC_DIR / "Modules/Setup", | ||||||
|  |         "Modules/Setup.local", | ||||||
|  |         "Modules/Setup.bootstrap", | ||||||
|  |         "Modules/Setup.stdlib", | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     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 | ||||||
|  | 
 | ||||||
|  |         Modules built by setup.py addext() have a MODULE_{modname}_STATE entry, | ||||||
|  |         but are not listed in MODSHARED_NAMES. | ||||||
|  | 
 | ||||||
|  |         Modules built by old-style setup.py add() have neither a  MODULE_{modname} | ||||||
|  |         entry nor an entry in MODSHARED_NAMES. | ||||||
|  |         """ | ||||||
|  |         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() | ||||||
|  | @ -7,10 +7,11 @@ | ||||||
| import sys | import sys | ||||||
| import sysconfig | import sysconfig | ||||||
| 
 | 
 | ||||||
|  | from check_extension_modules import ModuleChecker | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| SRC_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) | SRC_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) | ||||||
| STDLIB_PATH = os.path.join(SRC_DIR, 'Lib') | STDLIB_PATH = os.path.join(SRC_DIR, 'Lib') | ||||||
| MODULES_SETUP = os.path.join(SRC_DIR, 'Modules', 'Setup') |  | ||||||
| SETUP_PY = os.path.join(SRC_DIR, 'setup.py') | SETUP_PY = os.path.join(SRC_DIR, 'setup.py') | ||||||
| 
 | 
 | ||||||
| IGNORE = { | IGNORE = { | ||||||
|  | @ -41,23 +42,6 @@ | ||||||
|     'xxsubtype', |     'xxsubtype', | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| # Windows extension modules |  | ||||||
| WINDOWS_MODULES = ( |  | ||||||
|     '_msi', |  | ||||||
|     '_overlapped', |  | ||||||
|     '_testconsole', |  | ||||||
|     '_winapi', |  | ||||||
|     'msvcrt', |  | ||||||
|     'nt', |  | ||||||
|     'winreg', |  | ||||||
|     'winsound' |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| # macOS extension modules |  | ||||||
| MACOS_MODULES = ( |  | ||||||
|     '_scproxy', |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| # Pure Python modules (Lib/*.py) | # Pure Python modules (Lib/*.py) | ||||||
| def list_python_modules(names): | def list_python_modules(names): | ||||||
|     for filename in os.listdir(STDLIB_PATH): |     for filename in os.listdir(STDLIB_PATH): | ||||||
|  | @ -89,28 +73,11 @@ def list_setup_extensions(names): | ||||||
|     names |= set(extensions) |     names |= set(extensions) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Built-in and extension modules built by Modules/Setup | # Built-in and extension modules built by Modules/Setup* | ||||||
|  | # includes Windows and macOS extensions. | ||||||
| def list_modules_setup_extensions(names): | def list_modules_setup_extensions(names): | ||||||
|     assign_var = re.compile("^[A-Z]+=") |     checker = ModuleChecker() | ||||||
| 
 |     names.update(checker.list_module_names(all=True)) | ||||||
|     with open(MODULES_SETUP, encoding="utf-8") as modules_fp: |  | ||||||
|         for line in modules_fp: |  | ||||||
|             # Strip comment |  | ||||||
|             line = line.partition("#")[0] |  | ||||||
|             line = line.rstrip() |  | ||||||
|             if not line: |  | ||||||
|                 continue |  | ||||||
|             if assign_var.match(line): |  | ||||||
|                 # Ignore "VAR=VALUE" |  | ||||||
|                 continue |  | ||||||
|             if line in ("*disabled*", "*shared*"): |  | ||||||
|                 continue |  | ||||||
|             parts = line.split() |  | ||||||
|             if len(parts) < 2: |  | ||||||
|                 continue |  | ||||||
|             # "errno errnomodule.c" => write "errno" |  | ||||||
|             name = parts[0] |  | ||||||
|             names.add(name) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # List frozen modules of the PyImport_FrozenModules list (Python/frozen.c). | # List frozen modules of the PyImport_FrozenModules list (Python/frozen.c). | ||||||
|  | @ -134,7 +101,7 @@ def list_frozen(names): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def list_modules(): | def list_modules(): | ||||||
|     names = set(sys.builtin_module_names) | set(WINDOWS_MODULES) | set(MACOS_MODULES) |     names = set(sys.builtin_module_names) | ||||||
|     list_modules_setup_extensions(names) |     list_modules_setup_extensions(names) | ||||||
|     list_setup_extensions(names) |     list_setup_extensions(names) | ||||||
|     list_packages(names) |     list_packages(names) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Christian Heimes
						Christian Heimes