#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 版本号同步脚本:从根目录 VERSION 文件读取版本号,同步到各端配置文件。 单一数据源:agent-desktop/VERSION 同步目标: - console/src-tauri/tauri.conf.json (version 字段) - console/src-tauri/Cargo.toml (version 字段) - src/pineagents/branding.py (version 字段) - src/pineagents/config/config.py (app_version 默认值) 用法: uv run scripts/sync_version.py # 同步版本号 uv run scripts/sync_version.py --check # 仅检查是否一致,不修改 """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent VERSION_FILE = ROOT / "VERSION" def read_version() -> str: """从 VERSION 文件读取版本号。""" if not VERSION_FILE.is_file(): print(f"错误:VERSION 文件不存在: {VERSION_FILE}", file=sys.stderr) sys.exit(1) version = VERSION_FILE.read_text(encoding="utf-8").strip() if not version: print("错误:VERSION 文件为空", file=sys.stderr) sys.exit(1) return version def pep440_to_semver(version: str) -> str: """将 PEP 440 版本号转换为 semver 格式(Tauri 要求)。 转换规则: 0.0.1b3 -> 0.0.1-beta.3 0.0.1a1 -> 0.0.1-alpha.1 0.0.1rc1 -> 0.0.1-rc.1 0.0.1 -> 0.0.1 0.0.1.post1 -> 0.0.1-post.1 0.0.1.dev1 -> 0.0.1-dev.1 """ # 已经是 semver 格式(包含 -),直接返回 if "-" in version: return version # 匹配预发布标签:a/b/rc + 数字 match = re.match(r"^(\d+\.\d+\.\d+)(a|b|rc|alpha|beta)(\d+)$", version, re.IGNORECASE) if match: base, label, num = match.groups() label = label.lower() if label == "a": label = "alpha" elif label == "b": label = "beta" return f"{base}-{label}.{num}" # 匹配 .postN / .devN match = re.match(r"^(\d+\.\d+\.\d+)\.(post|dev)(\d+)$", version, re.IGNORECASE) if match: base, label, num = match.groups() return f"{base}-{label.lower()}.{num}" # 纯版本号,直接返回 return version def sync_tauri_conf(version: str, check_only: bool = False) -> bool: """同步版本号到 tauri.conf.json。 注意:Tauri 要求 version 字段必须是 semver 格式, 因此这里将 PEP 440 格式转换为 semver 格式后再写入。 例如:0.0.1b3 -> 0.0.1-beta.3 """ path = ROOT / "console" / "src-tauri" / "tauri.conf.json" if not path.is_file(): print(f"跳过:文件不存在 {path}") return True semver_version = pep440_to_semver(version) with open(path, "r", encoding="utf-8") as f: data = json.load(f) old_version = data.get("version", "") if old_version == semver_version: print(f" ✓ tauri.conf.json 已是 {semver_version} (semver)") return True if check_only: print(f" ✗ tauri.conf.json 不一致: {old_version} → {semver_version} (semver)") return False data["version"] = semver_version with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) f.write("\n") print(f" ✓ tauri.conf.json 已更新: {old_version} → {semver_version} (semver)") return True def sync_cargo_toml(version: str, check_only: bool = False) -> bool: """同步版本号到 Cargo.toml。 注意:Cargo 要求 version 字段必须是 semver 格式, 因此这里将 PEP 440 格式转换为 semver 格式后再写入。 例如:0.0.1b3 -> 0.0.1-beta.3 """ path = ROOT / "console" / "src-tauri" / "Cargo.toml" if not path.is_file(): print(f"跳过:文件不存在 {path}") return True semver_version = pep440_to_semver(version) content = path.read_text(encoding="utf-8") # 匹配 [package] 下的 version = "x.x.x" pattern = re.compile(r'^(\s*version\s*=\s*)"[^"]*"', re.MULTILINE) match = pattern.search(content) if not match: print(f" ✗ Cargo.toml 未找到 version 字段") return False old_version = match.group(0).split('"')[1] if old_version == semver_version: print(f" ✓ Cargo.toml 已是 {semver_version} (semver)") return True if check_only: print(f" ✗ Cargo.toml 不一致: {old_version} → {semver_version} (semver)") return False new_content = pattern.sub(f'{match.group(1)}"{semver_version}"', content, count=1) path.write_text(new_content, encoding="utf-8") print(f" ✓ Cargo.toml 已更新: {old_version} → {semver_version} (semver)") return True def sync_branding_py(version: str, check_only: bool = False) -> bool: """同步版本号到 branding.py。""" path = ROOT / "src" / "pineagents" / "branding.py" if not path.is_file(): print(f"跳过:文件不存在 {path}") return True content = path.read_text(encoding="utf-8") pattern = re.compile(r'(\s*version\s*:\s*str\s*=\s*)"[^"]*"') match = pattern.search(content) if not match: print(f" ✗ branding.py 未找到 version 字段") return False old_version = match.group(0).split('"')[1] if old_version == version: print(f" ✓ branding.py 已是 {version}") return True if check_only: print(f" ✗ branding.py 不一致: {old_version} → {version}") return False new_content = pattern.sub(f'{match.group(1)}"{version}"', content, count=1) path.write_text(new_content, encoding="utf-8") print(f" ✓ branding.py 已更新: {old_version} → {version}") return True def sync_config_py(version: str, check_only: bool = False) -> bool: """同步版本号到 config.py 的 app_version 默认值。""" path = ROOT / "src" / "pineagents" / "config" / "config.py" if not path.is_file(): print(f"跳过:文件不存在 {path}") return True content = path.read_text(encoding="utf-8") # 匹配 app_version: str = Field( default="x.x.x", pattern = re.compile(r'(app_version\s*:\s*str\s*=\s*Field\(\s*default\s*=\s*)"[^"]*"') match = pattern.search(content) if not match: print(f" ✗ config.py 未找到 app_version 字段") return False old_version = match.group(0).split('"')[1] if old_version == version: print(f" ✓ config.py 已是 {version}") return True if check_only: print(f" ✗ config.py 不一致: {old_version} → {version}") return False new_content = pattern.sub(f'{match.group(1)}"{version}"', content, count=1) path.write_text(new_content, encoding="utf-8") print(f" ✓ config.py 已更新: {old_version} → {version}") return True def main(): parser = argparse.ArgumentParser(description="同步版本号到各端配置文件") parser.add_argument("--check", action="store_true", help="仅检查是否一致,不修改") args = parser.parse_args() version = read_version() print(f"{'检查' if args.check else '同步'}版本号: {version}") print(f"数据源: {VERSION_FILE}") print() results = [] results.append(sync_tauri_conf(version, args.check)) results.append(sync_cargo_toml(version, args.check)) results.append(sync_branding_py(version, args.check)) results.append(sync_config_py(version, args.check)) print() if all(results): print("✓ 所有配置文件版本号一致") sys.exit(0) else: print("✗ 部分配置文件版本号不一致" + ("(请运行 uv run scripts/sync_version.py 同步)" if args.check else "")) sys.exit(1) if __name__ == "__main__": main()