2026-08-19 14:37:51 +08:00
|
|
|
|
"""
|
2026-08-25 17:03:55 +08:00
|
|
|
|
进程插件打包器 — 将 external 进程插件开发目录打包为可部署 zip
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
|
|
|
|
|
用法:
|
|
|
|
|
|
uv run build_plugin <plugin_name> [--output <dir>]
|
|
|
|
|
|
|
|
|
|
|
|
示例:
|
|
|
|
|
|
uv run build_plugin demo # 打包 workers/demo → dist/demo.zip
|
|
|
|
|
|
uv run build_plugin demo --output /tmp/pkg
|
|
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
开发目录即产物: workers/<name>/ 是自包含的插件项目根(= 容器工作目录 = zip 根),
|
|
|
|
|
|
打包只做校验 + 直接压缩,不再从仓库多处装配:
|
|
|
|
|
|
|
|
|
|
|
|
workers/<name>/ 插件项目根(zip 根即容器工作目录)
|
|
|
|
|
|
├── workers/<name>/ 插件代码(模块 workers.<name>)
|
|
|
|
|
|
├── config/workers/<name>/ 插件注册 + 端点配置
|
|
|
|
|
|
├── config/zenoh_config.json5 Zenoh 会话配置
|
|
|
|
|
|
├── config.toml 最小根配置(仅 [zenoh] 节,worker 启动必需)
|
|
|
|
|
|
├── pyproject.toml uv 依赖清单(chongming-worker + 扩展依赖)
|
|
|
|
|
|
└── start.sh 容器入口: uv sync + uv run worker_serve --type <name>
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
|
|
|
|
|
说明:
|
2026-08-25 17:03:55 +08:00
|
|
|
|
- 打包自动排除 __pycache__ / *.pyc / .venv / *.egg-info / .git
|
|
|
|
|
|
- start.sh 权限强制为 0o755(防止 git 丢执行位导致容器无法启动)
|
|
|
|
|
|
- 新项目骨架用 `uv run gen_plugin <name> --process` 生成
|
2026-08-19 14:37:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
import re
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import tomllib
|
2026-08-25 17:03:55 +08:00
|
|
|
|
import zipfile
|
2026-08-19 14:37:51 +08:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from ._root import find_project_root
|
|
|
|
|
|
|
|
|
|
|
|
NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
# 打包排除的路径模式(目录名 / glob)
|
|
|
|
|
|
EXCLUDE_DIRS = {"__pycache__", ".venv", ".git"}
|
|
|
|
|
|
EXCLUDE_SUFFIXES = (".pyc", ".pyo", ".egg-info")
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
|
|
|
|
|
# 项目根目录(CHONGMING_ROOT 环境变量 > CWD 向上查找,见 _root.py)
|
|
|
|
|
|
ROOT = find_project_root()
|
|
|
|
|
|
WORKERS_DIR = ROOT / "workers"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rel(path: Path) -> Path | str:
|
|
|
|
|
|
"""相对 ROOT 的路径(不在 ROOT 下时原样返回,供错误消息使用)。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return path.relative_to(ROOT)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return path
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
def _check_project_layout(project_dir: Path, name: str) -> dict:
|
|
|
|
|
|
"""
|
|
|
|
|
|
校验插件项目结构完整(运行时必需的文件),返回插件 [workers] 节配置。
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
Raises:
|
|
|
|
|
|
FileNotFoundError / ValueError: 结构不合法
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 1. 插件注册 + 端点配置(worker 进程自读)
|
|
|
|
|
|
conf_path = project_dir / "config" / "workers" / name / "config.toml"
|
|
|
|
|
|
if not conf_path.exists():
|
|
|
|
|
|
raise FileNotFoundError(
|
|
|
|
|
|
f"未找到插件配置: {conf_path.relative_to(project_dir)}/(缺失的话先运行 "
|
|
|
|
|
|
f"`uv run gen_plugin {name} --process` 生成骨架)"
|
|
|
|
|
|
)
|
|
|
|
|
|
with open(conf_path, "rb") as f:
|
|
|
|
|
|
plugin_conf = tomllib.load(f)
|
|
|
|
|
|
workers = plugin_conf.get("workers")
|
|
|
|
|
|
if not workers:
|
|
|
|
|
|
raise ValueError(f"{conf_path} 缺少 [workers] 节")
|
|
|
|
|
|
|
|
|
|
|
|
# 2. Zenoh 会话配置(worker 启动必需)
|
|
|
|
|
|
if not (project_dir / "config" / "zenoh_config.json5").exists():
|
|
|
|
|
|
raise FileNotFoundError(
|
|
|
|
|
|
f"未找到 Zenoh 配置: {_rel(project_dir / 'config' / 'zenoh_config.json5')}"
|
|
|
|
|
|
)
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
# 3. 最小根配置(worker_serve 读取 [zenoh] 节打开会话)
|
|
|
|
|
|
root_conf_path = project_dir / "config.toml"
|
|
|
|
|
|
if not root_conf_path.exists():
|
|
|
|
|
|
raise FileNotFoundError(f"未找到最小根配置: {_rel(root_conf_path)}")
|
|
|
|
|
|
with open(root_conf_path, "rb") as f:
|
2026-08-19 14:37:51 +08:00
|
|
|
|
root_conf = tomllib.load(f)
|
2026-08-25 17:03:55 +08:00
|
|
|
|
if not root_conf.get("zenoh"):
|
|
|
|
|
|
raise ValueError(f"{root_conf_path} 缺少 [zenoh] 节(worker 启动必需)")
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
return workers
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
def _zip_project(project_dir: Path, zip_path: Path) -> None:
|
|
|
|
|
|
"""把项目根内容压缩为 zip(zip 根即容器工作目录),排除缓存/环境目录。"""
|
|
|
|
|
|
zip_path.unlink(missing_ok=True)
|
|
|
|
|
|
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
|
|
|
|
for path in sorted(project_dir.rglob("*")):
|
|
|
|
|
|
if not path.is_file():
|
|
|
|
|
|
continue
|
|
|
|
|
|
rel = path.relative_to(project_dir)
|
|
|
|
|
|
if any(part in EXCLUDE_DIRS for part in rel.parts):
|
|
|
|
|
|
continue
|
|
|
|
|
|
if any(part.endswith(EXCLUDE_SUFFIXES) for part in rel.parts):
|
|
|
|
|
|
continue
|
|
|
|
|
|
info = zipfile.ZipInfo.from_file(path, str(rel))
|
|
|
|
|
|
if rel.name == "start.sh":
|
|
|
|
|
|
info.external_attr = 0o100755 << 16 # 强制可执行,防 git 丢执行位
|
|
|
|
|
|
else:
|
|
|
|
|
|
info.external_attr = (path.stat().st_mode & 0xFFFF) << 16
|
|
|
|
|
|
zf.writestr(info, path.read_bytes())
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path:
|
|
|
|
|
|
"""
|
|
|
|
|
|
打包进程插件为可部署 zip,返回产物路径。
|
|
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
失败(插件/配置缺失、结构不合法)抛 FileNotFoundError / ValueError。
|
2026-08-19 14:37:51 +08:00
|
|
|
|
"""
|
|
|
|
|
|
if not NAME_RE.match(name):
|
|
|
|
|
|
raise ValueError(f"插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
|
|
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
project_dir = WORKERS_DIR / name
|
|
|
|
|
|
if not project_dir.is_dir():
|
|
|
|
|
|
raise FileNotFoundError(f"未找到插件项目目录: {_rel(project_dir)}/")
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
2026-08-25 17:03:55 +08:00
|
|
|
|
workers = _check_project_layout(project_dir, name)
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
|
|
|
|
|
load_mode = workers.get("load_mode", "inline")
|
|
|
|
|
|
if load_mode != "external":
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"⚠️ [{name}] load_mode = \"{load_mode}\":网关也会在进程内加载该插件,"
|
|
|
|
|
|
"独立进程将与网关内副本重复服务。进程插件请在 [workers] 节设置 "
|
|
|
|
|
|
"load_mode = \"external\"。"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
output = Path(output_dir)
|
2026-08-25 17:03:55 +08:00
|
|
|
|
output.mkdir(parents=True, exist_ok=True)
|
2026-08-19 14:37:51 +08:00
|
|
|
|
zip_path = output / f"{name}.zip"
|
2026-08-25 17:03:55 +08:00
|
|
|
|
_zip_project(project_dir, zip_path)
|
2026-08-19 14:37:51 +08:00
|
|
|
|
return zip_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_size(path: Path) -> str:
|
|
|
|
|
|
size = path.stat().st_size
|
|
|
|
|
|
for unit in ("B", "KB", "MB", "GB"):
|
|
|
|
|
|
if size < 1024:
|
|
|
|
|
|
return f"{size:.2f} {unit}"
|
|
|
|
|
|
size /= 1024.0
|
|
|
|
|
|
return f"{size:.2f} TB"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="chongming 进程插件打包器")
|
|
|
|
|
|
parser.add_argument("name", help="插件名(如 demo)")
|
|
|
|
|
|
parser.add_argument("--output", default="dist", help="产物输出目录(默认 dist)")
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
zip_path = build_plugin_package(args.name, output_dir=args.output)
|
|
|
|
|
|
except (FileNotFoundError, ValueError) as e:
|
|
|
|
|
|
print(f"❌ {e}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"✅ 打包完成: {zip_path} ({_format_size(zip_path)})")
|
2026-08-25 17:03:55 +08:00
|
|
|
|
print(f"📦 产物 = {_rel(WORKERS_DIR / args.name)}/ 项目根的直接压缩")
|
|
|
|
|
|
print(f"🚀 容器内部署: unzip {args.name}.zip -d /app && cd /app && ./start.sh")
|
2026-08-19 14:37:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_plugin():
|
|
|
|
|
|
"""CLI 入口(供 pyproject.toml [project.scripts] 调用)"""
|
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|