From 43452f1ff93e60f86aecafb16fb99e249a321d19 Mon Sep 17 00:00:00 2001 From: tokisaki galaxy Date: Mon, 19 Oct 2026 16:00:06 +0800 Subject: [PATCH] build: derive a dev version string from version.txt scripts/dev-build-version.py turns version.txt plus the head commit into 0.7.1-dev.20261019.: the -dev display string for installer names and the native settings page, and a YYMMDD stamp for the fourth VERSIONINFO field. apply_version.py accepts an optional fourth quad field to carry that stamp; releases leave it at 0, so their behaviour is unchanged. The settings page's version parser strips the -dev suffix and compares numbers, which is what keeps the update check quiet on dev installs -- no opt-out flag needed. --- scripts/apply_version.py | 21 +++++--- scripts/dev-build-version.py | 100 +++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 scripts/dev-build-version.py diff --git a/scripts/apply_version.py b/scripts/apply_version.py index e76925cb..0dd558d4 100644 --- a/scripts/apply_version.py +++ b/scripts/apply_version.py @@ -5,7 +5,8 @@ release-please owns version.txt. The VERSIONINFO block in windows/src/IME/Metase same number in two shapes, a comma-separated quad and a dotted string, so it cannot be driven by release-please's generic updater. The release workflow runs this before configuring CMake. -The fourth field of the quad stays 0; only MAJOR.MINOR.PATCH are release-managed. +The fourth field of the quad stays 0 for releases; a dev build passes an explicit +--version MAJOR.MINOR.PATCH.YYMMDD to stamp its date there. python scripts/apply_version.py # stamp from version.txt python scripts/apply_version.py --check # exit 1 if the resource is out of date @@ -23,7 +24,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] VERSION_FILE = REPO_ROOT / "version.txt" RESOURCE_FILE = REPO_ROOT / "windows" / "src" / "IME" / "MetasequoiaIME.rc" -SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") +SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?$") # Each pattern captures the part before the version and the part after it, so only the digits move. SUBSTITUTIONS = ( @@ -34,7 +35,7 @@ SUBSTITUTIONS = ( ) -def read_version(explicit_version: str | None = None) -> tuple[str, str, str]: +def read_version(explicit_version: str | None = None) -> tuple[str, str, str, str]: if explicit_version is None: if not VERSION_FILE.is_file(): raise SystemExit(f"error: {VERSION_FILE} is missing") @@ -45,13 +46,17 @@ def read_version(explicit_version: str | None = None) -> tuple[str, str, str]: source = "--version" match = SEMVER.match(raw) if not match: - raise SystemExit(f"error: {source} must hold MAJOR.MINOR.PATCH, got {raw!r}") - return match.groups() + raise SystemExit( + f"error: {source} must hold MAJOR.MINOR.PATCH[.BUILD], got {raw!r}" + ) + # The optional fourth field carries a dev build's YYMMDD stamp; releases leave it at 0. + major, minor, patch, build_field = match.groups() + return major, minor, patch, build_field or "0" -def render(version: tuple[str, str, str]) -> tuple[str, str]: - major, minor, patch = version - return f"{major},{minor},{patch},0", f"{major}.{minor}.{patch}.0" +def render(version: tuple[str, str, str, str]) -> tuple[str, str]: + major, minor, patch, build_field = version + return f"{major},{minor},{patch},{build_field}", f"{major}.{minor}.{patch}.{build_field}" def apply(text: str, quad: str, dotted: str) -> tuple[str, int]: diff --git a/scripts/dev-build-version.py b/scripts/dev-build-version.py new file mode 100644 index 00000000..df31cf65 --- /dev/null +++ b/scripts/dev-build-version.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Derive the dev-build version from version.txt. + +The release owns version.txt (release-please), and dev builds must never masquerade as one. +The dev number keeps the release base for human recognition and appends a date and the short +commit: 0.7.1-dev.20261019.7fa6fb1a. + +The strings go to the installer name and the settings page. The TSF DLL's VERSIONINFO quad has +four 16-bit fields, so the date compresses to YYMMDD in the fourth slot: 0.7.1.261019. yy > 79 +sorts above every year this product exists, and any real release leaves that slot at 0, so a dev +DLL always reads as newer than its own base and older than the next release. The settings page's +parseVersion strips the -dev suffix and compares 0.7.1, which is why the update check stays quiet. + +Windows filenames and Inno Setup accept the dots in the short hash; a detached HEAD (workflow +dispatch on a raw SHA) falls back to the first twelve hex digits. + + python scripts/dev-build-version.py # all three outputs, one per line + python scripts/dev-build-version.py --display # 0.7.1-dev.20261019.7fa6fb1a + python scripts/dev-build-version.py --base # 0.7.1 + python scripts/dev-build-version.py --date # 261019 +""" + +from __future__ import annotations + +import argparse +import datetime +import re +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +VERSION_FILE = REPO_ROOT / "version.txt" + +SEMVER = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") +SHORT_SHA = re.compile(r"^[0-9a-f]{7,40}$") + + +def read_base() -> tuple[str, str, str]: + if not VERSION_FILE.is_file(): + raise SystemExit(f"error: {VERSION_FILE} is missing") + raw = VERSION_FILE.read_text(encoding="utf-8").strip() + match = SEMVER.match(raw) + if not match: + raise SystemExit(f"error: version.txt must hold MAJOR.MINOR.PATCH, got {raw!r}") + return match.groups() + + +def read_short_sha(explicit: str | None) -> str: + if explicit: + sha = explicit.strip().lower() + if not SHORT_SHA.match(sha): + raise SystemExit(f"error: --commit must be hex digits, got {explicit!r}") + return sha[:12] + result = subprocess.run( + ["git", "rev-parse", "--short=12", "HEAD"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise SystemExit(f"error: cannot read the commit: {result.stderr.strip()}") + return result.stdout.strip() + + +def build(base: tuple[str, str, str], today: datetime.date, sha: str) -> tuple[str, str]: + major, minor, patch = base + display = f"{major}.{minor}.{patch}-dev.{today:%Y%m%d}.{sha}" + # 四段 quad 的末位放 YYMMDD:YY > 79 恒大于本产品存在的一切年份,任何正式版该位是 0, + # 所以 dev DLL 恒读作比基线新、比下一个发布旧。 + date = today.strftime("%y%m%d") + quad = f"{major}.{minor}.{patch}.{date}" + return display, date + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + output = parser.add_mutually_exclusive_group() + output.add_argument("--display", action="store_true", help="print the display version only") + output.add_argument("--base", action="store_true", help="print the base version only") + output.add_argument("--date", action="store_true", help="print the YYMMDD stamp only") + parser.add_argument("--commit", metavar="SHA", help="use this short commit instead of reading git") + args = parser.parse_args() + + base = read_base() + display, date = build(base, datetime.date.today(), read_short_sha(args.commit)) + if args.display: + print(display) + elif args.base: + print(".".join(base)) + elif args.date: + print(date) + else: + print(display) + print(date) + return 0 + + +if __name__ == "__main__": + sys.exit(main())