Compare commits

...
6 changed files with 93 additions and 2 deletions
+5 -1
View File
@@ -587,11 +587,15 @@ def _make_registry_client() -> Any:
elsewhere, not by the PlatformIO registry. elsewhere, not by the PlatformIO registry.
""" """
from platformio.package.manager._registry import PackageManagerRegistryMixin from platformio.package.manager._registry import PackageManagerRegistryMixin
from platformio.registry.client import RegistryClient
class _Registry(PackageManagerRegistryMixin): class _Registry(PackageManagerRegistryMixin):
def __init__(self) -> None: def __init__(self) -> None:
self._registry_client = None
self.pkg_type = "library" self.pkg_type = "library"
self._registry_client = RegistryClient()
# The probe sleeps ~500 ms per lookup (see runner.patch_registry_private_packages);
# instance-level so the ESPHome process never patches PlatformIO's class
self._registry_client.allowed_private_packages = lambda: False
@staticmethod @staticmethod
def is_system_compatible(value: Any, custom_system: Any = None) -> bool: def is_system_compatible(value: Any, custom_system: Any = None) -> bool:
+2
View File
@@ -922,8 +922,10 @@ def main(argv: list[str]) -> int:
"""Subprocess entry point: ``prefetch <build_dir> <env_name>``.""" """Subprocess entry point: ``prefetch <build_dir> <env_name>``."""
from esphome.core import CORE from esphome.core import CORE
from esphome.log import setup_log from esphome.log import setup_log
from esphome.platformio.runner import patch_registry_private_packages
signal.signal(signal.SIGTERM, _sigterm) signal.signal(signal.SIGTERM, _sigterm)
patch_registry_private_packages()
raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL") raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL")
try: try:
level = int(raw_level) if raw_level is not None else logging.INFO level = int(raw_level) if raw_level is not None else logging.INFO
+13 -1
View File
@@ -2,7 +2,8 @@
Invoked via ``python -m esphome.platformio.runner`` instead of Invoked via ``python -m esphome.platformio.runner`` instead of
``python -m platformio`` so that the patches (incremental rebuild ``python -m platformio`` so that the patches (incremental rebuild
preservation, download retries) apply inside the subprocess. Running preservation, download retries, skipping the private-package probe) apply
inside the subprocess. Running
PlatformIO in a subprocess keeps its ``sys.path`` mutations and other PlatformIO in a subprocess keeps its ``sys.path`` mutations and other
global state from leaking into the ESPHome process. global state from leaking into the ESPHome process.
""" """
@@ -105,6 +106,16 @@ def patch_file_downloader() -> None:
FileDownloader.__init__ = patched_init FileDownloader.__init__ = patched_init
def patch_registry_private_packages() -> None:
"""Skip PlatformIO's private-package probe; it sleeps ~500 ms per lookup.
ESPHome never uses private packages, so the answer is always False.
"""
from platformio.registry.client import RegistryClient
RegistryClient.allowed_private_packages = staticmethod(lambda: False) # type: ignore[method-assign]
_IGNORE_LIB_WARNINGS = "(?:Hash|Update)" _IGNORE_LIB_WARNINGS = "(?:Hash|Update)"
# Regex patterns matched against each line of PlatformIO output. Lines that # Regex patterns matched against each line of PlatformIO output. Lines that
# match are dropped by RedirectText before they reach the parent process. # match are dropped by RedirectText before they reach the parent process.
@@ -152,6 +163,7 @@ FILTER_PLATFORMIO_LINES = [
def main() -> int: def main() -> int:
patch_structhash() patch_structhash()
patch_file_downloader() patch_file_downloader()
patch_registry_private_packages()
# Wrap stdout/stderr with RedirectText before PlatformIO runs: # Wrap stdout/stderr with RedirectText before PlatformIO runs:
# #
@@ -7,6 +7,7 @@ exercised in their own test modules)."""
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
from unittest.mock import Mock
import pytest import pytest
@@ -228,6 +229,24 @@ def test_resolve_registry_version_raises_without_pkg_file(monkeypatch):
_resolve_registry_version("owner", "pkg", set()) _resolve_registry_version("owner", "pkg", set())
def test_make_registry_client_skips_private_package_probe(monkeypatch):
"""Our client answers the probe locally without patching PlatformIO's class."""
from platformio.account.client import AccountClient
from platformio.registry.client import RegistryClient
pio_probe = RegistryClient.__dict__["allowed_private_packages"]
monkeypatch.setattr(
AccountClient,
"get_account_info",
Mock(side_effect=AssertionError("account probe must not run")),
)
client = lib._make_registry_client().get_registry_client_instance()
assert client.allowed_private_packages() is False
assert RegistryClient.__dict__["allowed_private_packages"] is pio_probe
def _patch_registry_resolve(monkeypatch: pytest.MonkeyPatch) -> None: def _patch_registry_resolve(monkeypatch: pytest.MonkeyPatch) -> None:
"""Stub the registry lookup so tests never touch the network.""" """Stub the registry lookup so tests never touch the network."""
monkeypatch.setattr( monkeypatch.setattr(
@@ -1151,6 +1151,20 @@ def test_main_runs_prefetch(tmp_path: Path) -> None:
mock_prefetch.assert_called_once_with(tmp_path, "testenv") mock_prefetch.assert_called_once_with(tmp_path, "testenv")
def test_main_skips_private_package_probe_before_prefetch(tmp_path: Path) -> None:
"""The registry probe patch is applied before any package manager runs."""
order: list[str] = []
with (
patch.object(pf, "_prefetch", side_effect=lambda *_: order.append("prefetch")),
patch(
"esphome.platformio.runner.patch_registry_private_packages",
side_effect=lambda: order.append("patch"),
),
):
assert pf.main([str(tmp_path), "testenv"]) == 0
assert order == ["patch", "prefetch"]
def test_main_bad_argv_is_a_distinct_exit( def test_main_bad_argv_is_a_distinct_exit(
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
@@ -6,7 +6,9 @@ from collections.abc import Callable
import io import io
import sys import sys
from types import ModuleType from types import ModuleType
from unittest.mock import Mock
from platformio.registry.client import RegistryClient
import pytest import pytest
from esphome.platformio import runner from esphome.platformio import runner
@@ -30,6 +32,7 @@ def _prepare_main(
monkeypatch.setattr(sys, "stderr", stream) monkeypatch.setattr(sys, "stderr", stream)
monkeypatch.setattr(runner, "patch_structhash", lambda: None) monkeypatch.setattr(runner, "patch_structhash", lambda: None)
monkeypatch.setattr(runner, "patch_file_downloader", lambda: None) monkeypatch.setattr(runner, "patch_file_downloader", lambda: None)
monkeypatch.setattr(runner, "patch_registry_private_packages", lambda: None)
platformio = ModuleType("platformio") platformio = ModuleType("platformio")
platformio_main = ModuleType("platformio.__main__") platformio_main = ModuleType("platformio.__main__")
@@ -91,3 +94,40 @@ def test_main_still_filters_a_drained_partial_line(
assert runner.main() == 0 assert runner.main() == 0
assert buf.getvalue() == b"" assert buf.getvalue() == b""
def test_main_applies_registry_private_packages_patch(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The probe is patched before PlatformIO runs."""
order: list[str] = []
_prepare_main(monkeypatch, lambda: order.append("pio") or 0)
monkeypatch.setattr(
runner, "patch_registry_private_packages", lambda: order.append("patch")
)
assert runner.main() == 0
assert order == ["patch", "pio"]
# Snapshot PlatformIO's own probe at import, before any test can patch it
_PIO_PROBE = RegistryClient.__dict__["allowed_private_packages"]
def test_patch_registry_private_packages_skips_account_probe(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Answers False without touching the account client."""
from platformio.account.client import AccountClient
monkeypatch.setattr(RegistryClient, "allowed_private_packages", _PIO_PROBE)
monkeypatch.setattr(
AccountClient,
"get_account_info",
Mock(side_effect=AssertionError("account probe must not run")),
)
runner.patch_registry_private_packages()
assert RegistryClient.allowed_private_packages() is False
assert RegistryClient().allowed_private_packages() is False