diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 30cc64a7a6..e045601178 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -28,6 +28,7 @@ from esphome.framework_helpers import ( failure_reason, get_python_env_executable_path, get_system_python_path, + is_expected_fetch_error, resume_fetch_job, rmdir, run_batch_downloads, @@ -297,8 +298,9 @@ def _run_idf_tools_script( ) -> tuple[bool, str | None, str | None]: """Run one of the sibling idf_tools-backed helper scripts. - The script is executed with the framework's ``tools`` directory on - PYTHONPATH so it imports the framework's own ``idf_tools`` module. + PYTHONPATH carries this directory (sibling imports like + ``_tool_resolution``), the esphome package root (``esphome.helpers``), + and the framework's ``tools`` dir (its own ``idf_tools`` module). """ cmd = [ get_system_python_path(), @@ -829,7 +831,12 @@ def _preinstall_idf_tool_archives( # surviving torn dir prints its own guidance there _LOGGER.warning("ESP-IDF tool pre-extraction failed; see above") except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught - _LOGGER.warning("ESP-IDF tool pre-extraction failed: %s", failure_reason(e)) + # A programming error keeps its traceback at WARNING + _LOGGER.warning( + "ESP-IDF tool pre-extraction failed: %s", + failure_reason(e), + exc_info=None if is_expected_fetch_error(e) else e, + ) _LOGGER.debug("Pre-extraction failure detail", exc_info=True) diff --git a/esphome/espidf/get_tool_downloads.py b/esphome/espidf/get_tool_downloads.py index 23b129f40f..fa8a2f3435 100644 --- a/esphome/espidf/get_tool_downloads.py +++ b/esphome/espidf/get_tool_downloads.py @@ -1,8 +1,9 @@ """Print JSON download info for the ESP-IDF tools an install would fetch. Run via ``python ...``. -PYTHONPATH must include ``/tools`` so ``idf_tools`` is -importable, and IDF_TOOLS_PATH must be set. Prints a JSON list of +PYTHONPATH must include this directory (for ``_tool_resolution``) and +``/tools`` (for ``idf_tools``), and IDF_TOOLS_PATH must +be set. Prints a JSON list of ``{name, url, size, sha256, dest}`` for every tool version that is not yet installed, where ``dest`` is the archive filename ``idf_tools.py install`` expects to find in ``/dist``. Tools with no download for the diff --git a/esphome/espidf/install_tool_archives.py b/esphome/espidf/install_tool_archives.py index 6bbabc4356..2c5b4df5e7 100644 --- a/esphome/espidf/install_tool_archives.py +++ b/esphome/espidf/install_tool_archives.py @@ -36,7 +36,8 @@ def collect_pending( for tool, name, version, download in iter_tool_downloads( targets_csv, tool_specs, on_broken ): - # An archive at its final name was sha256-verified by the prefetch + # Trusted as-is: the prefetch verifies archives at their final name, + # and the installer redoes anything this pass fails on if (name, version) in pending or not ( dist_path / archive_name(download) ).is_file(): @@ -52,8 +53,11 @@ def install_one(tool: object, name: str, version: str) -> bool | None: tool.install(version) # check_binary_valid exits via SystemExit; the installer redoes failures except (Exception, SystemExit) as e: # noqa: BLE001 # pylint: disable=broad-exception-caught + # Name the type: idf_tools' fatal() raises SystemExit(1), which + # would render as a bare "1" print( - f"pre-extracting {name}@{version} failed, leaving it to the installer: {e}", + f"pre-extracting {name}@{version} failed, leaving it to the " + f"installer: {type(e).__name__}: {e}", file=sys.stderr, ) # A torn dest dir must not look installed to the installer diff --git a/tests/unit_tests/test_espidf_framework.py b/tests/unit_tests/test_espidf_framework.py index 1c15427712..2285a166df 100644 --- a/tests/unit_tests/test_espidf_framework.py +++ b/tests/unit_tests/test_espidf_framework.py @@ -2086,7 +2086,8 @@ def test_preinstall_script_failure_only_warns( def test_preinstall_exception_only_warns( tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: - """An unexpected error must not become a new way for the install to fail.""" + """An unexpected error must not become a new way for the install to fail, + and keeps its traceback at WARNING.""" with ( patch( "esphome.espidf.framework._run_idf_tools_script", @@ -2095,7 +2096,8 @@ def test_preinstall_exception_only_warns( patch("esphome.espidf.framework.get_usable_cpu_count", return_value=1), ): _preinstall_idf_tool_archives(tmp_path, "esp32", ["required"], None) - assert "pre-extraction failed" in caplog.text + record = next(r for r in caplog.records if "pre-extraction failed" in r.message) + assert record.exc_info is not None # ---------------------------------------------------------------------------