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.
121 lines
4.6 KiB
Python
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())
|