4
4
import json
5
5
import platform
6
6
import shutil
7
- from typing import Optional , NamedTuple
7
+ from typing import Optional , NamedTuple , Dict , List
8
8
import yaml
9
9
from conan .api .conan_api import ConanAPI
10
10
from conan .api .model import ListPattern
16
16
from conans .model .version import Version
17
17
from pathlib import Path
18
18
19
+ class _ContainerConfig (NamedTuple ):
20
+ class Build (NamedTuple ):
21
+ dockerfile : Optional [str ] = None
22
+ build_context : Optional [str ] = None
23
+ build_args : Optional [Dict [str , str ]] = None
24
+ cache_from : Optional [List [str ]] = None
25
+
26
+ class Run (NamedTuple ):
27
+ name : Optional [str ] = None
28
+ environment : Optional [Dict [str , str ]] = None
29
+ user : Optional [str ] = None
30
+ privileged : Optional [bool ] = None
31
+ cap_add : Optional [List [str ]] = None
32
+ security_opt : Optional [List [str ]] = None
33
+ volumes : Optional [Dict [str , str ]] = None
34
+ network : Optional [str ] = None
35
+
36
+ image : Optional [str ] = None
37
+ build : Build = Build ()
38
+ run : Run = Run ()
39
+
19
40
20
41
class DockerRunner :
21
42
def __init__ (self , conan_api : ConanAPI , command : str , host_profile : Profile , build_profile : Profile , args : Namespace , raw_args : list [str ]):
22
- import docker
23
- import docker .api .build
24
- try :
25
- docker_base_urls = [
26
- None , # Default docker configuration, let the python library detect the socket
27
- os .environ .get ('DOCKER_HOST' ), # Connect to socket defined in DOCKER_HOST
28
- 'unix:///var/run/docker.sock' , # Default linux socket
29
- f'unix://{ os .path .expanduser ("~" )} /.rd/docker.sock' # Rancher linux socket
30
- ]
31
- for base_url in docker_base_urls :
32
- try :
33
- ConanOutput ().verbose (f'Trying to connect to docker "{ base_url or "default" } " socket' )
34
- self .docker_client = docker .DockerClient (base_url = base_url , version = 'auto' )
35
- ConanOutput ().verbose (f'Connected to docker "{ base_url or "default" } " socket' )
36
- break
37
- except :
38
- continue
39
- self .docker_api = self .docker_client .api
40
- docker .api .build .process_dockerfile = lambda dockerfile , path : ('Dockerfile' , dockerfile )
41
- except :
42
- raise ConanException ("Docker Client failed to initialize."
43
- "\n - Check if docker is installed and running"
44
- "\n - Run 'pip install conan[runners]'" )
43
+ self .docker_client = self ._initialize_docker_client ()
44
+ self .docker_api = self .docker_client .api
45
45
self .conan_api = conan_api
46
46
self .build_profile = build_profile
47
+ self .abs_host_path = self ._get_abs_host_path (args .path )
47
48
self .args = args
48
- abs_path = make_abs_path (args .path )
49
- if abs_path is None :
50
- raise ConanException ("Could not determine absolute path" )
51
- self .abs_host_path = Path (abs_path )
52
49
if args .format :
53
50
raise ConanException ("format argument is forbidden if running in a docker runner" )
54
51
55
- # Container config
56
- # https://containers.dev/implementors/json_reference/
57
- self .configfile = self .config_parser (host_profile .runner .get ('configfile' ))
52
+ self .configfile = self ._load_config (host_profile .runner .get ('configfile' ))
58
53
self .dockerfile = host_profile .runner .get ('dockerfile' ) or self .configfile .build .dockerfile
59
54
self .docker_build_context = host_profile .runner .get ('build_context' ) or self .configfile .build .build_context
60
55
self .image = host_profile .runner .get ('image' ) or self .configfile .image
@@ -64,28 +59,30 @@ def __init__(self, conan_api: ConanAPI, command: str, host_profile: Profile, bui
64
59
self .name = self .configfile .run .name or f'conan-runner-{ host_profile .runner .get ("suffix" , "docker" )} '
65
60
self .remove = str (host_profile .runner .get ('remove' , 'false' )).lower () == 'true'
66
61
self .cache = str (host_profile .runner .get ('cache' , 'clean' ))
62
+ if self .cache not in ['clean' , 'copy' , 'shared' ]:
63
+ raise ConanException (f'Invalid cache value: "{ self .cache } ". Valid values are: clean, copy, shared' )
67
64
self .container = None
68
65
69
- # Runner config>
66
+ # Runner config
70
67
self .abs_runner_home_path = self .abs_host_path / '.conanrunner'
71
68
self .docker_user_name = self .configfile .run .user or 'root'
72
69
self .abs_docker_path = os .path .join (f'/{ self .docker_user_name } /conanrunner' , os .path .basename (self .abs_host_path )).replace ("\\ " ,"/" )
73
70
74
71
# Update conan command and some paths to run inside the container
75
72
raw_args [raw_args .index (args .path )] = self .abs_docker_path
76
73
self .command = ' ' .join ([f'conan { command } ' ] + [f'"{ raw_arg } "' if ' ' in raw_arg else raw_arg for raw_arg in raw_args ] + ['-f json > create.json' ])
77
- self .logger = DockerOutput (self .name )
74
+ self .logger = DockerRunner . Output (self .name )
78
75
79
76
def run (self ) -> None :
80
77
"""
81
78
Run conan inside a Docker container
82
79
"""
83
- self .build_image ()
84
- self .start_container ()
80
+ self ._build_image ()
81
+ self ._start_container ()
85
82
try :
86
- self .init_container ()
87
- self .run_command (self .command )
88
- self .update_local_cache ()
83
+ self ._init_container ()
84
+ self ._run_command (self .command )
85
+ self ._update_local_cache ()
89
86
except RunnerException as e :
90
87
raise ConanException (f'"{ e .command } " inside docker fail'
91
88
f'\n \n Last command output: { str (e .stdout_log )} ' )
@@ -99,28 +96,56 @@ def run(self) -> None:
99
96
log ('Removing container' )
100
97
self .container .remove ()
101
98
102
- Build = NamedTuple ('Build' , [('dockerfile' , Optional [str ]), ('build_context' , Optional [str ]), ('build_args' , Optional [dict ]), ('cache_from' , Optional [list ])])
103
- Run = NamedTuple ('Run' , [('name' , Optional [str ]), ('environment' , Optional [dict ]), ('user' , Optional [str ]), ('privileged' , Optional [bool ]), ('cap_add' , Optional [list ]), ('security_opt' , Optional [list ]), ('volumes' , Optional [dict ]), ('network' , Optional [str ])])
104
- Conf = NamedTuple ('Conf' , [('image' , Optional [str ]), ('build' , Build ), ('run' , Run )])
99
+ def _initialize_docker_client (self ):
100
+ import docker
101
+ import docker .api .build
102
+
103
+ docker_base_urls = [
104
+ None , # Default docker configuration
105
+ os .environ .get ('DOCKER_HOST' ), # From DOCKER_HOST environment variable
106
+ 'unix:///var/run/docker.sock' , # Default Linux socket
107
+ f'unix://{ os .path .expanduser ("~" )} /.rd/docker.sock' # Rancher Linux socket
108
+ ]
109
+
110
+ for base_url in docker_base_urls :
111
+ try :
112
+ ConanOutput ().verbose (f'Trying to connect to Docker: "{ base_url or "default" } "' )
113
+ client = docker .DockerClient (base_url = base_url , version = 'auto' )
114
+ ConanOutput ().verbose (f'Connected to Docker: "{ base_url or "default" } "' )
115
+ docker .api .build .process_dockerfile = lambda dockerfile , path : ('Dockerfile' , dockerfile )
116
+ return client
117
+ except Exception :
118
+ continue
119
+ raise ConanException ("Docker Client failed to initialize."
120
+ "\n - Check if docker is installed and running"
121
+ "\n - Run 'pip install conan[runners]'" )
122
+
123
+ def _get_abs_host_path (self , path : str ) -> Path :
124
+ abs_path = make_abs_path (path )
125
+ if abs_path is None :
126
+ raise ConanException ("Could not determine the absolute path." )
127
+ return Path (abs_path )
105
128
106
129
@staticmethod
107
- def config_parser (file_path : str ) -> Conf :
130
+ def _load_config (file_path : Optional [str ]) -> _ContainerConfig :
131
+ # Container config
132
+ # https://containers.dev/implementors/json_reference/
108
133
if file_path :
109
134
def _instans_or_error (value , obj ):
110
135
if value and (not isinstance (value , obj )):
111
136
raise ConanException (f"docker runner configfile syntax error: { value } must be a { obj .__name__ } " )
112
137
return value
113
138
with open (file_path , 'r' ) as f :
114
139
runnerfile = yaml .safe_load (f )
115
- return DockerRunner . Conf (
140
+ return _ContainerConfig (
116
141
image = _instans_or_error (runnerfile .get ('image' ), str ),
117
- build = DockerRunner .Build (
142
+ build = _ContainerConfig .Build (
118
143
dockerfile = _instans_or_error (runnerfile .get ('build' , {}).get ('dockerfile' ), str ),
119
144
build_context = _instans_or_error (runnerfile .get ('build' , {}).get ('build_context' ), str ),
120
145
build_args = _instans_or_error (runnerfile .get ('build' , {}).get ('build_args' ), dict ),
121
146
cache_from = _instans_or_error (runnerfile .get ('build' , {}).get ('cacheFrom' ), list ),
122
147
),
123
- run = DockerRunner .Run (
148
+ run = _ContainerConfig .Run (
124
149
name = _instans_or_error (runnerfile .get ('run' , {}).get ('name' ), str ),
125
150
environment = _instans_or_error (runnerfile .get ('run' , {}).get ('containerEnv' ), dict ),
126
151
user = _instans_or_error (runnerfile .get ('run' , {}).get ('containerUser' ), str ),
@@ -132,14 +157,9 @@ def _instans_or_error(value, obj):
132
157
)
133
158
)
134
159
else :
135
- return DockerRunner .Conf (
136
- image = None ,
137
- build = DockerRunner .Build (dockerfile = None , build_context = None , build_args = None , cache_from = None ),
138
- run = DockerRunner .Run (name = None , environment = None , user = None , privileged = None , cap_add = None ,
139
- security_opt = None , volumes = None , network = None )
140
- )
160
+ return _ContainerConfig ()
141
161
142
- def build_image (self ) -> None :
162
+ def _build_image (self ) -> None :
143
163
if not self .dockerfile :
144
164
return
145
165
self .logger .status (f'Building the Docker image: { self .image } ' )
@@ -164,8 +184,8 @@ def build_image(self) -> None:
164
184
if stream :
165
185
ConanOutput ().status (stream .strip ())
166
186
167
- def start_container (self ) -> None :
168
- volumes , environment = self .create_runner_environment ()
187
+ def _start_container (self ) -> None :
188
+ volumes , environment = self ._create_runner_environment ()
169
189
try :
170
190
if self .docker_client .containers .list (all = True , filters = {'name' : self .name }):
171
191
self .logger .status ('Starting the docker container' , fg = Color .BRIGHT_MAGENTA )
@@ -195,7 +215,7 @@ def start_container(self) -> None:
195
215
raise ConanException (f'Imposible to run the container "{ self .name } " with image "{ self .image } "'
196
216
f'\n \n { str (e )} ' )
197
217
198
- def run_command (self , command : str , workdir : Optional [str ] = None , log : bool = True ) -> tuple [str , str ]:
218
+ def _run_command (self , command : str , workdir : Optional [str ] = None , log : bool = True ) -> tuple [str , str ]:
199
219
workdir = workdir or self .abs_docker_path
200
220
if log :
201
221
self .logger .status (f'Running in container: "{ command } "' )
@@ -224,7 +244,7 @@ def run_command(self, command: str, workdir: Optional[str] = None, log: bool = T
224
244
raise RunnerException (command = command , stdout_log = stdout_log , stderr_log = stderr_log )
225
245
return stdout_log , stderr_log
226
246
227
- def create_runner_environment (self ) -> tuple [dict , dict ]:
247
+ def _create_runner_environment (self ) -> tuple [dict , dict ]:
228
248
shutil .rmtree (self .abs_runner_home_path , ignore_errors = True )
229
249
volumes = {self .abs_host_path : {'bind' : self .abs_docker_path , 'mode' : 'rw' }}
230
250
environment = {'CONAN_RUNNER_ENVIRONMENT' : '1' }
@@ -250,35 +270,36 @@ def create_runner_environment(self) -> tuple[dict, dict]:
250
270
self .conan_api .cache .save (self .conan_api .list .select (ListPattern ("*:*" )), tgz_path )
251
271
return volumes , environment
252
272
253
- def init_container (self ) -> None :
273
+ def _init_container (self ) -> None :
254
274
min_conan_version = '2.1'
255
- stdout , _ = self .run_command ('conan --version' , log = True )
275
+ stdout , _ = self ._run_command ('conan --version' , log = True )
256
276
docker_conan_version = str (stdout .split ('Conan version ' )[1 ].replace ('\n ' , '' ).replace ('\r ' , '' )) # Remove all characters and color
257
277
if Version (docker_conan_version ) <= Version (min_conan_version ):
258
278
raise ConanException (f'conan version inside the container must be greater than { min_conan_version } ' )
259
279
if self .cache != 'shared' :
260
- self .run_command ('mkdir -p ${HOME}/.conan2/profiles' , log = False )
261
- self .run_command ('cp -r "' + self .abs_docker_path + '/.conanrunner/profiles/." ${HOME}/.conan2/profiles/.' , log = False )
280
+ self ._run_command ('mkdir -p ${HOME}/.conan2/profiles' , log = False )
281
+ self ._run_command ('cp -r "' + self .abs_docker_path + '/.conanrunner/profiles/." ${HOME}/.conan2/profiles/.' , log = False )
282
+
262
283
for file_name in ['global.conf' , 'settings.yml' , 'remotes.json' ]:
263
284
if os .path .exists ( os .path .join (self .abs_runner_home_path , file_name )):
264
- self .run_command ('cp "' + self .abs_docker_path + '/.conanrunner/' + file_name + '" ${HOME}/.conan2/' + file_name , log = False )
285
+ self ._run_command ('cp "' + self .abs_docker_path + '/.conanrunner/' + file_name + '" ${HOME}/.conan2/' + file_name , log = False )
265
286
if self .cache in ['copy' ]:
266
- self .run_command ('conan cache restore "' + self .abs_docker_path + '/.conanrunner/local_cache_save.tgz"' )
287
+ self ._run_command ('conan cache restore "' + self .abs_docker_path + '/.conanrunner/local_cache_save.tgz"' )
267
288
268
- def update_local_cache (self ) -> None :
289
+ def _update_local_cache (self ) -> None :
269
290
if self .cache != 'shared' :
270
- self .run_command ('conan list --graph=create.json --graph-binaries=build --format=json > pkglist.json' , log = False )
271
- self .run_command ('conan cache save --list=pkglist.json --file "' + self .abs_docker_path + '"/.conanrunner/docker_cache_save.tgz' )
291
+ self ._run_command ('conan list --graph=create.json --graph-binaries=build --format=json > pkglist.json' , log = False )
292
+ self ._run_command ('conan cache save --list=pkglist.json --file "' + self .abs_docker_path + '"/.conanrunner/docker_cache_save.tgz' )
272
293
tgz_path = os .path .join (self .abs_runner_home_path , 'docker_cache_save.tgz' )
273
294
self .logger .status (f'Restore host cache from: { tgz_path } ' )
274
295
self .conan_api .cache .restore (tgz_path )
275
296
276
- class DockerOutput (ConanOutput ):
277
- def __init__ (self , image : str ):
278
- super ().__init__ ()
279
- self .image = image
297
+ class Output (ConanOutput ):
298
+ def __init__ (self , image : str ):
299
+ super ().__init__ ()
300
+ self .image = image
280
301
281
- def _write_message (self , msg , fg = None , bg = None , newline = True ):
282
- super ()._write_message (f"===> Docker Runner ({ self .image } ): " , Color .BLACK ,
283
- Color .BRIGHT_YELLOW , newline = False )
284
- super ()._write_message (msg , fg , bg , newline )
302
+ def _write_message (self , msg , fg = None , bg = None , newline = True ):
303
+ super ()._write_message (f"===> Docker Runner ({ self .image } ): " , Color .BLACK ,
304
+ Color .BRIGHT_YELLOW , newline = False )
305
+ super ()._write_message (msg , fg , bg , newline )
0 commit comments