simplify approach

This commit is contained in:
J. Nick Koston
2025-12-13 08:53:51 -06:00
parent 15d2d3ff96
commit 1543f56f70
18 changed files with 119 additions and 217 deletions
+15 -15
View File
@@ -519,43 +519,43 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
if rc != 0:
return rc
# Check if firmware was rebuilt and emit buildinfo + create manifest
_check_and_emit_buildinfo()
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1
def _check_and_emit_buildinfo() -> None:
"""Check if firmware was rebuilt and emit buildinfo."""
def _check_and_emit_build_info() -> None:
"""Check if firmware was rebuilt and emit build_info."""
import json
firmware_path = CORE.firmware_bin
buildinfo_json_path = CORE.relative_build_path("buildinfo.json")
build_info_json_path = CORE.relative_build_path("build_info.json")
# Check if both files exist
if not firmware_path.exists() or not buildinfo_json_path.exists():
if not firmware_path.exists() or not build_info_json_path.exists():
return
# Check if firmware is newer than buildinfo (indicating a relink occurred)
if firmware_path.stat().st_mtime <= buildinfo_json_path.stat().st_mtime:
# Check if firmware is newer than build_info (indicating a relink occurred)
if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime:
return
# Read buildinfo from JSON
# Read build_info from JSON
try:
with open(buildinfo_json_path, encoding="utf-8") as f:
buildinfo = json.load(f)
with open(build_info_json_path, encoding="utf-8") as f:
build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read buildinfo: %s", e)
_LOGGER.debug("Failed to read build_info: %s", e)
return
config_hash = buildinfo.get("config_hash")
build_time = buildinfo.get("build_time")
config_hash = build_info.get("config_hash")
build_time = build_info.get("build_time")
if config_hash is None or build_time is None:
return
# Emit buildinfo
# Emit build_info
_LOGGER.info(
"Build Info: config_hash=0x%08x build_time=%s", config_hash, build_time
)
+5 -1
View File
@@ -19,6 +19,7 @@
#endif
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/build_info.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -1472,7 +1473,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
resp.set_esphome_version(ESPHOME_VERSION_REF);
resp.set_compilation_time(App.get_compilation_time_ref());
// Stack buffer for build time string
char build_time_str[BUILD_TIME_STR_SIZE];
get_build_time_string(build_time_str);
resp.set_compilation_time(StringRef(build_time_str));
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
#if defined(USE_ESP8266) || defined(USE_ESP32)
+4 -2
View File
@@ -2,7 +2,7 @@
#ifdef USE_MQTT
#include "esphome/core/application.h"
#include "esphome/core/build_info.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/version.h"
@@ -154,7 +154,9 @@ bool MQTTComponent::send_discovery_() {
device_info[MQTT_DEVICE_MANUFACTURER] =
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
#else
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_VERSION " (" + App.get_compilation_time_ref() + ")";
char build_time_str[BUILD_TIME_STR_SIZE];
get_build_time_string(build_time_str);
device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(ESPHOME_VERSION " (%s)", build_time_str);
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
#if defined(USE_ESP8266) || defined(USE_ESP32)
device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";
+3 -2
View File
@@ -1,4 +1,5 @@
#include "sen5x.h"
#include "esphome/core/build_info.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -154,10 +155,10 @@ void SEN5XComponent::setup() {
if (this->voc_sensor_ && this->store_baseline_) {
uint32_t combined_serial =
encode_uint24(this->serial_number_[0], this->serial_number_[1], this->serial_number_[2]);
// Hash with compilation time and serial number
// Hash with build time and serial number
// This ensures the baseline storage is cleared after OTA
// Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict
uint32_t hash = fnv1_hash(App.get_compilation_time_ref() + std::to_string(combined_serial));
uint32_t hash = static_cast<uint32_t>(get_build_time()) ^ combined_serial;
this->pref_ = global_preferences->make_preference<Sen5xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
+3 -3
View File
@@ -1,5 +1,5 @@
#include "sgp30.h"
#include "esphome/core/application.h"
#include "esphome/core/build_info.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -72,10 +72,10 @@ void SGP30Component::setup() {
return;
}
// Hash with compilation time and serial number
// Hash with build time and serial number
// This ensures the baseline storage is cleared after OTA
// Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict
uint32_t hash = fnv1_hash(App.get_compilation_time_ref() + std::to_string(this->serial_number_));
uint32_t hash = static_cast<uint32_t>(get_build_time()) ^ static_cast<uint32_t>(this->serial_number_);
this->pref_ = global_preferences->make_preference<SGP30Baselines>(hash, true);
if (this->store_baseline_ && this->pref_.load(&this->baselines_storage_)) {
+3 -2
View File
@@ -1,4 +1,5 @@
#include "sgp4x.h"
#include "esphome/core/build_info.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
@@ -56,10 +57,10 @@ void SGP4xComponent::setup() {
ESP_LOGD(TAG, "Version 0x%0X", featureset);
if (this->store_baseline_) {
// Hash with compilation time and serial number
// Hash with build time and serial number
// This ensures the baseline storage is cleared after OTA
// Serial numbers are unique to each sensor, so mulitple sensors can be used without conflict
uint32_t hash = fnv1_hash(App.get_compilation_time_ref() + std::to_string(this->serial_number_));
uint32_t hash = static_cast<uint32_t>(get_build_time()) ^ static_cast<uint32_t>(this->serial_number_);
this->pref_ = global_preferences->make_preference<SGP4xBaselines>(hash, true);
if (this->pref_.load(&this->voc_baselines_storage_)) {
@@ -1,6 +1,6 @@
#include "version_text_sensor.h"
#include "esphome/core/build_info.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "esphome/core/version.h"
#include "esphome/core/helpers.h"
@@ -13,7 +13,9 @@ void VersionTextSensor::setup() {
if (this->hide_timestamp_) {
this->publish_state(ESPHOME_VERSION);
} else {
this->publish_state(str_sprintf(ESPHOME_VERSION " %s", App.get_compilation_time_ref().c_str()));
char build_time_str[BUILD_TIME_STR_SIZE];
get_build_time_string(build_time_str);
this->publish_state(str_sprintf(ESPHOME_VERSION " %s", build_time_str));
}
}
float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; }
+2 -1
View File
@@ -2,6 +2,7 @@
#ifdef USE_WIFI
#include <cassert>
#include <cinttypes>
#include "esphome/core/build_info.h"
#ifdef USE_ESP32
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
@@ -360,7 +361,7 @@ void WiFiComponent::start() {
get_mac_address_pretty_into_buffer(mac_s));
this->last_connected_ = millis();
uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time_ref().c_str()) : 88491487UL;
uint32_t hash = this->has_sta() ? static_cast<uint32_t>(get_build_time()) : 88491487UL;
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
#ifdef USE_WIFI_FAST_CONNECT
+4 -1
View File
@@ -1,4 +1,5 @@
#include "esphome/core/application.h"
#include "esphome/core/build_info.h"
#include "esphome/core/log.h"
#include "esphome/core/version.h"
#include "esphome/core/hal.h"
@@ -191,7 +192,9 @@ void Application::loop() {
if (this->dump_config_at_ < this->components_.size()) {
if (this->dump_config_at_ == 0) {
ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", this->compilation_time_);
char build_time_str[BUILD_TIME_STR_SIZE];
get_build_time_string(build_time_str);
ESP_LOGI(TAG, "ESPHome version " ESPHOME_VERSION " compiled on %s", build_time_str);
#ifdef ESPHOME_PROJECT_NAME
ESP_LOGI(TAG, "Project " ESPHOME_PROJECT_NAME " version " ESPHOME_PROJECT_VERSION);
#endif
+1 -7
View File
@@ -101,7 +101,7 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick
class Application {
public:
void pre_setup(const std::string &name, const std::string &friendly_name, const char *comment,
const char *compilation_time, bool name_add_mac_suffix) {
bool name_add_mac_suffix) {
arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix;
if (name_add_mac_suffix) {
@@ -121,7 +121,6 @@ class Application {
this->friendly_name_ = friendly_name;
}
this->comment_ = comment;
this->compilation_time_ = compilation_time;
}
#ifdef USE_DEVICES
@@ -261,10 +260,6 @@ class Application {
bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; }
std::string get_compilation_time() const { return this->compilation_time_; }
/// Get the compilation time as StringRef (for API usage)
StringRef get_compilation_time_ref() const { return StringRef(this->compilation_time_); }
/// Get the cached time in milliseconds from when the current component started its loop execution
inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; }
@@ -478,7 +473,6 @@ class Application {
// Pointer-sized members first
Component *current_component_{nullptr};
const char *comment_{nullptr};
const char *compilation_time_{nullptr};
// std::vector (3 pointers each: begin, end, capacity)
// Partitioned vector design for looping components
+24
View File
@@ -0,0 +1,24 @@
#include "build_info.h"
#include "build_info_data.h"
#include <cstring>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
namespace esphome {
uint32_t get_config_hash() { return ESPHOME_CONFIG_HASH; }
time_t get_build_time() { return ESPHOME_BUILD_TIME; }
void get_build_time_string(std::span<char, BUILD_TIME_STR_SIZE> buffer) {
#ifdef USE_ESP8266
strncpy_P(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size());
#else
strncpy(buffer.data(), ESPHOME_BUILD_TIME_STR, buffer.size());
#endif
buffer[buffer.size() - 1] = '\0';
}
} // namespace esphome
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include <ctime>
#include <span>
namespace esphome {
/// Size of buffer required for build time string (including null terminator)
static constexpr size_t BUILD_TIME_STR_SIZE = 24;
/// Get the config hash as a 32-bit integer
uint32_t get_config_hash();
/// Get the build time as a Unix timestamp
time_t get_build_time();
/// Copy the build time string into the provided buffer
/// Buffer must be BUILD_TIME_STR_SIZE bytes (compile-time enforced)
void get_build_time_string(std::span<char, BUILD_TIME_STR_SIZE> buffer);
} // namespace esphome
+10
View File
@@ -0,0 +1,10 @@
#pragma once
// This file is not used by the runtime, instead, a version is generated during
// compilation with the actual build info values.
//
// This file is only used by static analyzers and IDEs.
#define ESPHOME_CONFIG_HASH 0x12345678U
#define ESPHOME_BUILD_TIME 1700000000
static const char ESPHOME_BUILD_TIME_STR[] = "Jan 01 2024, 00:00:00";
-82
View File
@@ -1,82 +0,0 @@
#include <cstdint>
// Build information is passed in via symbols defined in a linker script
// as that is the simplest way to include build timestamps without the
// changed timestamp itself causing a rebuild through dependencies, as
// it would if it were in a header file like version.h.
//
// It's passed in in *string* form so that it can go directly into the
// flash as .rodata instead of using precious RAM to build a date string
// from a time_t at runtime.
//
// Determining the target endianness and word size from the generation
// side is problematic, so it emits *four* sets of symbols into the
// linker script, for each of little-endian and big-endiand, 32-bit and
// 64-bit targets.
//
// The LINKERSYM macro gymnastics select the correct symbol for the
// target, named e.g. 'ESPHOME_BUILD_TIME_STR_32LE_0'.
// Not all targets have <endian.h> (e.g. LibreTiny on BK72xx).
// Use the compiler built-in macros but defensively default to
// little-endian and 32-bit.
#if !defined(__BYTE_ORDER__) || __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
#define BO LE
#else
#define BO BE
#endif
#if defined(__SIZEOF_POINTER__) && __SIZEOF_POINTER__ == 8
#define WS 64 // NOLINT
#else
#define WS 32 // NOLINT
#define USE_32BIT
#endif
// If you have to ask, you don't want to know...
#define LINKERSYM2(name, ws, bo, us, num) ESPHOME_##name##_##ws##bo##us##num
#define LINKERSYM1(name, ws, bo, us, num) LINKERSYM2(name, ws, bo, us, num)
#define LINKERSYM(name, num) LINKERSYM1(name, WS, BO, _, num)
extern "C" {
extern const char ESPHOME_BUILD_TIME[];
extern const char LINKERSYM(CONFIG_HASH_STR, 0)[];
extern const char LINKERSYM(CONFIG_HASH_STR, 1)[];
extern const char LINKERSYM(BUILD_TIME_STR, 0)[];
extern const char LINKERSYM(BUILD_TIME_STR, 1)[];
extern const char LINKERSYM(BUILD_TIME_STR, 2)[];
extern const char LINKERSYM(BUILD_TIME_STR, 3)[];
extern const char LINKERSYM(BUILD_TIME_STR, 4)[];
extern const char LINKERSYM(BUILD_TIME_STR, 5)[];
}
namespace esphome::buildinfo {
// An 8-byte string plus terminating NUL.
struct ConfigHashStruct {
uintptr_t data0;
#ifdef USE_32BIT
uintptr_t data1;
#endif
char nul;
} __attribute__((packed));
extern const ConfigHashStruct CONFIG_HASH_STR = {(uintptr_t) &LINKERSYM(CONFIG_HASH_STR, 0),
#ifdef USE_32BIT
(uintptr_t) &LINKERSYM(CONFIG_HASH_STR, 1),
#endif
0};
// A 21-byte string plus terminating NUL, in 24 bytes
extern const uintptr_t BUILD_TIME_STR[] = {
(uintptr_t) &LINKERSYM(BUILD_TIME_STR, 0), (uintptr_t) &LINKERSYM(BUILD_TIME_STR, 1),
(uintptr_t) &LINKERSYM(BUILD_TIME_STR, 2),
#ifdef USE_32BIT
(uintptr_t) &LINKERSYM(BUILD_TIME_STR, 3), (uintptr_t) &LINKERSYM(BUILD_TIME_STR, 4),
(uintptr_t) &LINKERSYM(BUILD_TIME_STR, 5),
#endif
};
extern const uintptr_t BUILD_TIME = (uintptr_t) &ESPHOME_BUILD_TIME;
} // namespace esphome::buildinfo
-23
View File
@@ -1,23 +0,0 @@
#pragma once
#include <cstdint>
#include <ctime>
// Build information functions that provide config hash and build time.
// The actual values are provided by linker-defined symbols to avoid
// unnecessary rebuilds when only the build time changes.
// This is kept in its own file so that only files that need build-specific
// information have to include it explicitly.
namespace esphome::buildinfo {
extern const char CONFIG_HASH_STR[];
extern const char BUILD_TIME_STR[];
extern const uintptr_t BUILD_TIME;
static inline const char *get_config_hash() { return CONFIG_HASH_STR; }
static inline time_t get_build_time() { return (time_t) BUILD_TIME; }
static inline const char *get_build_time_string() { return BUILD_TIME_STR; }
} // namespace esphome::buildinfo
-2
View File
@@ -1,2 +0,0 @@
Import("env") # noqa: F821
env.Append(LINKFLAGS=["buildinfo.ld"]) # noqa: F821
-1
View File
@@ -501,7 +501,6 @@ async def to_code(config: ConfigType) -> None:
config[CONF_NAME],
config[CONF_FRIENDLY_NAME],
config.get(CONF_COMMENT, ""),
cg.RawExpression("esphome::buildinfo::get_build_time_string()"),
config[CONF_NAME_ADD_MAC_SUFFIX],
)
)
+20 -73
View File
@@ -7,7 +7,6 @@ from pathlib import Path
import re
import shutil
import stat
import struct
import time
from types import TracebackType
@@ -250,21 +249,16 @@ def copy_src_tree():
write_file_if_changed(
CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h()
)
# Write buildinfo linker script, JSON metadata, and copy the PlatformIO script
config_hash, build_time, build_time_str = get_buildinfo()
write_file(
CORE.relative_build_path("buildinfo.ld"),
generate_buildinfo_ld(config_hash, build_time, build_time_str),
# Write build_info header and JSON metadata
config_hash, build_time, build_time_str = get_build_info()
write_file_if_changed(
CORE.relative_src_path("esphome", "core", "build_info_data.h"),
generate_build_info_data_h(config_hash, build_time, build_time_str),
)
write_file(
CORE.relative_build_path("buildinfo.json"),
CORE.relative_build_path("build_info.json"),
json.dumps({"config_hash": config_hash, "build_time": build_time}),
)
copy_file_if_changed(
Path(__file__).parent / "core" / "buildinfo.py.script",
CORE.relative_build_path("buildinfo.py"),
)
CORE.add_platformio_option("extra_scripts", ["pre:buildinfo.py"])
platform = "esphome.components." + CORE.target_platform
try:
@@ -290,33 +284,8 @@ def generate_version_h():
)
def _encode_string_symbols(
text: str, prefix: str, bits: int, bit_suffix: str, endian: str, endian_suffix: str
) -> list[str]:
"""Encode a string as linker symbols for given word size and endianness."""
symbols: list[str] = []
# Pad to word boundary with NUL (build time strings need trailing NUL)
padded = text if prefix == "CONFIG_HASH_STR" else text + "\0"
while len(padded) % bits != 0:
padded += "\0"
for i in range(0, len(padded), bits):
chunk = padded[i : i + bits].encode("utf-8")
if bits == 8:
value = struct.unpack(endian + "Q", chunk)[0]
symbols.append(
f"ESPHOME_{prefix}_{bit_suffix}{endian_suffix}_{i // bits} = 0x{value:016x};"
)
else:
value = struct.unpack(endian + "I", chunk)[0]
symbols.append(
f"ESPHOME_{prefix}_{bit_suffix}{endian_suffix}_{i // bits} = 0x{value:08x};"
)
return symbols
def get_buildinfo() -> tuple[int, int, str]:
"""Calculate buildinfo values from current config.
def get_build_info() -> tuple[int, int, str]:
"""Calculate build_info values from current config.
Returns:
Tuple of (config_hash, build_time, build_time_str)
@@ -331,42 +300,20 @@ def get_buildinfo() -> tuple[int, int, str]:
return config_hash, build_time, build_time_str
def generate_buildinfo_ld(
def generate_build_info_data_h(
config_hash: int, build_time: int, build_time_str: str
) -> str:
"""Generate buildinfo linker script with config hash and build time."""
config_hash_str = f"{config_hash:08x}"
# Generate symbols for all 4 variants: 32LE, 32BE, 64LE, 64BE
all_variants: list[str] = []
for bits, bit_suffix in [(4, "32"), (8, "64")]:
for endian, endian_suffix in [("<", "LE"), (">", "BE")]:
all_variants.extend(
_encode_string_symbols(
config_hash_str,
"CONFIG_HASH_STR",
bits,
bit_suffix,
endian,
endian_suffix,
)
)
all_variants.extend(
_encode_string_symbols(
build_time_str,
"BUILD_TIME_STR",
bits,
bit_suffix,
endian,
endian_suffix,
)
)
return f"""/* Auto-generated buildinfo symbols */
ESPHOME_BUILD_TIME = {build_time};
ESPHOME_CONFIG_HASH = 0x{config_hash:08x};
{chr(10).join(all_variants)}
"""Generate build_info_data.h header with config hash and build time."""
return f"""#pragma once
// Auto-generated build_info data
#define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U
#define ESPHOME_BUILD_TIME {build_time}
#ifdef USE_ESP8266
#include <pgmspace.h>
static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}";
#else
static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}";
#endif
"""