1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 16:51:52 +00:00

Pass config hash and build date in as strings via linker symbols

This saves the RAM we were using to build it at runtime.
This commit is contained in:
David Woodhouse
2025-12-13 01:38:07 +09:00
parent 25805da008
commit 07d784b0bf
3 changed files with 143 additions and 62 deletions

View File

@@ -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 <cstdio>
#include <stdint.h>
// 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 <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
#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

View File

@@ -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

View File

@@ -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):