/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
- 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_behavior
- from conans.util.config_parser import get_bool_from_text
- from conans.util.files import mkdir, get_abs_path, walk, decode_text
- from conans.util.runners import version_runner
- class CMake(object):
- def __new__(cls, conanfile, *args, **kwargs):
- """ Inject the proper CMake base class in the hierarchy """
- from conans import ConanFile
- if not isinstance(conanfile, ConanFile):
- raise ConanException("First argument of CMake() has to be ConanFile. Use CMake(self)")
- # If already injected, create and return
- from conans.client.build.cmake_toolchain_build_helper import CMakeToolchainBuildHelper
- if CMakeToolchainBuildHelper in cls.__bases__ or CMakeBuildHelper in cls.__bases__:
- return super(CMake, cls).__new__(cls)
- # If not, add the proper CMake implementation
- if hasattr(conanfile, "toolchain"):
- CustomCMakeClass = type("CustomCMakeClass", (cls, CMakeToolchainBuildHelper), {})
- else:
- CustomCMakeClass = type("CustomCMakeClass", (cls, CMakeBuildHelper), {})
- return CustomCMakeClass.__new__(CustomCMakeClass, conanfile, *args, **kwargs)
- def __init__(self, *args, **kwargs):
- super(CMake, self).__init__(*args, **kwargs)
- @staticmethod
- def get_version():
- # FIXME: Conan 2.0 This function is require for python2
- return CMakeBuildHelper.get_version()
- class CMakeBuildHelper(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)
- """
- 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
- self.definitions = builder.get_definitions()
- self.definitions["CONAN_EXPORTED"] = "1"
- 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")
- if not compiler:
- conan_v2_behavior("compiler setting should be defined.",
- v1_behavior=self._conanfile.output.warn)
- 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", "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", "NMake Makefiles", "NMake Makefiles JOM",
- "Unix Makefiles"]:
- compilervars_dict = tools.compilervars_dict(self._conanfile, force=True)
- context = _environment_add(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
- if not self._build_type:
- conan_v2_behavior("build_type setting should be defined.",
- v1_behavior=self._conanfile.output.warn)
- 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:
- 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))