Compare commits

...
2 changed files with 70 additions and 12 deletions
+32 -9
View File
@@ -371,14 +371,27 @@ def _registry_jobs(
return jobs, failed, installable return jobs, failed, installable
def _is_vcs_spec_uri(url: str) -> bool:
"""Whether pio's ``install_from_uri`` would clone this URI rather than
copy or download it (PackageSpec normalizes git URLs to ``git+``)."""
return not url.startswith(("file://", "symlink://", "http://", "https://"))
def _spec_name(spec: Any, url: str) -> str:
"""The spec's name; the URL basename fallback is defensive only
(PackageSpec derives a name from the URI itself)."""
return spec.name or url.split("#", 1)[0].rsplit("/", 1)[-1]
def _uri_jobs( def _uri_jobs(
manager: Any, specs: list[Any], seen: set[str] manager: Any, specs: list[Any], seen: set[str]
) -> tuple[list[tuple[str, int, Any]], int, list[tuple[str, Any]]]: ) -> tuple[list[tuple[str, int, Any]], int, list[tuple[str, Any]]]:
"""Jobs for direct-URL specs; a HEAD sizes each for the combined bar. """Jobs for direct-URL specs; a HEAD sizes each for the combined bar.
Also returns how many HEAD probes errored (an absent length is not an Also returns how many HEAD probes errored (an absent length is not an
error) and the ``(name, spec)`` pairs whose archives will be error) and the ``(name, spec)`` pairs to pre-install: downloaded
installable. archives, plus VCS specs, which have no archive -- the pre-install
itself clones them, in parallel instead of one at a time in pio run.
""" """
from esphome.net_retry import fetch_with_retry, http_request from esphome.net_retry import fetch_with_retry, http_request
@@ -386,13 +399,17 @@ def _uri_jobs(
installable: list[tuple[str, Any]] = [] installable: list[tuple[str, Any]] = []
for spec in specs: for spec in specs:
url = spec.uri url = spec.uri
if not url or not url.startswith(("http://", "https://")): if not url:
continue # git+/file specs are cloned/copied, not downloaded continue
if url.split("#", 1)[0].endswith(".git"): is_vcs = _is_vcs_spec_uri(url)
continue # bare-URL VCS spec; PlatformIO clones it if not is_vcs and not url.startswith(("http://", "https://")):
continue # file/symlink specs are copied in place by pio run
if manager.get_package(spec): if manager.get_package(spec):
continue continue
name = spec.name or url.rsplit("/", 1)[-1] name = _spec_name(spec, url)
if is_vcs:
installable.append((name, spec))
continue
# PlatformIO downloads URL specs with no checksum # PlatformIO downloads URL specs with no checksum
dl_path = Path(manager.compute_download_path(url, "")) dl_path = Path(manager.compute_download_path(url, ""))
if dl_path.is_file(): if dl_path.is_file():
@@ -406,7 +423,7 @@ def _uri_jobs(
if str(dl_path) in seen: if str(dl_path) in seen:
continue # another spec already claimed this .part continue # another spec already claimed this .part
seen.add(str(dl_path)) seen.add(str(dl_path))
candidates.append((spec.name, url, dl_path, spec)) candidates.append((name, url, dl_path, spec))
errors: list[str] = [] errors: list[str] = []
@@ -915,8 +932,14 @@ def _prefetch(build_dir: Path, env: str) -> None:
if name not in failed_names if name not in failed_names
} }
if to_install: if to_install:
# Clones first: they wait on the network, so they must not
# queue behind CPU-bound archive extractions
ordered = sorted(
to_install.values(),
key=lambda entry: not ((url := entry[1].uri) and _is_vcs_spec_uri(url)),
)
try: try:
_preinstall(mgr, list(to_install.values())) _preinstall(mgr, ordered)
if is_platform: if is_platform:
platform_packages_installed = True platform_packages_installed = True
except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught
+38 -3
View File
@@ -661,7 +661,8 @@ def test_registry_jobs_one_bad_spec_keeps_the_rest(tmp_path: Path) -> None:
def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None: def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None:
"""HEAD sizes direct-URL specs; git and unreachable URLs are skipped.""" """HEAD sizes direct-URL specs; VCS specs skip the download but are
still installable (the pre-install clones them in parallel)."""
m = _fake_manager(tmp_path) m = _fake_manager(tmp_path)
resp = MagicMock() resp = MagicMock()
resp.headers = {"content-length": "2222"} resp.headers = {"content-length": "2222"}
@@ -671,14 +672,13 @@ def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None:
[ [
_FakeSpec(uri="https://x/big.zip", name="big", custom_name=True), _FakeSpec(uri="https://x/big.zip", name="big", custom_name=True),
_FakeSpec(uri="git+https://x/repo.git", name="repo"), _FakeSpec(uri="git+https://x/repo.git", name="repo"),
_FakeSpec(uri="https://x/repo.git#v1", name="barevcs"),
_FakeSpec(name="registry"), _FakeSpec(name="registry"),
], ],
set(), set(),
) )
assert failed == 0 assert failed == 0
assert [(n, s) for n, s, _ in jobs] == [("big", 2222)] assert [(n, s) for n, s, _ in jobs] == [("big", 2222)]
assert [n for n, _ in installable] == ["big"] assert [n for n, _ in installable] == ["repo", "big"]
# a successful HEAD with no Content-Length is a clean skip # a successful HEAD with no Content-Length is a clean skip
resp.headers = {} resp.headers = {}
with patch("esphome.net_retry.http_request", return_value=resp): with patch("esphome.net_retry.http_request", return_value=resp):
@@ -687,6 +687,35 @@ def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None:
) == ([], 0, []) ) == ([], 0, [])
def test_uri_jobs_vcs_specs_installable_without_probe(tmp_path: Path) -> None:
"""VCS specs never probe the network here (there is no archive); an
uninstalled one is handed to the pre-install, an installed one and
file/symlink specs are skipped."""
m = _fake_manager(tmp_path)
with patch("esphome.net_retry.http_request") as mock_head:
jobs, failed, installable = pf._uri_jobs(
m,
[
_FakeSpec(uri="git+https://x/tool.git#1.0", name="tool"),
_FakeSpec(uri="hg+https://x/old", name="mercurial"),
# Name falls back to the URL basename, fragment excluded
_FakeSpec(uri="git+https://x/noname#v2", name=None),
_FakeSpec(uri="file:///local/dir", name="local"),
_FakeSpec(uri="symlink:///local/dir", name="link"),
],
set(),
)
mock_head.assert_not_called()
assert (jobs, failed) == ([], 0)
assert [n for n, _ in installable] == ["tool", "mercurial", "noname"]
m.get_package.return_value = object() # already installed: warm and silent
with patch("esphome.net_retry.http_request"):
assert pf._uri_jobs(
m, [_FakeSpec(uri="git+https://x/tool.git#1.0", name="tool")], set()
) == ([], 0, [])
def test_uri_jobs_head_failure_counts_as_unresolved( def test_uri_jobs_head_failure_counts_as_unresolved(
tmp_path: Path, caplog: pytest.LogCaptureFixture tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
@@ -1890,3 +1919,9 @@ def test_platformio_private_api_contract() -> None:
derived = PackageSpec("https://x/y/archive/master.zip") derived = PackageSpec("https://x/y/archive/master.zip")
assert derived.name and not derived.has_custom_name() assert derived.name and not derived.has_custom_name()
assert PackageSpec("Foo=https://x/y/archive/master.zip").has_custom_name() assert PackageSpec("Foo=https://x/y/archive/master.zip").has_custom_name()
# _is_vcs_spec_uri relies on bare .git URLs normalizing to git+, on
# both parse paths (raw string, and requirements= for platform tools)
assert PackageSpec("https://github.com/x/y.git#v1").uri.startswith("git+")
assert PackageSpec(
owner="o", name="tool-x", requirements="https://github.com/x/y.git"
).uri.startswith("git+")