modify:进程插件开发环境目录结构变更
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
进程插件打包器 — 将 external 进程插件打包为可部署 zip
|
||||
进程插件打包器 — 将 external 进程插件开发目录打包为可部署 zip
|
||||
|
||||
用法:
|
||||
uv run build_plugin <plugin_name> [--output <dir>]
|
||||
@@ -8,46 +8,42 @@
|
||||
uv run build_plugin demo # 打包 workers/demo → dist/demo.zip
|
||||
uv run build_plugin demo --output /tmp/pkg
|
||||
|
||||
产物布局(zip 根即容器工作目录,unzip 后直接 ./start.sh 启动):
|
||||
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>
|
||||
开发目录即产物: 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>
|
||||
|
||||
说明:
|
||||
- 插件扩展依赖声明在 workers/<name>/requirements.txt(可选,每行一个包),
|
||||
打包时自动合并进 pyproject.toml dependencies
|
||||
- 根 config.toml 仅提取 [zenoh] 节重新生成,避免把网关侧配置(如 JWT 密钥)泄露进插件容器
|
||||
- 打包自动排除 __pycache__ / *.pyc / .venv / *.egg-info / .git
|
||||
- start.sh 权限强制为 0o755(防止 git 丢执行位导致容器无法启动)
|
||||
- 新项目骨架用 `uv run gen_plugin <name> --process` 生成
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tomllib
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from ._root import find_project_root
|
||||
|
||||
NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
||||
|
||||
# chongming-worker 内网 git 源(与网关 pyproject.toml [tool.uv.sources] 保持一致)
|
||||
CHONGMING_WORKER_GIT = "http://124.71.68.6:3000/chongming/chongming-utils-worker.git"
|
||||
PYPI_MIRROR = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
|
||||
# 打包排除的路径模式(目录名 / glob)
|
||||
EXCLUDE_DIRS = {"__pycache__", ".venv", ".git"}
|
||||
EXCLUDE_SUFFIXES = (".pyc", ".pyo", ".egg-info")
|
||||
|
||||
# 项目根目录(CHONGMING_ROOT 环境变量 > CWD 向上查找,见 _root.py)
|
||||
ROOT = find_project_root()
|
||||
WORKERS_DIR = ROOT / "workers"
|
||||
PLUGINS_CONFIG_DIR = ROOT / "config" / "workers"
|
||||
ZENOH_CONFIG = ROOT / "config" / "zenoh_config.json5"
|
||||
ROOT_CONFIG = ROOT / "config.toml"
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 产物生成
|
||||
# ============================================================
|
||||
|
||||
def _rel(path: Path) -> Path | str:
|
||||
"""相对 ROOT 的路径(不在 ROOT 下时原样返回,供错误消息使用)。"""
|
||||
try:
|
||||
@@ -56,103 +52,79 @@ def _rel(path: Path) -> Path | str:
|
||||
return path
|
||||
|
||||
|
||||
def _read_plugin_conf(name: str) -> dict:
|
||||
"""读取 config/workers/<name>/config.toml 完整内容(校验含 [workers] 节)。"""
|
||||
path = PLUGINS_CONFIG_DIR / name / "config.toml"
|
||||
with open(path, "rb") as f:
|
||||
conf = tomllib.load(f)
|
||||
if not conf.get("workers"):
|
||||
raise ValueError(f"{path.relative_to(ROOT)} 缺少 [workers] 节")
|
||||
return conf
|
||||
def _check_project_layout(project_dir: Path, name: str) -> dict:
|
||||
"""
|
||||
校验插件项目结构完整(运行时必需的文件),返回插件 [workers] 节配置。
|
||||
|
||||
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] 节")
|
||||
|
||||
def _gen_root_config_toml() -> str:
|
||||
"""生成最小根 config.toml(仅 [zenoh] 节,worker 进程只读它)。"""
|
||||
with open(ROOT_CONFIG, "rb") as f:
|
||||
# 2. Zenoh 会话配置(worker 启动必需)
|
||||
if not (project_dir / "config" / "zenoh_config.json5").exists():
|
||||
raise FileNotFoundError(
|
||||
f"未找到 Zenoh 配置: {_rel(project_dir / 'config' / 'zenoh_config.json5')}"
|
||||
)
|
||||
|
||||
# 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:
|
||||
root_conf = tomllib.load(f)
|
||||
json5_path = root_conf.get("zenoh", {}).get("json5_path", "config/zenoh_config.json5")
|
||||
return f"""# 最小根配置(由 build_plugin 自动生成,仅含 worker 进程所需的 [zenoh] 节)
|
||||
[zenoh]
|
||||
json5_path = "{json5_path}"
|
||||
"""
|
||||
if not root_conf.get("zenoh"):
|
||||
raise ValueError(f"{root_conf_path} 缺少 [zenoh] 节(worker 启动必需)")
|
||||
|
||||
return workers
|
||||
|
||||
|
||||
def _gen_pyproject_toml(name: str, version: str, extra_deps: list[str]) -> str:
|
||||
"""生成插件 uv 依赖清单(chongming-worker + 插件扩展依赖)。"""
|
||||
deps = [
|
||||
"chongming-worker",
|
||||
"orjson", # chongming-worker 运行时依赖(SDK metadata 未声明,容器内需显式安装)
|
||||
*extra_deps,
|
||||
]
|
||||
deps_lines = "\n".join(f' "{d}",' for d in deps)
|
||||
return f"""# {name} 进程插件依赖清单(由 build_plugin 自动生成)
|
||||
[project]
|
||||
name = "{name}-worker"
|
||||
version = "{version}"
|
||||
description = "{name} 进程插件(external 模式,独立容器部署)"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
{deps_lines}
|
||||
]
|
||||
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())
|
||||
|
||||
[project.scripts]
|
||||
worker_serve = "chongming_worker.process:worker_serve"
|
||||
|
||||
[[tool.uv.index]]
|
||||
url = "{PYPI_MIRROR}"
|
||||
|
||||
[tool.uv.sources]
|
||||
chongming-worker = {{ git = "{CHONGMING_WORKER_GIT}" }}
|
||||
"""
|
||||
|
||||
|
||||
def _gen_start_sh(name: str) -> str:
|
||||
"""生成容器入口脚本:uv 引导依赖 + 启动 worker 进程。"""
|
||||
return f"""#!/usr/bin/env bash
|
||||
# {name} 进程插件容器入口(由 build_plugin 自动生成)
|
||||
# 要求: 镜像预装 uv(https://docs.astral.sh/uv/)
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
uv sync --no-dev
|
||||
exec uv run worker_serve --type {name} "$@"
|
||||
"""
|
||||
|
||||
|
||||
def _read_extra_deps(name: str) -> list[str]:
|
||||
"""读取 workers/<name>/requirements.txt(可选),每行一个依赖。"""
|
||||
path = WORKERS_DIR / name / "requirements.txt"
|
||||
if not path.exists():
|
||||
return []
|
||||
deps = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
dep = line.strip()
|
||||
if dep and not dep.startswith("#"):
|
||||
deps.append(dep)
|
||||
return deps
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 打包主逻辑
|
||||
# ============================================================
|
||||
|
||||
def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path:
|
||||
"""
|
||||
打包进程插件为可部署 zip,返回产物路径。
|
||||
|
||||
失败(插件/配置缺失、契约不合法)抛 FileNotFoundError / ValueError。
|
||||
失败(插件/配置缺失、结构不合法)抛 FileNotFoundError / ValueError。
|
||||
"""
|
||||
if not NAME_RE.match(name):
|
||||
raise ValueError(f"插件名不合法: '{name}'(需小写字母/数字/下划线,且以字母开头)")
|
||||
|
||||
workers_src = WORKERS_DIR / name
|
||||
if not workers_src.is_dir():
|
||||
raise FileNotFoundError(f"未找到插件代码目录: {_rel(workers_src)}/")
|
||||
project_dir = WORKERS_DIR / name
|
||||
if not project_dir.is_dir():
|
||||
raise FileNotFoundError(f"未找到插件项目目录: {_rel(project_dir)}/")
|
||||
|
||||
conf = _read_plugin_conf(name) # 缺配置在此抛 FileNotFoundError
|
||||
workers = conf["workers"]
|
||||
workers = _check_project_layout(project_dir, name)
|
||||
|
||||
plugin_type = workers.get("type", name)
|
||||
load_mode = workers.get("load_mode", "inline")
|
||||
if load_mode != "external":
|
||||
print(
|
||||
@@ -160,48 +132,11 @@ def build_plugin_package(name: str, output_dir: str | Path = "dist") -> Path:
|
||||
"独立进程将与网关内副本重复服务。进程插件请在 [workers] 节设置 "
|
||||
"load_mode = \"external\"。"
|
||||
)
|
||||
version = str(conf.get("worker", {}).get("version", "0.1.0"))
|
||||
|
||||
output = Path(output_dir)
|
||||
stage = output / name
|
||||
if stage.exists():
|
||||
shutil.rmtree(stage)
|
||||
stage.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# ---- 1. 插件代码 ----
|
||||
shutil.copytree(
|
||||
workers_src,
|
||||
stage / "workers" / name,
|
||||
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
||||
)
|
||||
|
||||
# ---- 2. 插件配置 ----
|
||||
shutil.copytree(
|
||||
PLUGINS_CONFIG_DIR / name,
|
||||
stage / "config" / "workers" / name,
|
||||
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
||||
)
|
||||
|
||||
# ---- 3. Zenoh 会话配置 ----
|
||||
if not ZENOH_CONFIG.exists():
|
||||
raise FileNotFoundError(f"未找到 Zenoh 配置: {_rel(ZENOH_CONFIG)}")
|
||||
shutil.copy(ZENOH_CONFIG, stage / "config" / "zenoh_config.json5")
|
||||
|
||||
# ---- 4. 生成最小根配置 + 依赖清单 + 启动脚本 ----
|
||||
(stage / "config.toml").write_text(_gen_root_config_toml(), encoding="utf-8")
|
||||
(stage / "pyproject.toml").write_text(
|
||||
_gen_pyproject_toml(name, version, _read_extra_deps(name)), encoding="utf-8"
|
||||
)
|
||||
start_sh = stage / "start.sh"
|
||||
start_sh.write_text(_gen_start_sh(plugin_type), encoding="utf-8")
|
||||
start_sh.chmod(0o755)
|
||||
|
||||
# ---- 5. 压缩(zip 根即容器工作目录)----
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
zip_path = output / f"{name}.zip"
|
||||
zip_path.unlink(missing_ok=True)
|
||||
shutil.make_archive(str(output / name), "zip", root_dir=stage)
|
||||
|
||||
# 暂存目录保留便于排查,也可直接作为容器工作目录
|
||||
_zip_project(project_dir, zip_path)
|
||||
return zip_path
|
||||
|
||||
|
||||
@@ -227,14 +162,8 @@ def main() -> None:
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✅ 打包完成: {zip_path} ({_format_size(zip_path)})")
|
||||
print("📦 产物布局(zip 根即容器工作目录):")
|
||||
print(f" workers/{args.name}/ 插件代码")
|
||||
print(f" config/workers/{args.name}/ 插件配置")
|
||||
print(" config/zenoh_config.json5 Zenoh 会话配置")
|
||||
print(" config.toml 最小根配置(仅 [zenoh] 节)")
|
||||
print(" pyproject.toml uv 依赖清单")
|
||||
print(" start.sh 容器入口")
|
||||
print(f"\n🚀 容器内部署: unzip {args.name}.zip -d /app && cd /app && ./start.sh")
|
||||
print(f"📦 产物 = {_rel(WORKERS_DIR / args.name)}/ 项目根的直接压缩")
|
||||
print(f"🚀 容器内部署: unzip {args.name}.zip -d /app && cd /app && ./start.sh")
|
||||
|
||||
|
||||
def build_plugin():
|
||||
|
||||
Reference in New Issue
Block a user