1import os
2import platform
3from collections import OrderedDict
4
5from conan.tools.microsoft.visual import vs_ide_version
6from conans.client import tools
7from conans.client.build.compiler_flags import architecture_flag, parallel_compiler_cl_flag
8from conans.client.build.cppstd_flags import cppstd_from_settings, cppstd_flag_new as cppstd_flag
9from conans.client.tools import cross_building, Version
10from conans.client.tools.apple import is_apple_os
11from conans.client.tools.oss import get_cross_building_settings
12from conans.errors import ConanException
13from conans.model.build_info import DEFAULT_BIN, DEFAULT_INCLUDE, DEFAULT_LIB, DEFAULT_SHARE
14from conans.util.env_reader import get_env
15from conans.util.log import logger
16
17verbose_definition_name = "CMAKE_VERBOSE_MAKEFILE"
18cmake_install_prefix_var_name = "CMAKE_INSTALL_PREFIX"
19runtime_definition_var_name = "CONAN_LINK_RUNTIME"
20cmake_in_local_cache_var_name = "CONAN_IN_LOCAL_CACHE"
21
22
23def get_toolset(settings, generator):
24    compiler = settings.get_safe("compiler")
25    compiler_base = settings.get_safe("compiler.base")
26    if compiler == "Visual Studio":
27        subs_toolset = settings.get_safe("compiler.toolset")
28        if subs_toolset:
29            return subs_toolset
30    elif compiler == "intel" and compiler_base == "Visual Studio" and "Visual" in generator:
31        compiler_version = settings.get_safe("compiler.version")
32        if compiler_version:
33            compiler_version = compiler_version if "." in compiler_version else \
34                "%s.0" % compiler_version
35            return "Intel C++ Compiler " + compiler_version
36    return None
37
38
39def get_generator(conanfile):
40    # Returns the name of the generator to be used by CMake
41    if "CONAN_CMAKE_GENERATOR" in os.environ:
42        return os.environ["CONAN_CMAKE_GENERATOR"]
43
44    compiler = conanfile.settings.get_safe("compiler")
45    compiler_base = conanfile.settings.get_safe("compiler.base")
46    arch = conanfile.settings.get_safe("arch")
47    compiler_version = conanfile.settings.get_safe("compiler.version")
48    compiler_base_version = conanfile.settings.get_safe("compiler.base.version")
49    os_build, _, _, _ = get_cross_building_settings(conanfile)
50
51    if not compiler or not compiler_version or not arch:
52        if os_build == "Windows":
53            logger.warning("CMake generator could not be deduced from settings")
54            return None
55        return "Unix Makefiles"
56
57    cmake_years = {'8': '8 2005',
58                   '9': '9 2008',
59                   '10': '10 2010',
60                   '11': '11 2012',
61                   '12': '12 2013',
62                   '14': '14 2015',
63                   '15': '15 2017',
64                   '16': '16 2019',
65                   '17': '17 2022'}
66
67    if compiler == "msvc":
68        if compiler_version is None:
69            raise ConanException("compiler.version must be defined")
70        vs_version = vs_ide_version(conanfile)
71        return "Visual Studio %s" % cmake_years[vs_version]
72
73    if compiler == "Visual Studio" or compiler_base == "Visual Studio":
74        version = compiler_base_version or compiler_version
75        major_version = version.split('.', 1)[0]
76        _visuals = cmake_years.get(major_version, "UnknownVersion %s" % version)
77        base = "Visual Studio %s" % _visuals
78        return base
79
80    # The generator depends on the build machine, not the target
81    if os_build == "Windows" and compiler != "qcc":
82        return "MinGW Makefiles"  # it is valid only under Windows
83
84    return "Unix Makefiles"
85
86
87def get_generator_platform(settings, generator):
88    # Returns the generator platform to be used by CMake
89    if "CONAN_CMAKE_GENERATOR_PLATFORM" in os.environ:
90        return os.environ["CONAN_CMAKE_GENERATOR_PLATFORM"]
91
92    compiler = settings.get_safe("compiler")
93    compiler_base = settings.get_safe("compiler.base")
94    arch = settings.get_safe("arch")
95
96    if settings.get_safe("os") == "WindowsCE":
97        return settings.get_safe("os.platform")
98
99    if (compiler == "Visual Studio" or compiler_base == "Visual Studio") and \
100            generator and "Visual" in generator:
101        return {"x86": "Win32",
102                "x86_64": "x64",
103                "armv7": "ARM",
104                "armv8": "ARM64"}.get(arch)
105    return None
106
107
108def is_multi_configuration(generator):
109    if not generator:
110        return False
111    return "Visual" in generator or "Xcode" in generator or "Multi-Config" in generator
112
113
114def is_toolset_supported(generator):
115    # https://cmake.org/cmake/help/v3.14/variable/CMAKE_GENERATOR_TOOLSET.html
116    if not generator:
117        return False
118    return "Visual" in generator or "Xcode" in generator or "Green Hills MULTI" in generator
119
120
121def is_generator_platform_supported(generator):
122    # https://cmake.org/cmake/help/v3.14/variable/CMAKE_GENERATOR_PLATFORM.html
123    if not generator:
124        return False
125    return "Visual" in generator or "Green Hills MULTI" in generator
126
127
128def verbose_definition(value):
129    return {verbose_definition_name: "ON" if value else "OFF"}
130
131
132def in_local_cache_definition(value):
133    return {cmake_in_local_cache_var_name: "ON" if value else "OFF"}
134
135
136def runtime_definition(runtime):
137    return {runtime_definition_var_name: "/%s" % runtime} if runtime else {}
138
139
140def build_type_definition(new_build_type, old_build_type, generator, output):
141    if new_build_type and new_build_type != old_build_type:
142        output.warn("Forced CMake build type ('%s') different from the settings build type ('%s')"
143                    % (new_build_type, old_build_type))
144
145    build_type = new_build_type or old_build_type
146    if build_type and not is_multi_configuration(generator):
147        return {"CMAKE_BUILD_TYPE": build_type}
148    return {}
149
150
151class CMakeDefinitionsBuilder(object):
152
153    def __init__(self, conanfile, cmake_system_name=True, make_program=None,
154                 parallel=True, generator=None, set_cmake_flags=False,
155                 forced_build_type=None, output=None):
156        self._conanfile = conanfile
157        self._forced_cmake_system_name = cmake_system_name
158        self._make_program = make_program
159        self._parallel = parallel
160        self._generator = generator
161        self._set_cmake_flags = set_cmake_flags
162        self._forced_build_type = forced_build_type
163        self._output = output
164
165    def _ss(self, setname):
166        """safe setting"""
167        return self._conanfile.settings.get_safe(setname)
168
169    def _get_cpp_standard_vars(self):
170        cppstd = cppstd_from_settings(self._conanfile.settings)
171
172        if not cppstd:
173            return {}
174
175        definitions = {}
176        if cppstd.startswith("gnu"):
177            definitions["CONAN_CMAKE_CXX_STANDARD"] = cppstd[3:]
178            definitions["CONAN_CMAKE_CXX_EXTENSIONS"] = "ON"
179        else:
180            definitions["CONAN_CMAKE_CXX_STANDARD"] = cppstd
181            definitions["CONAN_CMAKE_CXX_EXTENSIONS"] = "OFF"
182
183        definitions["CONAN_STD_CXX_FLAG"] = cppstd_flag(self._conanfile.settings)
184        return definitions
185
186    def _cmake_cross_build_defines(self, cmake_version):
187        os_ = self._ss("os")
188        arch = self._ss("arch")
189        os_ver_str = "os.api_level" if os_ == "Android" else "os.version"
190        op_system_version = self._ss(os_ver_str)
191
192        env_sn = get_env("CONAN_CMAKE_SYSTEM_NAME", "")
193        env_sn = {"False": False, "True": True, "": None}.get(env_sn, env_sn)
194        cmake_system_name = env_sn or self._forced_cmake_system_name
195
196        os_build, _, _, _ = get_cross_building_settings(self._conanfile)
197        compiler = self._ss("compiler")
198        libcxx = self._ss("compiler.libcxx")
199
200        definitions = OrderedDict()
201        os_ver = get_env("CONAN_CMAKE_SYSTEM_VERSION", op_system_version)
202        toolchain_file = get_env("CONAN_CMAKE_TOOLCHAIN_FILE", "")
203
204        if toolchain_file != "":
205            logger.info("Setting Cross build toolchain file: %s" % toolchain_file)
206            definitions["CMAKE_TOOLCHAIN_FILE"] = toolchain_file
207            return definitions
208
209        if cmake_system_name is False:
210            return definitions
211
212        # System name and system version
213        if cmake_system_name is not True:  # String not empty
214            definitions["CMAKE_SYSTEM_NAME"] = cmake_system_name
215        else:  # detect if we are cross building and the system name and version
216            skip_x64_x86 = os_ in ['Windows', 'Linux', 'SunOS', 'AIX']
217            if cross_building(self._conanfile, skip_x64_x86=skip_x64_x86):  # We are cross building
218                apple_system_name = "Darwin" if cmake_version and Version(cmake_version) < Version(
219                    "3.14") or not cmake_version else None
220                cmake_system_name_map = {"Macos": "Darwin",
221                                         "iOS": apple_system_name or "iOS",
222                                         "tvOS": apple_system_name or "tvOS",
223                                         "watchOS": apple_system_name or "watchOS",
224                                         "Neutrino": "QNX",
225                                         "": "Generic",
226                                         None: "Generic"}
227                definitions["CMAKE_SYSTEM_NAME"] = cmake_system_name_map.get(os_, os_)
228
229        if os_ver:
230            definitions["CMAKE_SYSTEM_VERSION"] = os_ver
231            if is_apple_os(os_):
232                definitions["CMAKE_OSX_DEPLOYMENT_TARGET"] = os_ver
233
234        # system processor
235        cmake_system_processor = os.getenv("CONAN_CMAKE_SYSTEM_PROCESSOR")
236        if cmake_system_processor:
237            definitions["CMAKE_SYSTEM_PROCESSOR"] = cmake_system_processor
238
239        if definitions:  # If enabled cross compile
240            for env_var in ["CONAN_CMAKE_FIND_ROOT_PATH",
241                            "CONAN_CMAKE_FIND_ROOT_PATH_MODE_PROGRAM",
242                            "CONAN_CMAKE_FIND_ROOT_PATH_MODE_LIBRARY",
243                            "CONAN_CMAKE_FIND_ROOT_PATH_MODE_INCLUDE"]:
244
245                value = os.getenv(env_var)
246                if value:
247                    definitions[env_var] = value
248
249            if self._conanfile and self._conanfile.deps_cpp_info.sysroot:
250                sysroot_path = self._conanfile.deps_cpp_info.sysroot
251
252                if sysroot_path:
253                    # Needs to be set here, can't be managed in the cmake generator, CMake needs
254                    # to know about the sysroot before any other thing
255                    definitions["CMAKE_SYSROOT"] = sysroot_path.replace("\\", "/")
256
257            cmake_sysroot = os.getenv("CONAN_CMAKE_SYSROOT")
258            if cmake_sysroot is not None:
259                definitions["CMAKE_SYSROOT"] = cmake_sysroot.replace("\\", "/")
260
261            # Adjust Android stuff
262            if str(os_) == "Android" and definitions["CMAKE_SYSTEM_NAME"] == "Android":
263                arch_abi_settings = tools.to_android_abi(arch)
264                if arch_abi_settings:
265                    definitions["CMAKE_ANDROID_ARCH_ABI"] = arch_abi_settings
266                    definitions["ANDROID_ABI"] = arch_abi_settings
267
268                conan_cmake_android_ndk = os.getenv("CONAN_CMAKE_ANDROID_NDK")
269                if conan_cmake_android_ndk:
270                    definitions["ANDROID_NDK"] = conan_cmake_android_ndk
271
272                definitions["ANDROID_PLATFORM"] = "android-%s" % op_system_version
273                definitions["ANDROID_TOOLCHAIN"] = compiler
274
275                # More details about supported stdc++ libraries here:
276                # https://developer.android.com/ndk/guides/cpp-support.html
277                if libcxx:
278                    definitions["ANDROID_STL"] = libcxx
279                else:
280                    definitions["ANDROID_STL"] = 'none'
281
282        logger.info("Setting Cross build flags: %s"
283                    % ", ".join(["%s=%s" % (k, v) for k, v in definitions.items()]))
284        return definitions
285
286    def _get_make_program_definition(self):
287        make_program = os.getenv("CONAN_MAKE_PROGRAM") or self._make_program
288        if make_program:
289            if not tools.which(make_program):
290                self._output.warn("The specified make program '%s' cannot be found and will be "
291                                  "ignored" % make_program)
292            else:
293                self._output.info("Using '%s' as CMAKE_MAKE_PROGRAM" % make_program)
294                return {"CMAKE_MAKE_PROGRAM": make_program}
295
296        return {}
297
298    def get_definitions(self, cmake_version):
299
300        compiler = self._ss("compiler")
301        compiler_base = self._ss("compiler.base")
302        compiler_version = self._ss("compiler.version")
303        arch = self._ss("arch")
304        os_ = self._ss("os")
305        libcxx = self._ss("compiler.libcxx")
306        runtime = self._ss("compiler.runtime")
307        build_type = self._ss("build_type")
308
309        definitions = OrderedDict()
310        definitions.update(runtime_definition(runtime))
311        definitions.update(build_type_definition(self._forced_build_type, build_type,
312                                                 self._generator, self._output))
313
314        # don't attempt to override variables set within toolchain
315        if (tools.is_apple_os(os_) and "CONAN_CMAKE_TOOLCHAIN_FILE" not in os.environ
316                and "CMAKE_TOOLCHAIN_FILE" not in definitions):
317            apple_arch = tools.to_apple_arch(arch)
318            if apple_arch:
319                definitions["CMAKE_OSX_ARCHITECTURES"] = apple_arch
320            # xcrun is only available on macOS, otherwise it's cross-compiling and it needs to be
321            # set within CMake toolchain. also, if SDKROOT is set, CMake will use it, and it's not
322            # needed to run xcrun.
323            if platform.system() == "Darwin" and "SDKROOT" not in os.environ:
324                sdk_path = tools.XCRun(self._conanfile.settings).sdk_path
325                if sdk_path:
326                    definitions["CMAKE_OSX_SYSROOT"] = sdk_path
327
328        definitions.update(self._cmake_cross_build_defines(cmake_version))
329        definitions.update(self._get_cpp_standard_vars())
330
331        definitions.update(in_local_cache_definition(self._conanfile.in_local_cache))
332
333        if compiler:
334            definitions["CONAN_COMPILER"] = compiler
335        if compiler_version:
336            definitions["CONAN_COMPILER_VERSION"] = str(compiler_version)
337
338        # C, CXX, LINK FLAGS
339        if compiler == "Visual Studio" or compiler_base == "Visual Studio":
340            if self._parallel:
341                flag = parallel_compiler_cl_flag(output=self._output)
342                definitions['CONAN_CXX_FLAGS'] = flag
343                definitions['CONAN_C_FLAGS'] = flag
344        else:  # arch_flag is only set for non Visual Studio
345            arch_flag = architecture_flag(self._conanfile.settings)
346            if arch_flag:
347                definitions['CONAN_CXX_FLAGS'] = arch_flag
348                definitions['CONAN_SHARED_LINKER_FLAGS'] = arch_flag
349                definitions['CONAN_C_FLAGS'] = arch_flag
350                if self._set_cmake_flags:
351                    definitions['CMAKE_CXX_FLAGS'] = arch_flag
352                    definitions['CMAKE_SHARED_LINKER_FLAGS'] = arch_flag
353                    definitions['CMAKE_C_FLAGS'] = arch_flag
354
355        if libcxx:
356            definitions["CONAN_LIBCXX"] = libcxx
357
358        # Shared library
359        try:
360            definitions["BUILD_SHARED_LIBS"] = "ON" if self._conanfile.options.shared else "OFF"
361        except ConanException:
362            pass
363
364        # Install to package folder
365        try:
366            if self._conanfile.package_folder:
367                definitions["CMAKE_INSTALL_PREFIX"] = self._conanfile.package_folder
368                definitions["CMAKE_INSTALL_BINDIR"] = DEFAULT_BIN
369                definitions["CMAKE_INSTALL_SBINDIR"] = DEFAULT_BIN
370                definitions["CMAKE_INSTALL_LIBEXECDIR"] = DEFAULT_BIN
371                definitions["CMAKE_INSTALL_LIBDIR"] = DEFAULT_LIB
372                definitions["CMAKE_INSTALL_INCLUDEDIR"] = DEFAULT_INCLUDE
373                definitions["CMAKE_INSTALL_OLDINCLUDEDIR"] = DEFAULT_INCLUDE
374                definitions["CMAKE_INSTALL_DATAROOTDIR"] = DEFAULT_SHARE
375        except AttributeError:
376            pass
377
378        # fpic
379        if not str(os_).startswith("Windows"):
380            fpic = self._conanfile.options.get_safe("fPIC")
381            if fpic is not None:
382                shared = self._conanfile.options.get_safe("shared")
383                fpic_value = "ON" if (fpic or shared) else "OFF"
384                definitions["CONAN_CMAKE_POSITION_INDEPENDENT_CODE"] = fpic_value
385
386        # Adjust automatically the module path in case the conanfile is using the
387        # cmake_find_package or cmake_find_package_multi
388        install_folder = self._conanfile.install_folder.replace("\\", "/")
389        if "cmake_find_package" in self._conanfile.generators:
390            definitions["CMAKE_MODULE_PATH"] = install_folder
391
392        if ("cmake_find_package_multi" in self._conanfile.generators
393                or "CMakeDeps" in self._conanfile.generators):
394            # The cmake_find_package_multi only works with targets and generates XXXConfig.cmake
395            # that require the prefix path and the module path
396            definitions["CMAKE_PREFIX_PATH"] = install_folder
397            definitions["CMAKE_MODULE_PATH"] = install_folder
398
399        definitions.update(self._get_make_program_definition())
400
401        # Disable CMake export registry #3070 (CMake installing modules in user home's)
402        definitions["CMAKE_EXPORT_NO_PACKAGE_REGISTRY"] = "ON"
403        return definitions
404