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.<hash>: 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.
This commit is contained in:
tokisaki galaxy committed 2026-10-19 16:00:06 +08:00
1 parent dd018f7985
commit 43452f1ff9
2 files changed
+113 -8

No files matched your search

+13 -8
View File
@@ -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]:
+100
View File
@@ -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())