diff --git a/esphome/core/buildinfo.cpp b/esphome/core/buildinfo.cpp index dfeb073578..4947f7fe7f 100644 --- a/esphome/core/buildinfo.cpp +++ b/esphome/core/buildinfo.cpp @@ -1,61 +1,84 @@ -// Build information using linker-provided symbols -// -// Including build information into the build is fun, because we *don't* -// want the mere fact of changing the build time to *itself* cause a -// rebuild if nothing else had changed. If we do the naïve thing of -// just putting #defines in a header like version.h, we'll cause exactly -// that. -// -// So instead we provide the config hash and build time in a linker -// script, so they get pulled in only if the firmware is already being -// rebuilt. -#include "esphome/core/buildinfo.h" -#include +#include + +// 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 .rodate 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 (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 +#else +#define WS 32 +#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) -// Linker-provided symbols - declare as extern variables, not functions extern "C" { -extern const char ESPHOME_CONFIG_HASH[]; 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 { namespace buildinfo { -#if __SIZEOF_POINTER__ > 4 -// On 64-bit platforms, reference the linker symbols as uintptr_t from the *data* section to -// avoid issues with pc-relative relocations. Don't let the compiler know they're const or -// it'll optimise away the whole thing and emit a relocation to the ESPHOME_XXX symbols -// directly, which defeats the whole point! -// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables) -static uintptr_t config_hash_ptr = (uintptr_t) &ESPHOME_CONFIG_HASH; -static uintptr_t build_time_ptr = (uintptr_t) &ESPHOME_BUILD_TIME; -// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) -#define config_hash config_hash_ptr -#define build_time build_time_ptr -#else -#define config_hash ((uintptr_t) &ESPHOME_CONFIG_HASH) -#define build_time ((uintptr_t) &ESPHOME_BUILD_TIME) +// An 8-byte string plus terminating NUL. +struct config_hash_struct { + uintptr_t data0; +#ifdef USE_32BIT + uintptr_t data1; #endif + char nul; +} __attribute__((packed)); -const char *get_config_hash() { - static char hash_str[9]; - if (!hash_str[0]) { - snprintf(hash_str, sizeof(hash_str), "%08x", (uint32_t) config_hash); - } - return hash_str; -} +extern const config_hash_struct CONFIG_HASH_STR = {(uintptr_t) &LINKERSYM(CONFIG_HASH_STR, 0), +#ifdef USE_32BIT + (uintptr_t) &LINKERSYM(CONFIG_HASH_STR, 1), +#endif + 0}; -time_t get_build_time() { return (time_t) build_time; } +// 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 +}; -const char *get_build_time_string() { - static char time_str[32]; - if (!time_str[0]) { - time_t bt = get_build_time(); - struct tm *tm_info = localtime(&bt); - strftime(time_str, sizeof(time_str), "%b %d %Y, %H:%M:%S", tm_info); - } - return time_str; -} +extern const uintptr_t BUILD_TIME = (uintptr_t) &ESPHOME_BUILD_TIME; } // namespace buildinfo } // namespace esphome diff --git a/esphome/core/buildinfo.h b/esphome/core/buildinfo.h index 664b7985dd..2217b8bc57 100644 --- a/esphome/core/buildinfo.h +++ b/esphome/core/buildinfo.h @@ -11,9 +11,15 @@ namespace esphome { namespace buildinfo { -const char *get_config_hash(); -time_t get_build_time(); -const char *get_build_time_string(); +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 buildinfo } // namespace esphome diff --git a/esphome/writer.py b/esphome/writer.py index f955b22b79..b6bcfaeab4 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -176,7 +176,6 @@ VERSION_H_FORMAT = """\ """ DEFINES_H_TARGET = "esphome/core/defines.h" VERSION_H_TARGET = "esphome/core/version.h" -BUILDINFO_H_TARGET = "esphome/core/buildinfo.h" ESPHOME_README_TXT = """ THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY @@ -287,24 +286,77 @@ def generate_buildinfo_script(): config_str = yaml_util.dump(CORE.config, show_secrets=True) config_hash = hashlib.md5(config_str.encode("utf-8")).hexdigest()[:8] - config_hash_int = int(config_hash, 16) build_time = int(time.time()) - # Generate linker script content - linker_script = f"""/* Auto-generated buildinfo symbols */ -ESPHOME_CONFIG_HASH = 0x{config_hash_int:08x}; -ESPHOME_BUILD_TIME = {build_time}; -""" + # Generate build time string + build_time_str = time.strftime("%b %d %Y, %H:%M:%S", time.localtime(build_time)) - # Write linker script file - with open(CORE.relative_build_path("buildinfo.ld"), "w", encoding="utf-8") as f: - f.write(linker_script) - - return """#!/usr/bin/env python3 -# Buildinfo linker script already generated + return ( + """#!/usr/bin/env python3 +# Generate buildinfo with target-specific encoding Import("env") +import struct +import subprocess +import tempfile +import os + +# Generate all four variants of both config hash and build time strings +# to be handled by esphome/core/buildinfo.cpp +build_time_str = \"""" + + build_time_str + + """\" +config_hash_str = \"""" + + config_hash + + """\" + +# Generate symbols for all 4 variants: 32LE, 32BE, 64LE, 64BE +all_variants = [] + +for bits, bit_suffix in [(4, "32"), (8, "64")]: + for endian, endian_suffix in [("<", "LE"), (">", "BE")]: + # Config hash string (8 hex chars) + config_padded = config_hash_str + while len(config_padded) % bits != 0: + config_padded += '\\0' + + for i in range(0, len(config_padded), bits): + chunk = config_padded[i:i+bits].encode('utf-8') + if bits == 8: + value = struct.unpack(endian + "Q", chunk)[0] + all_variants.append(f"ESPHOME_CONFIG_HASH_STR_{bit_suffix}{endian_suffix}_{i//bits} = 0x{value:016x};") + else: + value = struct.unpack(endian + "I", chunk)[0] + all_variants.append(f"ESPHOME_CONFIG_HASH_STR_{bit_suffix}{endian_suffix}_{i//bits} = 0x{value:08x};") + + # Build time string + build_padded = build_time_str + '\\0' + while len(build_padded) % bits != 0: + build_padded += '\\0' + + for i in range(0, len(build_padded), bits): + chunk = build_padded[i:i+bits].encode('utf-8') + if bits == 8: + value = struct.unpack(endian + "Q", chunk)[0] + all_variants.append(f"ESPHOME_BUILD_TIME_STR_{bit_suffix}{endian_suffix}_{i//bits} = 0x{value:016x};") + else: + value = struct.unpack(endian + "I", chunk)[0] + all_variants.append(f"ESPHOME_BUILD_TIME_STR_{bit_suffix}{endian_suffix}_{i//bits} = 0x{value:08x};") + +# Write linker script with all variants +linker_script = f'''/* Auto-generated buildinfo symbols */ +ESPHOME_BUILD_TIME = """ + + str(build_time) + + """; +{chr(10).join(all_variants)} +''' + +with open("buildinfo.ld", "w") as f: + f.write(linker_script) + +# Compile and link env.Append(LINKFLAGS=["buildinfo.ld"]) """ + ) def write_cpp(code_s):