Skip to content
This repository was archived by the owner on Sep 23, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix: add Windows binary version info
  • Loading branch information
he-yufeng committed May 11, 2026
commit d44bca92bbbd939e7ba00772685b954e6df3ebb6
15 changes: 15 additions & 0 deletions .github/workflows/ci-kimi-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,21 @@ jobs:
env:
BINARY_PATH: ${{ matrix.binary_path }}

- name: Check Windows file version metadata
if: runner.os == 'Windows'
shell: pwsh
run: |
$binary = (Resolve-Path $env:BINARY_PATH).Path
$info = (Get-Item $binary).VersionInfo
if (-not $info.FileVersion -or -not $info.ProductVersion) {
throw "Missing Windows FileVersionInfo on $binary"
}
if ($info.FileDescription -ne "Kimi Code CLI") {
throw "Unexpected FileDescription: $($info.FileDescription)"
}
env:
BINARY_PATH: ${{ matrix.binary_path }}

- name: Upload binary artifact
if: success()
uses: actions/upload-artifact@v4
Expand Down
18 changes: 18 additions & 0 deletions kimi.spec
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
# -*- mode: python ; coding: utf-8 -*-

import os
import sys
from pathlib import Path

spec_root = Path(globals().get("SPECPATH", os.getcwd())).resolve()
if str(spec_root) not in sys.path:
sys.path.insert(0, str(spec_root))

from scripts.pyinstaller_version_info import write_version_info
from kimi_cli.utils.pyinstaller import datas, hiddenimports

# Read codesign identity from environment variable (for macOS signing in CI)
Expand All @@ -10,6 +17,15 @@ codesign_identity = os.environ.get("APPLE_SIGNING_IDENTITY", None)
# Read build mode from environment variable (onedir mode for directory-based distribution)
onedir_mode = os.environ.get("PYINSTALLER_ONEDIR", "0") == "1"

version_info_file = None
if os.name == "nt":
version_info_file = str(
write_version_info(
spec_root / "pyproject.toml",
spec_root / "dist" / "kimi_version_info.txt",
)
)

a = Analysis(
["src/kimi_cli/cli/__main__.py"],
pathex=[],
Expand Down Expand Up @@ -45,6 +61,7 @@ if onedir_mode:
target_arch=None,
codesign_identity=codesign_identity,
entitlements_file=None,
version=version_info_file,
)
coll = COLLECT(
exe,
Expand Down Expand Up @@ -76,4 +93,5 @@ else:
target_arch=None,
codesign_identity=codesign_identity,
entitlements_file=None,
version=version_info_file,
)
80 changes: 80 additions & 0 deletions scripts/pyinstaller_version_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from __future__ import annotations

import argparse
import re
import tomllib
from pathlib import Path

VERSION_PART_RE = re.compile(r"^\d+$")


def read_project_version(pyproject_path: Path) -> str:
data = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
version = data.get("project", {}).get("version")
if not isinstance(version, str) or not version:
raise ValueError(f"Missing project.version in {pyproject_path}")
return version


def parse_fixed_file_version(version: str) -> tuple[int, int, int, int]:
public_version = version.split("+", 1)[0].split("-", 1)[0]
parts = public_version.split(".")
if not 1 <= len(parts) <= 4 or any(not VERSION_PART_RE.match(part) for part in parts):
raise ValueError(f"Version must start with numeric dot-separated parts: {version}")
return (*[int(part) for part in parts], *([0] * (4 - len(parts)))) # type: ignore[return-value]


def render_version_info(version: str) -> str:
fixed_version = parse_fixed_file_version(version)
fixed = ", ".join(str(part) for part in fixed_version)
return f"""# UTF-8
VSVersionInfo(
ffi=FixedFileInfo(
filevers=({fixed}),
prodvers=({fixed}),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0),
),
kids=[
StringFileInfo([
StringTable(
"040904B0",
[
StringStruct("CompanyName", "Moonshot AI"),
StringStruct("FileDescription", "Kimi Code CLI"),
StringStruct("FileVersion", "{version}"),
StringStruct("InternalName", "kimi"),
StringStruct("OriginalFilename", "kimi.exe"),
StringStruct("ProductName", "Kimi Code CLI"),
StringStruct("ProductVersion", "{version}"),
],
)
]),
VarFileInfo([VarStruct("Translation", [1033, 1200])]),
],
)
"""


def write_version_info(pyproject_path: Path, output_path: Path) -> Path:
version = read_project_version(pyproject_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(render_version_info(version), encoding="utf-8")
return output_path


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Generate PyInstaller Windows version info.")
parser.add_argument("--pyproject", type=Path, default=Path("pyproject.toml"))
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args(argv)
write_version_info(args.pyproject, args.output)
return 0


if __name__ == "__main__":
raise SystemExit(main())
30 changes: 30 additions & 0 deletions tests/test_pyinstaller_version_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pathlib import Path

from scripts.pyinstaller_version_info import (
parse_fixed_file_version,
render_version_info,
write_version_info,
)


def test_parse_fixed_file_version_pads_to_four_parts() -> None:
assert parse_fixed_file_version("1.41.0") == (1, 41, 0, 0)
assert parse_fixed_file_version("2.3.4.5") == (2, 3, 4, 5)


def test_render_version_info_uses_project_version() -> None:
text = render_version_info("1.41.0")

assert "filevers=(1, 41, 0, 0)" in text
assert 'StringStruct("FileVersion", "1.41.0")' in text
assert 'StringStruct("ProductVersion", "1.41.0")' in text
assert 'StringStruct("FileDescription", "Kimi Code CLI")' in text


def test_write_version_info_reads_pyproject(tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
output = tmp_path / "dist" / "kimi_version_info.txt"
pyproject.write_text('[project]\nname = "kimi-cli"\nversion = "1.42.0"\n', encoding="utf-8")

assert write_version_info(pyproject, output) == output
assert 'StringStruct("ProductVersion", "1.42.0")' in output.read_text(encoding="utf-8")