|
| 1 | +import argparse |
| 2 | +import json |
| 3 | +import re |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +import git |
| 7 | + |
| 8 | +from release_helper import PackageReleaseList |
| 9 | + |
| 10 | + |
| 11 | +REPO_ROOT = Path(__file__).resolve().parents[2] |
| 12 | +CORE_PACKAGES = ("pikascript-core", "PikaStdLib") |
| 13 | + |
| 14 | + |
| 15 | +def parse_header_version(path: Path): |
| 16 | + text = path.read_text(encoding="utf-8") |
| 17 | + values = {} |
| 18 | + for field in ("MAJOR", "MINOR", "MICRO"): |
| 19 | + match = re.search(rf"^#define\s+PIKA_VERSION_{field}\s+(\d+)\s*$", text, re.M) |
| 20 | + if match is None: |
| 21 | + raise ValueError(f"PIKA_VERSION_{field} not found in {path}") |
| 22 | + values[field] = match.group(1) |
| 23 | + return f"{values['MAJOR']}.{values['MINOR']}.{values['MICRO']}" |
| 24 | + |
| 25 | + |
| 26 | +def parse_version_config(path: Path): |
| 27 | + text = path.read_text(encoding="utf-8") |
| 28 | + values = {} |
| 29 | + for field in ("MajorVersion", "MinorVersion", "MicroVersion"): |
| 30 | + match = re.search(rf'^{field}\s*=\s*["\'](\d+)["\']\s*$', text, re.M) |
| 31 | + if match is None: |
| 32 | + raise ValueError(f"{field} not found in {path}") |
| 33 | + values[field] = match.group(1) |
| 34 | + return ( |
| 35 | + f"{values['MajorVersion']}." |
| 36 | + f"{values['MinorVersion']}." |
| 37 | + f"{values['MicroVersion']}" |
| 38 | + ) |
| 39 | + |
| 40 | + |
| 41 | +def parse_cmsis_version(path: Path): |
| 42 | + text = path.read_text(encoding="utf-8") |
| 43 | + match = re.search(r'^version\s*=\s*["\'](\d+\.\d+\.\d+)["\']\s*$', text, re.M) |
| 44 | + if match is None: |
| 45 | + raise ValueError(f"version not found in {path}") |
| 46 | + return match.group(1) |
| 47 | + |
| 48 | + |
| 49 | +def collect_checks( |
| 50 | + repo: git.Repo, |
| 51 | + expected_version=None, |
| 52 | + tag=None, |
| 53 | + require_tag_at_head=False, |
| 54 | + require_clean=False, |
| 55 | +): |
| 56 | + checks = [] |
| 57 | + errors = [] |
| 58 | + warnings = [] |
| 59 | + |
| 60 | + def record(name, actual, expected): |
| 61 | + passed = actual == expected |
| 62 | + checks.append( |
| 63 | + { |
| 64 | + "name": name, |
| 65 | + "ok": passed, |
| 66 | + "actual": actual, |
| 67 | + "expected": expected, |
| 68 | + } |
| 69 | + ) |
| 70 | + if not passed: |
| 71 | + errors.append( |
| 72 | + { |
| 73 | + "code": "version-mismatch", |
| 74 | + "check": name, |
| 75 | + "actual": actual, |
| 76 | + "expected": expected, |
| 77 | + } |
| 78 | + ) |
| 79 | + |
| 80 | + config_version = parse_version_config( |
| 81 | + REPO_ROOT / "port/linux/version_config.py" |
| 82 | + ) |
| 83 | + expected = expected_version or config_version |
| 84 | + record("version_config", config_version, expected) |
| 85 | + record("runtime_header", parse_header_version(REPO_ROOT / "src/PikaVersion.h"), expected) |
| 86 | + record( |
| 87 | + "cmsis_pack", |
| 88 | + parse_cmsis_version(REPO_ROOT / "port/cmsis-pack/pikascript/makepdsc.py"), |
| 89 | + expected, |
| 90 | + ) |
| 91 | + |
| 92 | + releases = PackageReleaseList(REPO_ROOT / "packages.toml") |
| 93 | + for package_name in CORE_PACKAGES: |
| 94 | + package = releases.findPackage(package_name) |
| 95 | + actual = package.latestVersion().version if package is not None else None |
| 96 | + record(f"package:{package_name}", actual, f"v{expected}") |
| 97 | + |
| 98 | + invalid_commits = [] |
| 99 | + for package in releases.packages: |
| 100 | + try: |
| 101 | + commit = package.latestVersion().commit |
| 102 | + repo.commit(commit) |
| 103 | + except (ValueError, git.BadName): |
| 104 | + invalid_commits.append(package.name) |
| 105 | + checks.append( |
| 106 | + { |
| 107 | + "name": "package_commits", |
| 108 | + "ok": not invalid_commits, |
| 109 | + "invalidPackages": invalid_commits, |
| 110 | + } |
| 111 | + ) |
| 112 | + if invalid_commits: |
| 113 | + errors.append( |
| 114 | + { |
| 115 | + "code": "invalid-package-commit", |
| 116 | + "packages": invalid_commits, |
| 117 | + } |
| 118 | + ) |
| 119 | + |
| 120 | + if tag is not None: |
| 121 | + expected_tag = f"v{expected}" |
| 122 | + record("tag_name", tag, expected_tag) |
| 123 | + try: |
| 124 | + tag_commit = repo.commit(tag) |
| 125 | + checks.append( |
| 126 | + { |
| 127 | + "name": "tag_exists", |
| 128 | + "ok": True, |
| 129 | + "commit": tag_commit.hexsha, |
| 130 | + } |
| 131 | + ) |
| 132 | + if require_tag_at_head: |
| 133 | + record("tag_at_head", tag_commit.hexsha, repo.head.commit.hexsha) |
| 134 | + except (ValueError, git.BadName): |
| 135 | + checks.append({"name": "tag_exists", "ok": False, "tag": tag}) |
| 136 | + errors.append({"code": "tag-not-found", "tag": tag}) |
| 137 | + |
| 138 | + dirty = repo.is_dirty(untracked_files=True) |
| 139 | + checks.append({"name": "worktree_clean", "ok": not dirty}) |
| 140 | + if dirty: |
| 141 | + problem = {"code": "dirty-worktree"} |
| 142 | + if require_clean: |
| 143 | + errors.append(problem) |
| 144 | + else: |
| 145 | + warnings.append(problem) |
| 146 | + |
| 147 | + return { |
| 148 | + "ok": not errors, |
| 149 | + "expectedVersion": expected, |
| 150 | + "head": repo.head.commit.hexsha, |
| 151 | + "checks": checks, |
| 152 | + "errors": errors, |
| 153 | + "warnings": warnings, |
| 154 | + } |
| 155 | + |
| 156 | + |
| 157 | +def main(): |
| 158 | + parser = argparse.ArgumentParser( |
| 159 | + description="Validate PikaPython release metadata and Git state." |
| 160 | + ) |
| 161 | + parser.add_argument("--expected-version") |
| 162 | + parser.add_argument("--tag") |
| 163 | + parser.add_argument("--require-tag-at-head", action="store_true") |
| 164 | + parser.add_argument("--require-clean", action="store_true") |
| 165 | + args = parser.parse_args() |
| 166 | + |
| 167 | + try: |
| 168 | + result = collect_checks( |
| 169 | + git.Repo(REPO_ROOT), |
| 170 | + expected_version=args.expected_version, |
| 171 | + tag=args.tag, |
| 172 | + require_tag_at_head=args.require_tag_at_head, |
| 173 | + require_clean=args.require_clean, |
| 174 | + ) |
| 175 | + except Exception as exc: |
| 176 | + result = { |
| 177 | + "ok": False, |
| 178 | + "errors": [{"code": "release-check-failed", "message": str(exc)}], |
| 179 | + } |
| 180 | + print(json.dumps(result, ensure_ascii=False)) |
| 181 | + raise SystemExit(0 if result["ok"] else 1) |
| 182 | + |
| 183 | + |
| 184 | +if __name__ == "__main__": |
| 185 | + main() |
0 commit comments