Files
Unlucky-Launcher/packaging/check_pe_version.py
T
AskaEth a5d1dec495
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
2026-11-11 21:41:38 +08:00

189 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Assert the PE **version resource is really inside the artifact** (M190).
Writing `CompanyName` into `build.rs` proves nothing — this reads the produced `.exe` bytes and
looks for the `VS_VERSIONINFO` structure (`FIXEDFILEINFO.dwSignature = 0xFEEF04BD`, key strings in
UTF-16LE) plus the exact values Windows "Properties → Details" must show:
CompanyName Unlucky Launcher Project
ProductName Unlucky Launcher
FileDescription Unlucky Launcher
LegalCopyright © 2026 Unlucky Launcher Project · Apache-2.0
FileVersion <Cargo.toml version, four parts>
It is deliberately a *byte* check, not a "did the build script run" check: a resource that got lost
in linking still fails here. `--expect-version` also compares `dwFileVersionMS/LS` with the crate
version, so "version drifted from Cargo.toml" is caught.
python3 packaging/check_pe_version.py <exe>... --expect-version 0.2.3
python3 packaging/check_pe_version.py --self-test
"""
from __future__ import annotations
import argparse
import struct
import sys
from pathlib import Path
PE_MAGIC = bytes([0x50, 0x45, 0, 0]) # "PE" + two NUL
FIXED_SIGNATURE = b"\xbd\x04\xef\xfe" # FIXEDFILEINFO.dwSignature = 0xFEEF04BD
PE_VERSION_SOURCE = Path(__file__).resolve().parent / "pe_version.rs"
# 承诺的署名(与 packaging/pe_version.rs 同一份口径;`--self-test` 解析那个文件对拍)。
EXPECTED_STRINGS = {
"CompanyName": "Unlucky Launcher Project",
"ProductName": "Unlucky Launcher",
"FileDescription": "Unlucky Launcher",
"LegalCopyright": "© 2026 Unlucky Launcher Project · Apache-2.0",
}
def utf16(text: str) -> bytes:
return text.encode("utf-16-le")
def four_part(version: str) -> tuple[int, int, int, int]:
"""`v0.2.3` / `0.2.3-beta.1` → `(0, 2, 3, 0)`(与 `pe_version.rs::four_part` 同规则)。"""
numeric = version.split("-")[0].split("+")[0].strip().lstrip("v")
parts = [0, 0, 0, 0]
for index, piece in enumerate(numeric.split(".")[:4]):
try:
parts[index] = min(int(piece), 0xFFFF)
except ValueError:
parts[index] = 0
return (parts[0], parts[1], parts[2], parts[3])
def pe_subsystem(data: bytes) -> int | None:
"""可选头里的 `Subsystem`(2 = GUI,3 = 控制台);PE 头不对时返回 None。"""
if len(data) < 0x40 or data[:2] != b"MZ":
return None
offset = struct.unpack_from("<I", data, 0x3C)[0]
if offset + 0x5C > len(data) or data[offset : offset + 4] != PE_MAGIC:
return None
return struct.unpack_from("<H", data, offset + 24 + 0x44)[0]
def check(path: Path, expect_version: str | None) -> list[str]:
"""返回没过的判据(空 = 通过)。"""
failures: list[str] = []
data = path.read_bytes()
if len(data) < 0x100:
return [f"{path}: 文件太小({len(data)} 字节),不像 PE"]
subsystem = pe_subsystem(data)
if subsystem is None:
return [f"{path}: 不是可解析的 PE(MZ / PE 头不对)"]
has_resource = FIXED_SIGNATURE in data
if not has_resource:
failures.append(f"{path}: 找不到 FIXEDFILEINFO 签名 0xFEEF04BD —— 这个 exe 里**没有版本资源**")
for key in ("VS_VERSION_INFO", "StringFileInfo", "VarFileInfo", "Translation"):
if utf16(key) not in data:
failures.append(f"{path}: 版本资源里缺结构键 `{key}`(UTF-16LE 未命中)")
for field, value in EXPECTED_STRINGS.items():
if utf16(field) not in data:
failures.append(f"{path}: 缺字段名 `{field}`")
if utf16(value) not in data:
failures.append(f"{path}: 字段 `{field}` 的值不是 `{value}`")
if utf16(f"{path.stem}.exe") not in data:
failures.append(f"{path}: `OriginalFilename` 不是 `{path.stem}.exe`")
if expect_version:
parts = four_part(expect_version)
dotted = ".".join(str(part) for part in parts)
if utf16(dotted) not in data:
failures.append(f"{path}: 版本字符串 `{dotted}` 不在资源里(与 Cargo.toml 不同源?)")
index = data.find(FIXED_SIGNATURE)
if index >= 0 and index + 16 <= len(data):
ms, ls = struct.unpack_from("<II", data, index + 8)
actual = (ms >> 16, ms & 0xFFFF, ls >> 16, ls & 0xFFFF)
if actual != parts:
failures.append(f"{path}: 数值版本 {actual} 与期望 {parts} 不一致(dwFileVersion)")
else:
failures.append(f"{path}: FIXEDFILEINFO 之后读不到 dwFileVersion")
print(
f"{path.name}: {len(data)} 字节 / subsystem={subsystem}(2=GUI 3=控制台)"
f" / 版本资源={'有' if has_resource else '无'}"
)
return failures
def self_test() -> int:
"""口径表与 `pe_version.rs` 对拍 + 版本换算 + 「假 PE 必须红」。"""
import os
import tempfile
failures = 0
source = PE_VERSION_SOURCE.read_text(encoding="utf-8")
for field, value in (
("COMPANY_NAME", EXPECTED_STRINGS["CompanyName"]),
("PRODUCT_NAME", EXPECTED_STRINGS["ProductName"]),
("COPYRIGHT", EXPECTED_STRINGS["LegalCopyright"]),
):
if f'pub const {field}: &str = "{value}";' not in source:
print(f"FAIL: pe_version.rs 里找不到 {field} = {value!r}", file=sys.stderr)
failures += 1
for case, expect in (("0.2.3", (0, 2, 3, 0)), ("v1.2.3-beta.4", (1, 2, 3, 0)), ("1.2.3.4", (1, 2, 3, 4))):
if four_part(case) != expect:
print(f"FAIL: four_part({case!r}) = {four_part(case)},期望 {expect}", file=sys.stderr)
failures += 1
# 判据本身要能变红:造一个「像 PE 但没有版本资源」的最小文件,check() 必须报错
# (否则这个脚本对任何输入都会「绿」,门禁等于没有)。
fake = bytearray(0x200)
fake[0:2] = b"MZ"
struct.pack_into("<I", fake, 0x3C, 0x40)
fake[0x40:0x44] = PE_MAGIC
struct.pack_into("<H", fake, 0x40 + 24 + 0x44, 2)
with tempfile.NamedTemporaryFile(suffix=".exe", delete=False) as handle:
handle.write(bytes(fake))
name = handle.name
problems = check(Path(name), "0.2.3")
os.unlink(name)
if not problems:
print("FAIL: 没有版本资源的假 PE 竟然通过了(判据失效)", file=sys.stderr)
failures += 1
print("self-test:", "ok" if failures == 0 else f"{failures} 处失败")
return 0 if failures == 0 else 1
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="读产物字节断言 PE 版本资源(M190)")
parser.add_argument("artifacts", nargs="*", type=Path, help="要检查的 exe")
parser.add_argument("--expect-version", help="与 Cargo.toml 同源的版本号(如 0.2.3)")
parser.add_argument("--self-test", action="store_true", help="自检(口径表 + 版本换算 + 假 PE 必红)")
args = parser.parse_args(argv)
if args.self_test:
return self_test()
if not args.artifacts:
parser.print_help()
return 2
failures: list[str] = []
for artifact in args.artifacts:
if not artifact.is_file():
failures.append(f"{artifact}: 文件不存在")
continue
failures.extend(check(artifact, args.expect_version))
for line in failures:
print(f"error: {line}", file=sys.stderr)
if failures:
print(f"FAIL:{len(failures)} 条判据没过(版本资源没进产物就是没进,构建日志不算证据)", file=sys.stderr)
return 1
print(f"ok: {len(args.artifacts)} 个产物的版本资源齐全")
return 0
if __name__ == "__main__":
raise SystemExit(main())