Merge branch 'esp8266-native-build-infra' into esp8266-native-framework-installer

This commit is contained in:
J. Nick Koston
2026-08-22 23:21:07 -05:00
4 changed files with 92 additions and 16 deletions
+17 -11
View File
@@ -161,17 +161,19 @@ def parse_entry(
raw = os.path.normpath(directory / raw)
return raw.replace("\\", "/")
# A launcher-wrapped command ("ccache g++ ...") names the compiler second
if launcher is not None and tokens[:1] == [launcher]:
tokens = tokens[1:]
if not tokens:
# _split_command("") is [] by design; fail like _pick_entry does
# _split_command("") is [] by design, and a command that is only
# the launcher strips to nothing; fail like _pick_entry does
# instead of an IndexError traceback
raise ValueError(f"empty compile command for {entry.get('file')}")
# A launcher-wrapped command ("ccache g++ ...") names the compiler second
if launcher is not None and tokens[0] == launcher:
tokens = tokens[1:]
if _is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
# A stale compile DB built with a launcher the current run no longer
# configures: the real compiler is the next token.
_LOGGER.debug("Stripping unconfigured launcher %s", tokens[0])
# configures: the real compiler is the next token. Warn: the DB is
# stale and worth regenerating.
_LOGGER.warning("Stripping unconfigured launcher %s", tokens[0])
tokens = tokens[1:]
# token0 is the compiler path; the rest of the command already uses forward
# slashes on Windows, so normalize it too for a consistent idedata file.
@@ -332,11 +334,14 @@ def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> d
def _shape(entry: dict) -> str:
# The command minus its TU-specific paths: entries sharing a shape
# carry identical include sets (one ninja rule), so tokenize once
# per shape instead of once per TU
return (
entry["command"]
.replace(entry.get("file", ""), "")
.replace(entry.get("output", ""), "")
# per shape instead of once per TU. Response-file commands never
# dedupe: per-object .rsp names strip to one shape while the files
# may hold different include sets.
command = entry["command"]
if "@" in command:
return f"unique:{entry['file']}"
return command.replace(entry.get("file", ""), "").replace(
entry.get("output", ""), ""
)
seen_shapes = {_shape(representative)}
@@ -345,6 +350,7 @@ def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> d
continue
has_esphome_tu = True
if (shape := _shape(entry)) in seen_shapes:
_LOGGER.debug("Include union: %s shares a command shape", entry["file"])
continue
seen_shapes.add(shape)
for inc in parse_entry(entry, launcher)[2]:
+16 -1
View File
@@ -85,6 +85,12 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
_LOGGER.warning("Skipping size summary: %s", e)
return
if not isinstance(data, dict):
# Valid JSON that is not an object (truncated tool output) must
# not raise past a build that already linked
_LOGGER.warning("Skipping size summary: unexpected shape in %s", size_json)
return
memory_types = data.get("memory_types", {})
ram_region = memory_types.get("DRAM") or memory_types.get("DIRAM") or {}
ram_used = ram_region.get("used")
@@ -105,7 +111,16 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
return
try:
app_size = _find_app_partition_size(partitions_csv)
except ValueError as e:
except (ValueError, OSError) as e:
_LOGGER.warning("Skipping Flash summary: %s", e)
return
if app_size <= 0:
# A malformed partition row parses to 0; a 0% bar would feed CI's
# memory-impact extraction fabricated data
_LOGGER.warning(
"Skipping Flash summary: app partition size is %s in %s",
app_size,
partitions_csv,
)
return
print_size_line("Flash", image_size, app_size)
+37 -4
View File
@@ -151,11 +151,20 @@ def test_is_esphome_src_handles_backslash_paths() -> None:
assert not idedata._is_esphome_src(r"C:\b\src\esphome\core\app.h")
def test_parse_entry_empty_command_raises() -> None:
"""A blank command fails with a named ValueError, not an IndexError."""
entry = {"directory": "/b", "file": "/b/src/x.cpp", "command": ""}
@pytest.mark.parametrize(
("command", "launcher"),
[
("", None),
# A command that is only the launcher strips to nothing
("/usr/bin/ccache", "/usr/bin/ccache"),
],
)
def test_parse_entry_empty_command_raises(command: str, launcher: str | None) -> None:
"""A blank (or launcher-only) command fails with a named ValueError,
not an IndexError."""
entry = {"directory": "/b", "file": "/b/src/x.cpp", "command": command}
with pytest.raises(ValueError, match="empty compile command"):
idedata.parse_entry(entry)
idedata.parse_entry(entry, launcher)
def test_idedata_from_build_empty_includes_raises(tmp_path: Path) -> None:
@@ -180,6 +189,30 @@ def test_idedata_from_build_empty_includes_raises(tmp_path: Path) -> None:
idedata.idedata_from_build(compile_commands)
def test_idedata_from_build_rsp_commands_never_dedupe(tmp_path: Path) -> None:
"""Per-object response files strip to one shape while holding different
include sets; @-commands must tokenize per TU."""
entries = []
for name in ("a", "b"):
rsp = tmp_path / f"{name}.cpp.o.rsp"
rsp.write_text(f"-I{ABS}inc/{name}")
file = f"{ABS}build/src/esphome/core/{name}.cpp"
entries.append(
{
"directory": str(tmp_path),
"file": file,
"command": f"/tools/g++ @{rsp.name} -c {file} -o {name}.o",
"output": f"{name}.o",
}
)
compile_commands = tmp_path / "compile_commands.json"
compile_commands.write_text(json.dumps(entries))
with patch.object(idedata, "get_toolchain_includes", return_value=[]):
data = idedata.idedata_from_build(compile_commands)
joined = " ".join(data["includes"]["build"])
assert "inc/a" in joined and "inc/b" in joined
def test_idedata_from_build_dedupes_identical_command_shapes(
tmp_path: Path,
) -> None:
+22
View File
@@ -193,3 +193,25 @@ def test_print_summary_missing_flash_inputs_warn(
size_json = _write_size_json(tmp_path, data)
print_summary(size_json, partitions_csv=tmp_path / "partitions.cssv")
assert "no image_size" in caplog.text
def test_print_summary_non_dict_json_warns(tmp_path, caplog) -> None:
"""Valid JSON that is not an object must warn, not raise past a build
that already linked."""
size_json = tmp_path / "size.json"
size_json.write_text("[]")
print_summary(size_json, tmp_path / "partitions.csv")
assert "unexpected shape" in caplog.text
def test_print_summary_zero_app_partition_warns(tmp_path, caplog) -> None:
"""A malformed partition row parsing to 0 must not render a 0% bar for
CI's memory-impact extraction to ingest."""
size_json = tmp_path / "size.json"
size_json.write_text(
'{"memory_types": {"DRAM": {"used": 1, "size": 2}}, "image_size": 100}'
)
partitions = tmp_path / "partitions.csv"
partitions.write_text("app0, app, ota_0, 0x10000, ,\n")
print_summary(size_json, partitions)
assert "app partition size is" in caplog.text