Files
msime-windows/scripts/apply_version.py
T
tokisaki galaxy 43452f1ff9 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.
2026-10-19 16:00:06 +08:00

121 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Stamp the version from version.txt into the TSF DLL version resource.
release-please owns version.txt. The VERSIONINFO block in windows/src/IME/MetasequoiaIME.rc carries the
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 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
python scripts/apply_version.py --version 1.2.3 # stamp an explicit local package version
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
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+)(?:\.(\d+))?$")
# Each pattern captures the part before the version and the part after it, so only the digits move.
SUBSTITUTIONS = (
(re.compile(r"^(\s*FILEVERSION\s+)\d+,\d+,\d+,\d+(\s*)$", re.MULTILINE), "quad"),
(re.compile(r"^(\s*PRODUCTVERSION\s+)\d+,\d+,\d+,\d+(\s*)$", re.MULTILINE), "quad"),
(re.compile(r'^(\s*VALUE\s+"FileVersion",\s*")\d+\.\d+\.\d+\.\d+("\s*)$', re.MULTILINE), "dotted"),
(re.compile(r'^(\s*VALUE\s+"ProductVersion",\s*")\d+\.\d+\.\d+\.\d+("\s*)$', re.MULTILINE), "dotted"),
)
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")
raw = VERSION_FILE.read_text(encoding="utf-8").strip()
source = "version.txt"
else:
raw = explicit_version.strip()
source = "--version"
match = SEMVER.match(raw)
if not match:
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, 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]:
replaced = 0
for pattern, shape in SUBSTITUTIONS:
value = quad if shape == "quad" else dotted
text, count = pattern.subn(lambda m: f"{m.group(1)}{value}{m.group(2)}", text)
if count != 1:
raise SystemExit(
f"error: expected exactly one match for {pattern.pattern!r} in {RESOURCE_FILE}, found {count}"
)
replaced += count
return text, replaced
def read_resource() -> str:
# newline="" keeps the CRLF endings byte for byte, so only the version digits show up in a diff.
with RESOURCE_FILE.open("r", encoding="utf-8", newline="") as stream:
return stream.read()
def write_resource(text: str) -> None:
with RESOURCE_FILE.open("w", encoding="utf-8", newline="") as stream:
stream.write(text)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--check", action="store_true", help="report drift instead of rewriting the resource")
parser.add_argument(
"--version",
metavar="MAJOR.MINOR.PATCH",
help="use an explicit version instead of reading version.txt (for local packaging)",
)
args = parser.parse_args()
version = read_version(args.version)
version_source = "--version" if args.version is not None else "version.txt"
quad, dotted = render(version)
original = read_resource()
updated, replaced = apply(original, quad, dotted)
if args.check:
if updated != original:
print(f"{RESOURCE_FILE.relative_to(REPO_ROOT)} does not match {version_source} ({dotted})")
return 1
print(f"{RESOURCE_FILE.relative_to(REPO_ROOT)} matches {version_source} ({dotted})")
return 0
if updated == original:
print(f"Already at {dotted}, nothing to do.")
return 0
write_resource(updated)
print(f"Stamped {dotted} into {RESOURCE_FILE.relative_to(REPO_ROOT)} ({replaced} fields).")
return 0
if __name__ == "__main__":
sys.exit(main())