feat: 增加伴生包加载器、manifest配置,Excel处理优化

This commit is contained in:
2026-07-21 16:26:16 +08:00
parent 45e4f1ecf2
commit 91bb61444e
19 changed files with 1364 additions and 82 deletions
+106
View File
@@ -0,0 +1,106 @@
"""伴生包加载器:把纯 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} 构造 specruntime 源码),跳过")
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} 构造 specpyd={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