Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
07e931b46This file is pinned to the selected release. Download the original to open it in your design tool.
"""The six places a version lives agree, and the images they name exist.
python firmware/toolchain/check_versions.py
A release is one version written in six files: PF_IMPROV_FW_VERSION in
net_config.h, PF_VARIANT_VERSION in each edition's overrides.h, the "current"
lines in AGENTS.md, the flasher manifest, and the /editions cards. For a long
time they were edited by hand, one at a time, and drifted: AGENTS.md said
v3.7.0 while the shelf served v3.8.0; Audio v0.4.0 shipped still believing it
was v0.3.1. release.py is the one writer now; this is the check that says so,
and it runs in CI beside the boundary checks.
It reads, never writes. Exit 1 with every disagreement listed.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
EDITIONS = ("audio", "performance")
def text(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")
def one(pattern: str, source: str, what: str, problems: list[str], flags: int = 0) -> str | None:
m = re.search(pattern, source, flags)
if not m:
problems.append(f"{what}: not found")
return None
return m.group(1)
def main() -> int:
problems: list[str] = []
core = one(r'#define PF_IMPROV_FW_VERSION "([^"]+)"', text("firmware/patternflow/net_config.h"),
"PF_IMPROV_FW_VERSION in net_config.h", problems)
core_v = f"v{core}" if core else None
editions: dict[str, str | None] = {}
for name in EDITIONS:
editions[name] = one(r'#define PF_VARIANT_VERSION\s+"([^"]+)"', text(f"firmware/bundles/{name}/overrides.h"),
f"PF_VARIANT_VERSION in bundles/{name}/overrides.h", problems)
agents = text("AGENTS.md")
agents_core = one(r"- Project: (v\d+\.\d+\.\d+) \(current, released \d{4}-\d{2}-\d{2}\)", agents,
"the 'Project: vX (current…)' line in AGENTS.md", problems)
# "Audio vA, Performance vB, Utility vC at the time of writing" - one
# clause per edition, in EDITIONS order.
agents_editions: dict[str, str | None] = {}
clause = r"(v\d+\.\d+\.\d+)"
line = ", ".join(f"{n.capitalize()} {clause}" for n in EDITIONS) + " at the time of writing"
m = re.search(line, agents)
if not m:
problems.append("the editions' 'at the time of writing' line in AGENTS.md: not found")
for i, name in enumerate(EDITIONS):
agents_editions[name] = m.group(i + 1) if m else None
manifest = json.loads(text("web/public/flash/manifest.json"))
manifest_v = manifest.get("version")
manifest_paths = [p["path"] for b in manifest.get("builds", []) for p in b.get("parts", [])]
cards = text("web/src/app/editions/editions-data.ts")
card_versions: dict[str, str | None] = {}
card_urls: dict[str, str | None] = {}
for ident in ("core", *EDITIONS):
m = re.search(rf"id: '{ident}',(.*?)(?=id: '|\Z)", cards, re.S)
if not m:
problems.append(f"editions-data.ts: no card with id '{ident}'")
continue
block = m.group(1)
card_versions[ident] = one(r"version: '(v[\d.]+)'", block, f"version in the {ident} card", problems)
card_urls[ident] = one(r"url: '([^']+)'", block, f"url in the {ident} card", problems)
# ── agreement ──
if core_v and agents_core and core_v != agents_core:
problems.append(f"core: net_config.h says {core_v}, AGENTS.md says {agents_core}")
if core_v and manifest_v and core_v != manifest_v:
problems.append(f"core: net_config.h says {core_v}, manifest.json says {manifest_v}")
if core_v and card_versions.get("core") and core_v != card_versions["core"]:
problems.append(f"core: net_config.h says {core_v}, the /editions card says {card_versions['core']}")
for path in manifest_paths:
if core_v and f"bin/core-{core_v}/" not in path:
problems.append(f"manifest.json part {path} is not under bin/core-{core_v}/")
if not (ROOT / "web/public/flash" / path).is_file():
problems.append(f"manifest.json names a file that does not exist: web/public/flash/{path}")
for name, want_agents in agents_editions.items():
v = editions.get(name)
if v and want_agents and v != want_agents:
problems.append(f"{name}: overrides.h says {v}, AGENTS.md says {want_agents}")
if v and card_versions.get(name) and v != card_versions[name]:
problems.append(f"{name}: overrides.h says {v}, the /editions card says {card_versions[name]}")
for ident, url in card_urls.items():
v = core_v if ident == "core" else editions.get(ident)
if not url or not v:
continue
folder = f"{ident}-{v}"
if f"/flash/bin/{folder}/" not in url:
problems.append(f"the {ident} card's url is not under flash/bin/{folder}/: {url}")
if not (ROOT / "web/public/flash/bin" / folder / "patternflow.ino.bin").is_file():
problems.append(f"the {ident} card names an image that is not on the shelf: web/public/flash/bin/{folder}/")
# Try-out images: frozen copies of compositions that are not on the shelf
# (the clock, the USB-MIDI build), which the features page installs for
# trying. release.py never touches them; they are named in features-data.ts
# and must exist, and they count as named below.
features = text("web/src/app/features/features-data.ts")
tryouts = set()
for path, folder in re.findall(r'url: "(/flash/bin/([^/"]+)/patternflow\.ino\.bin)"', features):
tryouts.add(folder)
if not (ROOT / "web/public" / path.lstrip("/")).is_file():
problems.append(f"features-data.ts names a try-out image that is not there: web/public{path}")
# The shelf holds only what the cards and the try-out rows name.
named = {f"core-{core_v}"} | {f"{n}-{v}" for n, v in editions.items() if v} | tryouts
on_shelf = {p.name for p in (ROOT / "web/public/flash/bin").iterdir() if p.is_dir()}
for extra in sorted(on_shelf - named):
problems.append(f"web/public/flash/bin/{extra} is on the shelf but no card, manifest or try-out row names it")
if problems:
print(f"{len(problems)} version problem(s):")
for p in problems:
print(f" - {p}")
return 1
print(f"versions agree: core {core_v}, " + ", ".join(f"{n} {v}" for n, v in editions.items())
+ " - in net_config.h, the overrides, AGENTS.md, the manifest and the /editions cards, images on the shelf"
+ (f"; try-out images: {', '.join(sorted(tryouts))}" if tryouts else ""))
return 0
if __name__ == "__main__":
sys.exit(main())