202279c6d9
新增 Core、Engine、Runtime、SQLite、Provider、MCP、Skill、Codex、CLI 与 DAG crate 补齐 OpenAI endpoint 配置、Provider 实例/协议路由和统一工具权限边界 加入持久化、lease、checkpoint、reconciliation、审批恢复与消息历史回归 加入独立 workspace CI、依赖边界、能力集和 Fake Agent 测试脚本 同步建设计划、TODO、架构、测试与验收文档
180 lines
7.0 KiB
Bash
Executable File
180 lines
7.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
# 只做本地发布前预检:验证 package 元数据、workspace 内部依赖版本和
|
||
# `cargo package --list` 文件边界。它不访问 registry、不上传 crate,也不把
|
||
# 尚未发布的内部依赖伪装成已经可安装的包。
|
||
|
||
usage() {
|
||
cat <<'EOF'
|
||
用法:
|
||
./scripts/check-package-manifests.sh [workspace/Cargo.toml]
|
||
|
||
检查内容:
|
||
- 所有 workspace package 都有版本、Rust edition、最低 Rust 版本、license 和 description;
|
||
- path dependency 只能指向 workspace 内部,并且带明确版本要求;
|
||
- 每个 package 的 cargo package 文件清单不包含 target、.git、数据库或环境文件。
|
||
|
||
该检查使用 `cargo package --offline --list --no-verify`,只验证本地 manifest
|
||
和打包边界;未显式设置 `CARGO_TARGET_DIR` 时,中间产物会放在
|
||
`~/data/tmp`(可用 `AGENT_PACKAGE_TMPDIR` 覆盖)并在退出时清理。依赖其它内部
|
||
crate 的完整 package 校验仍需目标 registry 先按依赖顺序发布这些 crate。
|
||
EOF
|
||
}
|
||
|
||
if (($# == 1)) && [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||
usage
|
||
exit 0
|
||
fi
|
||
|
||
if (($# > 1)); then
|
||
usage >&2
|
||
exit 2
|
||
fi
|
||
|
||
caller_pwd="$(pwd -P)"
|
||
script_path="${BASH_SOURCE[0]}"
|
||
case "$script_path" in
|
||
/*) ;;
|
||
*) script_path="$caller_pwd/$script_path" ;;
|
||
esac
|
||
script_dir="$(cd -- "$(dirname -- "$script_path")" && pwd -P)"
|
||
script_workspace_root="$(cd -- "$script_dir/.." && pwd -P)"
|
||
|
||
manifest_input="${1:-}"
|
||
if [[ -z "$manifest_input" ]]; then
|
||
workspace_manifest="$script_workspace_root/Cargo.toml"
|
||
elif [[ "$manifest_input" == /* ]]; then
|
||
workspace_manifest="$manifest_input"
|
||
elif [[ -f "$caller_pwd/$manifest_input" ]]; then
|
||
workspace_manifest="$caller_pwd/$manifest_input"
|
||
else
|
||
workspace_manifest="$script_workspace_root/$manifest_input"
|
||
fi
|
||
|
||
manifest_dir="$(cd -- "$(dirname -- "$workspace_manifest")" && pwd -P)"
|
||
workspace_manifest="$manifest_dir/$(basename -- "$workspace_manifest")"
|
||
workspace_root="$manifest_dir"
|
||
|
||
# `cargo package --list` 仍可能为 workspace 生成 target/package 中间文件。
|
||
# 未显式指定 CARGO_TARGET_DIR 时,把它放到本轮临时目录并在退出时清理,
|
||
# 避免 manifest 预检污染仓库;调用方显式指定时则保留其生命周期和位置。
|
||
package_target_dir_owned=0
|
||
if [[ -z "${CARGO_TARGET_DIR:-}" ]]; then
|
||
package_tmp_parent="${AGENT_PACKAGE_TMPDIR:-${TMPDIR:-${HOME:?HOME 未设置}/data/tmp}}"
|
||
mkdir -p -- "$package_tmp_parent"
|
||
package_target_dir="$(mktemp -d "${package_tmp_parent%/}/agent-package-manifests.XXXXXX")"
|
||
export CARGO_TARGET_DIR="$package_target_dir"
|
||
package_target_dir_owned=1
|
||
fi
|
||
|
||
cleanup_package_target() {
|
||
if (( package_target_dir_owned == 1 )) && [[ -n "${package_target_dir:-}" ]] &&
|
||
[[ -d "$package_target_dir" ]]; then
|
||
rm -r -- "$package_target_dir"
|
||
fi
|
||
}
|
||
trap cleanup_package_target EXIT
|
||
|
||
[[ -r "$workspace_manifest" ]] || {
|
||
echo "workspace manifest 不存在或不可读:$workspace_manifest" >&2
|
||
exit 1
|
||
}
|
||
|
||
grep -Eq '^[[:space:]]*\[workspace\][[:space:]]*$' "$workspace_manifest" || {
|
||
echo "Cargo.toml 不是 workspace manifest:$workspace_manifest" >&2
|
||
exit 1
|
||
}
|
||
|
||
metadata="$(cargo metadata --offline --locked --manifest-path "$workspace_manifest" \
|
||
--format-version 1 --no-deps)"
|
||
|
||
# Python 负责解析 Cargo metadata,避免用 grep 解析 TOML/JSON 时误判依赖。
|
||
# 使用命令替换而不是 process substitution,确保 Python 校验失败时不会被
|
||
# `mapfile` 吞掉退出码,也不会拿着不完整的名称列表继续打包检查。
|
||
package_names_text="$(printf '%s\n' "$metadata" | \
|
||
WORKSPACE_ROOT="$workspace_root" python3 -c '
|
||
import json
|
||
import os
|
||
import pathlib
|
||
import sys
|
||
|
||
root = pathlib.Path(os.environ["WORKSPACE_ROOT"]).resolve()
|
||
metadata = json.load(sys.stdin)
|
||
packages = metadata.get("packages", [])
|
||
workspace_ids = set(metadata.get("workspace_members", []))
|
||
workspace_packages = [p for p in packages if p.get("id") in workspace_ids]
|
||
if not workspace_packages:
|
||
raise SystemExit("workspace 没有可检查的 package")
|
||
if len(workspace_packages) != len(workspace_ids):
|
||
raise SystemExit("workspace_members 与 package metadata 数量不一致")
|
||
|
||
names = {p.get("name") for p in workspace_packages}
|
||
if len(names) != len(workspace_packages):
|
||
raise SystemExit("workspace package 名称重复")
|
||
|
||
for package in sorted(workspace_packages, key=lambda item: item["name"]):
|
||
name = package.get("name", "")
|
||
if not name.strip():
|
||
raise SystemExit("workspace package 缺少名称")
|
||
manifest = pathlib.Path(package["manifest_path"]).resolve()
|
||
if root not in manifest.parents or manifest == root / "Cargo.toml":
|
||
raise SystemExit(f"package manifest 不在 crates/ 下:{manifest}")
|
||
for field in ("version", "edition", "rust_version", "license", "description"):
|
||
if not str(package.get(field) or "").strip():
|
||
raise SystemExit(f"{name} 缺少 package.{field}")
|
||
if package["edition"] != "2024":
|
||
raise SystemExit(f"{name} 使用了非 Rust 2024 edition:{package['edition']}")
|
||
if package["rust_version"] != "1.96":
|
||
raise SystemExit(f"{name} 的 rust-version 不是 1.96:{package['rust_version']}")
|
||
for dependency in package.get("dependencies", []):
|
||
if dependency.get("source") is not None:
|
||
continue
|
||
dependency_name = dependency.get("name", "")
|
||
dependency_path = dependency.get("path")
|
||
if dependency_name not in names:
|
||
raise SystemExit(
|
||
f"path dependency 指向 workspace 外部:{name} -> {dependency_name}"
|
||
)
|
||
if not dependency_path or root not in pathlib.Path(dependency_path).resolve().parents:
|
||
raise SystemExit(
|
||
f"path dependency 路径越过 workspace 边界:{name} -> {dependency_name}"
|
||
)
|
||
requirement = str(dependency.get("req") or "").strip()
|
||
if requirement in ("", "*", "latest"):
|
||
raise SystemExit(
|
||
f"path dependency 缺少明确版本要求:{name} -> {dependency_name}"
|
||
)
|
||
print(name)
|
||
')" || exit 1
|
||
|
||
[[ -n "$package_names_text" ]] || {
|
||
echo "没有找到 workspace package" >&2
|
||
exit 1
|
||
}
|
||
mapfile -t package_names <<<"$package_names_text"
|
||
|
||
for package in "${package_names[@]}"; do
|
||
package_files="$(cargo package --offline --locked --manifest-path "$workspace_manifest" \
|
||
-p "$package" --allow-dirty --no-verify --list 2>/dev/null)" || {
|
||
echo "cargo package 文件清单失败:$package" >&2
|
||
exit 1
|
||
}
|
||
grep -Fxq 'Cargo.toml' <<<"$package_files" || {
|
||
echo "package 缺少 Cargo.toml:$package" >&2
|
||
exit 1
|
||
}
|
||
grep -Eq '(^|/)src/' <<<"$package_files" || {
|
||
echo "package 缺少 src 文件:$package" >&2
|
||
exit 1
|
||
}
|
||
if grep -Eiq '(^|/)(\.git|target|\.env|.*\.db([.-]|$)|.*\.sqlite([.-]|$))' \
|
||
<<<"$package_files"; then
|
||
echo "package 清单包含不应发布的文件:$package" >&2
|
||
exit 1
|
||
fi
|
||
echo "package manifest passed: $package"
|
||
done
|
||
|
||
echo "package manifest check passed: ${#package_names[@]} packages"
|