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

Add buildinfo system with config hash and build time

To allow for more selective managed updates, allow the config hash and
build time to be built into the image itself.

To avoid triggering unneeded rebuilds, do this through a linker script
so that the new config hash and timestamp are included only if the
firmware is actually relinked.

Add a _check_and_emit_buildinfo() step after building, which prints
the information after the firmware was rebuilt. A subsequent commit
will emit a manifest here, or at least the HMAC-MD5 for signing OTA
updates using the hmac_key configured in this image.
This commit is contained in:
David Woodhouse
2025-12-11 13:04:07 +09:00
parent 74218bc742
commit edc320fef8
4 changed files with 141 additions and 0 deletions

View File

@@ -518,10 +518,58 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# Check if firmware was rebuilt and emit buildinfo + create manifest
_check_and_emit_buildinfo()
idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1
def _check_and_emit_buildinfo():
"""Check if firmware was rebuilt and emit buildinfo."""
firmware_path = CORE.firmware_bin
buildinfo_script_path = CORE.relative_build_path("buildinfo.ld")
# Check if both files exist
if not firmware_path.exists() or not buildinfo_script_path.exists():
return
# Check if firmware is newer than buildinfo script (indicating a relink occurred)
if firmware_path.stat().st_mtime <= buildinfo_script_path.stat().st_mtime:
return
# Read buildinfo values from linker script
try:
with open(buildinfo_script_path, encoding="utf-8") as f:
content = f.read()
config_hash_match = re.search(
r"ESPHOME_CONFIG_HASH = 0x([0-9a-fA-F]+);", content
)
build_time_match = re.search(r"ESPHOME_BUILD_TIME = (\d+);", content)
if not config_hash_match or not build_time_match:
return
config_hash = config_hash_match.group(1)
build_time = int(build_time_match.group(1))
# Emit buildinfo
print("=== ESPHome Build Info ===")
print(f"Config Hash: 0x{config_hash}")
print(
f"Build Time: {build_time} ({time.strftime('%Y-%m-%d %H:%M:%S %z', time.localtime(build_time))})"
)
print("===========================")
# TODO: Future commit will create JSON manifest with OTA metadata here
except OSError as e:
_LOGGER.debug("Failed to emit buildinfo: %s", e)
def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:

View File

@@ -0,0 +1,38 @@
// 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>
// Linker-provided symbols - declare as extern variables, not functions
extern "C" {
extern const char ESPHOME_CONFIG_HASH[];
extern const char ESPHOME_BUILD_TIME[];
}
namespace esphome {
namespace buildinfo {
// Reference the linker symbols as uintptr_t from the *data* section to
// avoid issues with pc-relative relocations on 64-bit platforms.
static const uintptr_t config_hash = (uintptr_t) &ESPHOME_CONFIG_HASH;
static const uintptr_t build_time = (uintptr_t) &ESPHOME_BUILD_TIME;
const char *get_config_hash() {
static char hash_str[9];
snprintf(hash_str, sizeof(hash_str), "%08x", (uint32_t) config_hash);
return hash_str;
}
time_t get_build_time() { return (time_t) build_time; }
} // namespace buildinfo
} // namespace esphome

18
esphome/core/buildinfo.h Normal file
View File

@@ -0,0 +1,18 @@
#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 {
namespace buildinfo {
const char *get_config_hash();
time_t get_build_time();
} // namespace buildinfo
} // namespace esphome

View File

@@ -1,4 +1,5 @@
from collections.abc import Callable
import hashlib
import importlib
import logging
import os
@@ -6,6 +7,7 @@ from pathlib import Path
import re
import shutil
import stat
import time
from types import TracebackType
from esphome import loader
@@ -23,6 +25,7 @@ from esphome.helpers import (
is_ha_addon,
read_file,
walk_files,
write_file,
write_file_if_changed,
)
from esphome.storage_json import StorageJSON, storage_path
@@ -173,6 +176,7 @@ 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
@@ -245,6 +249,12 @@ def copy_src_tree():
write_file_if_changed(
CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h()
)
# Write buildinfo generation script
write_file(
CORE.relative_build_path("generate_buildinfo.py"), generate_buildinfo_script()
)
# Add buildinfo script to platformio extra_scripts
CORE.add_platformio_option("extra_scripts", ["pre:generate_buildinfo.py"])
platform = "esphome.components." + CORE.target_platform
try:
@@ -270,6 +280,33 @@ def generate_version_h():
)
def generate_buildinfo_script():
from esphome import yaml_util
# Use the same clean YAML representation as 'esphome config' command
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};
"""
# 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
Import("env")
env.Append(LINKFLAGS=["buildinfo.ld"])
"""
def write_cpp(code_s):
path = CORE.relative_src_path("main.cpp")
if path.is_file():