File indexing completed on 2025-02-16 05:12:09
0001 import os 0002 import platform 0003 import re 0004 from itertools import chain 0005 0006 from six import StringIO # Python 2 and 3 compatible 0007 0008 from conans.client import tools 0009 from conans.client.build import defs_to_string, join_arguments 0010 from conans.client.build.cmake_flags import CMakeDefinitionsBuilder, \ 0011 get_generator, is_multi_configuration, verbose_definition, verbose_definition_name, \ 0012 cmake_install_prefix_var_name, get_toolset, build_type_definition, \ 0013 cmake_in_local_cache_var_name, runtime_definition_var_name, get_generator_platform, \ 0014 is_generator_platform_supported, is_toolset_supported 0015 from conans.client.output import ConanOutput 0016 from conans.client.tools.env import environment_append, _environment_add 0017 from conans.client.tools.oss import cpu_count, args_to_string 0018 from conans.errors import ConanException 0019 from conans.model.version import Version 0020 from conans.util.conan_v2_mode import conan_v2_error 0021 from conans.util.config_parser import get_bool_from_text 0022 from conans.util.env_reader import get_env 0023 from conans.util.files import mkdir, get_abs_path, walk, decode_text 0024 from conans.util.runners import version_runner 0025 0026 0027 class CMake(object): 0028 0029 def __init__(self, conanfile, generator=None, cmake_system_name=True, 0030 parallel=True, build_type=None, toolset=None, make_program=None, 0031 set_cmake_flags=False, msbuild_verbosity="minimal", cmake_program=None, 0032 generator_platform=None, append_vcvars=False): 0033 """ 0034 :param conanfile: Conanfile instance 0035 :param generator: Generator name to use or none to autodetect 0036 :param cmake_system_name: False to not use CMAKE_SYSTEM_NAME variable, 0037 True for auto-detect or directly a string with the system name 0038 :param parallel: Try to build with multiple cores if available 0039 :param build_type: Overrides default build type coming from settings 0040 :param toolset: Toolset name to use (such as llvm-vs2014) or none for default one, 0041 applies only to certain generators (e.g. Visual Studio) 0042 :param set_cmake_flags: whether or not to set CMake flags like CMAKE_CXX_FLAGS, 0043 CMAKE_C_FLAGS, etc. it's vital to set for certain projects 0044 (e.g. using CMAKE_SIZEOF_VOID_P or CMAKE_LIBRARY_ARCHITECTURE) 0045 :param msbuild_verbosity: verbosity level for MSBuild (in case of Visual Studio generator) 0046 :param cmake_program: Path to the custom cmake executable 0047 :param generator_platform: Generator platform name or none to autodetect (-A cmake option) 0048 """ 0049 self._append_vcvars = append_vcvars 0050 self._conanfile = conanfile 0051 self._settings = conanfile.settings 0052 self._build_type = build_type or conanfile.settings.get_safe("build_type") 0053 self._cmake_program = os.getenv("CONAN_CMAKE_PROGRAM") or cmake_program or "cmake" 0054 0055 self.generator_platform = generator_platform 0056 self.generator = generator or get_generator(conanfile) 0057 0058 if not self.generator: 0059 self._conanfile.output.warn("CMake generator could not be deduced from settings") 0060 self.parallel = parallel 0061 # Initialize definitions (won't be updated if conanfile or any of these variables change) 0062 builder = CMakeDefinitionsBuilder(self._conanfile, 0063 cmake_system_name=cmake_system_name, 0064 make_program=make_program, parallel=parallel, 0065 generator=self.generator, 0066 set_cmake_flags=set_cmake_flags, 0067 forced_build_type=build_type, 0068 output=self._conanfile.output) 0069 # FIXME CONAN 2.0: CMake() interface should be always the constructor and self.definitions. 0070 # FIXME CONAN 2.0: Avoid properties and attributes to make the user interface more clear 0071 0072 try: 0073 cmake_version = self.get_version() 0074 self.definitions = builder.get_definitions(cmake_version) 0075 except ConanException: 0076 self.definitions = builder.get_definitions(None) 0077 0078 self.definitions["CONAN_EXPORTED"] = "1" 0079 0080 if hasattr(self._conanfile, 'settings_build'): 0081 # https://github.com/conan-io/conan/issues/9202 0082 if self._conanfile.settings_build.get_safe("os") == "Macos" and \ 0083 self._conanfile.settings.get_safe("os") == "iOS": 0084 self.definitions["CMAKE_FIND_ROOT_PATH_MODE_INCLUDE"] = "BOTH" 0085 self.definitions["CMAKE_FIND_ROOT_PATH_MODE_LIBRARY"] = "BOTH" 0086 self.definitions["CMAKE_FIND_ROOT_PATH_MODE_PACKAGE"] = "BOTH" 0087 0088 self.toolset = toolset or get_toolset(self._settings, self.generator) 0089 self.build_dir = None 0090 self.msbuild_verbosity = os.getenv("CONAN_MSBUILD_VERBOSITY") or msbuild_verbosity 0091 0092 @property 0093 def generator(self): 0094 return self._generator 0095 0096 @generator.setter 0097 def generator(self, value): 0098 self._generator = value 0099 if not self._generator_platform_is_assigned: 0100 self._generator_platform = get_generator_platform(self._settings, self._generator) 0101 0102 @property 0103 def generator_platform(self): 0104 return self._generator_platform 0105 0106 @generator_platform.setter 0107 def generator_platform(self, value): 0108 self._generator_platform = value 0109 self._generator_platform_is_assigned = bool(value is not None) 0110 0111 @property 0112 def build_folder(self): 0113 return self.build_dir 0114 0115 @build_folder.setter 0116 def build_folder(self, value): 0117 self.build_dir = value 0118 0119 @property 0120 def build_type(self): 0121 return self._build_type 0122 0123 @build_type.setter 0124 def build_type(self, build_type): 0125 settings_build_type = self._settings.get_safe("build_type") 0126 self.definitions.pop("CMAKE_BUILD_TYPE", None) 0127 self.definitions.update(build_type_definition(build_type, settings_build_type, 0128 self.generator, self._conanfile.output)) 0129 self._build_type = build_type 0130 0131 @property 0132 def in_local_cache(self): 0133 try: 0134 in_local_cache = self.definitions[cmake_in_local_cache_var_name] 0135 return get_bool_from_text(str(in_local_cache)) 0136 except KeyError: 0137 return False 0138 0139 @property 0140 def runtime(self): 0141 return defs_to_string(self.definitions.get(runtime_definition_var_name)) 0142 0143 @property 0144 def flags(self): 0145 return defs_to_string(self.definitions) 0146 0147 @property 0148 def is_multi_configuration(self): 0149 return is_multi_configuration(self.generator) 0150 0151 @property 0152 def command_line(self): 0153 if self.generator_platform and not is_generator_platform_supported(self.generator): 0154 raise ConanException('CMake does not support generator platform with generator ' 0155 '"%s:. Please check your conan profile to either remove the ' 0156 'generator platform, or change the CMake generator.' 0157 % self.generator) 0158 0159 if self.toolset and not is_toolset_supported(self.generator): 0160 raise ConanException('CMake does not support toolsets with generator "%s:.' 0161 'Please check your conan profile to either remove the toolset,' 0162 ' or change the CMake generator.' % self.generator) 0163 0164 generator = self.generator 0165 generator_platform = self.generator_platform 0166 0167 if self.generator_platform and 'Visual Studio' in generator: 0168 # FIXME: Conan 2.0 We are adding the platform to the generator instead of using 0169 # the -A argument to keep previous implementation, but any modern CMake will support 0170 # (and recommend) passing the platform in its own argument. 0171 # Get the version from the generator, as it could have been defined by user argument 0172 compiler_version = re.search("Visual Studio ([0-9]*)", generator).group(1) 0173 if Version(compiler_version) < "16" and self._settings.get_safe("os") != "WindowsCE": 0174 if self.generator_platform == "x64": 0175 generator += " Win64" if not generator.endswith(" Win64") else "" 0176 generator_platform = None 0177 elif self.generator_platform == "ARM": 0178 generator += " ARM" if not generator.endswith(" ARM") else "" 0179 generator_platform = None 0180 elif self.generator_platform == "Win32": 0181 generator_platform = None 0182 0183 args = ['-G "{}"'.format(generator)] if generator else [] 0184 if generator_platform: 0185 args.append('-A "{}"'.format(generator_platform)) 0186 0187 args.append(self.flags) 0188 args.append('-Wno-dev') 0189 0190 if self.toolset: 0191 args.append('-T "%s"' % self.toolset) 0192 0193 return join_arguments(args) 0194 0195 @property 0196 def build_config(self): 0197 """ cmake --build tool have a --config option for Multi-configuration IDEs 0198 """ 0199 if self._build_type and self.is_multi_configuration: 0200 return "--config %s" % self._build_type 0201 return "" 0202 0203 def _get_dirs(self, source_folder, build_folder, source_dir, build_dir, cache_build_folder): 0204 if (source_folder or build_folder) and (source_dir or build_dir): 0205 raise ConanException("Use 'build_folder'/'source_folder' arguments") 0206 0207 def get_dir(folder, origin): 0208 if folder: 0209 if os.path.isabs(folder): 0210 return folder 0211 return os.path.join(origin, folder) 0212 return origin 0213 0214 if source_dir or build_dir: # OLD MODE 0215 build_ret = build_dir or self.build_dir or self._conanfile.build_folder 0216 source_ret = source_dir or self._conanfile.source_folder 0217 else: 0218 build_ret = get_dir(build_folder, self._conanfile.build_folder) 0219 source_ret = get_dir(source_folder, self._conanfile.source_folder) 0220 0221 if self._conanfile.in_local_cache and cache_build_folder: 0222 build_ret = get_dir(cache_build_folder, self._conanfile.build_folder) 0223 0224 return source_ret, build_ret 0225 0226 def _run(self, command): 0227 compiler = self._settings.get_safe("compiler") 0228 conan_v2_error("compiler setting should be defined.", not compiler) 0229 the_os = self._settings.get_safe("os") 0230 is_clangcl = the_os == "Windows" and compiler == "clang" 0231 is_msvc = compiler == "Visual Studio" 0232 is_intel = compiler == "intel" 0233 context = tools.no_op() 0234 0235 if (is_msvc or is_clangcl) and platform.system() == "Windows": 0236 if self.generator in ["Ninja", "Ninja Multi-Config", 0237 "NMake Makefiles", "NMake Makefiles JOM"]: 0238 vcvars_dict = tools.vcvars_dict(self._settings, force=True, filter_known_paths=False, 0239 output=self._conanfile.output) 0240 context = _environment_add(vcvars_dict, post=self._append_vcvars) 0241 elif is_intel: 0242 if self.generator in ["Ninja", "Ninja Multi-Config", 0243 "NMake Makefiles", "NMake Makefiles JOM", "Unix Makefiles"]: 0244 intel_compilervars_dict = tools.intel_compilervars_dict(self._conanfile, force=True) 0245 context = _environment_add(intel_compilervars_dict, post=self._append_vcvars) 0246 with context: 0247 self._conanfile.run(command) 0248 0249 def configure(self, args=None, defs=None, source_dir=None, build_dir=None, 0250 source_folder=None, build_folder=None, cache_build_folder=None, 0251 pkg_config_paths=None): 0252 0253 # TODO: Deprecate source_dir and build_dir in favor of xxx_folder 0254 if not self._conanfile.should_configure: 0255 return 0256 args = args or [] 0257 defs = defs or {} 0258 source_dir, self.build_dir = self._get_dirs(source_folder, build_folder, 0259 source_dir, build_dir, 0260 cache_build_folder) 0261 mkdir(self.build_dir) 0262 arg_list = join_arguments([ 0263 self.command_line, 0264 args_to_string(args), 0265 defs_to_string(defs), 0266 args_to_string([source_dir]) 0267 ]) 0268 0269 if pkg_config_paths: 0270 pkg_env = {"PKG_CONFIG_PATH": 0271 os.pathsep.join(get_abs_path(f, self._conanfile.install_folder) 0272 for f in pkg_config_paths)} 0273 else: 0274 # If we are using pkg_config generator automate the pcs location, otherwise it could 0275 # read wrong files 0276 set_env = "pkg_config" in self._conanfile.generators \ 0277 and "PKG_CONFIG_PATH" not in os.environ 0278 pkg_env = {"PKG_CONFIG_PATH": self._conanfile.install_folder} if set_env else None 0279 0280 with environment_append(pkg_env): 0281 command = "cd %s && %s %s" % (args_to_string([self.build_dir]), self._cmake_program, 0282 arg_list) 0283 if platform.system() == "Windows" and self.generator == "MinGW Makefiles": 0284 with tools.remove_from_path("sh"): 0285 self._run(command) 0286 else: 0287 self._run(command) 0288 0289 def build(self, args=None, build_dir=None, target=None): 0290 if not self._conanfile.should_build: 0291 return 0292 conan_v2_error("build_type setting should be defined.", not self._build_type) 0293 self._build(args, build_dir, target) 0294 0295 def _build(self, args=None, build_dir=None, target=None): 0296 args = args or [] 0297 build_dir = build_dir or self.build_dir or self._conanfile.build_folder 0298 if target is not None: 0299 args = ["--target", target] + args 0300 0301 if self.generator and self.parallel: 0302 if ("Makefiles" in self.generator or "Ninja" in self.generator) and \ 0303 "NMake" not in self.generator: 0304 if "--" not in args: 0305 args.append("--") 0306 args.append("-j%i" % cpu_count(self._conanfile.output)) 0307 elif "Visual Studio" in self.generator: 0308 compiler_version = re.search("Visual Studio ([0-9]*)", self.generator).group(1) 0309 if Version(compiler_version) >= "10": 0310 if "--" not in args: 0311 args.append("--") 0312 # Parallel for building projects in the solution 0313 args.append("/m:%i" % cpu_count(output=self._conanfile.output)) 0314 0315 if self.generator and self.msbuild_verbosity: 0316 if "Visual Studio" in self.generator: 0317 compiler_version = re.search("Visual Studio ([0-9]*)", self.generator).group(1) 0318 if Version(compiler_version) >= "10": 0319 if "--" not in args: 0320 args.append("--") 0321 args.append("/verbosity:%s" % self.msbuild_verbosity) 0322 0323 arg_list = join_arguments([ 0324 args_to_string([build_dir]), 0325 self.build_config, 0326 args_to_string(args) 0327 ]) 0328 command = "%s --build %s" % (self._cmake_program, arg_list) 0329 self._run(command) 0330 0331 def install(self, args=None, build_dir=None): 0332 if not self._conanfile.should_install: 0333 return 0334 mkdir(self._conanfile.package_folder) 0335 if not self.definitions.get(cmake_install_prefix_var_name): 0336 raise ConanException("%s not defined for 'cmake.install()'\n" 0337 "Make sure 'package_folder' is " 0338 "defined" % cmake_install_prefix_var_name) 0339 self._build(args=args, build_dir=build_dir, target="install") 0340 0341 def test(self, args=None, build_dir=None, target=None, output_on_failure=False): 0342 if not self._conanfile.should_test or not get_env("CONAN_RUN_TESTS", True) or \ 0343 self._conanfile.conf["tools.build:skip_test"]: 0344 return 0345 if not target: 0346 target = "RUN_TESTS" if self.is_multi_configuration else "test" 0347 0348 test_env = {'CTEST_OUTPUT_ON_FAILURE': '1' if output_on_failure else '0'} 0349 if self.parallel: 0350 test_env['CTEST_PARALLEL_LEVEL'] = str(cpu_count(self._conanfile.output)) 0351 with environment_append(test_env): 0352 self._build(args=args, build_dir=build_dir, target=target) 0353 0354 @property 0355 def verbose(self): 0356 try: 0357 verbose = self.definitions[verbose_definition_name] 0358 return get_bool_from_text(str(verbose)) 0359 except KeyError: 0360 return False 0361 0362 @verbose.setter 0363 def verbose(self, value): 0364 self.definitions.update(verbose_definition(value)) 0365 0366 def patch_config_paths(self): 0367 """ 0368 changes references to the absolute path of the installed package and its dependencies in 0369 exported cmake config files to the appropriate conan variable. This makes 0370 most (sensible) cmake config files portable. 0371 0372 For example, if a package foo installs a file called "fooConfig.cmake" to 0373 be used by cmake's find_package method, normally this file will contain 0374 absolute paths to the installed package folder, for example it will contain 0375 a line such as: 0376 0377 SET(Foo_INSTALL_DIR /home/developer/.conan/data/Foo/1.0.0/...) 0378 0379 This will cause cmake find_package() method to fail when someone else 0380 installs the package via conan. 0381 0382 This function will replace such mentions to 0383 0384 SET(Foo_INSTALL_DIR ${CONAN_FOO_ROOT}) 0385 0386 which is a variable that is set by conanbuildinfo.cmake, so that find_package() 0387 now correctly works on this conan package. 0388 0389 For dependent packages, if a package foo installs a file called "fooConfig.cmake" to 0390 be used by cmake's find_package method and if it depends to a package bar, 0391 normally this file will contain absolute paths to the bar package folder, 0392 for example it will contain a line such as: 0393 0394 SET_TARGET_PROPERTIES(foo PROPERTIES 0395 INTERFACE_INCLUDE_DIRECTORIES 0396 "/home/developer/.conan/data/Bar/1.0.0/user/channel/id/include") 0397 0398 This function will replace such mentions to 0399 0400 SET_TARGET_PROPERTIES(foo PROPERTIES 0401 INTERFACE_INCLUDE_DIRECTORIES 0402 "${CONAN_BAR_ROOT}/include") 0403 0404 If the install() method of the CMake object in the conan file is used, this 0405 function should be called _after_ that invocation. For example: 0406 0407 def build(self): 0408 cmake = CMake(self) 0409 cmake.configure() 0410 cmake.build() 0411 cmake.install() 0412 cmake.patch_config_paths() 0413 """ 0414 0415 if not self._conanfile.should_install: 0416 return 0417 if not self._conanfile.name: 0418 raise ConanException("cmake.patch_config_paths() can't work without package name. " 0419 "Define name in your recipe") 0420 pf = self.definitions.get(cmake_install_prefix_var_name) 0421 replstr = "${CONAN_%s_ROOT}" % self._conanfile.name.upper() 0422 allwalk = chain(walk(self._conanfile.build_folder), walk(self._conanfile.package_folder)) 0423 0424 # We don't want warnings printed because there is no replacement of the abs path. 0425 # there could be MANY cmake files in the package and the normal thing is to not find 0426 # the abs paths 0427 _null_out = ConanOutput(StringIO()) 0428 for root, _, files in allwalk: 0429 for f in files: 0430 if f.endswith(".cmake") and not f.startswith("conan"): 0431 path = os.path.join(root, f) 0432 0433 tools.replace_path_in_file(path, pf, replstr, strict=False, 0434 output=_null_out) 0435 0436 # patch paths of dependent packages that are found in any cmake files of the 0437 # current package 0438 for dep in self._conanfile.deps_cpp_info.deps: 0439 from_str = self._conanfile.deps_cpp_info[dep].rootpath 0440 dep_str = "${CONAN_%s_ROOT}" % dep.upper() 0441 ret = tools.replace_path_in_file(path, from_str, dep_str, strict=False, 0442 output=_null_out) 0443 if ret: 0444 self._conanfile.output.info("Patched paths for %s: %s to %s" 0445 % (dep, from_str, dep_str)) 0446 0447 @staticmethod 0448 def get_version(): 0449 try: 0450 out = version_runner(["cmake", "--version"]) 0451 version_line = decode_text(out).split('\n', 1)[0] 0452 version_str = version_line.rsplit(' ', 1)[-1] 0453 return Version(version_str) 0454 except Exception as e: 0455 raise ConanException("Error retrieving CMake version: '{}'".format(e))