Files
msime-windows/scripts/apply_version.py
T
Evan Hu 03b3f5f45a ci: build and publish the installer from a release workflow
The Windows installer has been produced by hand: build the TSF DLLs and the Server locally, build the settings page, copy dictionaries out of a developer checkout, then run the msime-installer scripts against a self-signed certificate. This automates that end to end, keeping the same artifact name the releases have always carried, MetasequoiaIME_Setup_v<version>.exe.

The workflow follows the shape of MSIME-Apple's release.yml. release-please owns version.txt and opens a release PR; merging it creates a draft release, which the publish path then builds and fills in. A manual dispatch can rebuild an existing draft tag, and validates that the tag matches version.txt at an immutable commit SHA before anything is built.

Four jobs feed the packaging job: the TSF DLLs for x64 and Win32, the Server Release build, and the settings page. msime-installer needs no changes because the packaging job stages each checkout under the historical directory name its scripts already address, so Prepare-PackageFiles.ps1 and Compile-Installer.ps1 run unmodified. windows-2025 ships Inno Setup 6.7.1, above the 6.6 the scripts require.

Dictionaries are not rebuilt here. They are downloaded from a dict-* release of metasequoiaime/MSIME-Dict and checked against its SHA256SUMS.txt, with the Mozc IPAdic / ICOT / Okinawa notice placed where Prepare-PackageFiles.ps1 expects so it reaches the installed product.

Signing follows the pattern MSIME-Apple uses: sign when the certificate secrets are configured, otherwise publish with an -unsigned suffix and say in the release notes that uiAccess will not take effect. Sign-PackageBinaries-Local.ps1 mints a self-signed certificate for local testing and is never invoked here.

release-please cannot drive the VERSIONINFO block in the resource script, which spells the version both as a comma quad and as a dotted string, so scripts/apply_version.py stamps it from version.txt before the release build configures CMake. It also has a --check mode. The committed resource moves off the stale 1.0.0.1 placeholder to the current 0.0.9.

The manifest starts at 0.0.9 rather than the four-part 0.0.9.2 that semver cannot express, so the next release will be 0.0.10, which still sorts after every existing tag.
2026-09-05 16:10:33 +09:00

104 lines
3.7 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 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; only MAJOR.MINOR.PATCH are release-managed.
python scripts/apply_version.py # stamp from version.txt
python scripts/apply_version.py --check # exit 1 if the resource is out of date
"""
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 / "src" / "IME" / "MetasequoiaIME.rc"
SEMVER = re.compile(r"^(\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() -> 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 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 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")
args = parser.parse_args()
version = read_version()
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.txt ({dotted})")
return 1
print(f"{RESOURCE_FILE.relative_to(REPO_ROOT)} matches version.txt ({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())