Files

119 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""宿主启动时的 runtime hook。
宿主打包时通过 --runtime-hook 指定本文件,宿主 exe 启动时会在 main 之前执行本脚本。
作用:把 exe 旁边的 plugins/ 目录加进 sys.path
让插件自带的 .pyd + stdlib 子包(含 C 扩展)可以被 Python import 机制找到。
宿主打包命令示例(PyInstaller):
pyinstaller --onefile --runtime-hook tools/runtime_hook.py host_main.py
宿主打包命令示例(Nuitka):
nuitka --standalone --onefile --include-module=runtime_hook host_main.py
宿主部署目录布局:
host.exe
plugins/ ← 由 mil_sdk/tools/export_runtime.py 生成
└── mil/ ← 每插件一个子目录(zip 解压产物)
├── mil.cp311-*.pyd
├── openpyxl.cp311-*.pyd
├── et_xmlfile.cp311-*.pyd
├── xml/ ← 插件自带的 stdlib 子包(含 _elementtree.pyd
│ └── etree/
└── manifest.json
兼容旧扁平布局:plugins/ 直接含 manifest.json(过渡期)。
注意:本文件不依赖任何第三方包,只用 stdlibos / sys / json / pathlib)。
因为它在宿主 sys.path 配置好之前执行,不能 import 任何外部包。
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
def _resolve_plugins_dir() -> Path | None:
"""定位 plugins/ 目录。
优先级:
1. 环境变量 MIL_PLUGIN_DIR
2. sys.executable 旁边的 plugins/onedir 模式)
3. sys._MEIPASS 旁边的 plugins/onefile 模式解压目录)
"""
# 1) 环境变量
env = os.environ.get("MIL_PLUGIN_DIR")
if env:
p = Path(env)
if p.is_dir():
return p.resolve()
# 2) onedirexe 旁边的 plugins/
exe_dir = Path(sys.executable).resolve().parent
plugins = exe_dir / "plugins"
if plugins.is_dir():
return plugins.resolve()
# 3) onefilePyInstaller 解压目录旁边的 plugins/
meipass = getattr(sys, "_MEIPASS", None)
if meipass:
plugins = Path(meipass).parent / "plugins"
if plugins.is_dir():
return plugins.resolve()
return None
def _check_python_version(manifest_path: Path) -> None:
"""校验插件的 Python 版本与当前解释器兼容(minor 版本必须一致)。"""
import json
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except Exception:
return # manifest 损坏不阻止启动,让 import 错误自己暴露
plugin_py = manifest.get("python_version", "")
host_py = f"{sys.version_info.major}.{sys.version_info.minor}"
if not plugin_py.startswith(host_py):
sys.stderr.write(
f"[mil] WARNING: 插件 Python 版本({plugin_py}) 与宿主({host_py}) 可能不兼容\n"
)
def _insert_path(path: Path) -> None:
"""把 path 加到 sys.path[0],已存在则跳过。"""
p = str(path)
if p not in sys.path:
sys.path.insert(0, p)
def setup() -> None:
"""把 plugins/ 或 plugins/*/ 子目录加进 sys.path 最前面。
兼容两种布局:
- 旧扁平:plugins/ 直接含 manifest.json(过渡期)
- 新子目录:plugins/<插件名>/ 各自含 manifest.json
"""
plugins_dir = _resolve_plugins_dir()
if plugins_dir is None:
return
# 旧扁平布局:plugins/ 直接有 manifest.json
if (plugins_dir / "manifest.json").exists():
_check_python_version(plugins_dir / "manifest.json")
_insert_path(plugins_dir)
return
# 新子目录布局:逐个扫描 plugins/*/
for sub in plugins_dir.iterdir():
if not sub.is_dir():
continue
if (sub / "manifest.json").exists():
_check_python_version(sub / "manifest.json")
_insert_path(sub)
# 模块加载时立即执行(PyInstaller runtime hook 的约定)
setup()