fix(release): PE 版本读回在 Windows runner 上崩在 print 编码上(判据内部一条未降)
v0.2.4 Release run 43031591576 的 windows-x64 / windows-arm64 都红在「PE 版本资源读回」, 而 cargo build 是绿的、注解 API 上只有一句 Process completed with exit code 1. 根因:check_pe_version.py 把中文打在 stdout/stderr,本机 cp936 能编,Windows runner 是 en-US 镜像(代码页 1252),print() 直接 UnicodeEncodeError —— 判据全过也红,而且崩在 print 上, 日志里只剩 traceback。同仓 5 个 packaging 脚本已有 reconfigure(errors=backslashreplace) 守卫, Windows 槽位跑的脚本里只有这一个漏了。 - check_pe_version.py: safe_streams()(管道钉 UTF-8、终端只放宽 errors)、 每产物摘要行 flush(stderr 的判据不再跑到摘要前面)、模块 docstring 补判据清单; --self-test 增加「版本资源齐全的合成 PE 必须绿」正控 + 真子进程 cp1252/cp437 回归(红/绿两路) - release-build.yml: 读回步骤先 ls -l 落产物大小,输出 tee 后逐行转 ::error:: 注解 (下次红能看到原因,不再只有退出码) - 判据未降:仍然是读产物字节、仍然 exit 1,未改任何断言 实测:RUSTFLAGS='-C target-feature=+crt-static' 的 release 产物本机读回 exit 0; 同产物同命令在 PYTHONIOENCODING=cp1252 下,修前 exit 1 + traceback、修后 exit 0。
This commit is contained in:
1 parent
03b644fa0c
commit
47386ef2ff
2 files changed
+158
-18
No files matched your search
+136
-17
@@ -17,12 +17,29 @@ 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
|
||||
|
||||
# 什么时候退出非 0(判据清单;排查红步骤时照这张表读日志)
|
||||
|
||||
逐产物(退出码 1,每条一条 `error:`):
|
||||
|
||||
1. 文件不存在;
|
||||
2. 小于 0x100 字节,或不是可解析的 PE(MZ / PE 头不对);
|
||||
3. 找不到 `FIXEDFILEINFO` 签名 0xFEEF04BD —— **版本资源根本没进产物**;
|
||||
4. 缺结构键 `VS_VERSION_INFO` / `StringFileInfo` / `VarFileInfo` / `Translation`;
|
||||
5. 缺承诺字段名,或字段值不等于承诺值(CompanyName / ProductName / FileDescription / LegalCopyright);
|
||||
6. `OriginalFilename` 不是 `<bin>.exe`;
|
||||
7. 给了 `--expect-version` 时:四段式版本字符串不在资源里,或 `dwFileVersion` 与它不一致。
|
||||
|
||||
其它:`--self-test` 自检失败 → 1;没给产物也没给 `--self-test` → 2(打印用法)。
|
||||
输出本身**不再是**失败原因:`safe_streams()` 保证「汉字打不出来」不会把一条通过的判据变成红。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -109,14 +126,89 @@ def check(path: Path, expect_version: str | None) -> list[str]:
|
||||
|
||||
print(
|
||||
f"{path.name}: {len(data)} 字节 / subsystem={subsystem}(2=GUI 3=控制台)"
|
||||
f" / 版本资源={'有' if has_resource else '无'}"
|
||||
f" / 版本资源={'有' if has_resource else '无'}",
|
||||
# 进度行走在 stderr 的判据之前:CI 的 stdout 是块缓冲,不 flush 就会「错误先、摘要后」,读起来像两份日志。
|
||||
flush=True,
|
||||
)
|
||||
return failures
|
||||
|
||||
|
||||
def safe_streams() -> None:
|
||||
"""把 stdout/stderr 钉成「汉字打不出来也不许抛异常」。
|
||||
|
||||
CI 的 stdout 是**管道**:GitHub Actions 按 UTF-8 解码,而 Windows runner 是 en-US 镜像、默认代码页
|
||||
1252,于是 `print("ok: 2 个产物的版本资源齐全")` 会 `UnicodeEncodeError` —— 判据全过也红,而且崩在
|
||||
`print` 上,日志里只剩一段 traceback(`::error::` 注解一条都没有)。本机 cp936 能编汉字,复现不出来。
|
||||
管道里钉 UTF-8(Actions 那边正好按 UTF-8 解),终端里只放宽 `errors`(不改编码,免得本机控制台变乱码)。
|
||||
"""
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
reconfigure = getattr(stream, "reconfigure", None) # Python < 3.7 没有,那就只能用默认行为
|
||||
if reconfigure is None:
|
||||
continue
|
||||
try:
|
||||
if stream.isatty():
|
||||
reconfigure(errors="backslashreplace")
|
||||
else:
|
||||
reconfigure(encoding="utf-8", errors="backslashreplace")
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
|
||||
def synthetic_pe(stem: str, version: tuple[int, int, int, int], *, with_resource: bool = True) -> bytes:
|
||||
"""合成一个 MZ/PE 头合法的最小文件:`with_resource` 决定版本资源齐不齐。
|
||||
|
||||
两种用途:`False` 是**负控**(判据必须报错),`True` 是**正控**(判据全过 —— 否则「绿的时候能不能
|
||||
正常打印/正常退出 0」这条根本没法验)。
|
||||
"""
|
||||
pe_offset = 0x40
|
||||
body = bytearray(0x100)
|
||||
body[0:2] = b"MZ"
|
||||
struct.pack_into("<I", body, 0x3C, pe_offset)
|
||||
body[pe_offset : pe_offset + 4] = PE_MAGIC
|
||||
struct.pack_into("<H", body, pe_offset + 24 + 0x44, 2) # Subsystem = WINDOWS_GUI
|
||||
if not with_resource:
|
||||
return bytes(body)
|
||||
|
||||
ms = (version[0] << 16) | version[1]
|
||||
ls = (version[2] << 16) | version[3]
|
||||
fixed = FIXED_SIGNATURE + struct.pack("<I", 0x00010000) + struct.pack("<II", ms, ls) + struct.pack("<II", ms, ls)
|
||||
dotted = ".".join(str(part) for part in version)
|
||||
texts = [
|
||||
"VS_VERSION_INFO",
|
||||
"StringFileInfo",
|
||||
"VarFileInfo",
|
||||
"Translation",
|
||||
"CompanyName",
|
||||
EXPECTED_STRINGS["CompanyName"],
|
||||
"ProductName",
|
||||
EXPECTED_STRINGS["ProductName"],
|
||||
"FileDescription",
|
||||
EXPECTED_STRINGS["FileDescription"],
|
||||
"LegalCopyright",
|
||||
EXPECTED_STRINGS["LegalCopyright"],
|
||||
dotted,
|
||||
f"{stem}.exe",
|
||||
]
|
||||
return bytes(body) + fixed + b"".join(utf16(text) for text in texts)
|
||||
|
||||
|
||||
def spawn_with_io_encoding(artifact: Path, expect: str, io_encoding: str) -> subprocess.CompletedProcess[str]:
|
||||
"""按指定 I/O 编码起一个真子进程跑本脚本(复刻 CI 上的 Windows 管道环境)。"""
|
||||
env = {key: value for key, value in os.environ.items() if key not in ("PYTHONUTF8", "PYTHONIOENCODING")}
|
||||
env["PYTHONIOENCODING"] = io_encoding
|
||||
return subprocess.run(
|
||||
[sys.executable, str(Path(__file__).resolve()), str(artifact), "--expect-version", expect],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def self_test() -> int:
|
||||
"""口径表与 `pe_version.rs` 对拍 + 版本换算 + 「假 PE 必须红」。"""
|
||||
import os
|
||||
"""口径表与 `pe_version.rs` 对拍 + 版本换算 + 「假 PE 必须红」+ 「管道编码不许崩」。"""
|
||||
import tempfile
|
||||
|
||||
failures = 0
|
||||
@@ -137,19 +229,45 @@ def self_test() -> int:
|
||||
|
||||
# 判据本身要能变红:造一个「像 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
|
||||
with tempfile.TemporaryDirectory() as work:
|
||||
red = Path(work) / "red.exe"
|
||||
red.write_bytes(synthetic_pe("red", (0, 2, 3, 0), with_resource=False))
|
||||
problems = check(red, "0.2.3")
|
||||
if not problems:
|
||||
print("FAIL: 没有版本资源的假 PE 竟然通过了(判据失效)", file=sys.stderr)
|
||||
failures += 1
|
||||
|
||||
# 正控:同一套判据对「齐全」的合成 PE 必须绿,否则下面的「绿的时候能不能正常退出」无从验起。
|
||||
green = Path(work) / "green.exe"
|
||||
green.write_bytes(synthetic_pe("green", (0, 2, 3, 0)))
|
||||
problems = check(green, "0.2.3")
|
||||
if problems:
|
||||
print(f"FAIL: 版本资源齐全的合成 PE 竟然报错 {problems}(判据过度)", file=sys.stderr)
|
||||
failures += 1
|
||||
|
||||
# 回归(v0.2.4 Release run 43031591576 两个 Windows 槽位红在这里):CI 的 stdout 是管道,
|
||||
# Windows runner 默认代码页 1252 打不出汉字。下面两条用**真子进程 + 真管道**把那个环境复刻出来:
|
||||
# 判据全过时必须退 0(不是崩在 print 上),判据没过时必须退 1 且报错文本还在(不许被吞成 traceback)。
|
||||
for case, artifact, expect, want in (
|
||||
("cp1252 绿", green, "0.2.3", 0),
|
||||
("cp437 绿", green, "0.2.3", 0),
|
||||
("cp1252 红", red, "0.2.3", 1),
|
||||
):
|
||||
result = spawn_with_io_encoding(artifact, expect, "cp1252" if "cp1252" in case else "cp437")
|
||||
crashed = "Traceback" in result.stderr or "UnicodeEncodeError" in result.stderr
|
||||
if crashed or result.returncode != want:
|
||||
print(
|
||||
f"FAIL: {case} 时退出码 {result.returncode}(期望 {want})"
|
||||
f"{',且崩在编码上' if crashed else ''}:{result.stderr.strip()[-300:]}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
failures += 1
|
||||
if want == 0 and "ok:" not in result.stdout:
|
||||
print(f"FAIL: {case} 时 stdout 没有 ok 行:{result.stdout.strip()[-200:]}", file=sys.stderr)
|
||||
failures += 1
|
||||
if want == 1 and "error:" not in result.stderr:
|
||||
print(f"FAIL: {case} 时报错文本丢了:{result.stderr.strip()[-200:]}", file=sys.stderr)
|
||||
failures += 1
|
||||
|
||||
print("self-test:", "ok" if failures == 0 else f"{failures} 处失败")
|
||||
return 0 if failures == 0 else 1
|
||||
@@ -159,7 +277,7 @@ 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 必红)")
|
||||
parser.add_argument("--self-test", action="store_true", help="自检(口径表 + 版本换算 + 假 PE 必红/齐全必绿 + 管道编码)")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.self_test:
|
||||
@@ -185,4 +303,5 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
safe_streams()
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user