Files
lucky-cat/tools/pack_embedded_assets.py

64 lines
2.3 KiB
Python

#!/usr/bin/env python3
"""将素材确定性地打包为只读资源,随应用镜像一起参与 OTA。"""
from __future__ import annotations
import argparse
import struct
from pathlib import Path
MAGIC = b"TQASSET1"
# 与 platform_asset_bundle.c 的解析格式一致:小端包头及条目头,后接名称和原始字节。
HEADER = struct.Struct("<8sI")
ENTRY = struct.Struct("<HI")
ALLOWED = {"lcd": ".png", "audio_prompts": ".opus"}
def pack(source: Path) -> bytes:
# 排序固定条目顺序,保证相同输入生成相同的包;只接受当前运行时使用的两类素材。
files = sorted(path for path in source.rglob("*") if path.is_file())
if not files:
raise ValueError("asset staging directory is empty")
records: list[tuple[bytes, bytes]] = []
names: set[str] = set()
for path in files:
relative = path.relative_to(source)
if len(relative.parts) != 2 or relative.parts[0] not in ALLOWED:
raise ValueError(f"unexpected asset path: {relative}")
if path.suffix.lower() != ALLOWED[relative.parts[0]]:
raise ValueError(f"unexpected asset type: {relative}")
name = relative.as_posix()
if name in names:
raise ValueError(f"duplicate asset: {name}")
names.add(name)
encoded = name.encode("utf-8")
payload = path.read_bytes()
if not encoded or len(encoded) > 0xFFFF or not payload or len(payload) > 0xFFFFFFFF:
raise ValueError(f"invalid asset size: {name}")
records.append((encoded, payload))
output = bytearray(HEADER.pack(MAGIC, len(records)))
for name, payload in records:
output.extend(ENTRY.pack(len(name), len(payload)))
output.extend(name)
output.extend(payload)
print(f"Embedded assets: {len(records)} files, {sum(len(data) for _, data in records)} payload bytes, {len(output)} bundle bytes")
return bytes(output)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()
data = pack(args.source)
args.output.parent.mkdir(parents=True, exist_ok=True)
if not args.output.exists() or args.output.read_bytes() != data:
args.output.write_bytes(data)
if __name__ == "__main__":
main()