fix(packaging): 修复 Windows 安装与固件打包并完善客户端文案
- fix(installer): 修复低完整性产物无法写入临时目录的问题,并在 CI 打包前后检查完整性标签 - fix(firmware): 从本次 Cargo ELF 打包并同步配套镜像,修正短目录说明与 LLVM 搜索顺序 - fix(installer): 修复中文完成页和页眉遮挡,侧栏使用等比缩放的原始 Logo - style(client): 应用图标增加圆角,统一“裁判系统客户端”命名 - fix(config): 安装提示改用可选本地热点配置,排除 Wi-Fi 凭据 - docs: 润色项目说明并统一裁判系统术语 - test(packaging): 补充 Windows 完整性和固件打包回归,覆盖外部输出目录、旧镜像与重复生成 验证:Windows NSIS 构建及原生页面预览;通用与本地安装配置编译;固件 release 构建、重复打包和客户端离线校验;6 项固件打包回归与 Windows 权限回归;git diff --check。未执行实机烧录。
This commit is contained in:
+63
-22
@@ -3,9 +3,9 @@
|
||||
|
||||
流程(在 cargo build 之后):
|
||||
|
||||
make_bundle.py → bundle.json(沿用现有清单)
|
||||
package_firmware.py
|
||||
├─ 读 bundle.json / BUILD-INFO.json
|
||||
├─ 从 Cargo 产物导出 App,同步 Bootloader / 分区表
|
||||
├─ 刷新 BUILD-INFO.json,并调用 make_bundle.py 生成 bundle.json
|
||||
├─ 固定条目顺序与 mtime,生成可复现的 zip
|
||||
└─ 写 <zip>.sha256
|
||||
|
||||
@@ -32,7 +32,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -59,19 +59,31 @@ def parse_epoch(build: dict) -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def export_app_if_stale(repo: Path, firmware: Path, allow_stale: bool) -> None:
|
||||
"""编译后 App 镜像可能还没导出;优先用 espflash 从最新 ELF 重新导出。"""
|
||||
elf = repo / "target" / "xtensa-esp32s3-espidf" / "release" / "esp32"
|
||||
def resolve_elf(repo: Path, explicit: str) -> Path:
|
||||
if explicit:
|
||||
return Path(explicit).resolve()
|
||||
# Cargo metadata also respects build.target-dir in Cargo configuration.
|
||||
result = subprocess.run(
|
||||
["cargo", "metadata", "--no-deps", "--format-version", "1"],
|
||||
cwd=repo, capture_output=True, text=True, check=True,
|
||||
)
|
||||
target = Path(json.loads(result.stdout)["target_directory"])
|
||||
return target / "xtensa-esp32s3-espidf" / "release" / "esp32"
|
||||
|
||||
|
||||
def export_app(repo: Path, firmware: Path, elf: Path, allow_stale: bool) -> None:
|
||||
"""从选中的 ELF 导出 App,并同步同次构建的配套镜像。"""
|
||||
app = firmware / "esp32s3-app.bin"
|
||||
if not elf.is_file():
|
||||
return
|
||||
if app.is_file() and app.stat().st_mtime >= elf.stat().st_mtime:
|
||||
return
|
||||
if allow_stale and app.is_file():
|
||||
print(f"警告:缺少 {elf};继续打包现有镜像", file=sys.stderr)
|
||||
return
|
||||
raise SystemExit(f"固件 ELF 不存在:{elf}。请先构建,或用 --elf 指定本次构建的 ELF。")
|
||||
|
||||
if app.is_file():
|
||||
print(f"提示:{app} 早于 {elf},将尝试从最新 ELF 重新导出", file=sys.stderr)
|
||||
else:
|
||||
print(f"提示:缺少 {app},将尝试从最新 ELF 导出", file=sys.stderr)
|
||||
companions = [elf.parent / name for name in ("bootloader.bin", "partition-table.bin")]
|
||||
for source in companions:
|
||||
if not source.is_file():
|
||||
raise SystemExit(f"缺少同次构建的配套镜像:{source}")
|
||||
|
||||
espflash = repo / "target" / "tools" / "espflash" / "espflash.exe"
|
||||
if not espflash.is_file():
|
||||
@@ -82,7 +94,7 @@ def export_app_if_stale(repo: Path, firmware: Path, allow_stale: bool) -> None:
|
||||
if allow_stale:
|
||||
print(f"警告:{message};继续打包现有镜像", file=sys.stderr)
|
||||
return
|
||||
raise SystemExit(f"{message}。请先导出 target/firmware/esp32s3-app.bin,或加 --allow-stale。")
|
||||
raise SystemExit(f"{message}。请安装 espflash,或加 --allow-stale 打包现有镜像。")
|
||||
|
||||
firmware.mkdir(parents=True, exist_ok=True)
|
||||
print(f"导出 App:{elf} -> {app}")
|
||||
@@ -94,6 +106,13 @@ def export_app_if_stale(repo: Path, firmware: Path, allow_stale: bool) -> None:
|
||||
"esp32s3",
|
||||
"--format",
|
||||
"esp-idf",
|
||||
"--flash-size",
|
||||
"16mb",
|
||||
"--flash-mode",
|
||||
"dio",
|
||||
"--flash-freq",
|
||||
"40mhz",
|
||||
"--skip-update-check",
|
||||
str(elf),
|
||||
str(app),
|
||||
]
|
||||
@@ -101,28 +120,50 @@ def export_app_if_stale(repo: Path, firmware: Path, allow_stale: bool) -> None:
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"espflash save-image 失败(exit {result.returncode})")
|
||||
|
||||
for source in companions:
|
||||
destination = firmware / source.name
|
||||
if source.resolve() != destination.resolve():
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
# Do not attach a previous build's hardware/performance claims to new images.
|
||||
revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
|
||||
dirty = subprocess.check_output(
|
||||
["git", "status", "--porcelain", "--untracked-files=normal"], cwd=repo, text=True,
|
||||
).strip()
|
||||
build = {
|
||||
"packaged_at": datetime.fromtimestamp(elf.stat().st_mtime, timezone.utc).isoformat(),
|
||||
"source_revision": revision,
|
||||
"includes_local_changes": bool(dirty),
|
||||
"target": "xtensa-esp32s3-espidf",
|
||||
"app_sha256": hashlib.sha256(app.read_bytes()).hexdigest(),
|
||||
"verification": {"hardware_flashed_and_tested": False},
|
||||
}
|
||||
(firmware / "BUILD-INFO.json").write_text(
|
||||
json.dumps(build, ensure_ascii=False, indent=2) + "\n", encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="生成官方 .zip 固件包")
|
||||
parser.add_argument("--firmware", default="target/firmware", help="固件产物目录")
|
||||
parser.add_argument("--elf", default="", help="本次构建的 ELF;默认通过 cargo metadata 查找 release 产物")
|
||||
parser.add_argument("--name", default="", help="传递给 make_bundle.py 的名称")
|
||||
parser.add_argument("--version", default="", help="传递给 make_bundle.py 的版本")
|
||||
parser.add_argument("--version", default="", help="传递给 make_bundle.py 的版本;默认读取 version.json")
|
||||
parser.add_argument("--skip-manifest", action="store_true", help="不重新生成 bundle.json")
|
||||
parser.add_argument("--allow-stale", action="store_true", help="允许打包早于 ELF 的 App 镜像")
|
||||
parser.add_argument("--allow-stale", action="store_true", help="缺少 ELF 或 espflash 时,允许打包目录中的现有镜像")
|
||||
parser.add_argument("--output", default="", help="输出 zip 路径;默认 target/firmware/camfw-*.zip")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo = Path(__file__).resolve().parent.parent
|
||||
if not args.version and (repo / "version.json").is_file():
|
||||
version = json.loads((repo / "version.json").read_text(encoding="utf-8"))
|
||||
args.version = f"v{version['version']}+{version['build']}"
|
||||
firmware = Path(args.firmware)
|
||||
if not firmware.is_absolute():
|
||||
firmware = (Path.cwd() / firmware).resolve()
|
||||
if not firmware.is_dir():
|
||||
print(f"固件目录不存在:{firmware}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
export_app_if_stale(repo, firmware, args.allow_stale)
|
||||
except SystemExit as error:
|
||||
export_app(repo, firmware, resolve_elf(repo, args.elf), args.allow_stale)
|
||||
except (SystemExit, OSError, subprocess.CalledProcessError, ValueError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user