diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index eeebec76f3..179346e072 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -791,7 +791,7 @@ def _prefetch_idf_tool_archives( # failure_reason: a message-less exception must not log blank _LOGGER.warning("Could not prefetch %s: %s", name, failure_reason(e)) _LOGGER.debug("Prefetch failure detail", exc_info=e) - if failures and len(failures) == len(entries): + if len(failures) == len(entries): # A systematic fault, not one flaky mirror: the resume # workaround (#17703) is off for this whole install _LOGGER.error( diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index fd3f3bd1b8..11d98986c9 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -776,16 +776,14 @@ def run_batch_downloads( try: fetch(checked) 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 - # A bar-frame write failure must not displace the download error - with suppress(Exception): - tracker(0) - return failure + # The cancelled arm exists for the tracker rollback below; the + # batch re-raises the interrupt, so the list is never returned + # after Ctrl-C. A bar-frame write failure must not displace the + # download error. + with suppress(Exception): + tracker(0) + return (name, err) + return None ex = ThreadPoolExecutor(max_workers=max_workers) try: @@ -1316,11 +1314,13 @@ def download_from_mirrors( ) # 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 + done = 0 + if progress is not None: + 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 diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 6c36abcb49..50e1408b13 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -446,6 +446,27 @@ def split_list_by_condition( return matched, non_matched +def _valid_manifest_shape(data: Any) -> bool: + """Whether the manifest has the dict shapes every backend dereferences. + + A bare json.load imposes no shape; validating once here means a + malformed third-party manifest fails by library name instead of a raw + TypeError/AttributeError in a backend. + """ + if not isinstance(data, dict): + return False + build = data.get("build", {}) + esphome_data = data.get(ESPHOME_DATA_KEY, {}) + return ( + isinstance(build, dict) + and isinstance(esphome_data, dict) + and isinstance(esphome_data.get(ESPHOME_DATA_LINK_FLAGS_KEY, []), list) + and isinstance(build.get("srcDir", ""), str) + and isinstance(build.get("includeDir", ""), str) + and isinstance(build.get("srcFilter", ""), (str, list)) + ) + + def check_library_data(data: dict, platform: str | None, framework: str): """ Check whether a library manifest is compatible with the target toolchain. @@ -1132,23 +1153,7 @@ def convert_libraries( f"library.properties in {source_dir}" ) - # A bare json.load imposes no shape; every backend dereferences - # these fields, so validate once here and name the library - malformed = not isinstance(component.data, dict) - if not malformed: - build = component.data.get("build", {}) - esphome_data = component.data.get(ESPHOME_DATA_KEY, {}) - malformed = ( - not isinstance(build, dict) - or not isinstance(esphome_data, dict) - or not isinstance( - esphome_data.get(ESPHOME_DATA_LINK_FLAGS_KEY, []), list - ) - or not isinstance(build.get("srcDir", ""), str) - or not isinstance(build.get("includeDir", ""), str) - or not isinstance(build.get("srcFilter", ""), (str, list)) - ) - if malformed: + if not _valid_manifest_shape(component.data): # Fail fast only for a library the user asked for; a defect # in an unrequested corner of the graph must not block the # build