diff --git a/esphome/arduino8266/component.py b/esphome/arduino8266/component.py index 50c5a5f76d..111e407dbc 100644 --- a/esphome/arduino8266/component.py +++ b/esphome/arduino8266/component.py @@ -90,7 +90,16 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary: if tok.startswith("-I"): include_flags.append(tok[2:]) elif tok.startswith("-L"): - lib.link_dirs.append((read_path / tok[2:]).resolve()) + link_dir = (read_path / tok[2:]).resolve() + if not link_dir.is_dir(): + # Kept anyway (the linker ignores missing -L dirs); the + # warning names the culprit before a bare "cannot find -lfoo" + _LOGGER.warning( + "Library %s declares library dir %s which does not exist", + name, + tok[2:], + ) + lib.link_dirs.append(link_dir) elif tok.startswith("-l"): lib.link_libs.append(tok[2:]) elif tok.startswith("-Wl,"): diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 80c0730aa0..112f90172a 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -15,6 +15,7 @@ from the build flags with the same precedence as the PlatformIO builder. from __future__ import annotations from dataclasses import dataclass, field +import logging import os from pathlib import Path import re @@ -41,6 +42,8 @@ from esphome.framework_helpers import get_project_cxx_compile_flags from esphome.helpers import mkdir_p, write_file_if_changed from esphome.platformio.library import join_flag_args +_LOGGER = logging.getLogger(__name__) + # Compile rule per source suffix; keys must cover SRC_FILE_EXTENSIONS so any # source a library manifest selects has a rule (pinned by a drift test). _RULE_FOR_SUFFIX = { @@ -607,6 +610,12 @@ def write_project(paths: dict[str, Path]) -> bool: for lib in libraries: if not lib.sources: + # Header-only libraries are legitimate; the log makes an empty + # srcFilter or broken tree traceable before link errors do. + _LOGGER.debug( + "Library %s has no source files; contributing includes only", + lib.name, + ) continue lib_root = _common_parent(lib.sources) objs = _ninja_compile_edges( diff --git a/script/determine-jobs.py b/script/determine-jobs.py index ba670d0db1..6bc6d8a875 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -652,7 +652,10 @@ ESP8266_NATIVE_TRIGGER_FILES = frozenset( def _esp8266_native_path_or_file_trigger(files: list[str]) -> bool: """Whether any changed file is native-ESP8266 infrastructure / harness.""" - return _path_or_file_trigger( + # base_python_changed covers the top-level esphome/*.py modules the + # native backend imports directly (framework_helpers, helpers, writer, + # __main__); without it a change there would silently skip this job. + return base_python_changed(files) or _path_or_file_trigger( files, ESP8266_NATIVE_TRIGGER_FILES, ESP8266_NATIVE_TRIGGER_PATH_PREFIXES ) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index c8ff9bd808..5aa67af73c 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -3083,6 +3083,9 @@ def test_esp8266_native_components_full_list_on_infra_change() -> None: # Shared modules the native build depends on ["esphome/espidf/idedata.py"], ["esphome/platformio/library.py"], + # Top-level esphome/*.py modules the backend imports directly + ["esphome/framework_helpers.py"], + ["esphome/writer.py"], ): with ( patch.object(determine_jobs, "changed_files", return_value=changed), diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 59866af6b7..3ffbf7309d 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -11,6 +11,7 @@ PlatformIO toolchain produces for the same configuration. from __future__ import annotations from collections.abc import Generator +import logging from pathlib import Path from unittest.mock import MagicMock, patch @@ -454,7 +455,9 @@ def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None: assert "len = 0x2000000" in patched -def test_write_project_libraries_and_variant(tmp_path: Path) -> None: +def test_write_project_libraries_and_variant( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: from esphome.arduino8266.component import ArduinoLibrary paths = _make_framework(tmp_path) @@ -477,14 +480,17 @@ def test_write_project_libraries_and_variant(tmp_path: Path) -> None: ) _set_flags("-DPIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS") - content = _write_ninja( - paths, libraries=[library, headers_only], ccache="/cc/ccache" - ) + with caplog.at_level(logging.DEBUG, logger="esphome.build_gen.arduino8266"): + content = _write_ninja( + paths, libraries=[library, headers_only], ccache="/cc/ccache" + ) assert "build libFrameworkArduinoVariant.a: ar" in content assert "build libMyLib.a: ar" in content - # A headers-only library contributes includes but no archive + # A headers-only library contributes includes but no archive, with a + # debug log distinguishing it from a resolution failure assert "libHeadersOnly.a" not in content + assert "Library HeadersOnly has no source files" in caplog.text assert " flags = -DMYLIB=1" in content assert "-lalgobsec" in content # Library link flags reach the firmware link line; .cc compiles as C++ diff --git a/tests/unit_tests/test_arduino8266_component.py b/tests/unit_tests/test_arduino8266_component.py index 28ceb5a4c3..0165ee54af 100644 --- a/tests/unit_tests/test_arduino8266_component.py +++ b/tests/unit_tests/test_arduino8266_component.py @@ -84,6 +84,18 @@ def test_library_info_flags_parsing(tmp_path: Path) -> None: assert lib.link_flags == ["-Wl,--wrap=malloc"] +def test_library_info_missing_link_dir_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + read_path = tmp_path / "lib" + read_path.mkdir() + data = {"build": {"flags": ["-Lmissing_blobs"]}} + lib = component._library_info("x", read_path, data) + assert "declares library dir missing_blobs which does not exist" in caplog.text + # Kept anyway: the linker ignores missing -L dirs + assert lib.link_dirs == [(read_path / "missing_blobs").resolve()] + + def test_library_info_no_src_dir(tmp_path: Path) -> None: read_path = tmp_path / "empty" read_path.mkdir()