Skip to content

Commit 605d489

Browse files
authored
Merge branch 'develop2' into feature/enable-bazel-7.1
2 parents 6641656 + bd60587 commit 605d489

File tree

23 files changed

+1556
-63
lines changed

23 files changed

+1556
-63
lines changed

conan/cli/commands/create.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def create(conan_api, parser, *args):
3333
parser.add_argument("-bt", "--build-test", action="append",
3434
help="Same as '--build' but only for the test_package requires. By default"
3535
" if not specified it will take the '--build' value if specified")
36+
raw_args = args[0]
3637
args = parser.parse_args(*args)
3738

3839
if args.test_missing and args.test_folder == "":
@@ -62,6 +63,25 @@ def create(conan_api, parser, *args):
6263
lockfile = conan_api.lockfile.update_lockfile_export(lockfile, conanfile, ref, is_build)
6364

6465
print_profiles(profile_host, profile_build)
66+
if profile_host.runner and not os.environ.get("CONAN_RUNNER_ENVIRONMENT"):
67+
from conan.internal.runner.docker import DockerRunner
68+
from conan.internal.runner.ssh import SSHRunner
69+
from conan.internal.runner.wsl import WSLRunner
70+
try:
71+
runner_type = profile_host.runner['type'].lower()
72+
except KeyError:
73+
raise ConanException(f"Invalid runner configuration. 'type' must be defined")
74+
runner_instances_map = {
75+
'docker': DockerRunner,
76+
# 'ssh': SSHRunner,
77+
# 'wsl': WSLRunner,
78+
}
79+
try:
80+
runner_instance = runner_instances_map[runner_type]
81+
except KeyError:
82+
raise ConanException(f"Invalid runner type '{runner_type}'. Allowed values: {', '.join(runner_instances_map.keys())}")
83+
return runner_instance(conan_api, 'create', profile_host, profile_build, args, raw_args).run()
84+
6585
if args.build is not None and args.build_test is None:
6686
args.build_test = args.build
6787

conan/internal/api/detect_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,8 @@ def detect_default_compiler():
360360
if cc or cxx: # Env defined, use them
361361
output.info("CC and CXX: %s, %s " % (cc or "None", cxx or "None"))
362362
command = cc or cxx
363+
if "/usr/bin/cc" == command or "/usr/bin/c++" == command: # Symlinks of linux "alternatives"
364+
return _cc_compiler(command)
363365
if "clang" in command.lower():
364366
return detect_clang_compiler(command)
365367
if "gnu-cc" in command or "gcc" in command or "g++" in command or "c++" in command:
@@ -424,10 +426,9 @@ def _detect_vs_ide_version():
424426
return None
425427

426428

427-
def _cc_compiler():
429+
def _cc_compiler(compiler_exe="cc"):
428430
# Try to detect the "cc" linux system "alternative". It could point to gcc or clang
429431
try:
430-
compiler_exe = "cc"
431432
ret, out = detect_runner('%s --version' % compiler_exe)
432433
if ret != 0:
433434
return None, None, None

conan/internal/runner/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class RunnerException(Exception):
2+
def __init__(self, *args, **kwargs):
3+
self.command = kwargs.pop("command", None)
4+
self.stdout_log = kwargs.pop("stdout_log", None)
5+
self.stderr_log = kwargs.pop("stderr_log", None)
6+
super(RunnerException, self).__init__(*args, **kwargs)

conan/internal/runner/docker.py

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
from collections import namedtuple
2+
import os
3+
import json
4+
import platform
5+
import shutil
6+
import yaml
7+
from conan.api.model import ListPattern
8+
from conan.api.output import Color, ConanOutput
9+
from conan.api.conan_api import ConfigAPI
10+
from conan.cli import make_abs_path
11+
from conan.internal.runner import RunnerException
12+
from conans.client.profile_loader import ProfileLoader
13+
from conans.errors import ConanException
14+
from conans.model.version import Version
15+
16+
17+
def config_parser(file_path):
18+
Build = namedtuple('Build', ['dockerfile', 'build_context', 'build_args', 'cache_from'])
19+
Run = namedtuple('Run', ['name', 'environment', 'user', 'privileged', 'cap_add', 'security_opt', 'volumes'])
20+
Conf = namedtuple('Conf', ['image', 'build', 'run'])
21+
if file_path:
22+
def _instans_or_error(value, obj):
23+
if value and (not isinstance(value, obj)):
24+
raise ConanException(f"docker runner configfile syntax error: {value} must be a {obj.__name__}")
25+
return value
26+
with open(file_path, 'r') as f:
27+
runnerfile = yaml.safe_load(f)
28+
return Conf(
29+
image=_instans_or_error(runnerfile.get('image'), str),
30+
build=Build(
31+
dockerfile=_instans_or_error(runnerfile.get('build', {}).get('dockerfile'), str),
32+
build_context=_instans_or_error(runnerfile.get('build', {}).get('build_context'), str),
33+
build_args=_instans_or_error(runnerfile.get('build', {}).get('build_args'), dict),
34+
cache_from=_instans_or_error(runnerfile.get('build', {}).get('cacheFrom'), list),
35+
),
36+
run=Run(
37+
name=_instans_or_error(runnerfile.get('run', {}).get('name'), str),
38+
environment=_instans_or_error(runnerfile.get('run', {}).get('containerEnv'), dict),
39+
user=_instans_or_error(runnerfile.get('run', {}).get('containerUser'), str),
40+
privileged=_instans_or_error(runnerfile.get('run', {}).get('privileged'), bool),
41+
cap_add=_instans_or_error(runnerfile.get('run', {}).get('capAdd'), list),
42+
security_opt=_instans_or_error(runnerfile.get('run', {}).get('securityOpt'), list),
43+
volumes=_instans_or_error(runnerfile.get('run', {}).get('mounts'), dict),
44+
)
45+
)
46+
else:
47+
return Conf(
48+
image=None,
49+
build=Build(dockerfile=None, build_context=None, build_args=None, cache_from=None),
50+
run=Run(name=None, environment=None, user=None, privileged=None, cap_add=None, security_opt=None, volumes=None)
51+
)
52+
53+
54+
def _docker_info(msg, error=False):
55+
fg=Color.BRIGHT_MAGENTA
56+
if error:
57+
fg=Color.BRIGHT_RED
58+
ConanOutput().status('\n┌'+'─'*(2+len(msg))+'┐', fg=fg)
59+
ConanOutput().status(f'| {msg} |', fg=fg)
60+
ConanOutput().status('└'+'─'*(2+len(msg))+'┘\n', fg=fg)
61+
62+
63+
class DockerRunner:
64+
def __init__(self, conan_api, command, host_profile, build_profile, args, raw_args):
65+
import docker
66+
import docker.api.build
67+
try:
68+
self.docker_client = docker.from_env()
69+
self.docker_api = docker.APIClient()
70+
docker.api.build.process_dockerfile = lambda dockerfile, path: ('Dockerfile', dockerfile)
71+
except:
72+
raise ConanException("Docker Client failed to initialize."
73+
"\n - Check if docker is installed and running"
74+
"\n - Run 'pip install pip install conan[runners]'")
75+
self.conan_api = conan_api
76+
self.build_profile = build_profile
77+
self.args = args
78+
self.abs_host_path = make_abs_path(args.path)
79+
if args.format:
80+
raise ConanException("format argument is forbidden if running in a docker runner")
81+
82+
# Runner config
83+
self.abs_runner_home_path = os.path.join(self.abs_host_path, '.conanrunner')
84+
self.abs_docker_path = os.path.join('/root/conanrunner', os.path.basename(self.abs_host_path)).replace("\\","/")
85+
86+
# Update conan command and some paths to run inside the container
87+
raw_args[raw_args.index(args.path)] = self.abs_docker_path
88+
self.profiles = []
89+
if self.args.profile_build and self.args.profile_host:
90+
profile_list = set(self.args.profile_build + self.args.profile_host)
91+
else:
92+
profile_list = self.args.profile_host or self.args.profile_build
93+
94+
# Update the profile paths
95+
for i, raw_arg in enumerate(raw_args):
96+
for i, raw_profile in enumerate(profile_list):
97+
_profile = ProfileLoader.get_profile_path(os.path.join(ConfigAPI(self.conan_api).home(), 'profiles'), raw_profile, os.getcwd())
98+
_name = f'{os.path.basename(_profile)}_{i}'
99+
if raw_profile in raw_arg:
100+
raw_args[raw_args.index(raw_arg)] = raw_arg.replace(raw_profile, os.path.join(self.abs_docker_path, '.conanrunner/profiles', _name))
101+
self.profiles.append([_profile, os.path.join(self.abs_runner_home_path, 'profiles', _name)])
102+
103+
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'])
104+
105+
# Container config
106+
# https://containers.dev/implementors/json_reference/
107+
self.configfile = config_parser(host_profile.runner.get('configfile'))
108+
self.dockerfile = host_profile.runner.get('dockerfile') or self.configfile.build.dockerfile
109+
self.docker_build_context = host_profile.runner.get('build_context') or self.configfile.build.build_context
110+
self.image = host_profile.runner.get('image') or self.configfile.image
111+
if not (self.dockerfile or self.image):
112+
raise ConanException("'dockerfile' or docker image name is needed")
113+
self.image = self.image or 'conan-runner-default'
114+
self.name = self.configfile.image or f'conan-runner-{host_profile.runner.get("suffix", "docker")}'
115+
self.remove = str(host_profile.runner.get('remove', 'false')).lower() == 'true'
116+
self.cache = str(host_profile.runner.get('cache', 'clean'))
117+
self.container = None
118+
119+
def run(self):
120+
"""
121+
run conan inside a Docker continer
122+
"""
123+
if self.dockerfile:
124+
_docker_info(f'Building the Docker image: {self.image}')
125+
self.build_image()
126+
volumes, environment = self.create_runner_environment()
127+
error = False
128+
try:
129+
if self.docker_client.containers.list(all=True, filters={'name': self.name}):
130+
_docker_info('Starting the docker container')
131+
self.container = self.docker_client.containers.get(self.name)
132+
self.container.start()
133+
else:
134+
if self.configfile.run.environment:
135+
environment.update(self.configfile.run.environment)
136+
if self.configfile.run.volumes:
137+
volumes.update(self.configfile.run.volumes)
138+
_docker_info('Creating the docker container')
139+
self.container = self.docker_client.containers.run(
140+
self.image,
141+
"/bin/bash -c 'while true; do sleep 30; done;'",
142+
name=self.name,
143+
volumes=volumes,
144+
environment=environment,
145+
user=self.configfile.run.user,
146+
privileged=self.configfile.run.privileged,
147+
cap_add=self.configfile.run.cap_add,
148+
security_opt=self.configfile.run.security_opt,
149+
detach=True,
150+
auto_remove=False)
151+
_docker_info(f'Container {self.name} running')
152+
except Exception as e:
153+
raise ConanException(f'Imposible to run the container "{self.name}" with image "{self.image}"'
154+
f'\n\n{str(e)}')
155+
try:
156+
self.init_container()
157+
self.run_command(self.command)
158+
self.update_local_cache()
159+
except ConanException as e:
160+
error = True
161+
raise e
162+
except RunnerException as e:
163+
error = True
164+
raise ConanException(f'"{e.command}" inside docker fail'
165+
f'\n\nLast command output: {str(e.stdout_log)}')
166+
finally:
167+
if self.container:
168+
error_prefix = 'ERROR: ' if error else ''
169+
_docker_info(f'{error_prefix}Stopping container', error)
170+
self.container.stop()
171+
if self.remove:
172+
_docker_info(f'{error_prefix}Removing container', error)
173+
self.container.remove()
174+
175+
def build_image(self):
176+
dockerfile_file_path = self.dockerfile
177+
if os.path.isdir(self.dockerfile):
178+
dockerfile_file_path = os.path.join(self.dockerfile, 'Dockerfile')
179+
with open(dockerfile_file_path) as f:
180+
build_path = self.docker_build_context or os.path.dirname(dockerfile_file_path)
181+
ConanOutput().highlight(f"Dockerfile path: '{dockerfile_file_path}'")
182+
ConanOutput().highlight(f"Docker build context: '{build_path}'\n")
183+
docker_build_logs = self.docker_api.build(
184+
path=build_path,
185+
dockerfile=f.read(),
186+
tag=self.image,
187+
buildargs=self.configfile.build.build_args,
188+
cache_from=self.configfile.build.cache_from,
189+
)
190+
for chunk in docker_build_logs:
191+
for line in chunk.decode("utf-8").split('\r\n'):
192+
if line:
193+
stream = json.loads(line).get('stream')
194+
if stream:
195+
ConanOutput().status(stream.strip())
196+
197+
def run_command(self, command, log=True):
198+
if log:
199+
_docker_info(f'Running in container: "{command}"')
200+
exec_instance = self.docker_api.exec_create(self.container.id, f"/bin/bash -c '{command}'", tty=True)
201+
exec_output = self.docker_api.exec_start(exec_instance['Id'], tty=True, stream=True, demux=True,)
202+
stderr_log, stdout_log = '', ''
203+
try:
204+
for (stdout_out, stderr_out) in exec_output:
205+
if stdout_out is not None:
206+
stdout_log += stdout_out.decode('utf-8', errors='ignore').strip()
207+
if log:
208+
ConanOutput().status(stdout_out.decode('utf-8', errors='ignore').strip())
209+
if stderr_out is not None:
210+
stderr_log += stderr_out.decode('utf-8', errors='ignore').strip()
211+
if log:
212+
ConanOutput().status(stderr_out.decode('utf-8', errors='ignore').strip())
213+
except Exception as e:
214+
if platform.system() == 'Windows':
215+
import pywintypes
216+
if isinstance(e, pywintypes.error):
217+
pass
218+
else:
219+
raise e
220+
exit_metadata = self.docker_api.exec_inspect(exec_instance['Id'])
221+
if exit_metadata['Running'] or exit_metadata['ExitCode'] > 0:
222+
raise RunnerException(command=command, stdout_log=stdout_log, stderr_log=stderr_log)
223+
return stdout_log, stderr_log
224+
225+
def create_runner_environment(self):
226+
shutil.rmtree(self.abs_runner_home_path, ignore_errors=True)
227+
volumes = {self.abs_host_path: {'bind': self.abs_docker_path, 'mode': 'rw'}}
228+
environment = {'CONAN_RUNNER_ENVIRONMENT': '1'}
229+
if self.cache == 'shared':
230+
volumes[ConfigAPI(self.conan_api).home()] = {'bind': '/root/.conan2', 'mode': 'rw'}
231+
if self.cache in ['clean', 'copy']:
232+
os.mkdir(self.abs_runner_home_path)
233+
os.mkdir(os.path.join(self.abs_runner_home_path, 'profiles'))
234+
235+
# Copy all conan config files to docker workspace
236+
for file_name in ['global.conf', 'settings.yml', 'remotes.json']:
237+
src_file = os.path.join(ConfigAPI(self.conan_api).home(), file_name)
238+
if os.path.exists(src_file):
239+
shutil.copy(src_file, os.path.join(self.abs_runner_home_path, file_name))
240+
241+
# Copy all profiles to docker workspace
242+
for current_path, new_path in self.profiles:
243+
shutil.copy(current_path, new_path)
244+
245+
if self.cache == 'copy':
246+
tgz_path = os.path.join(self.abs_runner_home_path, 'local_cache_save.tgz')
247+
_docker_info(f'Save host cache in: {tgz_path}')
248+
self.conan_api.cache.save(self.conan_api.list.select(ListPattern("*:*")), tgz_path)
249+
return volumes, environment
250+
251+
def init_container(self):
252+
min_conan_version = '2.1'
253+
stdout, _ = self.run_command('conan --version', log=True)
254+
docker_conan_version = str(stdout.split('Conan version ')[1].replace('\n', '').replace('\r', '')) # Remove all characters and color
255+
if Version(docker_conan_version) <= Version(min_conan_version):
256+
ConanOutput().status(f'ERROR: conan version inside the container must be greater than {min_conan_version}', fg=Color.BRIGHT_RED)
257+
raise ConanException( f'conan version inside the container must be greater than {min_conan_version}')
258+
if self.cache != 'shared':
259+
self.run_command('mkdir -p ${HOME}/.conan2/profiles', log=False)
260+
self.run_command('cp -r "'+self.abs_docker_path+'/.conanrunner/profiles/." ${HOME}/.conan2/profiles/.', log=False)
261+
for file_name in ['global.conf', 'settings.yml', 'remotes.json']:
262+
if os.path.exists( os.path.join(self.abs_runner_home_path, file_name)):
263+
self.run_command('cp "'+self.abs_docker_path+'/.conanrunner/'+file_name+'" ${HOME}/.conan2/'+file_name, log=False)
264+
if self.cache in ['copy']:
265+
self.run_command('conan cache restore "'+self.abs_docker_path+'/.conanrunner/local_cache_save.tgz"')
266+
267+
def update_local_cache(self):
268+
if self.cache != 'shared':
269+
self.run_command('conan list --graph=create.json --graph-binaries=build --format=json > pkglist.json', log=False)
270+
self.run_command('conan cache save --list=pkglist.json --file "'+self.abs_docker_path+'"/.conanrunner/docker_cache_save.tgz')
271+
tgz_path = os.path.join(self.abs_runner_home_path, 'docker_cache_save.tgz')
272+
_docker_info(f'Restore host cache from: {tgz_path}')
273+
package_list = self.conan_api.cache.restore(tgz_path)

0 commit comments

Comments
 (0)