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:
2 files changed
+113
-8
No files matched your search
@@ -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]:
|
||||
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user