From 4ba0e1bfa2796116ec831d5a46e8eb54954f18f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 10 Oct 2026 07:39:49 -1000 Subject: [PATCH] [core] Fix the native ESP8266 and host builds on Windows without ccache (#20468) --- esphome/build_gen/arduino8266.py | 2 +- esphome/build_gen/host.py | 2 +- esphome/build_helpers/ninja_gen.py | 21 ++++++++++++------- .../unit_tests/build_gen/test_arduino8266.py | 3 ++- tests/unit_tests/build_gen/test_host.py | 5 ++++- .../build_helpers/test_ninja_gen.py | 18 ++++++++++++++++ 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 44369136a0..b0ba766392 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -1057,7 +1057,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: cxx = (toolchain_tool(paths.toolchain, "g++"),) lines = [ *tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache), - *compile_rule_lines(), + *compile_rule_lines(ccache), *ar_rule_lines(toolchain_tool(paths.toolchain, "ar")), *pch_rule_lines(), "rule link", diff --git a/esphome/build_gen/host.py b/esphome/build_gen/host.py index f1e53e1ccb..3a8587891b 100644 --- a/esphome/build_gen/host.py +++ b/esphome/build_gen/host.py @@ -240,7 +240,7 @@ def write_project(compilers: HostCompilers, ccache: str | None) -> bool: lines = [ *tool_lines(compilers.cc, compilers.cxx, ccache), - *compile_rule_lines(), + *compile_rule_lines(ccache), *pch_rule_lines(), "rule link", " command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags", diff --git a/esphome/build_helpers/ninja_gen.py b/esphome/build_helpers/ninja_gen.py index a2ee244a29..c74262fdee 100644 --- a/esphome/build_helpers/ninja_gen.py +++ b/esphome/build_helpers/ninja_gen.py @@ -74,33 +74,40 @@ def tool_lines( # path gets f"python = {_q(strip_win_long_path_prefix(sys.executable))}", f"buildtool = {_q(_BUILD_TOOL)}", - f"ccache = {_q(ccache) if ccache else ''}", + *([f"ccache = {_q(ccache)}"] if ccache else []), "", ] -def compile_rule_lines() -> list[str]: - """The compile rules; ``$own_includes`` is empty unless an edge sets it.""" +def compile_rule_lines(ccache: str | None) -> list[str]: + """The compile rules; ``$own_includes`` is empty unless an edge sets it. + + ``ccache`` is the launcher ``tool_lines`` was given; the same test here + keeps the variable and the rules in step. It is spliced in rather than + left as an empty variable: a leading space on the command line makes + CreateProcess fail on Windows. + """ + launcher = "$ccache " if ccache else "" return [ "rule c", - " command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out", + f" command = {launcher}$cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out", " depfile = $out.d", " deps = gcc", " description = CC $out", "rule cxx", - " command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out", + f" command = {launcher}$cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out", " depfile = $out.d", " deps = gcc", " description = CXX $out", "rule aspp", - " command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out", + f" command = {launcher}$cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out", " depfile = $out.d", " deps = gcc", " description = AS $out", # Plain assembler, as SCons's ASCOM: no preprocessor, so no # depfile and no $flags (defines/includes) either "rule asm", - " command = $ccache $cc -x assembler $asflags -c $in -o $out", + f" command = {launcher}$cc -x assembler $asflags -c $in -o $out", " description = AS $out", ] diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 2ea4ffabe6..4c60b4e476 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -888,8 +888,9 @@ def test_write_project_plain_asm_rule_skips_preprocessor(tmp_path: Path) -> None _set_flags() content = _write_ninja(paths) assert "lowlevel.s.o: asm " in content + assert "rule asm\n command = $cc -x assembler $asflags -c $in -o $out" in content assert "rule asm\n command = $ccache $cc -x assembler $asflags -c $in -o $out" in ( - content + _write_ninja(paths, ccache="/cc/ccache") ) diff --git a/tests/unit_tests/build_gen/test_host.py b/tests/unit_tests/build_gen/test_host.py index 9e40698c91..c3936ee65a 100644 --- a/tests/unit_tests/build_gen/test_host.py +++ b/tests/unit_tests/build_gen/test_host.py @@ -308,7 +308,10 @@ def test_write_project_pch_skipped_for_a_force_include_in_the_compiler( def test_write_project_without_ccache(tmp_path: Path) -> None: _make_src(tmp_path, "main.cpp") _changed, ninja = _render() - assert "ccache = \n" in ninja + # No launcher variable, and no leading space on the commands either: + # CreateProcess on Windows rejects a command line that starts with one + assert "$ccache" not in ninja + assert " command = $cxx -MMD -MF $out.d" in ninja def test_write_project_routes_user_link_flags(tmp_path: Path) -> None: diff --git a/tests/unit_tests/build_helpers/test_ninja_gen.py b/tests/unit_tests/build_helpers/test_ninja_gen.py index e7f9b96af2..d51110efad 100644 --- a/tests/unit_tests/build_helpers/test_ninja_gen.py +++ b/tests/unit_tests/build_helpers/test_ninja_gen.py @@ -55,3 +55,21 @@ def test_collect_sources_skips_excluded_and_other_files(tmp_path: Path) -> None: tmp_path / "b.cpp", tmp_path / "sub" / "c.S", ] + + +@pytest.mark.parametrize("ccache", ["/usr/bin/ccache", None]) +def test_compile_rule_lines_splice_the_launcher(ccache: str | None) -> None: + """Without ccache no command keeps a leading space, which CreateProcess + on Windows rejects; with it every compile rule starts with the launcher.""" + commands = [ + line.removeprefix(" command = ") + for line in ninja_gen.compile_rule_lines(ccache) + if line.startswith(" command = ") + ] + assert len(commands) == 4 + for command in commands: + if ccache: + assert command.startswith("$ccache $c") + else: + assert command.startswith("$c") + assert "$ccache" not in command