Merge branch 'esp8266-native-build-surgery' into esp8266-native-toolchain-plumbing

This commit is contained in:
J. Nick Koston
2026-08-23 16:54:57 -05:00
4 changed files with 59 additions and 19 deletions
+22 -17
View File
@@ -773,11 +773,10 @@ def run_batch_downloads(
try:
fetch(checked)
except _BatchDownloadCancelled as err:
# Reported like a failure: an abandoned job must never read as
# a completed download if a caller sees the list after Ctrl-C
failure = (name, err)
except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught
except (_BatchDownloadCancelled, Exception) as err: # noqa: BLE001 # pylint: disable=broad-exception-caught
# A cancelled job reports like a failure: an abandoned download
# must never read as completed if a caller sees the list after
# Ctrl-C
failure = (name, err)
else:
return None
@@ -824,12 +823,12 @@ class _BatchDownloadProgress:
self._lock = threading.Lock()
def tracker(self) -> Callable[[int], None]:
if self._bar is None:
return lambda _: None
last = 0
def update(done: int) -> None:
nonlocal last
if self._bar is None:
return
with self._lock:
self._sum += done - last
last = done
@@ -859,13 +858,7 @@ class _BatchDownloadProgress:
class _EndRow(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
with lock:
if (
the_bar.last_progress is not None
and the_bar.last_progress != 100
):
the_bar.done()
# Force the next tick to redraw the frame
the_bar.last_progress = None
the_bar.interrupt()
return True
end_row = _EndRow()
@@ -879,6 +872,11 @@ class _BatchDownloadProgress:
handler.removeFilter(end_row)
def _part_path(dest: Path) -> Path:
"""The in-progress sidecar ``download_with_resume`` streams into."""
return dest.with_name(dest.name + ".part")
def _cancellable_sleep(
delay: float, progress: Callable[[int], None] | None, done: int
) -> None:
@@ -939,7 +937,7 @@ def download_with_resume(
ensure_happy_eyeballs()
dest = Path(dest)
part = dest.with_name(dest.name + ".part")
part = _part_path(dest)
meta = part.with_name(part.name + ".meta")
dest.parent.mkdir(parents=True, exist_ok=True)
last_error: Exception | None = None
@@ -1062,7 +1060,7 @@ def download_with_resume(
) from last_error
def _failure_reason(e: Exception) -> str:
def _failure_reason(e: BaseException) -> str:
"""Format a download exception for the aggregated error message.
``requests`` appends " for url: <url>" to HTTP errors; the URL is already
@@ -1333,7 +1331,14 @@ def download_from_mirrors(
sweep + 1,
_MIRROR_SWEEP_ATTEMPTS,
)
_cancellable_sleep(delay, progress, 0)
# Tick with the bytes already on disk so a combined bar holds
# steady during the backoff instead of rewinding to zero
if f is not None:
done = f.tell()
else:
part = _part_path(path_target)
done = part.stat().st_size if part.is_file() else 0
_cancellable_sleep(delay, progress, done)
# 4. Report every attempted URL if all mirrors failed. failures spans
# all sweeps (deduplicated by URL and reason), so neither an early
+10
View File
@@ -735,6 +735,16 @@ class ProgressBar:
sys.stderr.write("\n")
sys.stderr.flush()
def interrupt(self) -> None:
"""End a mid-row frame so the next write starts on its own row.
The next ``update()`` redraws the bar; a finished bar stays done.
"""
if self.last_progress == 100:
return
self.done()
self.last_progress = None
def docs_url(path: str) -> str:
"""Return the URL to the documentation for a given path."""
+6 -2
View File
@@ -77,6 +77,10 @@ SRC_FILE_EXTENSIONS = list(SOURCE_KIND_FOR_SUFFIX)
DOMAIN = "pio_components"
# Marks a cache dir whose archive finished extracting; a missing marker
# means a torn extraction that must be redone
_EXTRACTED_MARKER = ".esphome_extracted"
ESPHOME_DATA_KEY = "ESPHOME"
ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE"
@@ -118,7 +122,7 @@ class URLSource(Source):
def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool:
"""Whether a completed extraction already exists for this source."""
return (
self._cache_dir(dir_suffix, salt, namespace) / ".esphome_extracted"
self._cache_dir(dir_suffix, salt, namespace) / _EXTRACTED_MARKER
).is_file()
def download(
@@ -135,7 +139,7 @@ class URLSource(Source):
# extraction is correctly detected and re-run on the next invocation,
# and lets us extract directly into ``path`` — avoiding a
# post-extraction rename that races with antivirus on Windows.
extracted_marker = path / ".esphome_extracted"
extracted_marker = path / _EXTRACTED_MARKER
if not extracted_marker.is_file() or force:
rmdir(path, msg=f"Clean up library directory {path}")
@@ -1703,6 +1703,27 @@ class TestDownloadFromMirrors:
assert mock_get.call_count == 2
mock_sleep.assert_called_once_with(2)
def test_backoff_tick_reports_partial_bytes(self, tmp_path: Path) -> None:
"""The backoff tick carries the bytes already in the part file, so a
combined bar holds steady instead of rewinding to zero."""
dest = tmp_path / "out.bin"
(tmp_path / "out.bin.part").write_bytes(b"12345")
ticks: list[int] = []
with (
patch(
"requests.get",
side_effect=[
req.ConnectionError("down"),
_mock_response(b"data"),
],
),
patch("esphome.framework_helpers._cancellable_sleep") as mock_sleep,
):
download_from_mirrors(
["https://mirror1.com/f"], {}, dest, progress=ticks.append
)
assert mock_sleep.call_args == call(2, ticks.append, 5)
def test_permanent_failure_does_not_retry_sweep(self, tmp_path: Path) -> None:
"""An HTTP 404 will not heal on its own; fail after a single pass."""
with (