#!/usr/bin/env python # # Pre-build script: gzip webui/index.html into a PROGMEM C header so the # webconfig portal can serve the page straight from flash with # Content-Encoding: gzip. The generated header is .gitignored; this script # regenerates it whenever the source page (or this script) content changes. # # Freshness is decided by a content hash of the build inputs (not timestamps): # a generated header written with a skewed/future mtime would otherwise mask a # later source edit and embed stale UI. gzip output is deterministic (mtime=0), # so an unchanged source produces an unchanged header. # # The generator is idempotent and cheap: when the inputs are unchanged it only # hashes two small files and returns, so running it from esp32_base on every # ESP32 build is negligible even for targets that don't compile the portal. # # Output: src/helpers/esp32/WebConfigHtml.h # WEBCONFIG_HTML_GZ[] - gzipped page (PROGMEM) # WEBCONFIG_HTML_GZ_LEN - byte length # WEBCONFIG_HTML_ETAG - quoted strong ETag (sha256 prefix of the gz body) import gzip import hashlib import os import sys Import("env") # noqa: F821 SOURCE = os.path.join("webui", "index.html") OUTPUT = os.path.join("src", "helpers", "esp32", "WebConfigHtml.h") # __file__ is not defined inside PIO/SCons-executed extra_scripts SCRIPT = os.path.join("scripts", "generate_webconfig_html.py") HASH_MARKER = "// build-inputs-sha256: " def status(msg): sys.stderr.write("WebConfig HTML: %s\n" % msg) def content_hash(): # Hash the source page and this generator so any change to either forces a # regenerate, independent of file timestamps. h = hashlib.sha256() with open(SOURCE, "rb") as f: h.update(f.read()) if os.path.isfile(SCRIPT): h.update(b"\0") with open(SCRIPT, "rb") as f: h.update(f.read()) return h.hexdigest() def stored_hash(): try: with open(OUTPUT, "r") as f: for line in f: if line.startswith(HASH_MARKER): return line[len(HASH_MARKER):].strip() if line.startswith("#"): # reached the C preprocessor lines break except OSError: return None return None def main(): if not os.path.isfile(SOURCE): status("ERROR: %s not found" % SOURCE) sys.exit(2) src_hash = content_hash() if os.path.isfile(OUTPUT) and stored_hash() == src_hash: return with open(SOURCE, "rb") as f: raw = f.read() # mtime=0 keeps the gzip output (and therefore the ETag) deterministic gz = gzip.compress(raw, compresslevel=9, mtime=0) etag = hashlib.sha256(gz).hexdigest()[:16] lines = [] lines.append("// Auto-generated by scripts/generate_webconfig_html.py from %s" % SOURCE.replace(os.sep, "/")) lines.append("// DO NOT EDIT - edit webui/index.html instead.") lines.append("%s%s" % (HASH_MARKER, src_hash)) lines.append("#pragma once") lines.append("#include ") lines.append("#include ") lines.append("") lines.append("const uint32_t WEBCONFIG_HTML_GZ_LEN = %d;" % len(gz)) lines.append('const char WEBCONFIG_HTML_ETAG[] = "\\"%s\\"";' % etag) lines.append("const uint8_t WEBCONFIG_HTML_GZ[] PROGMEM = {") for i in range(0, len(gz), 16): chunk = gz[i:i + 16] lines.append(" " + "".join("0x%02x," % b for b in chunk)) lines.append("};") lines.append("") os.makedirs(os.path.dirname(OUTPUT), exist_ok=True) with open(OUTPUT, "w") as f: f.write("\n".join(lines)) status("%s -> %s (%d bytes raw, %d bytes gzipped)" % (SOURCE, OUTPUT, len(raw), len(gz))) main()