mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 23:37:34 +00:00
Add the OBJECT_DEPENDS edge so a rebuilt gch recompiles its consumers
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
@@ -331,11 +332,16 @@ def _pch_cmake() -> str:
|
||||
if not pch_enabled():
|
||||
return ""
|
||||
return f"""
|
||||
# ESPHome precompiled header (see esphome/build_helpers/pch.py)
|
||||
# ESPHome precompiled header (see esphome/build_helpers/pch.py). The
|
||||
# OBJECT_DEPENDS edge is on the header, not the .gch: headers baked into
|
||||
# a .gch drop out of the TU depfiles, and prepare_pch() touches the
|
||||
# header whenever it rebuilds the .gch so consumers recompile.
|
||||
target_compile_options(${{COMPONENT_LIB}} PRIVATE
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:-include>"
|
||||
"$<$<COMPILE_LANGUAGE:CXX>:{PCH_HEADER_NAME}>"
|
||||
)
|
||||
set_source_files_properties(${{app_sources}} PROPERTIES
|
||||
OBJECT_DEPENDS "${{CMAKE_BINARY_DIR}}/{PCH_HEADER_NAME}")
|
||||
"""
|
||||
|
||||
|
||||
@@ -349,12 +355,13 @@ def _pch_compile_command(build_dir: Path, header: Path, gch: Path) -> list[str]
|
||||
except (OSError, json.JSONDecodeError) as err:
|
||||
_LOGGER.debug("No usable compile database, skipping pch: %s", err)
|
||||
return None
|
||||
src_prefix = str(CORE.relative_src_path())
|
||||
# Windows compile DBs use backslashes; normalize both sides
|
||||
src_prefix = str(CORE.relative_src_path()).replace("\\", "/")
|
||||
entry = next(
|
||||
(
|
||||
e
|
||||
for e in entries
|
||||
if e.get("file", "").startswith(src_prefix)
|
||||
if e.get("file", "").replace("\\", "/").startswith(src_prefix)
|
||||
and e.get("file", "").endswith(_CXX_SOURCE_SUFFIXES)
|
||||
),
|
||||
None,
|
||||
@@ -420,6 +427,9 @@ def prepare_pch() -> None:
|
||||
return
|
||||
cmd = _pch_compile_command(build_dir, header, gch)
|
||||
if cmd is None:
|
||||
# The checksum is stale; a leftover .gch must not be consumed
|
||||
gch.unlink(missing_ok=True)
|
||||
sum_path.unlink(missing_ok=True)
|
||||
return
|
||||
# Keyed on the checksum and the compile command: a failure caused by
|
||||
# the command alone must retry when the command changes
|
||||
@@ -433,12 +443,14 @@ def prepare_pch() -> None:
|
||||
return
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, cwd=build_dir, capture_output=True, text=True, check=False
|
||||
cmd, cwd=build_dir, capture_output=True, text=True, check=False, timeout=300
|
||||
)
|
||||
error = None
|
||||
if result.returncode != 0:
|
||||
error = result.stderr.strip() or f"exit code {result.returncode}"
|
||||
except OSError as err:
|
||||
elif not gch.is_file():
|
||||
error = "compiler produced no .gch"
|
||||
except (OSError, subprocess.SubprocessError) as err:
|
||||
error = str(err)
|
||||
if error is not None:
|
||||
_LOGGER.warning(
|
||||
@@ -448,9 +460,13 @@ def prepare_pch() -> None:
|
||||
sum_path.unlink(missing_ok=True)
|
||||
# Skip retries until a header/flag/sdkconfig/command change
|
||||
failed_marker.write_text(marker_key + "\n", encoding="utf-8")
|
||||
os.utime(header)
|
||||
return
|
||||
failed_marker.unlink(missing_ok=True)
|
||||
sum_path.write_text(checksum + "\n", encoding="utf-8")
|
||||
# The OBJECT_DEPENDS edge watches the header; bump it so consumers of
|
||||
# the previous .gch recompile
|
||||
os.utime(header)
|
||||
|
||||
|
||||
def write_project(
|
||||
|
||||
@@ -198,8 +198,10 @@ def parse_entry(
|
||||
|
||||
it = iter(tokens[1:])
|
||||
for tok in it:
|
||||
if tok in ("-c", "-o"):
|
||||
next(it, None) # drop the flag and its argument (input/output)
|
||||
if tok in ("-c", "-o", "-include"):
|
||||
# Drop the flag and its argument; the injected relative
|
||||
# -include esphome_pch.h does not resolve outside the build dir
|
||||
next(it, None)
|
||||
elif tok.startswith("-D"):
|
||||
# ``.strip()`` handles tokens like ``-D CONFIGURED=1`` (a single
|
||||
# quoted arg with a space after -D) that some flags arrive as.
|
||||
|
||||
@@ -500,6 +500,13 @@ def _make_pch_device(tmp_path: Path, name: str) -> Path:
|
||||
path = dev / "src" / header
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("")
|
||||
# A real quoted include chain and a per-device-named sdkconfig with
|
||||
# identical content: the closure and sdkconfig inputs must be exercised
|
||||
(dev / "src" / "esphome" / "core" / "defines.h").write_text(
|
||||
'#include "esphome/core/macros.h"\n'
|
||||
)
|
||||
(dev / "src" / "esphome" / "core" / "macros.h").write_text("#define M 1\n")
|
||||
(dev / f"sdkconfig.{name}").write_text("CONFIG_X=y\n")
|
||||
build = dev / "build"
|
||||
build.mkdir(exist_ok=True)
|
||||
from esphome.build_helpers.pch import pch_header_text
|
||||
@@ -736,3 +743,70 @@ def test_write_project_writes_pch_header(tmp_path: Path) -> None:
|
||||
assert (tmp_path / "build" / "esphome_pch.h").read_text() == pch_header_text(
|
||||
_PCH_HEADERS
|
||||
)
|
||||
|
||||
|
||||
def test_prepare_pch_stale_bailout_removes_gch(tmp_path: Path) -> None:
|
||||
"""A stale .gch must not survive when no compile command is available."""
|
||||
from esphome.build_gen.espidf import prepare_pch
|
||||
|
||||
dev = _make_pch_device(tmp_path, "dev_s")
|
||||
CORE.build_path = dev
|
||||
gch = dev / "build" / "esphome_pch.h.gch"
|
||||
gch.write_bytes(b"stale")
|
||||
(dev / "build" / "esphome_pch.h.gch.sum").write_text("stale-sum\n")
|
||||
(dev / "build" / "compile_commands.json").unlink()
|
||||
with patch.object(CORE, "name", "test"):
|
||||
prepare_pch()
|
||||
assert not gch.exists()
|
||||
assert not (dev / "build" / "esphome_pch.h.gch.sum").exists()
|
||||
|
||||
|
||||
def test_prepare_pch_zero_exit_without_gch_is_failure(tmp_path: Path) -> None:
|
||||
from esphome.build_gen.espidf import prepare_pch
|
||||
|
||||
dev = _make_pch_device(tmp_path, "dev_z")
|
||||
CORE.build_path = dev
|
||||
|
||||
def no_output(cmd, **kwargs):
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
with (
|
||||
patch.object(CORE, "name", "test"),
|
||||
patch("esphome.build_gen.espidf.subprocess.run", side_effect=no_output),
|
||||
):
|
||||
prepare_pch()
|
||||
assert not (dev / "build" / "esphome_pch.h.gch.sum").exists()
|
||||
assert (dev / "build" / "esphome_pch.h.gch.failed").exists()
|
||||
|
||||
|
||||
def test_prepare_pch_bumps_header_for_object_depends(tmp_path: Path) -> None:
|
||||
"""The OBJECT_DEPENDS edge watches the header; a rebuilt .gch must bump
|
||||
it so pch-consuming TUs recompile."""
|
||||
import os as _os
|
||||
|
||||
from esphome.build_gen.espidf import prepare_pch
|
||||
|
||||
dev = _make_pch_device(tmp_path, "dev_t")
|
||||
CORE.build_path = dev
|
||||
header = dev / "build" / "esphome_pch.h"
|
||||
gch = dev / "build" / "esphome_pch.h.gch"
|
||||
_os.utime(header, (0, 0))
|
||||
before = header.stat().st_mtime
|
||||
|
||||
def fake_compile(cmd, **kwargs):
|
||||
gch.write_bytes(b"gch")
|
||||
return subprocess.CompletedProcess(cmd, 0, "", "")
|
||||
|
||||
with (
|
||||
patch.object(CORE, "name", "test"),
|
||||
patch("esphome.build_gen.espidf.subprocess.run", side_effect=fake_compile),
|
||||
):
|
||||
prepare_pch()
|
||||
assert header.stat().st_mtime > before
|
||||
|
||||
|
||||
def test_component_cmakelists_pch_object_depends() -> None:
|
||||
from esphome.build_gen.espidf import get_component_cmakelists
|
||||
|
||||
content = get_component_cmakelists()
|
||||
assert 'OBJECT_DEPENDS "${CMAKE_BINARY_DIR}/esphome_pch.h"' in content
|
||||
|
||||
Reference in New Issue
Block a user