1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00
This commit is contained in:
J. Nick Koston
2026-02-06 14:23:36 +01:00
parent 136606a435
commit 475ece94ac

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import contextlib
from contextlib import contextmanager
from dataclasses import dataclass
from enum import StrEnum
import io
import json
import logging
@@ -20,6 +21,15 @@ import tarfile
from typing import Any
from esphome import const, yaml_util
from esphome.const import (
CONF_ESPHOME,
CONF_EXTERNAL_COMPONENTS,
CONF_INCLUDES,
CONF_INCLUDES_C,
CONF_PATH,
CONF_SOURCE,
CONF_TYPE,
)
from esphome.core import CORE, EsphomeError
_LOGGER = logging.getLogger(__name__)
@@ -32,6 +42,17 @@ MAX_DECOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB
_PRESERVE_DIRS = (".esphome", ".pioenvs", ".pio")
_BUNDLE_STAGING_DIR = ".bundle_staging"
class ManifestKey(StrEnum):
"""Keys used in bundle manifest.json."""
MANIFEST_VERSION = "manifest_version"
ESPHOME_VERSION = "esphome_version"
CONFIG_FILENAME = "config_filename"
FILES = "files"
HAS_SECRETS = "has_secrets"
# String prefixes that are never local file paths
_NON_PATH_PREFIXES = ("http://", "https://", "ftp://", "mdi:", "<")
@@ -283,8 +304,8 @@ class ConfigBundleCreator:
# --- Core ESPHome concepts needing explicit handling ---
# esphome.includes / includes_c - can be relative paths and directories
esphome_conf = config.get(const.CONF_ESPHOME, {})
for include_path in esphome_conf.get("includes", []):
esphome_conf = config.get(CONF_ESPHOME, {})
for include_path in esphome_conf.get(CONF_INCLUDES, []):
resolved = _resolve_include_path(include_path)
if resolved is None:
continue
@@ -292,27 +313,22 @@ class ConfigBundleCreator:
self._add_directory(resolved)
else:
self._add_file(resolved)
for include_path in esphome_conf.get("includes_c", []):
for include_path in esphome_conf.get(CONF_INCLUDES_C, []):
resolved = _resolve_include_path(include_path)
if resolved is not None:
self._add_file(resolved)
# external_components with source: local - directories
for ext_conf in config.get("external_components", []):
source = ext_conf.get("source", {})
if source.get("type") == "local":
path = source.get("path")
for ext_conf in config.get(CONF_EXTERNAL_COMPONENTS, []):
source = ext_conf.get(CONF_SOURCE, {})
if source.get(CONF_TYPE) == "local":
path = source.get(CONF_PATH)
if path:
p = Path(path)
if not p.is_absolute():
p = CORE.relative_config_path(p)
self._add_directory(p)
# custom_components/ directory if present (deprecated but still supported)
custom_dir = self._config_dir / "custom_components"
if custom_dir.is_dir():
self._add_directory(custom_dir)
def _walk_config_for_files(self, obj: Any) -> None:
"""Recursively walk the config dict looking for file path references."""
if isinstance(obj, dict):
@@ -387,11 +403,11 @@ class ConfigBundleCreator:
) -> dict[str, Any]:
"""Build the manifest.json content."""
return {
"manifest_version": CURRENT_MANIFEST_VERSION,
"esphome_version": const.__version__,
"config_filename": self._config_path.name,
"files": [f.path for f in files],
"has_secrets": has_secrets,
ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION,
ManifestKey.ESPHOME_VERSION: const.__version__,
ManifestKey.CONFIG_FILENAME: self._config_path.name,
ManifestKey.FILES: [f.path for f in files],
ManifestKey.HAS_SECRETS: has_secrets,
}
@staticmethod
@@ -441,7 +457,7 @@ def extract_bundle(
except tarfile.TarError as err:
raise EsphomeError(f"Failed to extract bundle: {err}") from err
config_filename = manifest["config_filename"]
config_filename = manifest[ManifestKey.CONFIG_FILENAME]
config_path = target_dir / config_filename
if not config_path.is_file():
raise EsphomeError(
@@ -472,11 +488,11 @@ def read_bundle_manifest(bundle_path: Path) -> BundleManifest:
raise EsphomeError(f"Failed to read bundle: {err}") from err
return BundleManifest(
manifest_version=manifest["manifest_version"],
esphome_version=manifest.get("esphome_version", "unknown"),
config_filename=manifest["config_filename"],
files=manifest.get("files", []),
has_secrets=manifest.get("has_secrets", False),
manifest_version=manifest[ManifestKey.MANIFEST_VERSION],
esphome_version=manifest.get(ManifestKey.ESPHOME_VERSION, "unknown"),
config_filename=manifest[ManifestKey.CONFIG_FILENAME],
files=manifest.get(ManifestKey.FILES, []),
has_secrets=manifest.get(ManifestKey.HAS_SECRETS, False),
)
@@ -498,7 +514,7 @@ def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict[str, Any]:
raise EsphomeError(f"Invalid bundle: malformed manifest.json: {err}") from err
# Version check
version = manifest.get("manifest_version")
version = manifest.get(ManifestKey.MANIFEST_VERSION)
if version is None:
raise EsphomeError("Invalid bundle: manifest.json missing 'manifest_version'")
if not isinstance(version, int) or version < 1:
@@ -513,7 +529,7 @@ def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict[str, Any]:
)
# Required fields
if "config_filename" not in manifest:
if ManifestKey.CONFIG_FILENAME not in manifest:
raise EsphomeError("Invalid bundle: manifest.json missing 'config_filename'")
return manifest