diff --git a/README.md b/README.md index 1c04a1a..8c8cf2d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,10 @@ uv run gen_models workers/myplugin/config/workers/myplugin workers/myplugin/work - `--encrypt`:打开加密与混淆 —— `.mbank` 加密存字节码并写 `.key` 密钥文件、做 PyArmor 混淆 - `--license`:打开授权 —— 注入厂商授权公钥(缺公钥则构建失败)、跑授权私钥泄漏守卫 +打包收集清单分两处:网关本体依赖在根 `config.toml` `[pyinstaller_build_import]`; +内置插件专属依赖(网关本体不 import、只在插件里用)在根目录 `plugins_deps.toml` +(按插件分节,可选文件),`pack` 时自动并入 `--hidden-import` / `--include-module`。 + ## 项目根目录定位 脚本需要在项目根目录下读写 `config/`、`src/plugins/`、`workers/` 等路径。 diff --git a/pyproject.toml b/pyproject.toml index 833a967..0099524 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = ["hatchling"] [project] name = "chongming-cli" -version = "0.3.3" +version = "0.3.4" description = "chongming 命令脚本集合:构建打包 / 进程插件打包 / 插件脚手架 / 模型生成 / 机器码授权签发" readme = "README.md" requires-python = ">=3.12" diff --git a/src/chongming_cli/build.py b/src/chongming_cli/build.py index d483dc2..3250546 100644 --- a/src/chongming_cli/build.py +++ b/src/chongming_cli/build.py @@ -35,6 +35,13 @@ uv run build nuitka --only mbank,prepare uv run build --encrypt --license # 对外分发:加密模块 + 注入授权公钥 uv run build # 内部调试:明文模块、不带授权 + +依赖收集(--only pack 生效): + 打包收集清单 = 根 config.toml [pyinstaller_build_import](网关本体依赖) + + 根目录 plugins_deps.toml(内置插件专属依赖 —— 网关本体不 import、只在 + mbank 内 import 的包,按插件分节声明;文件可选)。 + mbank 里的 import 静态分析看不到,两条清单都会被展开为 --hidden-import / + --include-module,缺一个都会在运行期加载插件时才 ImportError。 """ import os @@ -45,6 +52,7 @@ import subprocess import sys import asyncio import time +import tomllib from dataclasses import dataclass, field from pathlib import Path @@ -54,6 +62,8 @@ from sqlite_vfs.core import SQLiteVFS from sqlite_vfs.folder_packer import FolderPacker from chongming_config import get_config, Config +from ._root import find_project_root + # ── 常量 ──────────────────────────────────────────────────────────────────── PYINSTALLER = "pyinstaller" @@ -65,6 +75,12 @@ STEPS = ("mbank", "prepare", "obfuscate", "pack", "clean", "license_guard", "vue is_windows = platform.system() == "Windows" +# 插件专属依赖清单(项目根,可选文件):内置插件(src/plugins/*)专属、网关本体 +# (src/chongming/)不 import 的第三方依赖,按插件分节声明。放在插件自己的清单里 +# 而不是 config.toml [pyinstaller_build_import],避免插件依赖混进网关构建配置: +# 插件下线/改 external 进程插件时删掉对应节即可,网关配置不受影响。 +PLUGIN_DEPS_FILE = "plugins_deps.toml" + # ── 构建结果数据结构 ───────────────────────────────────────────────────────── @@ -517,16 +533,73 @@ def _stage_pyarmor_obfuscate() -> str: return "✅ 所有文件混淆成功" +# ── 打包收集清单(网关本体依赖 + 插件专属依赖) ───────────────────────────── + + +def _load_plugin_imports() -> tuple[list[str], str]: + """读取插件专属依赖清单(项目根 plugins_deps.toml,可选),返回 (模块列表, 摘要)。 + + 文件结构(节名 = src/plugins/ 下的插件名): + + imports = ["httpx"] # 多个插件共用的插件专属依赖 + [doubao] + imports = ["openai", ...] # 单插件专属 + + 文件不存在 = 无插件专属依赖(其他项目复用本 CLI 时不受影响),返回 ([], "")。 + 配置写错(imports 不是字符串列表、顶层出现非表非 imports 的键)直接抛错: + 漏收依赖打包期不报错、运行期加载插件才 ImportError,静默放过的排查成本高得多。 + """ + path = find_project_root() / PLUGIN_DEPS_FILE + if not path.exists(): + return [], "" + + with open(path, "rb") as f: + data = tomllib.load(f) + + modules: list[str] = [] + notes: list[str] = [] + + def collect(section: str, entry) -> None: + if not isinstance(entry, list) or not all(isinstance(m, str) for m in entry): + raise ValueError( + f"{PLUGIN_DEPS_FILE} 的 [{section}] imports 必须是字符串列表" + ) + modules.extend(entry) + if entry: + notes.append(f"{section}:{len(entry)}") + + for name, value in data.items(): + if name == "imports": + collect("共用", value) + elif isinstance(value, dict): + collect(name, value.get("imports")) + else: + raise ValueError( + f"{PLUGIN_DEPS_FILE} 顶层 [{name}] 只能是共用 imports 列表或 [插件名] 表" + ) + return modules, "、".join(notes) + + +def _collect_build_imports(config: Config) -> tuple[list[str], str]: + """打包收集清单 = 网关本体依赖(config.toml)+ 插件专属依赖(plugins_deps.toml)。 + + 去重且保持顺序(网关本体在前);摘要空串 = 未声明插件专属依赖。 + """ + base = config.get("pyinstaller_build_import", {}) + items = list(base.get("base", [])) + items.extend(base.get("windows", []) if is_windows else base.get("linux", [])) + plugin_items, summary = _load_plugin_imports() + return list(dict.fromkeys([*items, *plugin_items])), summary + + # ── 阶段 4: PyInstaller 打包 ──────────────────────────────────────────────── def _stage_pyinstaller(config: Config) -> tuple[str, str]: """执行 PyInstaller 打包,返回 (状态消息, 可执行文件路径)""" - base_imports = config.get("pyinstaller_build_import", {}) - build_imports = list(base_imports.get("base", [])) - build_imports.extend( - base_imports.get("windows", []) if is_windows else base_imports.get("linux", []) - ) + build_imports, plugin_summary = _collect_build_imports(config) + if plugin_summary: + print(f" 📎 并入插件专属依赖 {PLUGIN_DEPS_FILE}({plugin_summary})") icon = Path("../../public/chongming.ico") cmd = [ @@ -581,10 +654,9 @@ def _stage_pyinstaller(config: Config) -> tuple[str, str]: def _stage_nuitka(config: Config) -> tuple[str, str]: """执行 Nuitka 打包,返回 (状态消息, 可执行文件路径)""" - base_imports = config.get("pyinstaller_build_import", {}).get("base", []) - extra = config.get("pyinstaller_build_import", {}).get( - "windows" if is_windows else "linux", [] - ) + build_imports, plugin_summary = _collect_build_imports(config) + if plugin_summary: + print(f" 📎 并入插件专属依赖 {PLUGIN_DEPS_FILE}({plugin_summary})") cmd = [ sys.executable, @@ -609,7 +681,7 @@ def _stage_nuitka(config: Config) -> tuple[str, str]: if shutil.which("upx"): cmd.append("--enable-plugin=upx") - for mod in list(base_imports) + extra: + for mod in build_imports: cmd.append(f"--include-module={mod}") if is_windows: diff --git a/uv.lock b/uv.lock index 3b17528..168b41e 100644 --- a/uv.lock +++ b/uv.lock @@ -238,7 +238,7 @@ wheels = [ [[package]] name = "chongming-cli" -version = "0.3.2" +version = "0.3.4" source = { editable = "." } dependencies = [ { name = "chongming-config" },