106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""伴生包加载器:把纯 Python 伴生包从产物目录挂到 sys.modules。
|
||
|
||
设计目标:宿主 exe 一次打包后,mil 的伴生依赖(openpyxl / et_xmlfile / ...)
|
||
可通过"覆盖 build/<pkg>/ 下的 .pyd 或 mil_runtime/<pkg>/ 下的源码"增量更新,
|
||
宿主不需要重打。
|
||
|
||
加载顺序(命中即停):
|
||
1. sys.modules 已存在(不动)
|
||
2. mil_runtime/<pkg>/ 下的纯 Python 源码(数据化部署)
|
||
3. build/<pkg>/ 下的预编译 .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,开发侧不一定有)。
|
||
"""
|
||
# 本文件位于 <root>/mil/core/companion_loader.py,往上两级就是 <root>
|
||
return Path(__file__).resolve().parent.parent.parent
|
||
|
||
|
||
def _load_from_source(runtime_dir: Path, pkg: str) -> ModuleType | None:
|
||
"""尝试从 mil_runtime/<pkg>/ 加载纯 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/<pkg>/<pkg>.cp311-*.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:
|
||
logger.debug(f"伴生包 {pkg} 已在 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 |