From 96cb697e5a9179ed19406efea14bba1d9d518649 Mon Sep 17 00:00:00 2001 From: itsrez <35066771+whiteov3rflow@users.noreply.github.com> Date: Sun, 23 Aug 2026 01:16:49 +0200 Subject: [PATCH] feat: support amd64 images on ARM64 hosts --- nihil/manager/manager.py | 11 ++++++- nihil/utils/__init__.py | 4 +-- nihil/utils/platform_info.py | 12 ++++++++ tests/test_nihilManager.py | 58 ++++++++++++++++++++++++++++++++++++ tests/test_platform_info.py | 25 ++++++++++++++++ 5 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 tests/test_platform_info.py diff --git a/nihil/manager/manager.py b/nihil/manager/manager.py index d2e324a..df6ee4d 100644 --- a/nihil/manager/manager.py +++ b/nihil/manager/manager.py @@ -222,9 +222,11 @@ def _pull_with_progress(self, image: str) -> None: TextColumn, ) from rich.console import Console + from nihil.utils.platform_info import get_image_platform console = Console() tasks: dict = {} totals: dict = {} + image_platform = get_image_platform() with Progress( TextColumn("[bold cyan]{task.fields[layer]:<14}[/]"), TextColumn("[bold white]{task.fields[status]:<20}[/]"), @@ -235,7 +237,10 @@ def _pull_with_progress(self, image: str) -> None: console=console, transient=False, ) as progress: - for event in self.client.api.pull(image, stream=True, decode=True): + pull_options = {"stream": True, "decode": True} + if image_platform: + pull_options["platform"] = image_platform + for event in self.client.api.pull(image, **pull_options): layer_id = event.get("id", "") status = event.get("status", "") detail = event.get("progressDetail") or {} @@ -426,6 +431,10 @@ def create_container( host_binding = ("127.0.0.1", browser_ui_port) container_config["ports"] = container_config.get("ports") or {} container_config["ports"][port_key] = host_binding + from nihil.utils.platform_info import get_image_platform + image_platform = get_image_platform() + if image_platform: + container_config["platform"] = image_platform try: container = self.client.containers.create(**container_config) return container diff --git a/nihil/utils/__init__.py b/nihil/utils/__init__.py index a6666cb..e964e9b 100644 --- a/nihil/utils/__init__.py +++ b/nihil/utils/__init__.py @@ -1,6 +1,6 @@ # Utilitaires : historique, doctor, platform from nihil.utils.history import log_command, HISTORY_PATH from nihil.utils.doctor import NihilDoctor, DoctorCheckResult -from nihil.utils.platform_info import get_host_os, get_docker_engine, host_network_supported, HostOS, DockerEngine +from nihil.utils.platform_info import get_host_os, get_docker_engine, get_image_platform, host_network_supported, HostOS, DockerEngine -__all__ = ["log_command", "HISTORY_PATH", "NihilDoctor", "DoctorCheckResult", "get_host_os", "get_docker_engine", "host_network_supported", "HostOS", "DockerEngine"] +__all__ = ["log_command", "HISTORY_PATH", "NihilDoctor", "DoctorCheckResult", "get_host_os", "get_docker_engine", "get_image_platform", "host_network_supported", "HostOS", "DockerEngine"] diff --git a/nihil/utils/platform_info.py b/nihil/utils/platform_info.py index b7ed5b4..b6dea56 100644 --- a/nihil/utils/platform_info.py +++ b/nihil/utils/platform_info.py @@ -4,6 +4,7 @@ import platform from enum import Enum +from typing import Optional class HostOS(Enum): @@ -53,3 +54,14 @@ def host_network_supported(host_os: HostOS, engine: DockerEngine) -> bool: if host_os == HostOS.WSL and engine == DockerEngine.NATIVE: return True return False + + +NIHIL_IMAGE_PLATFORM = "linux/amd64" + + +def get_image_platform() -> Optional[str]: + """Return the Nihil image platform required by the current host.""" + machine = platform.machine().lower() + if machine in ("arm64", "aarch64"): + return NIHIL_IMAGE_PLATFORM + return None diff --git a/tests/test_nihilManager.py b/tests/test_nihilManager.py index 1521059..007c3f6 100644 --- a/tests/test_nihilManager.py +++ b/tests/test_nihilManager.py @@ -65,6 +65,36 @@ def test_ensure_image_exists_pull_success(self, mock_docker_client, mock_formatt assert result is True mock_pull.assert_called_once_with("test-image:latest") + def test_pull_with_progress_sets_platform_on_arm(self, mock_docker_client): + """Test pull force linux/amd64 sur un hôte ARM.""" + mock_docker_client.api.pull.return_value = [] + + with patch('nihil.manager.manager.docker.from_env', return_value=mock_docker_client): + with patch('nihil.manager.manager.ensure_filesystem'): + with patch('nihil.utils.platform_info.get_image_platform', return_value='linux/amd64'): + with patch.object(NihilManager, 'snapshot_local_image_as_version'): + manager = NihilManager() + manager._pull_with_progress("test-image:latest") + + mock_docker_client.api.pull.assert_called_once_with( + "test-image:latest", stream=True, decode=True, platform="linux/amd64" + ) + + def test_pull_with_progress_uses_native_platform_on_amd64(self, mock_docker_client): + """Test pull laisse Docker choisir sur un hôte amd64.""" + mock_docker_client.api.pull.return_value = [] + + with patch('nihil.manager.manager.docker.from_env', return_value=mock_docker_client): + with patch('nihil.manager.manager.ensure_filesystem'): + with patch('nihil.utils.platform_info.get_image_platform', return_value=None): + with patch.object(NihilManager, 'snapshot_local_image_as_version'): + manager = NihilManager() + manager._pull_with_progress("test-image:latest") + + mock_docker_client.api.pull.assert_called_once_with( + "test-image:latest", stream=True, decode=True + ) + def test_ensure_image_exists_pull_fails(self, mock_docker_client): """Test ensure_image_exists quand le pull échoue (manager utilise _pull_with_progress).""" mock_docker_client.images.get.side_effect = docker.errors.ImageNotFound("Not found") @@ -100,6 +130,34 @@ def test_create_container_config_defaults(self, mock_docker_client): assert config["privileged"] is False assert config["hostname"] == "test-container" + def test_create_container_sets_platform_on_arm(self, mock_docker_client): + """Test création de container force linux/amd64 sur un hôte ARM.""" + mock_container = MagicMock() + mock_docker_client.containers.create.return_value = mock_container + mock_docker_client.images.get.return_value = MagicMock() + + with patch('nihil.manager.manager.docker.from_env', return_value=mock_docker_client): + with patch('nihil.manager.manager.ensure_filesystem'): + with patch('nihil.utils.platform_info.get_image_platform', return_value='linux/amd64'): + manager = NihilManager() + manager.create_container("test-container") + + assert mock_docker_client.containers.create.call_args.kwargs["platform"] == "linux/amd64" + + def test_create_container_uses_native_platform_on_amd64(self, mock_docker_client): + """Test création de container laisse Docker choisir sur un hôte amd64.""" + mock_container = MagicMock() + mock_docker_client.containers.create.return_value = mock_container + mock_docker_client.images.get.return_value = MagicMock() + + with patch('nihil.manager.manager.docker.from_env', return_value=mock_docker_client): + with patch('nihil.manager.manager.ensure_filesystem'): + with patch('nihil.utils.platform_info.get_image_platform', return_value=None): + manager = NihilManager() + manager.create_container("test-container") + + assert "platform" not in mock_docker_client.containers.create.call_args.kwargs + def test_create_container_with_privileged(self, mock_docker_client): """Test création de container avec --privileged""" mock_container = MagicMock() diff --git a/tests/test_platform_info.py b/tests/test_platform_info.py new file mode 100644 index 0000000..7f5decd --- /dev/null +++ b/tests/test_platform_info.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests unitaires pour platform_info.py""" + +from unittest.mock import patch + +from nihil.utils.platform_info import get_image_platform + + +def test_get_image_platform_native_amd64(): + """Ne force pas la plateforme sur un hôte amd64 natif.""" + with patch("nihil.utils.platform_info.platform.machine", return_value="x86_64"): + assert get_image_platform() is None + + +def test_get_image_platform_arm64(): + """Force linux/amd64 sur un hôte ARM64 pour les images Nihil.""" + with patch("nihil.utils.platform_info.platform.machine", return_value="arm64"): + assert get_image_platform() == "linux/amd64" + + +def test_get_image_platform_unknown_architecture(): + """Laisse Docker choisir sur une architecture non prise en charge.""" + with patch("nihil.utils.platform_info.platform.machine", return_value="riscv64"): + assert get_image_platform() is None