mirror of
https://github.com/esphome/esphome.git
synced 2026-09-08 22:08:49 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ff9f3f2af | ||
|
|
fbabe13dc8 | ||
|
|
bc3ffe19e8 |
@@ -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
|
||||||
|
|||||||
@@ -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+")
|
||||||
|
|||||||
Reference in New Issue
Block a user