1
0
mirror of https://github.com/home-assistant/supervisor.git synced 2025-12-20 02:18:59 +00:00

Improve CpuArch type safety across codebase (#6372)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Stefan Agner
2025-12-01 19:56:05 +01:00
committed by GitHub
parent ba82eb0620
commit fa490210cd
10 changed files with 73 additions and 53 deletions

View File

@@ -20,6 +20,7 @@ from ..const import (
FILE_SUFFIX_CONFIGURATION, FILE_SUFFIX_CONFIGURATION,
META_ADDON, META_ADDON,
SOCKET_DOCKER, SOCKET_DOCKER,
CpuArch,
) )
from ..coresys import CoreSys, CoreSysAttributes from ..coresys import CoreSys, CoreSysAttributes
from ..docker.const import DOCKER_HUB from ..docker.const import DOCKER_HUB
@@ -67,7 +68,7 @@ class AddonBuild(FileConfiguration, CoreSysAttributes):
raise RuntimeError() raise RuntimeError()
@cached_property @cached_property
def arch(self) -> str: def arch(self) -> CpuArch:
"""Return arch of the add-on.""" """Return arch of the add-on."""
return self.sys_arch.match([self.addon.arch]) return self.sys_arch.match([self.addon.arch])

View File

@@ -87,6 +87,7 @@ from ..const import (
AddonBootConfig, AddonBootConfig,
AddonStage, AddonStage,
AddonStartup, AddonStartup,
CpuArch,
) )
from ..coresys import CoreSys from ..coresys import CoreSys
from ..docker.const import Capabilities from ..docker.const import Capabilities
@@ -548,7 +549,7 @@ class AddonModel(JobGroup, ABC):
return self.data.get(ATTR_MACHINE, []) return self.data.get(ATTR_MACHINE, [])
@property @property
def arch(self) -> str: def arch(self) -> CpuArch:
"""Return architecture to use for the addon's image.""" """Return architecture to use for the addon's image."""
if ATTR_IMAGE in self.data: if ATTR_IMAGE in self.data:
return self.sys_arch.match(self.data[ATTR_ARCH]) return self.sys_arch.match(self.data[ATTR_ARCH])

View File

@@ -4,6 +4,7 @@ import logging
from pathlib import Path from pathlib import Path
import platform import platform
from .const import CpuArch
from .coresys import CoreSys, CoreSysAttributes from .coresys import CoreSys, CoreSysAttributes
from .exceptions import ConfigurationFileError, HassioArchNotFound from .exceptions import ConfigurationFileError, HassioArchNotFound
from .utils.json import read_json_file from .utils.json import read_json_file
@@ -12,38 +13,40 @@ _LOGGER: logging.Logger = logging.getLogger(__name__)
ARCH_JSON: Path = Path(__file__).parent.joinpath("data/arch.json") ARCH_JSON: Path = Path(__file__).parent.joinpath("data/arch.json")
MAP_CPU = { MAP_CPU: dict[str, CpuArch] = {
"armv7": "armv7", "armv7": CpuArch.ARMV7,
"armv6": "armhf", "armv6": CpuArch.ARMHF,
"armv8": "aarch64", "armv8": CpuArch.AARCH64,
"aarch64": "aarch64", "aarch64": CpuArch.AARCH64,
"i686": "i386", "i686": CpuArch.I386,
"x86_64": "amd64", "x86_64": CpuArch.AMD64,
} }
class CpuArch(CoreSysAttributes): class CpuArchManager(CoreSysAttributes):
"""Manage available architectures.""" """Manage available architectures."""
def __init__(self, coresys: CoreSys) -> None: def __init__(self, coresys: CoreSys) -> None:
"""Initialize CPU Architecture handler.""" """Initialize CPU Architecture handler."""
self.coresys = coresys self.coresys = coresys
self._supported_arch: list[str] = [] self._supported_arch: list[CpuArch] = []
self._supported_set: set[str] = set() self._supported_set: set[CpuArch] = set()
self._default_arch: str self._default_arch: CpuArch
@property @property
def default(self) -> str: def default(self) -> CpuArch:
"""Return system default arch.""" """Return system default arch."""
return self._default_arch return self._default_arch
@property @property
def supervisor(self) -> str: def supervisor(self) -> CpuArch:
"""Return supervisor arch.""" """Return supervisor arch."""
return self.sys_supervisor.arch or self._default_arch if self.sys_supervisor.arch:
return CpuArch(self.sys_supervisor.arch)
return self._default_arch
@property @property
def supported(self) -> list[str]: def supported(self) -> list[CpuArch]:
"""Return support arch by CPU/Machine.""" """Return support arch by CPU/Machine."""
return self._supported_arch return self._supported_arch
@@ -65,7 +68,7 @@ class CpuArch(CoreSysAttributes):
return return
# Use configs from arch.json # Use configs from arch.json
self._supported_arch.extend(arch_data[self.sys_machine]) self._supported_arch.extend(CpuArch(a) for a in arch_data[self.sys_machine])
self._default_arch = self.supported[0] self._default_arch = self.supported[0]
# Make sure native support is in supported list # Make sure native support is in supported list
@@ -78,14 +81,14 @@ class CpuArch(CoreSysAttributes):
"""Return True if there is a supported arch by this platform.""" """Return True if there is a supported arch by this platform."""
return not self._supported_set.isdisjoint(arch_list) return not self._supported_set.isdisjoint(arch_list)
def match(self, arch_list: list[str]) -> str: def match(self, arch_list: list[str]) -> CpuArch:
"""Return best match for this CPU/Platform.""" """Return best match for this CPU/Platform."""
for self_arch in self.supported: for self_arch in self.supported:
if self_arch in arch_list: if self_arch in arch_list:
return self_arch return self_arch
raise HassioArchNotFound() raise HassioArchNotFound()
def detect_cpu(self) -> str: def detect_cpu(self) -> CpuArch:
"""Return the arch type of local CPU.""" """Return the arch type of local CPU."""
cpu = platform.machine() cpu = platform.machine()
for check, value in MAP_CPU.items(): for check, value in MAP_CPU.items():
@@ -96,9 +99,10 @@ class CpuArch(CoreSysAttributes):
"Unknown CPU architecture %s, falling back to Supervisor architecture.", "Unknown CPU architecture %s, falling back to Supervisor architecture.",
cpu, cpu,
) )
return self.sys_supervisor.arch return CpuArch(self.sys_supervisor.arch)
_LOGGER.warning( _LOGGER.warning(
"Unknown CPU architecture %s, assuming CPU architecture equals Supervisor architecture.", "Unknown CPU architecture %s, assuming CPU architecture equals Supervisor architecture.",
cpu, cpu,
) )
return cpu # Return the cpu string as-is, wrapped in CpuArch (may fail if invalid)
return CpuArch(cpu)

View File

@@ -13,7 +13,7 @@ from colorlog import ColoredFormatter
from .addons.manager import AddonManager from .addons.manager import AddonManager
from .api import RestAPI from .api import RestAPI
from .arch import CpuArch from .arch import CpuArchManager
from .auth import Auth from .auth import Auth
from .backups.manager import BackupManager from .backups.manager import BackupManager
from .bus import Bus from .bus import Bus
@@ -71,7 +71,7 @@ async def initialize_coresys() -> CoreSys:
coresys.jobs = await JobManager(coresys).load_config() coresys.jobs = await JobManager(coresys).load_config()
coresys.core = await Core(coresys).post_init() coresys.core = await Core(coresys).post_init()
coresys.plugins = await PluginManager(coresys).load_config() coresys.plugins = await PluginManager(coresys).load_config()
coresys.arch = CpuArch(coresys) coresys.arch = CpuArchManager(coresys)
coresys.auth = await Auth(coresys).load_config() coresys.auth = await Auth(coresys).load_config()
coresys.updater = await Updater(coresys).load_config() coresys.updater = await Updater(coresys).load_config()
coresys.api = RestAPI(coresys) coresys.api = RestAPI(coresys)

View File

@@ -29,7 +29,7 @@ from .const import (
if TYPE_CHECKING: if TYPE_CHECKING:
from .addons.manager import AddonManager from .addons.manager import AddonManager
from .api import RestAPI from .api import RestAPI
from .arch import CpuArch from .arch import CpuArchManager
from .auth import Auth from .auth import Auth
from .backups.manager import BackupManager from .backups.manager import BackupManager
from .bus import Bus from .bus import Bus
@@ -78,7 +78,7 @@ class CoreSys:
# Internal objects pointers # Internal objects pointers
self._docker: DockerAPI | None = None self._docker: DockerAPI | None = None
self._core: Core | None = None self._core: Core | None = None
self._arch: CpuArch | None = None self._arch: CpuArchManager | None = None
self._auth: Auth | None = None self._auth: Auth | None = None
self._homeassistant: HomeAssistant | None = None self._homeassistant: HomeAssistant | None = None
self._supervisor: Supervisor | None = None self._supervisor: Supervisor | None = None
@@ -266,17 +266,17 @@ class CoreSys:
self._plugins = value self._plugins = value
@property @property
def arch(self) -> CpuArch: def arch(self) -> CpuArchManager:
"""Return CpuArch object.""" """Return CpuArchManager object."""
if self._arch is None: if self._arch is None:
raise RuntimeError("CpuArch not set!") raise RuntimeError("CpuArchManager not set!")
return self._arch return self._arch
@arch.setter @arch.setter
def arch(self, value: CpuArch) -> None: def arch(self, value: CpuArchManager) -> None:
"""Set a CpuArch object.""" """Set a CpuArchManager object."""
if self._arch: if self._arch:
raise RuntimeError("CpuArch already set!") raise RuntimeError("CpuArchManager already set!")
self._arch = value self._arch = value
@property @property
@@ -733,8 +733,8 @@ class CoreSysAttributes:
return self.coresys.plugins return self.coresys.plugins
@property @property
def sys_arch(self) -> CpuArch: def sys_arch(self) -> CpuArchManager:
"""Return CpuArch object.""" """Return CpuArchManager object."""
return self.coresys.arch return self.coresys.arch
@property @property

View File

@@ -52,7 +52,7 @@ from .stats import DockerStats
_LOGGER: logging.Logger = logging.getLogger(__name__) _LOGGER: logging.Logger = logging.getLogger(__name__)
MAP_ARCH: dict[CpuArch | str, str] = { MAP_ARCH: dict[CpuArch, str] = {
CpuArch.ARMV7: "linux/arm/v7", CpuArch.ARMV7: "linux/arm/v7",
CpuArch.ARMHF: "linux/arm/v6", CpuArch.ARMHF: "linux/arm/v6",
CpuArch.AARCH64: "linux/arm64", CpuArch.AARCH64: "linux/arm64",
@@ -366,7 +366,7 @@ class DockerInterface(JobGroup, ABC):
if not image: if not image:
raise ValueError("Cannot pull without an image!") raise ValueError("Cannot pull without an image!")
image_arch = str(arch) if arch else self.sys_arch.supervisor image_arch = arch or self.sys_arch.supervisor
listener: EventListener | None = None listener: EventListener | None = None
_LOGGER.info("Downloading docker image %s with tag %s.", image, version) _LOGGER.info("Downloading docker image %s with tag %s.", image, version)
@@ -603,9 +603,7 @@ class DockerInterface(JobGroup, ABC):
expected_cpu_arch: CpuArch | None = None, expected_cpu_arch: CpuArch | None = None,
) -> None: ) -> None:
"""Check we have expected image with correct arch.""" """Check we have expected image with correct arch."""
expected_image_cpu_arch = ( arch = expected_cpu_arch or self.sys_arch.supervisor
str(expected_cpu_arch) if expected_cpu_arch else self.sys_arch.supervisor
)
image_name = f"{expected_image}:{version!s}" image_name = f"{expected_image}:{version!s}"
if self.image == expected_image: if self.image == expected_image:
try: try:
@@ -623,7 +621,7 @@ class DockerInterface(JobGroup, ABC):
# If we have an image and its the right arch, all set # If we have an image and its the right arch, all set
# It seems that newer Docker version return a variant for arm64 images. # It seems that newer Docker version return a variant for arm64 images.
# Make sure we match linux/arm64 and linux/arm64/v8. # Make sure we match linux/arm64 and linux/arm64/v8.
expected_image_arch = MAP_ARCH[expected_image_cpu_arch] expected_image_arch = MAP_ARCH[arch]
if image_arch.startswith(expected_image_arch): if image_arch.startswith(expected_image_arch):
return return
_LOGGER.info( _LOGGER.info(
@@ -636,7 +634,7 @@ class DockerInterface(JobGroup, ABC):
# We're missing the image we need. Stop and clean up what we have then pull the right one # We're missing the image we need. Stop and clean up what we have then pull the right one
with suppress(DockerError): with suppress(DockerError):
await self.remove() await self.remove()
await self.install(version, expected_image, arch=expected_image_cpu_arch) await self.install(version, expected_image, arch=arch)
@Job( @Job(
name="docker_interface_update", name="docker_interface_update",

View File

@@ -10,7 +10,7 @@ from awesomeversion import AwesomeVersion
import pytest import pytest
from supervisor.addons.addon import Addon from supervisor.addons.addon import Addon
from supervisor.arch import CpuArch from supervisor.arch import CpuArchManager
from supervisor.config import CoreConfig from supervisor.config import CoreConfig
from supervisor.const import AddonBoot, AddonStartup, AddonState, BusEvent from supervisor.const import AddonBoot, AddonStartup, AddonState, BusEvent
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
@@ -54,7 +54,9 @@ async def fixture_mock_arch_disk() -> AsyncGenerator[None]:
"""Mock supported arch and disk space.""" """Mock supported arch and disk space."""
with ( with (
patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))), patch("shutil.disk_usage", return_value=(42, 42, 2 * (1024.0**3))),
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])), patch.object(
CpuArchManager, "supported", new=PropertyMock(return_value=["amd64"])
),
): ):
yield yield

View File

@@ -9,7 +9,7 @@ import pytest
from supervisor.addons.addon import Addon from supervisor.addons.addon import Addon
from supervisor.addons.build import AddonBuild from supervisor.addons.build import AddonBuild
from supervisor.arch import CpuArch from supervisor.arch import CpuArchManager
from supervisor.const import AddonState from supervisor.const import AddonState
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.docker.addon import DockerAddon from supervisor.docker.addon import DockerAddon
@@ -236,7 +236,9 @@ async def test_api_addon_rebuild_healthcheck(
patch.object(AddonBuild, "is_valid", return_value=True), patch.object(AddonBuild, "is_valid", return_value=True),
patch.object(DockerAddon, "is_running", return_value=False), patch.object(DockerAddon, "is_running", return_value=False),
patch.object(Addon, "need_build", new=PropertyMock(return_value=True)), patch.object(Addon, "need_build", new=PropertyMock(return_value=True)),
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])), patch.object(
CpuArchManager, "supported", new=PropertyMock(return_value=["amd64"])
),
patch.object(DockerAddon, "run", new=container_events_task), patch.object(DockerAddon, "run", new=container_events_task),
patch.object( patch.object(
coresys.docker, coresys.docker,
@@ -308,7 +310,9 @@ async def test_api_addon_rebuild_force(
patch.object( patch.object(
Addon, "need_build", new=PropertyMock(return_value=False) Addon, "need_build", new=PropertyMock(return_value=False)
), # Image-based ), # Image-based
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])), patch.object(
CpuArchManager, "supported", new=PropertyMock(return_value=["amd64"])
),
): ):
resp = await api_client.post("/addons/local_ssh/rebuild") resp = await api_client.post("/addons/local_ssh/rebuild")
@@ -326,7 +330,9 @@ async def test_api_addon_rebuild_force(
patch.object( patch.object(
Addon, "need_build", new=PropertyMock(return_value=False) Addon, "need_build", new=PropertyMock(return_value=False)
), # Image-based ), # Image-based
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])), patch.object(
CpuArchManager, "supported", new=PropertyMock(return_value=["amd64"])
),
patch.object(DockerAddon, "run", new=container_events_task), patch.object(DockerAddon, "run", new=container_events_task),
patch.object( patch.object(
coresys.docker, coresys.docker,

View File

@@ -10,7 +10,7 @@ from awesomeversion import AwesomeVersion
import pytest import pytest
from supervisor.addons.addon import Addon from supervisor.addons.addon import Addon
from supervisor.arch import CpuArch from supervisor.arch import CpuArchManager
from supervisor.backups.manager import BackupManager from supervisor.backups.manager import BackupManager
from supervisor.config import CoreConfig from supervisor.config import CoreConfig
from supervisor.const import AddonState, CoreState from supervisor.const import AddonState, CoreState
@@ -191,7 +191,9 @@ async def test_api_store_update_healthcheck(
patch.object(DockerAddon, "run", new=container_events_task), patch.object(DockerAddon, "run", new=container_events_task),
patch.object(DockerInterface, "install"), patch.object(DockerInterface, "install"),
patch.object(DockerAddon, "is_running", return_value=False), patch.object(DockerAddon, "is_running", return_value=False),
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])), patch.object(
CpuArchManager, "supported", new=PropertyMock(return_value=["amd64"])
),
): ):
resp = await api_client.post(f"/store/addons/{TEST_ADDON_SLUG}/update") resp = await api_client.post(f"/store/addons/{TEST_ADDON_SLUG}/update")
@@ -548,7 +550,9 @@ async def test_api_store_addons_addon_availability_arch_not_supported(
coresys.addons.data.user[addon_obj.slug] = {"version": AwesomeVersion("0.0.1")} coresys.addons.data.user[addon_obj.slug] = {"version": AwesomeVersion("0.0.1")}
# Mock the system architecture to be different # Mock the system architecture to be different
with patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])): with patch.object(
CpuArchManager, "supported", new=PropertyMock(return_value=["amd64"])
):
resp = await api_client.request( resp = await api_client.request(
api_method, f"/store/addons/{addon_obj.slug}/{api_action}" api_method, f"/store/addons/{addon_obj.slug}/{api_action}"
) )

View File

@@ -9,7 +9,7 @@ from awesomeversion import AwesomeVersion
import pytest import pytest
from supervisor.addons.addon import Addon from supervisor.addons.addon import Addon
from supervisor.arch import CpuArch from supervisor.arch import CpuArchManager
from supervisor.backups.manager import BackupManager from supervisor.backups.manager import BackupManager
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from supervisor.exceptions import AddonNotSupportedError, StoreJobError from supervisor.exceptions import AddonNotSupportedError, StoreJobError
@@ -163,7 +163,9 @@ async def test_update_unavailable_addon(
with ( with (
patch.object(BackupManager, "do_backup_partial") as backup, patch.object(BackupManager, "do_backup_partial") as backup,
patch.object(AddonStore, "data", new=PropertyMock(return_value=addon_config)), patch.object(AddonStore, "data", new=PropertyMock(return_value=addon_config)),
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])), patch.object(
CpuArchManager, "supported", new=PropertyMock(return_value=["amd64"])
),
patch.object(CoreSys, "machine", new=PropertyMock(return_value="qemux86-64")), patch.object(CoreSys, "machine", new=PropertyMock(return_value="qemux86-64")),
patch.object( patch.object(
HomeAssistant, HomeAssistant,
@@ -219,7 +221,9 @@ async def test_install_unavailable_addon(
with ( with (
patch.object(AddonStore, "data", new=PropertyMock(return_value=addon_config)), patch.object(AddonStore, "data", new=PropertyMock(return_value=addon_config)),
patch.object(CpuArch, "supported", new=PropertyMock(return_value=["amd64"])), patch.object(
CpuArchManager, "supported", new=PropertyMock(return_value=["amd64"])
),
patch.object(CoreSys, "machine", new=PropertyMock(return_value="qemux86-64")), patch.object(CoreSys, "machine", new=PropertyMock(return_value="qemux86-64")),
patch.object( patch.object(
HomeAssistant, HomeAssistant,