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:
@@ -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:
|
||||
|
||||
38
esphome/core/buildinfo.cpp
Normal file
38
esphome/core/buildinfo.cpp
Normal 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
18
esphome/core/buildinfo.h
Normal 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
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user