/conans/client/build/cmake.py

https://github.com/conan-io/conan · Python · 473 lines · 422 code · 22 blank · 29 comment · 26 complexity · 183c6699929ed23ac6c3db8e8f026fd6 MD5 · raw file

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