-
Notifications
You must be signed in to change notification settings - Fork 986
/
cmake.py
466 lines (395 loc) · 21.4 KB
/
cmake.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
import os
import platform
import re
from itertools import chain
from six import StringIO # Python 2 and 3 compatible
from conans.client import tools
from conans.client.build import defs_to_string, join_arguments
from conans.client.build.cmake_flags import CMakeDefinitionsBuilder, \
get_generator, is_multi_configuration, verbose_definition, verbose_definition_name, \
cmake_install_prefix_var_name, get_toolset, build_type_definition, \
cmake_in_local_cache_var_name, runtime_definition_var_name, get_generator_platform, \
is_generator_platform_supported, is_toolset_supported
from conans.client.output import ConanOutput
from conans.client.tools.env import environment_append, _environment_add
from conans.client.tools.oss import cpu_count, args_to_string
from conans.errors import ConanException
from conans.model.version import Version
from conans.util.conan_v2_mode import conan_v2_error
from conans.util.config_parser import get_bool_from_text
from conans.util.env_reader import get_env
from conans.util.files import mkdir, get_abs_path, walk, decode_text
from conans.util.runners import version_runner
class CMake(object):
def __init__(self, conanfile, generator=None, cmake_system_name=True,
parallel=True, build_type=None, toolset=None, make_program=None,
set_cmake_flags=False, msbuild_verbosity="minimal", cmake_program=None,
generator_platform=None, append_vcvars=False):
"""
:param conanfile: Conanfile instance
:param generator: Generator name to use or none to autodetect
:param cmake_system_name: False to not use CMAKE_SYSTEM_NAME variable,
True for auto-detect or directly a string with the system name
:param parallel: Try to build with multiple cores if available
:param build_type: Overrides default build type coming from settings
:param toolset: Toolset name to use (such as llvm-vs2014) or none for default one,
applies only to certain generators (e.g. Visual Studio)
:param set_cmake_flags: whether or not to set CMake flags like CMAKE_CXX_FLAGS,
CMAKE_C_FLAGS, etc. it's vital to set for certain projects
(e.g. using CMAKE_SIZEOF_VOID_P or CMAKE_LIBRARY_ARCHITECTURE)
:param msbuild_verbosity: verbosity level for MSBuild (in case of Visual Studio generator)
:param cmake_program: Path to the custom cmake executable
:param generator_platform: Generator platform name or none to autodetect (-A cmake option)
"""
if getattr(conanfile, "must_use_new_helpers", None):
wrong_helper_msg = "Using the wrong 'CMake' helper. To use CMakeDeps, CMakeToolchain "\
"you should use 'from conan.tools.cmake import CMake'. "\
"Set environment variable CONAN_DISABLE_STRICT_MODE=1 to override "\
"this check (should only be used to build old packages)."
if get_env("CONAN_DISABLE_STRICT_MODE", False):
conanfile.output.warning(wrong_helper_msg)
else:
raise ConanException(wrong_helper_msg)
conanfile.output.warning(f"**** The 'from conans import CMake' helper is deprecated. "
"Please update your code and remove it. ****")
self._append_vcvars = append_vcvars
self._conanfile = conanfile
self._settings = conanfile.settings
self._build_type = build_type or conanfile.settings.get_safe("build_type")
self._cmake_program = os.getenv("CONAN_CMAKE_PROGRAM") or cmake_program or "cmake"
self.generator_platform = generator_platform
self.generator = generator or get_generator(conanfile)
if not self.generator:
self._conanfile.output.warn("CMake generator could not be deduced from settings")
self.parallel = parallel
# Initialize definitions (won't be updated if conanfile or any of these variables change)
builder = CMakeDefinitionsBuilder(self._conanfile,
cmake_system_name=cmake_system_name,
make_program=make_program, parallel=parallel,
generator=self.generator,
set_cmake_flags=set_cmake_flags,
forced_build_type=build_type,
output=self._conanfile.output)
# FIXME CONAN 2.0: CMake() interface should be always the constructor and self.definitions.
# FIXME CONAN 2.0: Avoid properties and attributes to make the user interface more clear
try:
cmake_version = self.get_version()
self.definitions = builder.get_definitions(cmake_version)
except ConanException:
self.definitions = builder.get_definitions(None)
self.definitions["CONAN_EXPORTED"] = "1"
if hasattr(self._conanfile, 'settings_build'):
# https://github.com/conan-io/conan/issues/9202
if self._conanfile.settings_build.get_safe("os") == "Macos" and \
self._conanfile.settings.get_safe("os") == "iOS":
self.definitions["CMAKE_FIND_ROOT_PATH_MODE_INCLUDE"] = "BOTH"
self.definitions["CMAKE_FIND_ROOT_PATH_MODE_LIBRARY"] = "BOTH"
self.definitions["CMAKE_FIND_ROOT_PATH_MODE_PACKAGE"] = "BOTH"
self.toolset = toolset or get_toolset(self._settings, self.generator)
self.build_dir = None
self.msbuild_verbosity = os.getenv("CONAN_MSBUILD_VERBOSITY") or msbuild_verbosity
@property
def generator(self):
return self._generator
@generator.setter
def generator(self, value):
self._generator = value
if not self._generator_platform_is_assigned:
self._generator_platform = get_generator_platform(self._settings, self._generator)
@property
def generator_platform(self):
return self._generator_platform
@generator_platform.setter
def generator_platform(self, value):
self._generator_platform = value
self._generator_platform_is_assigned = bool(value is not None)
@property
def build_folder(self):
return self.build_dir
@build_folder.setter
def build_folder(self, value):
self.build_dir = value
@property
def build_type(self):
return self._build_type
@build_type.setter
def build_type(self, build_type):
settings_build_type = self._settings.get_safe("build_type")
self.definitions.pop("CMAKE_BUILD_TYPE", None)
self.definitions.update(build_type_definition(build_type, settings_build_type,
self.generator, self._conanfile.output))
self._build_type = build_type
@property
def in_local_cache(self):
try:
in_local_cache = self.definitions[cmake_in_local_cache_var_name]
return get_bool_from_text(str(in_local_cache))
except KeyError:
return False
@property
def runtime(self):
return defs_to_string(self.definitions.get(runtime_definition_var_name))
@property
def flags(self):
return defs_to_string(self.definitions)
@property
def is_multi_configuration(self):
return is_multi_configuration(self.generator)
@property
def command_line(self):
if self.generator_platform and not is_generator_platform_supported(self.generator):
raise ConanException('CMake does not support generator platform with generator '
'"%s:. Please check your conan profile to either remove the '
'generator platform, or change the CMake generator.'
% self.generator)
if self.toolset and not is_toolset_supported(self.generator):
raise ConanException('CMake does not support toolsets with generator "%s:.'
'Please check your conan profile to either remove the toolset,'
' or change the CMake generator.' % self.generator)
generator = self.generator
generator_platform = self.generator_platform
if self.generator_platform and 'Visual Studio' in generator:
# FIXME: Conan 2.0 We are adding the platform to the generator instead of using
# the -A argument to keep previous implementation, but any modern CMake will support
# (and recommend) passing the platform in its own argument.
# Get the version from the generator, as it could have been defined by user argument
compiler_version = re.search("Visual Studio ([0-9]*)", generator).group(1)
if Version(compiler_version) < "16" and self._settings.get_safe("os") != "WindowsCE":
if self.generator_platform == "x64":
generator += " Win64" if not generator.endswith(" Win64") else ""
generator_platform = None
elif self.generator_platform == "ARM":
generator += " ARM" if not generator.endswith(" ARM") else ""
generator_platform = None
elif self.generator_platform == "Win32":
generator_platform = None
args = ['-G "{}"'.format(generator)] if generator else []
if generator_platform:
args.append('-A "{}"'.format(generator_platform))
args.append(self.flags)
args.append('-Wno-dev')
if self.toolset:
args.append('-T "%s"' % self.toolset)
return join_arguments(args)
@property
def build_config(self):
""" cmake --build tool have a --config option for Multi-configuration IDEs
"""
if self._build_type and self.is_multi_configuration:
return "--config %s" % self._build_type
return ""
def _get_dirs(self, source_folder, build_folder, source_dir, build_dir, cache_build_folder):
if (source_folder or build_folder) and (source_dir or build_dir):
raise ConanException("Use 'build_folder'/'source_folder' arguments")
def get_dir(folder, origin):
if folder:
if os.path.isabs(folder):
return folder
return os.path.join(origin, folder)
return origin
if source_dir or build_dir: # OLD MODE
build_ret = build_dir or self.build_dir or self._conanfile.build_folder
source_ret = source_dir or self._conanfile.source_folder
else:
build_ret = get_dir(build_folder, self._conanfile.build_folder)
source_ret = get_dir(source_folder, self._conanfile.source_folder)
if self._conanfile.in_local_cache and cache_build_folder:
build_ret = get_dir(cache_build_folder, self._conanfile.build_folder)
return source_ret, build_ret
def _run(self, command):
compiler = self._settings.get_safe("compiler")
conan_v2_error("compiler setting should be defined.", not compiler)
the_os = self._settings.get_safe("os")
is_clangcl = the_os == "Windows" and compiler == "clang"
is_msvc = compiler == "Visual Studio"
is_intel = compiler == "intel"
context = tools.no_op()
if (is_msvc or is_clangcl) and platform.system() == "Windows":
if self.generator in ["Ninja", "Ninja Multi-Config",
"NMake Makefiles", "NMake Makefiles JOM"]:
vcvars_dict = tools.vcvars_dict(self._settings, force=True, filter_known_paths=False,
output=self._conanfile.output)
context = _environment_add(vcvars_dict, post=self._append_vcvars)
elif is_intel:
if self.generator in ["Ninja", "Ninja Multi-Config",
"NMake Makefiles", "NMake Makefiles JOM", "Unix Makefiles"]:
intel_compilervars_dict = tools.intel_compilervars_dict(self._conanfile, force=True)
context = _environment_add(intel_compilervars_dict, post=self._append_vcvars)
with context:
self._conanfile.run(command)
def configure(self, args=None, defs=None, source_dir=None, build_dir=None,
source_folder=None, build_folder=None, cache_build_folder=None,
pkg_config_paths=None):
# TODO: Deprecate source_dir and build_dir in favor of xxx_folder
if not self._conanfile.should_configure:
return
args = args or []
defs = defs or {}
source_dir, self.build_dir = self._get_dirs(source_folder, build_folder,
source_dir, build_dir,
cache_build_folder)
mkdir(self.build_dir)
arg_list = join_arguments([
self.command_line,
args_to_string(args),
defs_to_string(defs),
args_to_string([source_dir])
])
if pkg_config_paths:
pkg_env = {"PKG_CONFIG_PATH":
os.pathsep.join(get_abs_path(f, self._conanfile.install_folder)
for f in pkg_config_paths)}
else:
# If we are using pkg_config generator automate the pcs location, otherwise it could
# read wrong files
set_env = "pkg_config" in self._conanfile.generators \
and "PKG_CONFIG_PATH" not in os.environ
pkg_env = {"PKG_CONFIG_PATH": self._conanfile.install_folder} if set_env else None
with environment_append(pkg_env):
command = "cd %s && %s %s" % (args_to_string([self.build_dir]), self._cmake_program,
arg_list)
if platform.system() == "Windows" and self.generator == "MinGW Makefiles":
with tools.remove_from_path("sh"):
self._run(command)
else:
self._run(command)
def build(self, args=None, build_dir=None, target=None):
if not self._conanfile.should_build:
return
conan_v2_error("build_type setting should be defined.", not self._build_type)
self._build(args, build_dir, target)
def _build(self, args=None, build_dir=None, target=None):
args = args or []
build_dir = build_dir or self.build_dir or self._conanfile.build_folder
if target is not None:
args = ["--target", target] + args
if self.generator and self.parallel:
if ("Makefiles" in self.generator or "Ninja" in self.generator) and \
"NMake" not in self.generator:
if "--" not in args:
args.append("--")
args.append("-j%i" % cpu_count(self._conanfile.output))
elif "Visual Studio" in self.generator:
compiler_version = re.search("Visual Studio ([0-9]*)", self.generator).group(1)
if Version(compiler_version) >= "10":
if "--" not in args:
args.append("--")
# Parallel for building projects in the solution
args.append("/m:%i" % cpu_count(output=self._conanfile.output))
if self.generator and self.msbuild_verbosity:
if "Visual Studio" in self.generator:
compiler_version = re.search("Visual Studio ([0-9]*)", self.generator).group(1)
if Version(compiler_version) >= "10":
if "--" not in args:
args.append("--")
args.append("/verbosity:%s" % self.msbuild_verbosity)
arg_list = join_arguments([
args_to_string([build_dir]),
self.build_config,
args_to_string(args)
])
command = "%s --build %s" % (self._cmake_program, arg_list)
self._run(command)
def install(self, args=None, build_dir=None):
if not self._conanfile.should_install:
return
mkdir(self._conanfile.package_folder)
if not self.definitions.get(cmake_install_prefix_var_name):
raise ConanException("%s not defined for 'cmake.install()'\n"
"Make sure 'package_folder' is "
"defined" % cmake_install_prefix_var_name)
self._build(args=args, build_dir=build_dir, target="install")
def test(self, args=None, build_dir=None, target=None, output_on_failure=False):
if not self._conanfile.should_test or not get_env("CONAN_RUN_TESTS", True) or \
self._conanfile.conf["tools.build:skip_test"]:
return
if not target:
target = "RUN_TESTS" if self.is_multi_configuration else "test"
test_env = {'CTEST_OUTPUT_ON_FAILURE': '1' if output_on_failure else '0'}
if self.parallel:
test_env['CTEST_PARALLEL_LEVEL'] = str(cpu_count(self._conanfile.output))
with environment_append(test_env):
self._build(args=args, build_dir=build_dir, target=target)
@property
def verbose(self):
try:
verbose = self.definitions[verbose_definition_name]
return get_bool_from_text(str(verbose))
except KeyError:
return False
@verbose.setter
def verbose(self, value):
self.definitions.update(verbose_definition(value))
def patch_config_paths(self):
"""
changes references to the absolute path of the installed package and its dependencies in
exported cmake config files to the appropriate conan variable. This makes
most (sensible) cmake config files portable.
For example, if a package foo installs a file called "fooConfig.cmake" to
be used by cmake's find_package method, normally this file will contain
absolute paths to the installed package folder, for example it will contain
a line such as:
SET(Foo_INSTALL_DIR /home/developer/.conan/data/Foo/1.0.0/...)
This will cause cmake find_package() method to fail when someone else
installs the package via conan.
This function will replace such mentions to
SET(Foo_INSTALL_DIR ${CONAN_FOO_ROOT})
which is a variable that is set by conanbuildinfo.cmake, so that find_package()
now correctly works on this conan package.
For dependent packages, if a package foo installs a file called "fooConfig.cmake" to
be used by cmake's find_package method and if it depends to a package bar,
normally this file will contain absolute paths to the bar package folder,
for example it will contain a line such as:
SET_TARGET_PROPERTIES(foo PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES
"/home/developer/.conan/data/Bar/1.0.0/user/channel/id/include")
This function will replace such mentions to
SET_TARGET_PROPERTIES(foo PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES
"${CONAN_BAR_ROOT}/include")
If the install() method of the CMake object in the conan file is used, this
function should be called _after_ that invocation. For example:
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()
cmake.install()
cmake.patch_config_paths()
"""
if not self._conanfile.should_install:
return
if not self._conanfile.name:
raise ConanException("cmake.patch_config_paths() can't work without package name. "
"Define name in your recipe")
pf = self.definitions.get(cmake_install_prefix_var_name)
replstr = "${CONAN_%s_ROOT}" % self._conanfile.name.upper()
allwalk = chain(walk(self._conanfile.build_folder), walk(self._conanfile.package_folder))
# We don't want warnings printed because there is no replacement of the abs path.
# there could be MANY cmake files in the package and the normal thing is to not find
# the abs paths
_null_out = ConanOutput(StringIO())
for root, _, files in allwalk:
for f in files:
if f.endswith(".cmake") and not f.startswith("conan"):
path = os.path.join(root, f)
tools.replace_path_in_file(path, pf, replstr, strict=False,
output=_null_out)
# patch paths of dependent packages that are found in any cmake files of the
# current package
for dep in self._conanfile.deps_cpp_info.deps:
from_str = self._conanfile.deps_cpp_info[dep].rootpath
dep_str = "${CONAN_%s_ROOT}" % dep.upper()
ret = tools.replace_path_in_file(path, from_str, dep_str, strict=False,
output=_null_out)
if ret:
self._conanfile.output.info("Patched paths for %s: %s to %s"
% (dep, from_str, dep_str))
@staticmethod
def get_version():
try:
out = version_runner(["cmake", "--version"])
version_line = decode_text(out).split('\n', 1)[0]
version_str = version_line.rsplit(' ', 1)[-1]
return Version(version_str)
except Exception as e:
raise ConanException("Error retrieving CMake version: '{}'".format(e))