Merge branch 'feat/releasefix-windows-pe-version-readback-r'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
cb051ca933
3 files changed
+206
-24
No files matched your search
@@ -217,19 +217,15 @@ jobs:
|
||||
# 版本号、FIXEDFILEINFO 的 dwFileVersion)全在 `packaging/check_pe_version.py`,本机可跑。
|
||||
# 只在 Windows 槽位跑:Linux/macOS 产物没有 PE 头,那边 `build.rs` 整段不执行(见
|
||||
# `packaging/pe_version.rs` 文件头)。**工具缺失也 fail**:找不到 python 就等于门禁失效。
|
||||
# 步骤体本身在 `packaging/ci-pe-version-gate.sh`:读回 + 同源取版本号 + 失败逐条转 `::error::`
|
||||
# 注解(M264 从本文件拆出,给 400 行硬门禁腾地方;判据没动,仍由检查器 exit 1)。
|
||||
- name: PE 版本资源读回(CompanyName / ProductName / 版权 / 版本号真的在产物里)
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
env:
|
||||
BUILD_DIR: ${{ inputs.build_dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PY=$(command -v python3 || command -v python)
|
||||
if [ -z "$PY" ]; then echo "::error::找不到 python,PE 版本资源门禁无法运行(门禁不降级)"; exit 1; fi
|
||||
# 版本号与 Cargo.toml **同源**:`[workspace.package] version = "x.y.z"`。
|
||||
version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)
|
||||
echo "expect-version=$version"
|
||||
"$PY" packaging/check_pe_version.py "$BUILD_DIR/unlucky-launcher-iced.exe" "$BUILD_DIR/ull.exe" --expect-version "$version"
|
||||
bash packaging/ci-pe-version-gate.sh "$BUILD_DIR"
|
||||
|
||||
- name: Windows 单文件自检(导入表不得出现 VC++ 可再发行 DLL)
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
+137
-17
@@ -17,12 +17,30 @@ 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` 与它不一致;
|
||||
③签名之后不足 16 字节、读不到 `dwFileVersion`(`FIXEDFILEINFO` 被截断)。
|
||||
|
||||
其它:`--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 +127,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 +230,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 +278,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 +304,5 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
safe_streams()
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env bash
|
||||
# PE 版本资源读回门禁(M190 判据在 CI 上的入口;M264 从 release-build.yml 拆出来)。
|
||||
#
|
||||
# 为什么是脚本而不是内联 step:`.github/workflows/release-build.yml` 撞着 400 行硬门禁
|
||||
# (`guards.yml` 的 file-size,用户硬约束,不许抬 LIMIT),而「读回两个产物 + 同源取版本号 +
|
||||
# 失败逐条转注解」内联进去必然顶上线。拆法与 `packaging/linux-smoke-docker.sh` /
|
||||
# `packaging/linux-check-docker.sh` 同一思路:workflow 只留一行调用。
|
||||
#
|
||||
# 判据一行没改,仍在 `packaging/check_pe_version.py`:
|
||||
# ①读的是**产物字节**(构建日志里打印过不算证据);②版本号与 `Cargo.toml` 同源;
|
||||
# ③任何一条判据没过就非 0 退出(含「产物不存在」「找不到 python」——工具缺失等于门禁失效,不降级)。
|
||||
# 本脚本只做「调用 + 把失败原因变成 `::error::` 注解」:v0.2.4 的 run 43031591576 两个 Windows
|
||||
# 槽位红在这里时,注解 API 上只剩一句 `Process completed with exit code 1.`,只能靠猜。
|
||||
#
|
||||
# 用法:
|
||||
# bash packaging/ci-pe-version-gate.sh [build_dir] # 默认 target/release
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_DIR="${1:-target/release}"
|
||||
|
||||
# 仓库根按脚本位置反推:workflow 在仓库根跑,但本地从别处调用时也不该依赖 cwd。
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
PY=$(command -v python3 || command -v python || true)
|
||||
if [ -z "$PY" ]; then
|
||||
echo "::error::找不到 python,PE 版本资源门禁无法运行(门禁不降级)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 先把「产物到底是哪个文件、多大」落成日志:这一步红了,第一件要排除的就是「读的不是那个文件」。
|
||||
# `|| echo` 是刻意的:让「文件不存在」的判据由检查器自己报(它的措辞比 ls 更适合当注解)。
|
||||
ls -l "$BUILD_DIR/unlucky-launcher-iced.exe" "$BUILD_DIR/ull.exe" \
|
||||
|| echo "::warning::ls 没列出产物(文件可能不存在),判据交给下面的检查器"
|
||||
|
||||
# 版本号与 `Cargo.toml` **同源**:`[workspace.package] version = "x.y.z"`。不在这里写死任何版本。
|
||||
version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)
|
||||
echo "expect-version=$version"
|
||||
|
||||
log=$(mktemp "${TMPDIR:-/tmp}/pe-version.XXXXXX.log" 2>/dev/null || true)
|
||||
if [ -n "$log" ]; then
|
||||
trap 'rm -f "$log"' EXIT INT TERM
|
||||
echo "log: $log"
|
||||
else
|
||||
# 连临时文件都建不出来时**不判红**:判据是产物的字节,不是本脚本的管道。原因看原始输出。
|
||||
echo "::warning::mktemp 失败,失败原因只能看下面的原始输出(判据不变)"
|
||||
fi
|
||||
|
||||
status=0
|
||||
"$PY" packaging/check_pe_version.py \
|
||||
"$BUILD_DIR/unlucky-launcher-iced.exe" "$BUILD_DIR/ull.exe" \
|
||||
--expect-version "$version" 2>&1 | tee "${log:-/dev/null}" || status=$?
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
if [ -n "$log" ]; then
|
||||
# 上限 30 行:正常失败是「每个产物几条判据」,异常失败(traceback)也不会被截掉开头。
|
||||
line_no=0
|
||||
while IFS= read -r line; do
|
||||
line_no=$((line_no + 1))
|
||||
if [ "$line_no" -le 30 ]; then echo "::error::PE 版本资源读回:$line"; fi
|
||||
done < "$log"
|
||||
fi
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in new issue
Block a user