fix: 修复插件卸载后重启失败问题,增加安装进度日志
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
"""宿主启动时的 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(过渡期)。
|
||||
|
||||
注意:本文件不依赖任何第三方包,只用 stdlib(os / 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) onedir:exe 旁边的 plugins/
|
||||
exe_dir = Path(sys.executable).resolve().parent
|
||||
plugins = exe_dir / "plugins"
|
||||
if plugins.is_dir():
|
||||
return plugins.resolve()
|
||||
|
||||
# 3) onefile:PyInstaller 解压目录旁边的 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()
|
||||
Reference in New Issue
Block a user