1212import uuid
1313from collections .abc import Mapping , Sequence
1414from dataclasses import dataclass , field
15+ from enum import Enum
1516from pathlib import Path , PurePath , PurePosixPath
1617from types import TracebackType
1718from typing import IO , Dict , Literal
1819
19- from ._compat .typing import Self
20+ from packaging .version import Version
21+
22+ from ._compat .typing import Self , assert_never
23+ from .errors import OCIEngineTooOldError
24+ from .logger import log
2025from .typing import PathOrStr , PopenBytes
2126from .util import (
2227 CIProvider ,
2934ContainerEngineName = Literal ["docker" , "podman" ]
3035
3136
37+ class OCIPlatform (Enum ):
38+ AMD64 = "linux/amd64"
39+ i386 = "linux/386"
40+ ARM64 = "linux/arm64"
41+ PPC64LE = "linux/ppc64le"
42+ S390X = "linux/s390x"
43+
44+
3245@dataclass (frozen = True )
3346class OCIContainerEngineConfig :
3447 name : ContainerEngineName
@@ -56,6 +69,15 @@ def from_config_string(config_string: str) -> OCIContainerEngineConfig:
5669 disable_host_mount = (
5770 strtobool (disable_host_mount_options [- 1 ]) if disable_host_mount_options else False
5871 )
72+ if "--platform" in create_args or any (arg .startswith ("--platform=" ) for arg in create_args ):
73+ msg = "Using '--platform' in 'container-engine::create_args' is deprecated. It will be ignored."
74+ log .warning (msg )
75+ if "--platform" in create_args :
76+ index = create_args .index ("--platform" )
77+ create_args .pop (index )
78+ create_args .pop (index )
79+ else :
80+ create_args = [arg for arg in create_args if not arg .startswith ("--platform=" )]
5981
6082 return OCIContainerEngineConfig (
6183 name = name , create_args = tuple (create_args ), disable_host_mount = disable_host_mount
@@ -75,6 +97,29 @@ def options_summary(self) -> str | dict[str, str]:
7597DEFAULT_ENGINE = OCIContainerEngineConfig ("docker" )
7698
7799
100+ def _check_minimum_engine_version (engine : OCIContainerEngineConfig ) -> None :
101+ try :
102+ version_string = call (engine .name , "version" , "-f" , "json" , capture_stdout = True ).strip ()
103+ version_info = json .loads (version_string )
104+ if engine .name == "docker" :
105+ client_api_version = Version (version_info ["Client" ]["ApiVersion" ])
106+ engine_api_version = Version (version_info ["Server" ]["ApiVersion" ])
107+ too_old = min (client_api_version , engine_api_version ) < Version ("1.32" )
108+ elif engine .name == "podman" :
109+ client_api_version = Version (version_info ["Client" ]["APIVersion" ])
110+ if "Server" in version_info :
111+ engine_api_version = Version (version_info ["Server" ]["APIVersion" ])
112+ else :
113+ engine_api_version = client_api_version
114+ too_old = min (client_api_version , engine_api_version ) < Version ("3" )
115+ else :
116+ assert_never (engine .name )
117+ if too_old :
118+ raise OCIEngineTooOldError () from None
119+ except (subprocess .CalledProcessError , KeyError ) as e :
120+ raise OCIEngineTooOldError () from e
121+
122+
78123class OCIContainer :
79124 """
80125 An object that represents a running OCI (e.g. Docker) container.
@@ -108,7 +153,7 @@ def __init__(
108153 self ,
109154 * ,
110155 image : str ,
111- enforce_32_bit : bool = False ,
156+ oci_platform : OCIPlatform ,
112157 cwd : PathOrStr | None = None ,
113158 engine : OCIContainerEngineConfig = DEFAULT_ENGINE ,
114159 ):
@@ -117,10 +162,15 @@ def __init__(
117162 raise ValueError (msg )
118163
119164 self .image = image
120- self .enforce_32_bit = enforce_32_bit
165+ self .oci_platform = oci_platform
121166 self .cwd = cwd
122167 self .name : str | None = None
123168 self .engine = engine
169+ # we need '--pull=always' otherwise some images with the wrong platform get re-used (e.g. 386 image for amd64)
170+ # c.f. https://github.com/moby/moby/issues/48197#issuecomment-2282802313
171+ self .platform_args = [f"--platform={ oci_platform .value } " , "--pull=always" ]
172+
173+ _check_minimum_engine_version (self .engine )
124174
125175 def __enter__ (self ) -> Self :
126176 self .name = f"cibuildwheel-{ uuid .uuid4 ()} "
@@ -134,13 +184,24 @@ def __enter__(self) -> Self:
134184 network_args = ["--network=host" ]
135185
136186 simulate_32_bit = False
137- if self .enforce_32_bit :
187+ if self .oci_platform == OCIPlatform . i386 :
138188 # If the architecture running the image is already the right one
139189 # or the image entrypoint takes care of enforcing this, then we don't need to
140190 # simulate this
141- container_machine = call (
142- self .engine .name , "run" , "--rm" , self .image , "uname" , "-m" , capture_stdout = True
143- ).strip ()
191+ run_cmd = [self .engine .name , "run" , "--rm" ]
192+ ctr_cmd = ["uname" , "-m" ]
193+ try :
194+ container_machine = call (
195+ * run_cmd , * self .platform_args , self .image , * ctr_cmd , capture_stdout = True
196+ ).strip ()
197+ except subprocess .CalledProcessError :
198+ # The image might have been built with amd64 architecture
199+ # Let's try that
200+ platform_args = ["--platform=linux/amd64" , * self .platform_args [1 :]]
201+ container_machine = call (
202+ * run_cmd , * platform_args , self .image , * ctr_cmd , capture_stdout = True
203+ ).strip ()
204+ self .platform_args = platform_args
144205 simulate_32_bit = container_machine != "i686"
145206
146207 shell_args = ["linux32" , "/bin/bash" ] if simulate_32_bit else ["/bin/bash" ]
@@ -155,6 +216,7 @@ def __enter__(self) -> Self:
155216 "--interactive" ,
156217 * (["--volume=/:/host" ] if not self .engine .disable_host_mount else []),
157218 * network_args ,
219+ * self .platform_args ,
158220 * self .engine .create_args ,
159221 self .image ,
160222 * shell_args ,
0 commit comments