Path: blob/main/Tools/build/check_extension_modules.py
12 views
"""Check extension modules12The script checks shared and built-in extension modules. It verifies that the3modules have been built and that they can be imported successfully. Missing4modules and failed imports are reported to the user. Shared extension5files are renamed on failed import.67Module information is parsed from several sources:89- core modules hard-coded in Modules/config.c.in10- Windows-specific modules that are hard-coded in PC/config.c11- MODULE_{name}_STATE entries in Makefile (provided through sysconfig)12- Various makesetup files:13- $(srcdir)/Modules/Setup14- Modules/Setup.[local|bootstrap|stdlib] files, which are generated15from $(srcdir)/Modules/Setup.*.in files1617See --help for more information18"""19import argparse20import collections21import enum22import logging23import os24import pathlib25import re26import sys27import sysconfig28import warnings2930from importlib._bootstrap import _load as bootstrap_load31from importlib.machinery import BuiltinImporter, ExtensionFileLoader, ModuleSpec32from importlib.util import spec_from_file_location, spec_from_loader33from typing import Iterable3435SRC_DIR = pathlib.Path(__file__).parent.parent.parent3637# core modules, hard-coded in Modules/config.h.in38CORE_MODULES = {39"_ast",40"_imp",41"_string",42"_tokenize",43"_warnings",44"builtins",45"gc",46"marshal",47"sys",48}4950# Windows-only modules51WINDOWS_MODULES = {52"_overlapped",53"_testconsole",54"_winapi",55"msvcrt",56"nt",57"winreg",58"winsound",59}606162logger = logging.getLogger(__name__)6364parser = argparse.ArgumentParser(65prog="check_extension_modules",66description=__doc__,67formatter_class=argparse.RawDescriptionHelpFormatter,68)6970parser.add_argument(71"--verbose",72action="store_true",73help="Verbose, report builtin, shared, and unavailable modules",74)7576parser.add_argument(77"--debug",78action="store_true",79help="Enable debug logging",80)8182parser.add_argument(83"--strict",84action=argparse.BooleanOptionalAction,85help=(86"Strict check, fail when a module is missing or fails to import"87"(default: no, unless env var PYTHONSTRICTEXTENSIONBUILD is set)"88),89default=bool(os.environ.get("PYTHONSTRICTEXTENSIONBUILD")),90)9192parser.add_argument(93"--cross-compiling",94action=argparse.BooleanOptionalAction,95help=(96"Use cross-compiling checks "97"(default: no, unless env var _PYTHON_HOST_PLATFORM is set)."98),99default="_PYTHON_HOST_PLATFORM" in os.environ,100)101102parser.add_argument(103"--list-module-names",104action="store_true",105help="Print a list of module names to stdout and exit",106)107108109class ModuleState(enum.Enum):110# Makefile state "yes"111BUILTIN = "builtin"112SHARED = "shared"113114DISABLED = "disabled"115MISSING = "missing"116NA = "n/a"117# disabled by Setup / makesetup rule118DISABLED_SETUP = "disabled_setup"119120def __bool__(self):121return self.value in {"builtin", "shared"}122123124ModuleInfo = collections.namedtuple("ModuleInfo", "name state")125126127class ModuleChecker:128pybuilddir_txt = "pybuilddir.txt"129130setup_files = (131# see end of configure.ac132"Modules/Setup.local",133"Modules/Setup.stdlib",134"Modules/Setup.bootstrap",135SRC_DIR / "Modules/Setup",136)137138def __init__(self, cross_compiling: bool = False, strict: bool = False):139self.cross_compiling = cross_compiling140self.strict_extensions_build = strict141self.ext_suffix = sysconfig.get_config_var("EXT_SUFFIX")142self.platform = sysconfig.get_platform()143self.builddir = self.get_builddir()144self.modules = self.get_modules()145146self.builtin_ok = []147self.shared_ok = []148self.failed_on_import = []149self.missing = []150self.disabled_configure = []151self.disabled_setup = []152self.notavailable = []153154def check(self):155for modinfo in self.modules:156logger.debug("Checking '%s' (%s)", modinfo.name, self.get_location(modinfo))157if modinfo.state == ModuleState.DISABLED:158self.disabled_configure.append(modinfo)159elif modinfo.state == ModuleState.DISABLED_SETUP:160self.disabled_setup.append(modinfo)161elif modinfo.state == ModuleState.MISSING:162self.missing.append(modinfo)163elif modinfo.state == ModuleState.NA:164self.notavailable.append(modinfo)165else:166try:167if self.cross_compiling:168self.check_module_cross(modinfo)169else:170self.check_module_import(modinfo)171except (ImportError, FileNotFoundError):172self.rename_module(modinfo)173self.failed_on_import.append(modinfo)174else:175if modinfo.state == ModuleState.BUILTIN:176self.builtin_ok.append(modinfo)177else:178assert modinfo.state == ModuleState.SHARED179self.shared_ok.append(modinfo)180181def summary(self, *, verbose: bool = False):182longest = max([len(e.name) for e in self.modules], default=0)183184def print_three_column(modinfos: list[ModuleInfo]):185names = [modinfo.name for modinfo in modinfos]186names.sort(key=str.lower)187# guarantee zip() doesn't drop anything188while len(names) % 3:189names.append("")190for l, m, r in zip(names[::3], names[1::3], names[2::3]):191print("%-*s %-*s %-*s" % (longest, l, longest, m, longest, r))192193if verbose and self.builtin_ok:194print("The following *built-in* modules have been successfully built:")195print_three_column(self.builtin_ok)196print()197198if verbose and self.shared_ok:199print("The following *shared* modules have been successfully built:")200print_three_column(self.shared_ok)201print()202203if self.disabled_configure:204print("The following modules are *disabled* in configure script:")205print_three_column(self.disabled_configure)206print()207208if self.disabled_setup:209print("The following modules are *disabled* in Modules/Setup files:")210print_three_column(self.disabled_setup)211print()212213if verbose and self.notavailable:214print(215f"The following modules are not available on platform '{self.platform}':"216)217print_three_column(self.notavailable)218print()219220if self.missing:221print("The necessary bits to build these optional modules were not found:")222print_three_column(self.missing)223print("To find the necessary bits, look in configure.ac and config.log.")224print()225226if self.failed_on_import:227print(228"Following modules built successfully "229"but were removed because they could not be imported:"230)231print_three_column(self.failed_on_import)232print()233234if any(235modinfo.name == "_ssl" for modinfo in self.missing + self.failed_on_import236):237print("Could not build the ssl module!")238print("Python requires a OpenSSL 1.1.1 or newer")239if sysconfig.get_config_var("OPENSSL_LDFLAGS"):240print("Custom linker flags may require --with-openssl-rpath=auto")241print()242243disabled = len(self.disabled_configure) + len(self.disabled_setup)244print(245f"Checked {len(self.modules)} modules ("246f"{len(self.builtin_ok)} built-in, "247f"{len(self.shared_ok)} shared, "248f"{len(self.notavailable)} n/a on {self.platform}, "249f"{disabled} disabled, "250f"{len(self.missing)} missing, "251f"{len(self.failed_on_import)} failed on import)"252)253254def check_strict_build(self):255"""Fail if modules are missing and it's a strict build"""256if self.strict_extensions_build and (self.failed_on_import or self.missing):257raise RuntimeError("Failed to build some stdlib modules")258259def list_module_names(self, *, all: bool = False) -> set:260names = {modinfo.name for modinfo in self.modules}261if all:262names.update(WINDOWS_MODULES)263return names264265def get_builddir(self) -> pathlib.Path:266try:267with open(self.pybuilddir_txt, encoding="utf-8") as f:268builddir = f.read()269except FileNotFoundError:270logger.error("%s must be run from the top build directory", __file__)271raise272builddir = pathlib.Path(builddir)273logger.debug("%s: %s", self.pybuilddir_txt, builddir)274return builddir275276def get_modules(self) -> list[ModuleInfo]:277"""Get module info from sysconfig and Modules/Setup* files"""278seen = set()279modules = []280# parsing order is important, first entry wins281for modinfo in self.get_core_modules():282modules.append(modinfo)283seen.add(modinfo.name)284for setup_file in self.setup_files:285for modinfo in self.parse_setup_file(setup_file):286if modinfo.name not in seen:287modules.append(modinfo)288seen.add(modinfo.name)289for modinfo in self.get_sysconfig_modules():290if modinfo.name not in seen:291modules.append(modinfo)292seen.add(modinfo.name)293logger.debug("Found %i modules in total", len(modules))294modules.sort()295return modules296297def get_core_modules(self) -> Iterable[ModuleInfo]:298"""Get hard-coded core modules"""299for name in CORE_MODULES:300modinfo = ModuleInfo(name, ModuleState.BUILTIN)301logger.debug("Found core module %s", modinfo)302yield modinfo303304def get_sysconfig_modules(self) -> Iterable[ModuleInfo]:305"""Get modules defined in Makefile through sysconfig306307MODBUILT_NAMES: modules in *static* block308MODSHARED_NAMES: modules in *shared* block309MODDISABLED_NAMES: modules in *disabled* block310"""311moddisabled = set(sysconfig.get_config_var("MODDISABLED_NAMES").split())312if self.cross_compiling:313modbuiltin = set(sysconfig.get_config_var("MODBUILT_NAMES").split())314else:315modbuiltin = set(sys.builtin_module_names)316317for key, value in sysconfig.get_config_vars().items():318if not key.startswith("MODULE_") or not key.endswith("_STATE"):319continue320if value not in {"yes", "disabled", "missing", "n/a"}:321raise ValueError(f"Unsupported value '{value}' for {key}")322323modname = key[7:-6].lower()324if modname in moddisabled:325# Setup "*disabled*" rule326state = ModuleState.DISABLED_SETUP327elif value in {"disabled", "missing", "n/a"}:328state = ModuleState(value)329elif modname in modbuiltin:330assert value == "yes"331state = ModuleState.BUILTIN332else:333assert value == "yes"334state = ModuleState.SHARED335336modinfo = ModuleInfo(modname, state)337logger.debug("Found %s in Makefile", modinfo)338yield modinfo339340def parse_setup_file(self, setup_file: pathlib.Path) -> Iterable[ModuleInfo]:341"""Parse a Modules/Setup file"""342assign_var = re.compile(r"^\w+=") # EGG_SPAM=foo343# default to static module344state = ModuleState.BUILTIN345logger.debug("Parsing Setup file %s", setup_file)346with open(setup_file, encoding="utf-8") as f:347for line in f:348line = line.strip()349if not line or line.startswith("#") or assign_var.match(line):350continue351match line.split():352case ["*shared*"]:353state = ModuleState.SHARED354case ["*static*"]:355state = ModuleState.BUILTIN356case ["*disabled*"]:357state = ModuleState.DISABLED358case ["*noconfig*"]:359state = None360case [*items]:361if state == ModuleState.DISABLED:362# *disabled* can disable multiple modules per line363for item in items:364modinfo = ModuleInfo(item, state)365logger.debug("Found %s in %s", modinfo, setup_file)366yield modinfo367elif state in {ModuleState.SHARED, ModuleState.BUILTIN}:368# *shared* and *static*, first item is the name of the module.369modinfo = ModuleInfo(items[0], state)370logger.debug("Found %s in %s", modinfo, setup_file)371yield modinfo372373def get_spec(self, modinfo: ModuleInfo) -> ModuleSpec:374"""Get ModuleSpec for builtin or extension module"""375if modinfo.state == ModuleState.SHARED:376location = os.fspath(self.get_location(modinfo))377loader = ExtensionFileLoader(modinfo.name, location)378return spec_from_file_location(modinfo.name, location, loader=loader)379elif modinfo.state == ModuleState.BUILTIN:380return spec_from_loader(modinfo.name, loader=BuiltinImporter)381else:382raise ValueError(modinfo)383384def get_location(self, modinfo: ModuleInfo) -> pathlib.Path:385"""Get shared library location in build directory"""386if modinfo.state == ModuleState.SHARED:387return self.builddir / f"{modinfo.name}{self.ext_suffix}"388else:389return None390391def _check_file(self, modinfo: ModuleInfo, spec: ModuleSpec):392"""Check that the module file is present and not empty"""393if spec.loader is BuiltinImporter:394return395try:396st = os.stat(spec.origin)397except FileNotFoundError:398logger.error("%s (%s) is missing", modinfo.name, spec.origin)399raise400if not st.st_size:401raise ImportError(f"{spec.origin} is an empty file")402403def check_module_import(self, modinfo: ModuleInfo):404"""Attempt to import module and report errors"""405spec = self.get_spec(modinfo)406self._check_file(modinfo, spec)407try:408with warnings.catch_warnings():409# ignore deprecation warning from deprecated modules410warnings.simplefilter("ignore", DeprecationWarning)411bootstrap_load(spec)412except ImportError as e:413logger.error("%s failed to import: %s", modinfo.name, e)414raise415except Exception as e:416logger.exception("Importing extension '%s' failed!", modinfo.name)417raise418419def check_module_cross(self, modinfo: ModuleInfo):420"""Sanity check for cross compiling"""421spec = self.get_spec(modinfo)422self._check_file(modinfo, spec)423424def rename_module(self, modinfo: ModuleInfo) -> None:425"""Rename module file"""426if modinfo.state == ModuleState.BUILTIN:427logger.error("Cannot mark builtin module '%s' as failed!", modinfo.name)428return429430failed_name = f"{modinfo.name}_failed{self.ext_suffix}"431builddir_path = self.get_location(modinfo)432if builddir_path.is_symlink():433symlink = builddir_path434module_path = builddir_path.resolve().relative_to(os.getcwd())435failed_path = module_path.parent / failed_name436else:437symlink = None438module_path = builddir_path439failed_path = self.builddir / failed_name440441# remove old failed file442failed_path.unlink(missing_ok=True)443# remove symlink444if symlink is not None:445symlink.unlink(missing_ok=True)446# rename shared extension file447try:448module_path.rename(failed_path)449except FileNotFoundError:450logger.debug("Shared extension file '%s' does not exist.", module_path)451else:452logger.debug("Rename '%s' -> '%s'", module_path, failed_path)453454455def main():456args = parser.parse_args()457if args.debug:458args.verbose = True459logging.basicConfig(460level=logging.DEBUG if args.debug else logging.INFO,461format="[%(levelname)s] %(message)s",462)463464checker = ModuleChecker(465cross_compiling=args.cross_compiling,466strict=args.strict,467)468if args.list_module_names:469names = checker.list_module_names(all=True)470for name in sorted(names):471print(name)472else:473checker.check()474checker.summary(verbose=args.verbose)475try:476checker.check_strict_build()477except RuntimeError as e:478parser.exit(1, f"\nError: {e}\n")479480481if __name__ == "__main__":482main()483484485