- Makefile:新增 验收数据库(生成→检查→库层全量,前置校验隔离库连接串)、数据库分片、浏览器测试三个入口; 格式写入 末尾就地重新生成;数据库测试 与 验收数据库 统一排除 浏览器/网络;快检 不再静默跳过类型门; 前端旅程 预检补齐四个必需变量;pytest 目标改用仓内解释器(uv run 在嵌套检出会解析到外层环境)。 - 工具/验证锁.py:验证会话持共享锁,写入口用 --执行 在独占锁内落盘,生成/格式写入/索引生成走同一闸门。 - 工具/并行数据库测试.py:按文件分片并行,缺连接串在采集前拒绝。 - 工具/构建编排.py、环境预检.py、构建资源包.py、维护索引.py 与上述口径对齐。 - .gitignore / CI / README / AGENTS:忽略构建产物、CI 与 Makefile 单一口径、README 按实际实现陈述、AGENTS 补日常入口与验证纪律。
134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
"""生成可审发布目录:固定轮子、带哈希依赖、工作台和部署清单;不发布或迁移。"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from zipfile import ZipFile
|
|
|
|
from 构建编排 import 执行构建
|
|
|
|
根 = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def 运行(args):
|
|
subprocess.run(args, cwd=根, env={**os.environ, "PYTHONPATH": str(根 / "src")}, check=True)
|
|
|
|
|
|
def 构建发布(output: Path, *, 生成=True):
|
|
output = output.resolve()
|
|
if output.exists():
|
|
raise ValueError("发布目录已存在;使用新目录保留已有版本")
|
|
# 生成参数为旧调用者保留;共享入口始终按内容和产物存在性判断复用。
|
|
报告 = 执行构建(轮子=True)
|
|
路径 = 报告.get("wheel")
|
|
if not isinstance(路径, str):
|
|
raise ValueError("增量构建未返回本批轮子")
|
|
wheel = Path(路径).resolve()
|
|
if not wheel.is_relative_to(根.resolve()) or wheel.suffix != ".whl" or not wheel.is_file():
|
|
raise ValueError("本批轮子不存在或不在仓库构建范围内")
|
|
运行([sys.executable, "工具/维护索引.py", "--检查"])
|
|
运行(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
"from muse.资源加载 import 核对资源; "
|
|
"from muse.基础设施.系统运行 import 运行代码哈希; "
|
|
"assert 核对资源()['代码哈希'] == 运行代码哈希(), '资源与当前代码不匹配,先重新构建'",
|
|
]
|
|
)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
with tempfile.TemporaryDirectory(prefix=".muse-release-", dir=output.parent) as temporary:
|
|
stage = Path(temporary)
|
|
(stage / "wheels").mkdir()
|
|
本批轮子 = stage / "wheels" / wheel.name
|
|
shutil.copyfile(wheel, 本批轮子)
|
|
运行(
|
|
[
|
|
"uv",
|
|
"export",
|
|
"--frozen",
|
|
"--no-dev",
|
|
"--no-emit-project",
|
|
"--no-header",
|
|
"--output-file",
|
|
str(stage / "requirements.txt"),
|
|
]
|
|
)
|
|
wheels = list((stage / "wheels").glob("*.whl"))
|
|
if len(wheels) != 1:
|
|
raise ValueError("发布目录必须对应一个明确轮子")
|
|
with ZipFile(wheels[0]) as archive:
|
|
names = archive.namelist()
|
|
if len(names) != len(set(names)) or any(
|
|
part in {".git", "tests", ".agents.local", ".env"}
|
|
for name in names
|
|
for part in Path(name).parts
|
|
):
|
|
raise ValueError("轮子含重复文件或发布范围之外的内容")
|
|
manifest = json.loads(archive.read("muse/资源/清单.json"))
|
|
for name in [
|
|
"Dockerfile",
|
|
"compose.yaml",
|
|
"compose.test.yaml",
|
|
"启动.sh",
|
|
"停止.sh",
|
|
"应用.example.toml",
|
|
"部署.example.env",
|
|
"Pi/package.json",
|
|
"Pi/package-lock.json",
|
|
]:
|
|
target = stage / name
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(根 / "部署" / name, target)
|
|
config = stage / "应用.example.toml"
|
|
config.write_text(config.read_text().replace("<固定资源构建身份>", manifest["构建身份"]))
|
|
for name in ["启动.sh", "停止.sh"]:
|
|
(stage / name).chmod(0o755)
|
|
shutil.copyfile(根 / "数据库/初始化/用途角色.sql", stage / "用途角色.sql")
|
|
payloads = [wheels[0], stage / "requirements.txt"]
|
|
(stage / "SHA256SUMS").write_text(
|
|
"".join(
|
|
f"{hashlib.sha256(p.read_bytes()).hexdigest()} {p.relative_to(stage).as_posix()}\n"
|
|
for p in payloads
|
|
)
|
|
)
|
|
files = {
|
|
p.relative_to(stage).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
|
|
for p in sorted(stage.rglob("*"))
|
|
if p.is_file()
|
|
}
|
|
release = {
|
|
"schema_version": 1,
|
|
"version": manifest["代码版本"],
|
|
"build_id": manifest["构建身份"],
|
|
"code_hash": manifest["代码哈希"],
|
|
"resource_release": manifest["发布身份"],
|
|
"files": files,
|
|
}
|
|
release["release_id"] = hashlib.sha256(
|
|
json.dumps(release, ensure_ascii=False, sort_keys=True).encode()
|
|
).hexdigest()
|
|
(stage / "发布清单.json").write_text(
|
|
json.dumps(release, ensure_ascii=False, indent=2) + "\n",
|
|
)
|
|
# 原子替换仅作用于本次新目录;此前已检查不可覆盖历史发布。
|
|
stage.rename(output)
|
|
return release
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--输出", required=True, type=Path)
|
|
parser.add_argument(
|
|
"--已生成", action="store_true", help="兼容参数;统一增量入口自动复用产物,仍校验资源和索引"
|
|
)
|
|
parser.add_argument("--缓存根", type=Path, help="显式发布缓存根;不接管普通目录或备份")
|
|
args = parser.parse_args()
|
|
print(json.dumps(构建发布(args.输出, 生成=not args.已生成), ensure_ascii=False))
|