1111import typing
1212import uuid
1313from collections .abc import Mapping , Sequence
14+ from dataclasses import dataclass
1415from pathlib import Path , PurePath , PurePosixPath
1516from types import TracebackType
1617from typing import IO , Dict
1718
1819from ._compat .typing import Literal
1920from .typing import PathOrStr , PopenBytes
20- from .util import CIProvider , detect_ci_provider
21+ from .util import CIProvider , detect_ci_provider , parse_key_value_string
2122
22- ContainerEngine = Literal ["docker" , "podman" ]
23+ ContainerEngineName = Literal ["docker" , "podman" ]
24+
25+
26+ @dataclass (frozen = True )
27+ class OCIContainerEngineConfig :
28+ name : ContainerEngineName
29+ create_args : Sequence [str ] = ()
30+
31+ @staticmethod
32+ def from_config_string (config_string : str ) -> OCIContainerEngineConfig :
33+ config_dict = parse_key_value_string (config_string , ["name" ])
34+ name = " " .join (config_dict ["name" ])
35+ if name not in {"docker" , "podman" }:
36+ msg = f"unknown container engine { name } "
37+ raise ValueError (msg )
38+
39+ name = typing .cast (ContainerEngineName , name )
40+ # some flexibility in the option name to cope with TOML conventions
41+ create_args = config_dict .get ("create_args" ) or config_dict .get ("create-args" ) or []
42+ return OCIContainerEngineConfig (name = name , create_args = create_args )
43+
44+ def options_summary (self ) -> str | dict [str , str ]:
45+ if not self .create_args :
46+ return self .name
47+ else :
48+ return {"name" : self .name , "create_args" : repr (self .create_args )}
49+
50+
51+ DEFAULT_ENGINE = OCIContainerEngineConfig ("docker" )
2352
2453
2554class OCIContainer :
@@ -57,7 +86,7 @@ def __init__(
5786 image : str ,
5887 simulate_32_bit : bool = False ,
5988 cwd : PathOrStr | None = None ,
60- engine : ContainerEngine = "docker" ,
89+ engine : OCIContainerEngineConfig = DEFAULT_ENGINE ,
6190 ):
6291 if not image :
6392 msg = "Must have a non-empty image to run."
@@ -84,13 +113,14 @@ def __enter__(self) -> OCIContainer:
84113
85114 subprocess .run (
86115 [
87- self .engine ,
116+ self .engine . name ,
88117 "create" ,
89118 "--env=CIBUILDWHEEL" ,
90119 f"--name={ self .name } " ,
91120 "--interactive" ,
92121 "--volume=/:/host" , # ignored on CircleCI
93122 * network_args ,
123+ * self .engine .create_args ,
94124 self .image ,
95125 * shell_args ,
96126 ],
@@ -99,7 +129,7 @@ def __enter__(self) -> OCIContainer:
99129
100130 self .process = subprocess .Popen (
101131 [
102- self .engine ,
132+ self .engine . name ,
103133 "start" ,
104134 "--attach" ,
105135 "--interactive" ,
@@ -137,7 +167,7 @@ def __exit__(
137167 self .bash_stdin .close ()
138168 self .bash_stdout .close ()
139169
140- if self .engine == "podman" :
170+ if self .engine . name == "podman" :
141171 # This works around what seems to be a race condition in the podman
142172 # backend. The full reason is not understood. See PR #966 for a
143173 # discussion on possible causes and attempts to remove this line.
@@ -147,7 +177,7 @@ def __exit__(
147177 assert isinstance (self .name , str )
148178
149179 subprocess .run (
150- [self .engine , "rm" , "--force" , "-v" , self .name ],
180+ [self .engine . name , "rm" , "--force" , "-v" , self .name ],
151181 stdout = subprocess .DEVNULL ,
152182 check = False ,
153183 )
@@ -162,7 +192,7 @@ def copy_into(self, from_path: Path, to_path: PurePath) -> None:
162192 if from_path .is_dir ():
163193 self .call (["mkdir" , "-p" , to_path ])
164194 subprocess .run (
165- f"tar cf - . | { self .engine } exec -i { self .name } tar --no-same-owner -xC { shell_quote (to_path )} -f -" ,
195+ f"tar cf - . | { self .engine . name } exec -i { self .name } tar --no-same-owner -xC { shell_quote (to_path )} -f -" ,
166196 shell = True ,
167197 check = True ,
168198 cwd = from_path ,
@@ -171,7 +201,7 @@ def copy_into(self, from_path: Path, to_path: PurePath) -> None:
171201 exec_process : subprocess .Popen [bytes ]
172202 with subprocess .Popen (
173203 [
174- self .engine ,
204+ self .engine . name ,
175205 "exec" ,
176206 "-i" ,
177207 str (self .name ),
@@ -198,29 +228,29 @@ def copy_out(self, from_path: PurePath, to_path: Path) -> None:
198228 # note: we assume from_path is a dir
199229 to_path .mkdir (parents = True , exist_ok = True )
200230
201- if self .engine == "podman" :
231+ if self .engine . name == "podman" :
202232 subprocess .run (
203233 [
204- self .engine ,
234+ self .engine . name ,
205235 "cp" ,
206236 f"{ self .name } :{ from_path } /." ,
207237 str (to_path ),
208238 ],
209239 check = True ,
210240 cwd = to_path ,
211241 )
212- elif self .engine == "docker" :
242+ elif self .engine . name == "docker" :
213243 # There is a bug in docker that prevents a simple 'cp' invocation
214244 # from working https://github.com/moby/moby/issues/38995
215- command = f"{ self .engine } exec -i { self .name } tar -cC { shell_quote (from_path )} -f - . | tar -xf -"
245+ command = f"{ self .engine . name } exec -i { self .name } tar -cC { shell_quote (from_path )} -f - . | tar -xf -"
216246 subprocess .run (
217247 command ,
218248 shell = True ,
219249 check = True ,
220250 cwd = to_path ,
221251 )
222252 else :
223- raise KeyError (self .engine )
253+ raise KeyError (self .engine . name )
224254
225255 def glob (self , path : PurePosixPath , pattern : str ) -> list [PurePosixPath ]:
226256 glob_pattern = path .joinpath (pattern )
@@ -338,10 +368,10 @@ def environment_executor(self, command: Sequence[str], environment: dict[str, st
338368 return self .call (command , env = environment , capture_output = True )
339369
340370 def debug_info (self ) -> str :
341- if self .engine == "podman" :
342- command = f"{ self .engine } info --debug"
371+ if self .engine . name == "podman" :
372+ command = f"{ self .engine . name } info --debug"
343373 else :
344- command = f"{ self .engine } info"
374+ command = f"{ self .engine . name } info"
345375 completed = subprocess .run (
346376 command ,
347377 shell = True ,
0 commit comments