From c95e0b90f58ff866779b3388d7e73d84816c0673 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 19:51:26 -0500 Subject: [PATCH] Raise on a traversing extraScript, survive sys.exit in vendored scripts, fix the caveats wording --- esphome/platformio/extra_script.py | 15 +++++++-------- .../unit_tests/test_platformio_extra_script.py | 17 ++++++++++++++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/esphome/platformio/extra_script.py b/esphome/platformio/extra_script.py index af6061a059..6474f4ce06 100644 --- a/esphome/platformio/extra_script.py +++ b/esphome/platformio/extra_script.py @@ -18,8 +18,8 @@ Caveats ------- * Only the ``env.Append`` API is captured. ``env.Replace``, ``env.Prepend``, ``env.AddPreAction``, SCons file generators, and any - arbitrary I/O are silently no-ops. Scripts that depend on those will - produce incomplete output. + arbitrary I/O are no-ops, logged once per method. Scripts that depend + on those will produce incomplete output. * Running arbitrary Python from third-party libraries is a non-trivial trust decision. The shim does no sandboxing — anything in the script's process can run. Use only with libraries whose source you @@ -65,12 +65,11 @@ def apply_extra_script( library_root = source_path.resolve() script_path = (source_path / extra_script).resolve() if not script_path.is_relative_to(library_root): - _LOGGER.warning( - "Ignoring extraScript %s of library %s: it escapes the library directory", - extra_script, - component.name, + # More hostile than a missing script; must not be quieter than it + raise EsphomeError( + f"extraScript {extra_script} of library {component.name} escapes " + "the library directory" ) - return if not script_path.is_file(): # A declared-but-absent script is a broken or half-downloaded # package, not an unsupported script; PlatformIO fails on it too @@ -210,7 +209,7 @@ def run_extra_script( "__name__": "__pio_extra_script__", }, ) - except Exception as e: # noqa: BLE001 # pylint: disable=broad-exception-caught + except (SystemExit, Exception) as e: # noqa: BLE001 # pylint: disable=broad-exception-caught # Discard any partial capture: folding half a script's flags into the # build could produce wrong-output firmware that links cleanly. The # warning plus the resulting loud link error point back here. diff --git a/tests/unit_tests/test_platformio_extra_script.py b/tests/unit_tests/test_platformio_extra_script.py index 262ab860db..ea352a8561 100644 --- a/tests/unit_tests/test_platformio_extra_script.py +++ b/tests/unit_tests/test_platformio_extra_script.py @@ -117,8 +117,10 @@ def test_apply_extra_script_path_traversal_is_rejected(tmp_path): c.path = library_dir c.data = {"build": {"extraScript": "../evil.py"}} - apply_extra_script(c, board_mcu=lambda: "esp32", pio_platform="espressif32") + from esphome.core import EsphomeError + with pytest.raises(EsphomeError, match="escapes the library directory"): + apply_extra_script(c, board_mcu=lambda: "esp32", pio_platform="espressif32") # Nothing was folded into flags: the traversal was rejected before # the script could run. assert "flags" not in c.data["build"] @@ -297,3 +299,16 @@ def test_unsupported_env_method_warns_once(caplog) -> None: env.Replace(CC="clang") env.Replace(CC="gcc") assert caplog.text.count("env.Replace(...) is not supported") == 1 + + +def test_run_extra_script_sys_exit_is_best_effort(tmp_path, caplog) -> None: + """sys.exit() in a vendored script must not kill the esphome run.""" + from esphome.platformio.extra_script import run_extra_script + + script = tmp_path / "extra.py" + script.write_text("import sys\nsys.exit(3)\n") + result = run_extra_script( + script, library_dir=tmp_path, board_mcu="esp32", pio_platform="espressif32" + ) + assert result.libs == [] + assert "ignoring its output" in caplog.text