Skip to content

Commit b270887

Browse files
committed
More refactors
1 parent 2e0a738 commit b270887

File tree

2 files changed

+101
-73
lines changed

2 files changed

+101
-73
lines changed

conan/internal/runner/docker.py

+94-73
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import json
55
import platform
66
import shutil
7-
from typing import Optional, NamedTuple
7+
from typing import Optional, NamedTuple, Dict, List
88
import yaml
99
from conan.api.conan_api import ConanAPI
1010
from conan.api.model import ListPattern
@@ -16,45 +16,40 @@
1616
from conans.model.version import Version
1717
from pathlib import Path
1818

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+
1940

2041
class DockerRunner:
2142
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
4545
self.conan_api = conan_api
4646
self.build_profile = build_profile
47+
self.abs_host_path = self._get_abs_host_path(args.path)
4748
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)
5249
if args.format:
5350
raise ConanException("format argument is forbidden if running in a docker runner")
5451

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'))
5853
self.dockerfile = host_profile.runner.get('dockerfile') or self.configfile.build.dockerfile
5954
self.docker_build_context = host_profile.runner.get('build_context') or self.configfile.build.build_context
6055
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
6459
self.name = self.configfile.run.name or f'conan-runner-{host_profile.runner.get("suffix", "docker")}'
6560
self.remove = str(host_profile.runner.get('remove', 'false')).lower() == 'true'
6661
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')
6764
self.container = None
6865

69-
# Runner config>
66+
# Runner config
7067
self.abs_runner_home_path = self.abs_host_path / '.conanrunner'
7168
self.docker_user_name = self.configfile.run.user or 'root'
7269
self.abs_docker_path = os.path.join(f'/{self.docker_user_name}/conanrunner', os.path.basename(self.abs_host_path)).replace("\\","/")
7370

7471
# Update conan command and some paths to run inside the container
7572
raw_args[raw_args.index(args.path)] = self.abs_docker_path
7673
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)
7875

7976
def run(self) -> None:
8077
"""
8178
Run conan inside a Docker container
8279
"""
83-
self.build_image()
84-
self.start_container()
80+
self._build_image()
81+
self._start_container()
8582
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()
8986
except RunnerException as e:
9087
raise ConanException(f'"{e.command}" inside docker fail'
9188
f'\n\nLast command output: {str(e.stdout_log)}')
@@ -99,28 +96,56 @@ def run(self) -> None:
9996
log('Removing container')
10097
self.container.remove()
10198

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)
105128

106129
@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/
108133
if file_path:
109134
def _instans_or_error(value, obj):
110135
if value and (not isinstance(value, obj)):
111136
raise ConanException(f"docker runner configfile syntax error: {value} must be a {obj.__name__}")
112137
return value
113138
with open(file_path, 'r') as f:
114139
runnerfile = yaml.safe_load(f)
115-
return DockerRunner.Conf(
140+
return _ContainerConfig(
116141
image=_instans_or_error(runnerfile.get('image'), str),
117-
build=DockerRunner.Build(
142+
build=_ContainerConfig.Build(
118143
dockerfile=_instans_or_error(runnerfile.get('build', {}).get('dockerfile'), str),
119144
build_context=_instans_or_error(runnerfile.get('build', {}).get('build_context'), str),
120145
build_args=_instans_or_error(runnerfile.get('build', {}).get('build_args'), dict),
121146
cache_from=_instans_or_error(runnerfile.get('build', {}).get('cacheFrom'), list),
122147
),
123-
run=DockerRunner.Run(
148+
run=_ContainerConfig.Run(
124149
name=_instans_or_error(runnerfile.get('run', {}).get('name'), str),
125150
environment=_instans_or_error(runnerfile.get('run', {}).get('containerEnv'), dict),
126151
user=_instans_or_error(runnerfile.get('run', {}).get('containerUser'), str),
@@ -132,14 +157,9 @@ def _instans_or_error(value, obj):
132157
)
133158
)
134159
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()
141161

142-
def build_image(self) -> None:
162+
def _build_image(self) -> None:
143163
if not self.dockerfile:
144164
return
145165
self.logger.status(f'Building the Docker image: {self.image}')
@@ -164,8 +184,8 @@ def build_image(self) -> None:
164184
if stream:
165185
ConanOutput().status(stream.strip())
166186

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()
169189
try:
170190
if self.docker_client.containers.list(all=True, filters={'name': self.name}):
171191
self.logger.status('Starting the docker container', fg=Color.BRIGHT_MAGENTA)
@@ -195,7 +215,7 @@ def start_container(self) -> None:
195215
raise ConanException(f'Imposible to run the container "{self.name}" with image "{self.image}"'
196216
f'\n\n{str(e)}')
197217

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]:
199219
workdir = workdir or self.abs_docker_path
200220
if log:
201221
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
224244
raise RunnerException(command=command, stdout_log=stdout_log, stderr_log=stderr_log)
225245
return stdout_log, stderr_log
226246

227-
def create_runner_environment(self) -> tuple[dict, dict]:
247+
def _create_runner_environment(self) -> tuple[dict, dict]:
228248
shutil.rmtree(self.abs_runner_home_path, ignore_errors=True)
229249
volumes = {self.abs_host_path: {'bind': self.abs_docker_path, 'mode': 'rw'}}
230250
environment = {'CONAN_RUNNER_ENVIRONMENT': '1'}
@@ -250,35 +270,36 @@ def create_runner_environment(self) -> tuple[dict, dict]:
250270
self.conan_api.cache.save(self.conan_api.list.select(ListPattern("*:*")), tgz_path)
251271
return volumes, environment
252272

253-
def init_container(self) -> None:
273+
def _init_container(self) -> None:
254274
min_conan_version = '2.1'
255-
stdout, _ = self.run_command('conan --version', log=True)
275+
stdout, _ = self._run_command('conan --version', log=True)
256276
docker_conan_version = str(stdout.split('Conan version ')[1].replace('\n', '').replace('\r', '')) # Remove all characters and color
257277
if Version(docker_conan_version) <= Version(min_conan_version):
258278
raise ConanException(f'conan version inside the container must be greater than {min_conan_version}')
259279
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+
262283
for file_name in ['global.conf', 'settings.yml', 'remotes.json']:
263284
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)
265286
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"')
267288

268-
def update_local_cache(self) -> None:
289+
def _update_local_cache(self) -> None:
269290
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')
272293
tgz_path = os.path.join(self.abs_runner_home_path, 'docker_cache_save.tgz')
273294
self.logger.status(f'Restore host cache from: {tgz_path}')
274295
self.conan_api.cache.restore(tgz_path)
275296

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
280301

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)

conans/model/conf.py

+7
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@
131131
"tools.build:linker_scripts": "List of linker script files to pass to the linker used by different toolchains like CMakeToolchain, AutotoolsToolchain, and MesonToolchain",
132132
# Package ID composition
133133
"tools.info.package_id:confs": "List of existing configuration to be part of the package ID",
134+
# Runners
135+
"runner.type": "Type of runner to use. Possible values: 'docker'",
136+
"runner.dockerfile": "(Docker) Path to the Dockerfile to use in case of building a docker image",
137+
"runner.image": "(Docker) Image name to download from registry or the name of the built image in case of defining dockerfile path",
138+
"runner.cache": "(Docker) Host's conan cache behavior. Possible values: 'clean' (use empty cache), 'copy' (copy whole cache) or 'shared' (mount chache as shared volume)",
139+
"runner.remove": "(Docker) (boolean) Remove the container after running the Conan command",
140+
"runner.configfile": "(Docker) Path to a configuration file with extra parameters (https://containers.dev/implementors/json_reference/#image-specific)",
134141
}
135142

136143
BUILT_IN_CONFS = {key: value for key, value in sorted(BUILT_IN_CONFS.items())}

0 commit comments

Comments
 (0)