trainlog/tools/generate_web_assets.py
2026-09-16 10:07:18 +02:00

104 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Generate a deterministic, bounded C manifest from a Vite dist tree."""
from __future__ import annotations
import hashlib
import pathlib
import sys
MAX_ASSETS = 128
MAX_ASSET_BYTES = 4 * 1024 * 1024
MAX_TOTAL_BYTES = 16 * 1024 * 1024
MIME = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".ico": "image/x-icon",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".svg": "image/svg+xml",
".webp": "image/webp",
}
def c_string(value: str) -> str:
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
def byte_array(name: str, data: bytes) -> str:
lines = []
for offset in range(0, len(data), 12):
chunk = data[offset : offset + 12]
lines.append(" " + ", ".join(f"0x{value:02x}" for value in chunk) + ",")
return f"static const unsigned char {name}[] = {{\n" + "\n".join(lines) + "\n};\n"
def main() -> int:
if len(sys.argv) != 3:
raise SystemExit("usage: generate_web_assets.py DIST OUTPUT_C")
root = pathlib.Path(sys.argv[1]).resolve()
output = pathlib.Path(sys.argv[2])
files = sorted(path for path in root.rglob("*") if path.is_file())
if not files or len(files) > MAX_ASSETS:
raise SystemExit(f"nombre d'assets invalide: {len(files)}")
assets: list[tuple[str, str, bytes, str, bool]] = []
total = 0
for path in files:
relative = path.relative_to(root)
if any(part in ("", ".", "..") for part in relative.parts):
raise SystemExit(f"chemin d'asset invalide: {relative}")
mime = MIME.get(path.suffix.lower())
if mime is None:
raise SystemExit(f"type d'asset non autorisé: {relative}")
data = path.read_bytes()
if len(data) > MAX_ASSET_BYTES:
raise SystemExit(f"asset trop grand: {relative}")
total += len(data)
if total > MAX_TOTAL_BYTES:
raise SystemExit("taille totale des assets dépassée")
url = "/" + relative.as_posix()
etag = '"sha256-' + hashlib.sha256(data).hexdigest() + '"'
immutable = relative.parts[0] == "assets"
assets.append((url, mime, data, etag, immutable))
if not any(url == "/index.html" for url, *_ in assets):
raise SystemExit("index.html absent du manifeste")
parts = [
"/* Generated deterministically by tools/generate_web_assets.py. */\n",
'#include "web_assets.h"\n\n',
"#include <string.h>\n\n",
]
for index, (_, _, data, _, _) in enumerate(assets):
parts.append(byte_array(f"trainlog_web_data_{index}", data))
parts.append("\n")
parts.append("static const TrainlogWebAsset trainlog_web_assets[] = {\n")
for index, (url, mime, data, etag, immutable) in enumerate(assets):
parts.append(
f" {{{c_string(url)}, {c_string(mime)}, trainlog_web_data_{index}, "
f"{len(data)}U, {c_string(etag)}, {'true' if immutable else 'false'}}},\n"
)
parts.append("};\n\n")
parts.append(
"bool trainlog_web_assets_available(void) { return true; }\n\n"
"const TrainlogWebAsset *trainlog_web_asset_find(const char *path)\n"
"{\n"
" size_t index;\n"
" if (path == NULL) return NULL;\n"
" for (index = 0U; index < sizeof(trainlog_web_assets) /\n"
" sizeof(trainlog_web_assets[0]); ++index)\n"
" if (strcmp(path, trainlog_web_assets[index].path) == 0)\n"
" return &trainlog_web_assets[index];\n"
" return NULL;\n"
"}\n\n"
"const TrainlogWebAsset *trainlog_web_index_asset(void)\n"
"{\n"
" return trainlog_web_asset_find(\"/index.html\");\n"
"}\n"
)
output.write_text("".join(parts), encoding="utf-8", newline="\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())