mirror of
https://github.com/esphome/esphome.git
synced 2026-05-20 17:52:00 +08:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
@@ -242,6 +242,10 @@ PROJECT_MAX_LENGTH = 127
|
||||
# Max board/model string length (must fit in single-byte varint for proto encoding)
|
||||
BOARD_MAX_LENGTH = 127
|
||||
|
||||
# Keep in sync with ESPHOME_COMMENT_SIZE_MAX in esphome/core/application.h
|
||||
# (C++ side includes the null terminator).
|
||||
COMMENT_MAX_LEN = 255
|
||||
|
||||
AREA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(Area),
|
||||
@@ -275,7 +279,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN)
|
||||
),
|
||||
cv.Optional(CONF_AREA): validate_area_config,
|
||||
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),
|
||||
cv.Optional(CONF_COMMENT): cv.All(
|
||||
cv.string, cv.ByteLength(max=COMMENT_MAX_LEN)
|
||||
),
|
||||
cv.Required(CONF_BUILD_PATH): cv.string,
|
||||
cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
|
||||
{
|
||||
|
||||
+76
-25
@@ -22,7 +22,6 @@ from esphome.helpers import (
|
||||
read_file,
|
||||
rmtree,
|
||||
walk_files,
|
||||
write_file,
|
||||
write_file_if_changed,
|
||||
)
|
||||
from esphome.storage_json import StorageJSON, storage_path
|
||||
@@ -171,6 +170,7 @@ VERSION_H_FORMAT = """\
|
||||
DEFINES_H_TARGET = "esphome/core/defines.h"
|
||||
VERSION_H_TARGET = "esphome/core/version.h"
|
||||
BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h"
|
||||
BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp"
|
||||
ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h"
|
||||
ESPHOME_README_TXT = """
|
||||
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
|
||||
@@ -209,13 +209,22 @@ def copy_src_tree():
|
||||
|
||||
source_files_copy = source_files_map.copy()
|
||||
ignore_targets = [
|
||||
Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET, BUILD_INFO_DATA_H_TARGET)
|
||||
Path(x)
|
||||
for x in (
|
||||
DEFINES_H_TARGET,
|
||||
VERSION_H_TARGET,
|
||||
BUILD_INFO_DATA_H_TARGET,
|
||||
BUILD_INFO_DATA_CPP_TARGET,
|
||||
)
|
||||
]
|
||||
for t in ignore_targets:
|
||||
source_files_copy.pop(t, None)
|
||||
|
||||
# Files to exclude from sources_changed tracking (generated files)
|
||||
generated_files = {Path("esphome/core/build_info_data.h")}
|
||||
generated_files = {
|
||||
Path("esphome/core/build_info_data.h"),
|
||||
Path("esphome/core/build_info_data.cpp"),
|
||||
}
|
||||
|
||||
sources_changed = False
|
||||
for fname in walk_files(CORE.relative_src_path("esphome")):
|
||||
@@ -268,12 +277,15 @@ def copy_src_tree():
|
||||
build_info_data_h_path = CORE.relative_src_path(
|
||||
"esphome", "core", "build_info_data.h"
|
||||
)
|
||||
build_info_data_cpp_path = CORE.relative_src_path(
|
||||
"esphome", "core", "build_info_data.cpp"
|
||||
)
|
||||
build_info_json_path = CORE.relative_build_path("build_info.json")
|
||||
config_hash, build_time, build_time_str, comment = get_build_info()
|
||||
|
||||
# Defensively force a rebuild if the build_info files don't exist, or if
|
||||
# there was a config change which didn't actually cause a source change
|
||||
if not build_info_data_h_path.exists():
|
||||
if not build_info_data_h_path.exists() or not build_info_data_cpp_path.exists():
|
||||
sources_changed = True
|
||||
else:
|
||||
try:
|
||||
@@ -288,13 +300,19 @@ def copy_src_tree():
|
||||
|
||||
# Write build_info header and JSON metadata
|
||||
if sources_changed:
|
||||
write_file(
|
||||
# write_file_if_changed avoids bumping mtime on identical content,
|
||||
# which is what makes the stable header actually isolate metadata churn.
|
||||
write_file_if_changed(
|
||||
build_info_data_h_path,
|
||||
generate_build_info_data_h(
|
||||
generate_build_info_data_h(),
|
||||
)
|
||||
write_file_if_changed(
|
||||
build_info_data_cpp_path,
|
||||
generate_build_info_data_cpp(
|
||||
config_hash, build_time, build_time_str, comment
|
||||
),
|
||||
)
|
||||
write_file(
|
||||
write_file_if_changed(
|
||||
build_info_json_path,
|
||||
json.dumps(
|
||||
{
|
||||
@@ -345,27 +363,60 @@ def get_build_info() -> tuple[int, int, str, str]:
|
||||
return config_hash, build_time, build_time_str, comment
|
||||
|
||||
|
||||
def generate_build_info_data_h(
|
||||
config_hash: int, build_time: int, build_time_str: str, comment: str
|
||||
) -> str:
|
||||
"""Generate build_info_data.h header with config hash, build time, and comment."""
|
||||
# cpp_string_escape returns '"escaped"', slice off the quotes since template has them
|
||||
escaped_comment = cpp_string_escape(comment)[1:-1]
|
||||
# +1 for null terminator
|
||||
comment_size = len(comment) + 1
|
||||
return f"""#pragma once
|
||||
// Auto-generated build_info data
|
||||
#define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT
|
||||
#define ESPHOME_BUILD_TIME {build_time} // NOLINT
|
||||
#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT
|
||||
def generate_build_info_data_h() -> str:
|
||||
"""Generate stable declarations for build info provided by generated C++."""
|
||||
return """#pragma once
|
||||
// Auto-generated build_info declarations
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}";
|
||||
static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}";
|
||||
#else
|
||||
static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}";
|
||||
static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}";
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
extern const uint32_t ESPHOME_CONFIG_HASH;
|
||||
extern const time_t ESPHOME_BUILD_TIME;
|
||||
extern const size_t ESPHOME_COMMENT_SIZE;
|
||||
#ifdef USE_ESP8266
|
||||
extern const char ESPHOME_BUILD_TIME_STR[] PROGMEM;
|
||||
extern const char ESPHOME_COMMENT_STR[] PROGMEM;
|
||||
#else
|
||||
extern const char ESPHOME_BUILD_TIME_STR[];
|
||||
extern const char ESPHOME_COMMENT_STR[];
|
||||
#endif
|
||||
} // namespace esphome
|
||||
"""
|
||||
|
||||
|
||||
def generate_build_info_data_cpp(
|
||||
config_hash: int, build_time: int, build_time_str: str, comment: str
|
||||
) -> str:
|
||||
"""Generate build_info_data.cpp with config hash, build time, and comment."""
|
||||
from esphome.core.config import COMMENT_MAX_LEN
|
||||
|
||||
# Defense-in-depth clamp; errors="ignore" drops a partial trailing UTF-8
|
||||
# sequence so the literal never decodes to a truncated codepoint.
|
||||
encoded = comment.encode("utf-8")[:COMMENT_MAX_LEN]
|
||||
comment = encoded.decode("utf-8", errors="ignore")
|
||||
# cpp_string_escape wraps in quotes; strip them since the template has them.
|
||||
escaped_comment = cpp_string_escape(comment)[1:-1]
|
||||
comment_size = len(comment.encode("utf-8")) + 1 # +1 for NUL
|
||||
return f"""// Auto-generated build_info data
|
||||
#include "esphome/core/build_info_data.h"
|
||||
|
||||
namespace esphome {{
|
||||
const uint32_t ESPHOME_CONFIG_HASH = 0x{config_hash:08x}U; // NOLINT
|
||||
const time_t ESPHOME_BUILD_TIME = {build_time}; // NOLINT
|
||||
const size_t ESPHOME_COMMENT_SIZE = {comment_size}; // NOLINT
|
||||
#ifdef USE_ESP8266
|
||||
const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}";
|
||||
const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}";
|
||||
#else
|
||||
const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}";
|
||||
const char ESPHOME_COMMENT_STR[] = "{escaped_comment}";
|
||||
#endif
|
||||
}} // namespace esphome
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -32,6 +33,7 @@ from esphome.writer import (
|
||||
clean_build,
|
||||
clean_cmake_cache,
|
||||
copy_src_tree,
|
||||
generate_build_info_data_cpp,
|
||||
generate_build_info_data_h,
|
||||
get_build_info,
|
||||
storage_should_clean,
|
||||
@@ -1617,49 +1619,62 @@ def test_get_build_info_build_time_str_format(
|
||||
|
||||
def test_generate_build_info_data_h_format() -> None:
|
||||
"""Test generate_build_info_data_h produces correct header content."""
|
||||
config_hash = 0x12345678
|
||||
build_time = 1700000000
|
||||
build_time_str = "2023-11-14 22:13:20 +0000"
|
||||
comment = "Test comment"
|
||||
|
||||
result = generate_build_info_data_h(
|
||||
config_hash, build_time, build_time_str, comment
|
||||
)
|
||||
result = generate_build_info_data_h()
|
||||
|
||||
assert "#pragma once" in result
|
||||
assert "#define ESPHOME_CONFIG_HASH 0x12345678U" in result
|
||||
assert "#define ESPHOME_BUILD_TIME 1700000000" in result
|
||||
assert "#define ESPHOME_COMMENT_SIZE 13" in result # len("Test comment") + 1
|
||||
assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result
|
||||
assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result
|
||||
assert "extern const uint32_t ESPHOME_CONFIG_HASH;" in result
|
||||
assert "extern const time_t ESPHOME_BUILD_TIME;" in result
|
||||
assert "extern const size_t ESPHOME_COMMENT_SIZE;" in result
|
||||
assert "extern const char ESPHOME_BUILD_TIME_STR[]" in result
|
||||
assert "extern const char ESPHOME_COMMENT_STR[]" in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_h_esp8266_progmem() -> None:
|
||||
"""Test generate_build_info_data_h includes PROGMEM for ESP8266."""
|
||||
result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test", "comment")
|
||||
result = generate_build_info_data_h()
|
||||
|
||||
# Should have ESP8266 PROGMEM conditional
|
||||
assert "#ifdef USE_ESP8266" in result
|
||||
assert "#include <pgmspace.h>" in result
|
||||
assert "PROGMEM" in result
|
||||
# Both build time and comment should have PROGMEM versions
|
||||
|
||||
|
||||
def test_generate_build_info_data_cpp_format() -> None:
|
||||
"""Test generate_build_info_data_cpp produces correct data definitions."""
|
||||
result = generate_build_info_data_cpp(
|
||||
0x12345678, 1700000000, "2023-11-14 22:13:20 +0000", "Test comment"
|
||||
)
|
||||
|
||||
assert '#include "esphome/core/build_info_data.h"' in result
|
||||
assert "const uint32_t ESPHOME_CONFIG_HASH = 0x12345678U;" in result
|
||||
assert "const time_t ESPHOME_BUILD_TIME = 1700000000;" in result
|
||||
assert "const size_t ESPHOME_COMMENT_SIZE = 13;" in result
|
||||
assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result
|
||||
assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_cpp_esp8266_progmem() -> None:
|
||||
"""Test generate_build_info_data_cpp includes PROGMEM definitions."""
|
||||
result = generate_build_info_data_cpp(0xABCDEF01, 1700000000, "test", "comment")
|
||||
|
||||
assert "#ifdef USE_ESP8266" in result
|
||||
assert 'ESPHOME_BUILD_TIME_STR[] PROGMEM = "test"' in result
|
||||
assert 'ESPHOME_COMMENT_STR[] PROGMEM = "comment"' in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_h_hash_formatting() -> None:
|
||||
"""Test generate_build_info_data_h formats hash with leading zeros."""
|
||||
def test_generate_build_info_data_cpp_hash_formatting() -> None:
|
||||
"""Test generate_build_info_data_cpp formats hash with leading zeros."""
|
||||
# Test with small hash value that needs leading zeros
|
||||
result = generate_build_info_data_h(0x00000001, 0, "test", "")
|
||||
assert "#define ESPHOME_CONFIG_HASH 0x00000001U" in result
|
||||
result = generate_build_info_data_cpp(0x00000001, 0, "test", "")
|
||||
assert "const uint32_t ESPHOME_CONFIG_HASH = 0x00000001U;" in result
|
||||
|
||||
# Test with larger hash value
|
||||
result = generate_build_info_data_h(0xFFFFFFFF, 0, "test", "")
|
||||
assert "#define ESPHOME_CONFIG_HASH 0xffffffffU" in result
|
||||
result = generate_build_info_data_cpp(0xFFFFFFFF, 0, "test", "")
|
||||
assert "const uint32_t ESPHOME_CONFIG_HASH = 0xffffffffU;" in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_h_comment_escaping() -> None:
|
||||
r"""Test generate_build_info_data_h properly escapes special characters in comment.
|
||||
def test_generate_build_info_data_cpp_comment_escaping() -> None:
|
||||
r"""Test generate_build_info_data_cpp properly escapes special characters in comment.
|
||||
|
||||
Uses cpp_string_escape which outputs octal escapes for special characters:
|
||||
- backslash (ASCII 92) -> \134
|
||||
@@ -1667,26 +1682,52 @@ def test_generate_build_info_data_h_comment_escaping() -> None:
|
||||
- newline (ASCII 10) -> \012
|
||||
"""
|
||||
# Test backslash escaping (ASCII 92 = octal 134)
|
||||
result = generate_build_info_data_h(0, 0, "test", "backslash\\here")
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "backslash\\here")
|
||||
assert 'ESPHOME_COMMENT_STR[] = "backslash\\134here"' in result
|
||||
|
||||
# Test quote escaping (ASCII 34 = octal 042)
|
||||
result = generate_build_info_data_h(0, 0, "test", 'has "quotes"')
|
||||
result = generate_build_info_data_cpp(0, 0, "test", 'has "quotes"')
|
||||
assert 'ESPHOME_COMMENT_STR[] = "has \\042quotes\\042"' in result
|
||||
|
||||
# Test newline escaping (ASCII 10 = octal 012)
|
||||
result = generate_build_info_data_h(0, 0, "test", "line1\nline2")
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "line1\nline2")
|
||||
assert 'ESPHOME_COMMENT_STR[] = "line1\\012line2"' in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_h_empty_comment() -> None:
|
||||
"""Test generate_build_info_data_h handles empty comment."""
|
||||
result = generate_build_info_data_h(0, 0, "test", "")
|
||||
def test_generate_build_info_data_cpp_empty_comment() -> None:
|
||||
"""Test generate_build_info_data_cpp handles empty comment."""
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "")
|
||||
|
||||
assert "#define ESPHOME_COMMENT_SIZE 1" in result # Just null terminator
|
||||
assert "const size_t ESPHOME_COMMENT_SIZE = 1;" in result # Just null terminator
|
||||
assert 'ESPHOME_COMMENT_STR[] = ""' in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_cpp_comment_size_counts_utf8_bytes() -> None:
|
||||
"""Comment size is in encoded UTF-8 bytes, not characters."""
|
||||
# "héllo" = 6 UTF-8 bytes + NUL.
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "héllo")
|
||||
assert "const size_t ESPHOME_COMMENT_SIZE = 7;" in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_cpp_comment_clamped_to_buffer() -> None:
|
||||
"""Generator clamps at byte level and never truncates mid-codepoint."""
|
||||
# 100 thermometer-with-VS-16 sequences = 700 bytes, past the 256 buffer.
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "🌡️" * 100)
|
||||
|
||||
match = re.search(r"ESPHOME_COMMENT_SIZE = (\d+);", result)
|
||||
assert match is not None
|
||||
size = int(match.group(1))
|
||||
assert 1 < size <= 256
|
||||
|
||||
lit_match = re.search(r'ESPHOME_COMMENT_STR\[\] = "([^"]*)"', result)
|
||||
assert lit_match is not None
|
||||
raw = re.sub(
|
||||
r"\\([0-7]{3})", lambda m: chr(int(m.group(1), 8)), lit_match.group(1)
|
||||
).encode("latin-1")
|
||||
raw.decode("utf-8") # raises if truncation left a partial UTF-8 sequence
|
||||
assert len(raw) == size - 1
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
@patch("esphome.writer.iter_components")
|
||||
@patch("esphome.writer.walk_files")
|
||||
@@ -1760,15 +1801,21 @@ def test_copy_src_tree_writes_build_info_files(
|
||||
):
|
||||
copy_src_tree()
|
||||
|
||||
# Verify build_info_data.h was written
|
||||
# Verify build_info_data.h declarations and build_info_data.cpp values were written
|
||||
build_info_h_path = esphome_core_path / "build_info_data.h"
|
||||
assert build_info_h_path.exists()
|
||||
build_info_h_content = build_info_h_path.read_text()
|
||||
assert "#define ESPHOME_CONFIG_HASH 0xdeadbeefU" in build_info_h_content
|
||||
assert "#define ESPHOME_BUILD_TIME" in build_info_h_content
|
||||
assert "extern const uint32_t ESPHOME_CONFIG_HASH;" in build_info_h_content
|
||||
assert "ESPHOME_BUILD_TIME_STR" in build_info_h_content
|
||||
assert "#define ESPHOME_COMMENT_SIZE" in build_info_h_content
|
||||
assert "extern const size_t ESPHOME_COMMENT_SIZE;" in build_info_h_content
|
||||
assert "ESPHOME_COMMENT_STR" in build_info_h_content
|
||||
build_info_cpp_path = esphome_core_path / "build_info_data.cpp"
|
||||
assert build_info_cpp_path.exists()
|
||||
build_info_cpp_content = build_info_cpp_path.read_text()
|
||||
assert "const uint32_t ESPHOME_CONFIG_HASH = 0xdeadbeefU;" in build_info_cpp_content
|
||||
assert "const time_t ESPHOME_BUILD_TIME" in build_info_cpp_content
|
||||
assert "const size_t ESPHOME_COMMENT_SIZE" in build_info_cpp_content
|
||||
assert "ESPHOME_COMMENT_STR" in build_info_cpp_content
|
||||
|
||||
# Verify build_info.json was written
|
||||
build_info_json_path = build_path / "build_info.json"
|
||||
@@ -1835,7 +1882,9 @@ def test_copy_src_tree_detects_config_hash_change(
|
||||
|
||||
# Verify build_info files were updated due to config_hash change
|
||||
assert build_info_h_path.exists()
|
||||
new_content = build_info_h_path.read_text()
|
||||
build_info_cpp_path = esphome_core_path / "build_info_data.cpp"
|
||||
assert build_info_cpp_path.exists()
|
||||
new_content = build_info_cpp_path.read_text()
|
||||
assert "0xdeadbeef" in new_content.lower()
|
||||
|
||||
new_json = json.loads(build_info_json_path.read_text())
|
||||
|
||||
Reference in New Issue
Block a user