From 7910d02b716b836b9eafad055361f4c22b4a4ff7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 8 Oct 2026 05:34:34 -1000 Subject: [PATCH] [web_server] Emit the resource tables with cg.extern_progmem_array (#20300) --- esphome/components/web_server/__init__.py | 14 ++++---- .../web_server/config/include.css | 1 + .../web_server/config/include.js | 1 + .../web_server/config/includes.yaml | 13 ++++++++ .../web_server/test_progmem_resources.py | 33 +++++++++++++++++++ 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 tests/component_tests/web_server/config/include.css create mode 100644 tests/component_tests/web_server/config/include.js create mode 100644 tests/component_tests/web_server/config/includes.yaml create mode 100644 tests/component_tests/web_server/test_progmem_resources.py diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 6bc4d7ab5c..5e77c7496a 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -364,12 +364,14 @@ def add_resource_as_progmem( content_encoded = content.encode("utf-8") if compress: content_encoded = gzip.compress(content_encoded) - content_encoded_size = len(content_encoded) - bytes_as_int = ", ".join(str(x) for x in content_encoded) - uint8_t = f"constexpr uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}" - size_t = f"constexpr size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}" - cg.add_global(cg.RawExpression(uint8_t)) - cg.add_global(cg.RawExpression(size_t)) + cg.extern_progmem_array( + f"ESPHOME_WEBSERVER_{resource_name}", cg.uint8, list(content_encoded) + ) + cg.add_global( + cg.RawExpression( + f"constexpr size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {len(content_encoded)}" + ) + ) @coroutine_with_priority(CoroPriority.WEB) diff --git a/tests/component_tests/web_server/config/include.css b/tests/component_tests/web_server/config/include.css new file mode 100644 index 0000000000..991912d894 --- /dev/null +++ b/tests/component_tests/web_server/config/include.css @@ -0,0 +1 @@ +body { color: red; } diff --git a/tests/component_tests/web_server/config/include.js b/tests/component_tests/web_server/config/include.js new file mode 100644 index 0000000000..d914c6066c --- /dev/null +++ b/tests/component_tests/web_server/config/include.js @@ -0,0 +1 @@ +console.log("hi"); diff --git a/tests/component_tests/web_server/config/includes.yaml b/tests/component_tests/web_server/config/includes.yaml new file mode 100644 index 0000000000..0b80652dde --- /dev/null +++ b/tests/component_tests/web_server/config/includes.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +web_server: + css_include: include.css + js_include: include.js diff --git a/tests/component_tests/web_server/test_progmem_resources.py b/tests/component_tests/web_server/test_progmem_resources.py new file mode 100644 index 0000000000..95875c2267 --- /dev/null +++ b/tests/component_tests/web_server/test_progmem_resources.py @@ -0,0 +1,33 @@ +"""Tests for the web_server resources emitted as flash tables.""" + +from collections.abc import Callable +import gzip +from pathlib import Path +import re + + +def _table(main_cpp: str, name: str) -> bytes: + # Anchored so a static table does not match + match = re.search( + rf"^const uint8_t ESPHOME_WEBSERVER_{name}\[\] PROGMEM = \{{([^}}]*)\}};", + main_cpp, + re.MULTILINE, + ) + assert match is not None + return bytes(int(byte) for byte in match.group(1).split(",")) + + +def test_resources_are_externally_linked_tables( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """web_server.cpp reads these through extern declarations, so they must not be static.""" + main_cpp = generate_main(component_config_path("includes.yaml")) + + assert b"" in _table(main_cpp, "INDEX_HTML") + assert gzip.decompress(_table(main_cpp, "CSS_INCLUDE")) == b"body { color: red; }\n" + assert gzip.decompress(_table(main_cpp, "JS_INCLUDE")) == b'console.log("hi");\n' + css_size = len(_table(main_cpp, "CSS_INCLUDE")) + assert ( + f"constexpr size_t ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE = {css_size};" in main_cpp + )