diff --git a/esphome/__main__.py b/esphome/__main__.py index 6d31a01eb9..06336c842c 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1264,8 +1264,8 @@ def _ensure_platform_packages_for_prebuilt_upload(config: ConfigType) -> int: return 0 _LOGGER.info( - "%s not found on this host; installing the PlatformIO %s " - "platform package so the upload can flash the prebuilt firmware...", + "%s not found on this host; configuring the PlatformIO %s " + "platform so the upload can flash the prebuilt firmware...", tool_label, CORE.target_platform, ) @@ -1275,7 +1275,10 @@ def _ensure_platform_packages_for_prebuilt_upload(config: ConfigType) -> int: from esphome import platformio_api - return platformio_api.run_pkg_install(config, CORE.verbose) + result = platformio_api.prepare_platform_for_upload(config, CORE.verbose) + # prepare_platform_for_upload returns str on capture_stdout=True or int + # on success/failure; in our call we don't capture stdout, so it's int. + return result if isinstance(result, int) else 0 def upload_program( diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index b3619c7be4..7f56ee86f7 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -84,20 +84,26 @@ def run_compile(config, verbose): return run_platformio_cli_run(config, verbose, *args) -def run_pkg_install(config, verbose) -> str | int: - """Install the PlatformIO platform + packages declared in platformio.ini - for ``CORE.name``'s env, without compiling. +def prepare_platform_for_upload(config, verbose) -> str | int: + """Configure the PlatformIO build environment for ``CORE.name`` without + compiling, so platform-specific flashing tools end up on disk. Used by ``esphome upload --prebuilt-dir`` on hosts that have never - compiled the target platform locally: the flash tools we need - (``ltchiptool`` for libretiny, ``picotool`` for RP2040) ship inside - the PlatformIO platform package, so we trigger a package install to - get them on disk without paying for a full compile. + compiled the target platform locally. ``pio pkg install`` alone isn't + enough on libretiny: the platform package downloads cleanly, but + ``ltchiptool`` lives in a platform-managed virtualenv at + ``~/.platformio/penv/.libretiny/`` that's only created by libretiny's + ``ConfigurePythonVenv`` SCons step, which runs during ``pio run`` (not + ``pio pkg install``). So we run ``pio run -t idedata`` instead: it + triggers SConscript -- creating the penv on libretiny, installing the + picotool tool package on RP2040 -- but skips the actual compile target + so this is much cheaper than a full build. + + The idedata JSON gets emitted to stdout as a side effect of the + target; we don't filter it out -- the install runs once per cold host + and the trailing JSON blob is harmless noise. """ - command = ["pkg", "install", "-e", CORE.name, "-d", str(CORE.build_path)] - if verbose: - command += ["-v"] - return run_platformio_cli(*command) + return run_platformio_cli_run(config, verbose, "-t", "idedata") def _run_idedata(config): diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index e1e7d6dd67..7162b72947 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -1621,8 +1621,8 @@ def test_upload_program_prebuilt_dir_installs_libretiny_platform_if_missing( patch("esphome.__main__.get_ltchiptool_path", return_value=None) as mock_find, patch("esphome.__main__.write_cpp", return_value=0) as mock_write_cpp, patch( - "esphome.platformio_api.run_pkg_install", return_value=0 - ) as mock_pkg_install, + "esphome.platformio_api.prepare_platform_for_upload", return_value=0 + ) as mock_prep, patch("esphome.__main__.upload_using_ltchiptool", return_value=0), ): exit_code, _ = upload_program({}, args, ["/dev/ttyUSB0"]) @@ -1630,7 +1630,7 @@ def test_upload_program_prebuilt_dir_installs_libretiny_platform_if_missing( assert exit_code == 0 mock_find.assert_called() mock_write_cpp.assert_called_once() - mock_pkg_install.assert_called_once() + mock_prep.assert_called_once() def test_upload_program_prebuilt_dir_skips_install_when_tool_present( @@ -1655,14 +1655,14 @@ def test_upload_program_prebuilt_dir_skips_install_when_tool_present( return_value=tmp_path / "ltchiptool", ), patch("esphome.__main__.write_cpp") as mock_write_cpp, - patch("esphome.platformio_api.run_pkg_install") as mock_pkg_install, + patch("esphome.platformio_api.prepare_platform_for_upload") as mock_prep, patch("esphome.__main__.upload_using_ltchiptool", return_value=0), ): exit_code, _ = upload_program({}, args, ["/dev/ttyUSB0"]) assert exit_code == 0 mock_write_cpp.assert_not_called() - mock_pkg_install.assert_not_called() + mock_prep.assert_not_called() def test_upload_program_prebuilt_dir_write_cpp_failure_aborts_upload( @@ -1684,14 +1684,14 @@ def test_upload_program_prebuilt_dir_write_cpp_failure_aborts_upload( with ( patch("esphome.__main__.get_ltchiptool_path", return_value=None), patch("esphome.__main__.write_cpp", return_value=3), - patch("esphome.platformio_api.run_pkg_install") as mock_pkg_install, + patch("esphome.platformio_api.prepare_platform_for_upload") as mock_prep, patch("esphome.__main__.upload_using_ltchiptool") as mock_upload, ): exit_code, host = upload_program({}, args, ["/dev/ttyUSB0"]) assert exit_code == 3 assert host is None - mock_pkg_install.assert_not_called() + mock_prep.assert_not_called() mock_upload.assert_not_called() @@ -1713,7 +1713,7 @@ def test_upload_program_prebuilt_dir_pkg_install_failure_aborts_upload( with ( patch("esphome.__main__._find_picotool", return_value=None), patch("esphome.__main__.write_cpp", return_value=0), - patch("esphome.platformio_api.run_pkg_install", return_value=2), + patch("esphome.platformio_api.prepare_platform_for_upload", return_value=2), patch("esphome.__main__.upload_using_picotool") as mock_upload, patch("esphome.__main__._rp2040_serial_reset_to_bootsel") as mock_reset, ):