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))