"""伴生包加载器:把纯 Python 伴生包从产物目录挂到 sys.modules。 设计目标:宿主 exe 一次打包后,mil 的伴生依赖(openpyxl / et_xmlfile / ...) 可通过"覆盖 build// 下的 .pyd 或 mil_runtime// 下的源码"增量更新, 宿主不需要重打。 加载顺序(命中即停): 1. sys.modules 已存在(不动) 2. mil_runtime// 下的纯 Python 源码(数据化部署) 3. build// 下的预编译 .pyd(本轮主路径) 4. 都不命中 → 跳过并 warning,由调用方决定是否兜底 约定: - mil 包的父目录下存在两个候选目录:mil_runtime/(源码)和 build/(.pyd)。 - 解析顺序可通过环境变量 MIL_RUNTIME_DIR / MIL_BUILD_DIR 覆盖。 """ from __future__ import annotations import importlib.util import logging import os import sys from pathlib import Path from types import ModuleType logger = logging.getLogger(__name__) DEFAULT_RUNTIME_DIRNAME = "mil_runtime" DEFAULT_BUILD_DIRNAME = "build" def _mil_root() -> Path: """获取 mil 包的父目录(即 mil_runtime/ 与 build/ 所在的工程根)。 不通过 `import mil` 解析位置,避免触发 mil 包完整初始化 (宿主侧必装 PySide6,开发侧不一定有)。 """ # 本文件位于 /mil/core/companion_loader.py,往上两级就是 return Path(__file__).resolve().parent.parent.parent def _load_from_source(runtime_dir: Path, pkg: str) -> ModuleType | None: """尝试从 mil_runtime// 加载纯 Python 源码包。""" pkg_dir = runtime_dir / pkg if not (pkg_dir / "__init__.py").is_file(): return None spec = importlib.util.spec_from_file_location( pkg, pkg_dir / "__init__.py", submodule_search_locations=[str(pkg_dir)], ) if spec is None or spec.loader is None: logger.warning(f"无法为 {pkg} 构造 spec(runtime 源码),跳过") return None module = importlib.util.module_from_spec(spec) sys.modules[pkg] = module spec.loader.exec_module(module) return module def _load_from_pyd(build_dir: Path, pkg: str) -> ModuleType | None: """尝试从 build//..pyd 加载预编译包。""" candidates = list(build_dir.glob(f"{pkg}.*.pyd")) if not candidates: return None pyd_path = candidates[0] spec = importlib.util.spec_from_file_location(pkg, pyd_path) if spec is None or spec.loader is None: logger.warning(f"无法为 {pkg} 构造 spec(pyd={pyd_path}),跳过") return None module = importlib.util.module_from_spec(spec) sys.modules[pkg] = module spec.loader.exec_module(module) return module def load_companions(packages: list[str]) -> list[str]: """按上述顺序把每个伴生包挂到 sys.modules,返回成功挂载的包名列表。""" root = _mil_root() runtime_dir = Path(os.environ.get("MIL_RUNTIME_DIR", root / DEFAULT_RUNTIME_DIRNAME)) build_dir = Path(os.environ.get("MIL_BUILD_DIR", root / DEFAULT_BUILD_DIRNAME)) loaded: list[str] = [] for pkg in packages: if pkg in sys.modules: loaded.append(pkg) continue mod = ( _load_from_source(runtime_dir, pkg) or _load_from_pyd(build_dir, pkg) ) if mod is not None: loaded.append(pkg) logger.info(f"伴生包已挂载: {pkg}") else: logger.warning( f"伴生包未找到: {pkg}(runtime={runtime_dir} / build={build_dir} 均无)" ) return loaded