diff --git a/conan/cps/cps.py b/conan/cps/cps.py index 67007d8570a..621a387ce27 100644 --- a/conan/cps/cps.py +++ b/conan/cps/cps.py @@ -85,7 +85,7 @@ def from_cpp_info(cpp_info, pkg_type, libname=None): cps_comp.type = CPSComponentType.INTERFACE return cps_comp - cpp_info.deduce_cps(pkg_type) + cpp_info.deduce_locations(pkg_type) cps_comp.type = CPSComponentType.from_conan(cpp_info.type) cps_comp.location = cpp_info.location cps_comp.link_location = cpp_info.link_location diff --git a/conan/tools/cmake/__init__.py b/conan/tools/cmake/__init__.py index d4123be7493..73c8a835b08 100644 --- a/conan/tools/cmake/__init__.py +++ b/conan/tools/cmake/__init__.py @@ -1,4 +1,14 @@ from conan.tools.cmake.toolchain.toolchain import CMakeToolchain from conan.tools.cmake.cmake import CMake -from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps from conan.tools.cmake.layout import cmake_layout + + +def CMakeDeps(conanfile): # noqa + if conanfile.conf.get("tools.cmake.cmakedeps:new", choices=["will_break_next"]): + from conan.tools.cmake.cmakedeps2.cmakedeps import CMakeDeps2 + conanfile.output.warning("Using the new CMakeDeps generator, behind the " + "'tools.cmake.cmakedeps:new' gate conf. This conf will change" + "next release, breaking, so use it only for testing and dev") + return CMakeDeps2(conanfile) + from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps as _CMakeDeps + return _CMakeDeps(conanfile) diff --git a/conan/tools/cmake/cmakedeps2/__init__.py b/conan/tools/cmake/cmakedeps2/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/conan/tools/cmake/cmakedeps2/cmakedeps.py b/conan/tools/cmake/cmakedeps2/cmakedeps.py new file mode 100644 index 00000000000..b92e8276ecb --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/cmakedeps.py @@ -0,0 +1,211 @@ +import os +import re +import textwrap + +from jinja2 import Template + +from conan.internal import check_duplicated_generator +from conan.tools.cmake.cmakedeps2.config import ConfigTemplate2 +from conan.tools.cmake.cmakedeps2.config_version import ConfigVersionTemplate2 +from conan.tools.cmake.cmakedeps2.target_configuration import TargetConfigurationTemplate2 +from conan.tools.cmake.cmakedeps2.targets import TargetsTemplate2 +from conan.tools.files import save +from conan.errors import ConanException +from conans.model.dependencies import get_transitive_requires +from conans.util.files import load + +FIND_MODE_MODULE = "module" +FIND_MODE_CONFIG = "config" +FIND_MODE_NONE = "none" +FIND_MODE_BOTH = "both" + + +class CMakeDeps2: + + def __init__(self, conanfile): + self._conanfile = conanfile + self.configuration = str(self._conanfile.settings.build_type) + + # These are just for legacy compatibility, but not use at al + self.build_context_activated = [] + self.build_context_build_modules = [] + self.build_context_suffix = {} + # Enable/Disable checking if a component target exists or not + self.check_components_exist = False + + self._properties = {} + + def generate(self): + check_duplicated_generator(self, self._conanfile) + # Current directory is the generators_folder + generator_files = self._content() + for generator_file, content in generator_files.items(): + save(self._conanfile, generator_file, content) + _PathGenerator(self, self._conanfile).generate() + + def _content(self): + host_req = self._conanfile.dependencies.host + build_req = self._conanfile.dependencies.direct_build + test_req = self._conanfile.dependencies.test + + # Iterate all the transitive requires + ret = {} + for require, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): + cmake_find_mode = self.get_property("cmake_find_mode", dep) + cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG + cmake_find_mode = cmake_find_mode.lower() + if cmake_find_mode == FIND_MODE_NONE: + continue + + config = ConfigTemplate2(self, dep) + ret[config.filename] = config.content() + config_version = ConfigVersionTemplate2(self, dep) + ret[config_version.filename] = config_version.content() + + targets = TargetsTemplate2(self, dep) + ret[targets.filename] = targets.content() + target_configuration = TargetConfigurationTemplate2(self, dep, require) + ret[target_configuration.filename] = target_configuration.content() + return ret + + def set_property(self, dep, prop, value, build_context=False): + """ + Using this method you can overwrite the :ref:`property` values set by + the Conan recipes from the consumer. + + :param dep: Name of the dependency to set the :ref:`property`. For + components use the syntax: ``dep_name::component_name``. + :param prop: Name of the :ref:`property`. + :param value: Value of the property. Use ``None`` to invalidate any value set by the + upstream recipe. + :param build_context: Set to ``True`` if you want to set the property for a dependency that + belongs to the build context (``False`` by default). + """ + build_suffix = "&build" if build_context else "" + self._properties.setdefault(f"{dep}{build_suffix}", {}).update({prop: value}) + + def get_property(self, prop, dep, comp_name=None, check_type=None): + dep_name = dep.ref.name + build_suffix = "&build" if dep.context == "build" else "" + dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}" + try: + value = self._properties[f"{dep_comp}{build_suffix}"][prop] + if check_type is not None and not isinstance(value, check_type): + raise ConanException(f'The expected type for {prop} is "{check_type.__name__}", ' + f'but "{type(value).__name__}" was found') + return value + except KeyError: + # Here we are not using the cpp_info = deduce_cpp_info(dep) because it is not + # necessary for the properties + return dep.cpp_info.get_property(prop, check_type=check_type) if not comp_name \ + else dep.cpp_info.components[comp_name].get_property(prop, check_type=check_type) + + def get_cmake_filename(self, dep, module_mode=None): + """Get the name of the file for the find_package(XXX)""" + # This is used by CMakeDeps to determine: + # - The filename to generate (XXX-config.cmake or FindXXX.cmake) + # - The name of the defined XXX_DIR variables + # - The name of transitive dependencies for calls to find_dependency + if module_mode and self._get_find_mode(dep) in [FIND_MODE_MODULE, FIND_MODE_BOTH]: + ret = self.get_property("cmake_module_file_name", dep) + if ret: + return ret + ret = self.get_property("cmake_file_name", dep) + return ret or dep.ref.name + + def _get_find_mode(self, dep): + """ + :param dep: requirement + :return: "none" or "config" or "module" or "both" or "config" when not set + """ + tmp = self.get_property("cmake_find_mode", dep) + if tmp is None: + return "config" + return tmp.lower() + + def get_transitive_requires(self, conanfile): + # Prepared to filter transitive tool-requires with visible=True + return get_transitive_requires(self._conanfile, conanfile) + + +class _PathGenerator: + _conan_cmakedeps_paths = "conan_cmakedeps_paths.cmake" + + def __init__(self, cmakedeps, conanfile): + self._conanfile = conanfile + self._cmakedeps = cmakedeps + + def generate(self): + template = textwrap.dedent("""\ + {% for pkg_name, folder in pkg_paths.items() %} + set({{pkg_name}}_DIR "{{folder}}") + {% endfor %} + {% if host_runtime_dirs %} + set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} ) + {% endif %} + """) + + host_req = self._conanfile.dependencies.host + build_req = self._conanfile.dependencies.direct_build + test_req = self._conanfile.dependencies.test + + # gen_folder = self._conanfile.generators_folder.replace("\\", "/") + # if not, test_cmake_add_subdirectory test fails + # content.append('set(CMAKE_FIND_PACKAGE_PREFER_CONFIG ON)') + pkg_paths = {} + for req, dep in list(host_req.items()) + list(build_req.items()) + list(test_req.items()): + cmake_find_mode = self._cmakedeps.get_property("cmake_find_mode", dep) + cmake_find_mode = cmake_find_mode or FIND_MODE_CONFIG + cmake_find_mode = cmake_find_mode.lower() + + pkg_name = self._cmakedeps.get_cmake_filename(dep) + # https://cmake.org/cmake/help/v3.22/guide/using-dependencies/index.html + if cmake_find_mode == FIND_MODE_NONE: + try: + # This is irrespective of the components, it should be in the root cpp_info + # To define the location of the pkg-config.cmake file + build_dir = dep.cpp_info.builddirs[0] + except IndexError: + build_dir = dep.package_folder + pkg_folder = build_dir.replace("\\", "/") if build_dir else None + if pkg_folder: + config_file = ConfigTemplate2(self._cmakedeps, dep).filename + if os.path.isfile(os.path.join(pkg_folder, config_file)): + pkg_paths[pkg_name] = pkg_folder + continue + + # If CMakeDeps generated, the folder is this one + # content.append(f'set({pkg_name}_ROOT "{gen_folder}")') + pkg_paths[pkg_name] = "${CMAKE_CURRENT_LIST_DIR}" + + context = {"host_runtime_dirs": self._get_host_runtime_dirs(), + "pkg_paths": pkg_paths} + content = Template(template, trim_blocks=True, lstrip_blocks=True).render(context) + save(self._conanfile, self._conan_cmakedeps_paths, content) + + def _get_host_runtime_dirs(self): + host_runtime_dirs = {} + + # Get the previous configuration + if os.path.exists(self._conan_cmakedeps_paths): + existing_toolchain = load(self._conan_cmakedeps_paths) + pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" + variable_match = re.search(pattern_lib_dirs, existing_toolchain) + if variable_match: + capture = variable_match.group(1) + matches = re.findall(r'"\$<\$:([^>]*)>"', capture) + for config, paths in matches: + host_runtime_dirs.setdefault(config, []).append(paths) + + is_win = self._conanfile.settings.get_safe("os") == "Windows" + for req in self._conanfile.dependencies.host.values(): + config = req.settings.get_safe("build_type", self._cmakedeps.configuration) + aggregated_cppinfo = req.cpp_info.aggregated_components() + runtime_dirs = aggregated_cppinfo.bindirs if is_win else aggregated_cppinfo.libdirs + for d in runtime_dirs: + d = d.replace("\\", "/") + existing = host_runtime_dirs.setdefault(config, []) + if d not in existing: + existing.append(d) + + return ' '.join(f'"$<$:{i}>"' for c, v in host_runtime_dirs.items() for i in v) diff --git a/conan/tools/cmake/cmakedeps2/config.py b/conan/tools/cmake/cmakedeps2/config.py new file mode 100644 index 00000000000..459d3b42171 --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/config.py @@ -0,0 +1,61 @@ +import textwrap + +import jinja2 +from jinja2 import Template + + +class ConfigTemplate2: + """ + FooConfig.cmake + foo-config.cmake + """ + def __init__(self, cmakedeps, conanfile): + self._cmakedeps = cmakedeps + self._conanfile = conanfile + + def content(self): + t = Template(self._template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + return t.render(self._context) + + @property + def filename(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + return f"{f}-config.cmake" if f == f.lower() else f"{f}Config.cmake" + + @property + def _context(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + targets_include = f"{f}Targets.cmake" + pkg_name = self._conanfile.ref.name + build_modules_paths = self._cmakedeps.get_property("cmake_build_modules", self._conanfile, + check_type=list) or [] + # FIXME: Proper escaping of paths for CMake and relativization + # FIXME: build_module_paths coming from last config only + build_modules_paths = [f.replace("\\", "/") for f in build_modules_paths] + return {"pkg_name": pkg_name, + "targets_include_file": targets_include, + "build_modules_paths": build_modules_paths} + + @property + def _template(self): + return textwrap.dedent("""\ + # Requires CMake > 3.15 + if(${CMAKE_VERSION} VERSION_LESS "3.15") + message(FATAL_ERROR "The 'CMakeDeps' generator only works with CMake >= 3.15") + endif() + + include(${CMAKE_CURRENT_LIST_DIR}/{{ targets_include_file }}) + + get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(NOT isMultiConfig AND NOT CMAKE_BUILD_TYPE) + message(FATAL_ERROR "Please, set the CMAKE_BUILD_TYPE variable when calling to CMake " + "adding the '-DCMAKE_BUILD_TYPE=' argument.") + endif() + + # build_modules_paths comes from last configuration only + {% for build_module in build_modules_paths %} + message(STATUS "Conan: Including build module from '{{build_module}}'") + include("{{ build_module }}") + {% endfor %} + """) diff --git a/conan/tools/cmake/cmakedeps2/config_version.py b/conan/tools/cmake/cmakedeps2/config_version.py new file mode 100644 index 00000000000..4a3212ba21c --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/config_version.py @@ -0,0 +1,102 @@ +import textwrap + +import jinja2 +from jinja2 import Template + +from conan.errors import ConanException + + +class ConfigVersionTemplate2: + """ + foo-config-version.cmake + """ + def __init__(self, cmakedeps, conanfile): + self._cmakedeps = cmakedeps + self._conanfile = conanfile + + def content(self): + t = Template(self._template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + return t.render(self._context) + + @property + def filename(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + return f"{f}-config-version.cmake" if f == f.lower() else f"{f}ConfigVersion.cmake" + + @property + def _context(self): + policy = self._cmakedeps.get_property("cmake_config_version_compat", self._conanfile) + if policy is None: + policy = "SameMajorVersion" + if policy not in ("AnyNewerVersion", "SameMajorVersion", "SameMinorVersion", "ExactVersion"): + raise ConanException(f"Unknown cmake_config_version_compat={policy} in {self._conanfile}") + version = self._cmakedeps.get_property("system_package_version", self._conanfile) + version = version or self._conanfile.ref.version + return {"version": version, + "policy": policy} + + @property + def _template(self): + # https://gitlab.kitware.com/cmake/cmake/blob/master/Modules/BasicConfigVersion-SameMajorVersion.cmake.in + # This will be at XXX-config-version.cmake + # AnyNewerVersion|SameMajorVersion|SameMinorVersion|ExactVersion + ret = textwrap.dedent("""\ + set(PACKAGE_VERSION "{{ version }}") + + if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) + set(PACKAGE_VERSION_COMPATIBLE FALSE) + else() + {% if policy == "AnyNewerVersion" %} + set(PACKAGE_VERSION_COMPATIBLE TRUE) + {% elif policy == "SameMajorVersion" %} + if("{{ version }}" MATCHES "^([0-9]+)\\\\.") + set(CVF_VERSION_MAJOR {{ '${CMAKE_MATCH_1}' }}) + else() + set(CVF_VERSION_MAJOR "{{ version }}") + endif() + + if(PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + else() + set(PACKAGE_VERSION_COMPATIBLE FALSE) + endif() + {% elif policy == "SameMinorVersion" %} + if("{{ version }}" MATCHES "^([0-9]+)\\.([0-9]+)") + set(CVF_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CVF_VERSION_MINOR "${CMAKE_MATCH_2}") + else() + set(CVF_VERSION_MAJOR "{{ version }}") + set(CVF_VERSION_MINOR "") + endif() + if((PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) AND + (PACKAGE_FIND_VERSION_MINOR STREQUAL CVF_VERSION_MINOR)) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + else() + set(PACKAGE_VERSION_COMPATIBLE FALSE) + endif() + {% elif policy == "ExactVersion" %} + if("{{ version }}" MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)") + set(CVF_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CVF_VERSION_MINOR "${CMAKE_MATCH_2}") + set(CVF_VERSION_MINOR "${CMAKE_MATCH_3}") + else() + set(CVF_VERSION_MAJOR "{{ version }}") + set(CVF_VERSION_MINOR "") + set(CVF_VERSION_PATCH "") + endif() + if((PACKAGE_FIND_VERSION_MAJOR STREQUAL CVF_VERSION_MAJOR) AND + (PACKAGE_FIND_VERSION_MINOR STREQUAL CVF_VERSION_MINOR) AND + (PACKAGE_FIND_VERSION_PATCH STREQUAL CVF_VERSION_PATCH)) + set(PACKAGE_VERSION_COMPATIBLE TRUE) + else() + set(PACKAGE_VERSION_COMPATIBLE FALSE) + endif() + {% endif %} + + if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_EXACT TRUE) + endif() + endif() + """) + return ret diff --git a/conan/tools/cmake/cmakedeps2/target_configuration.py b/conan/tools/cmake/cmakedeps2/target_configuration.py new file mode 100644 index 00000000000..5d8c2150413 --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/target_configuration.py @@ -0,0 +1,384 @@ +import os +import textwrap + +import jinja2 +from jinja2 import Template + +from conan.errors import ConanException +from conans.client.graph.graph import CONTEXT_BUILD, CONTEXT_HOST +from conans.model.pkg_type import PackageType + + +class TargetConfigurationTemplate2: + """ + FooTarget-release.cmake + """ + def __init__(self, cmakedeps, conanfile, require): + self._cmakedeps = cmakedeps + self._conanfile = conanfile # The dependency conanfile, not the consumer one + self._require = require + + def content(self): + t = Template(self._template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + return t.render(self._context) + + @property + def filename(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + # Fallback to consumer configuration if it doesn't have build_type + config = self._conanfile.settings.get_safe("build_type", self._cmakedeps.configuration) + config = (config or "none").lower() + build = "Build" if self._conanfile.context == CONTEXT_BUILD else "" + return f"{f}-Targets{build}-{config}.cmake" + + def _requires(self, info, components): + result = [] + requires = info.parsed_requires() + pkg_name = self._conanfile.ref.name + transitive_reqs = self._cmakedeps.get_transitive_requires(self._conanfile) + if not requires and not components: # global cpp_info without components definition + # require the pkgname::pkgname base (user defined) or INTERFACE base target + return [f"{d.ref.name}::{d.ref.name}" for d in transitive_reqs.values()] + + for required_pkg, required_comp in requires: + if required_pkg is None: # Points to a component of same package + dep_comp = components.get(required_comp) + assert dep_comp, f"Component {required_comp} not found in {self._conanfile}" + dep_target = self._cmakedeps.get_property("cmake_target_name", self._conanfile, + required_comp) + dep_target = dep_target or f"{pkg_name}::{required_comp}" + result.append(dep_target) + else: # Different package + try: + dep = transitive_reqs[required_pkg] + except KeyError: # The transitive dep might have been skipped + pass + else: + # To check if the component exist, it is ok to use the standard cpp_info + # No need to use the cpp_info = deduce_cpp_info(dep) + dep_comp = dep.cpp_info.components.get(required_comp) + if dep_comp is None: + # It must be the interface pkgname::pkgname target + assert required_pkg == required_comp + comp = None + else: + comp = required_comp + dep_target = self._cmakedeps.get_property("cmake_target_name", dep, comp) + dep_target = dep_target or f"{required_pkg}::{required_comp}" + result.append(dep_target) + return result + + @property + def _context(self): + cpp_info = self._conanfile.cpp_info.deduce_full_cpp_info(self._conanfile) + pkg_name = self._conanfile.ref.name + # fallback to consumer configuration if it doesn't have build_type + config = self._conanfile.settings.get_safe("build_type", self._cmakedeps.configuration) + config = config.upper() if config else None + pkg_folder = self._conanfile.package_folder.replace("\\", "/") + config_folder = f"_{config}" if config else "" + build = "_BUILD" if self._conanfile.context == CONTEXT_BUILD else "" + pkg_folder_var = f"{pkg_name}_PACKAGE_FOLDER{config_folder}{build}" + + libs = {} + # The BUILD context does not generate libraries targets atm + if self._conanfile.context == CONTEXT_HOST: + libs = self._get_libs(cpp_info, pkg_name, pkg_folder, pkg_folder_var) + self._add_root_lib_target(libs, pkg_name, cpp_info) + exes = self._get_exes(cpp_info, pkg_name, pkg_folder, pkg_folder_var) + + prefixes = self._cmakedeps.get_property("cmake_additional_variables_prefixes", + self._conanfile, check_type=list) or [] + f = self._cmakedeps.get_cmake_filename(self._conanfile) + prefixes = [f] + prefixes + include_dirs = definitions = libraries = None + if not self._require.build: # To add global variables for try_compile and legacy + aggregated_cppinfo = cpp_info.aggregated_components() + # FIXME: Proper escaping of paths for CMake and relativization + include_dirs = ";".join(i.replace("\\", "/") for i in aggregated_cppinfo.includedirs) + definitions = "" + root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + libraries = root_target_name or f"{pkg_name}::{pkg_name}" + + # TODO: Missing find_modes + dependencies = self._get_dependencies() + return {"dependencies": dependencies, + "pkg_folder": pkg_folder, + "pkg_folder_var": pkg_folder_var, + "config": config, + "exes": exes, + "libs": libs, + "context": self._conanfile.context, + # Extra global variables + "additional_variables_prefixes": prefixes, + "version": self._conanfile.ref.version, + "include_dirs": include_dirs, + "definitions": definitions, + "libraries": libraries + } + + def _get_libs(self, cpp_info, pkg_name, pkg_folder, pkg_folder_var) -> dict: + libs = {} + if cpp_info.has_components: + for name, component in cpp_info.components.items(): + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, + name) + target_name = target_name or f"{pkg_name}::{name}" + target = self._get_cmake_lib(component, cpp_info.components, pkg_folder, + pkg_folder_var) + if target is not None: + libs[target_name] = target + else: + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + target_name = target_name or f"{pkg_name}::{pkg_name}" + target = self._get_cmake_lib(cpp_info, None, pkg_folder, pkg_folder_var) + if target is not None: + libs[target_name] = target + return libs + + def _get_cmake_lib(self, info, components, pkg_folder, pkg_folder_var): + if info.exe or not (info.includedirs or info.libs): + return + + includedirs = ";".join(self._path(i, pkg_folder, pkg_folder_var) + for i in info.includedirs) if info.includedirs else "" + requires = " ".join(self._requires(info, components)) + defines = " ".join(info.defines) + # TODO: Missing escaping? + # TODO: Missing link language + # FIXME: Filter by lib traits!!!!! + if not self._require.headers: # If not depending on headers, paths and + includedirs = defines = None + system_libs = " ".join(info.system_libs) + target = {"type": "INTERFACE", + "includedirs": includedirs, + "defines": defines, + "requires": requires, + "cxxflags": " ".join(info.cxxflags), + "cflags": " ".join(info.cflags), + "sharedlinkflags": " ".join(info.sharedlinkflags), + "exelinkflags": " ".join(info.exelinkflags), + "system_libs": system_libs} + + if info.frameworks: + self._conanfile.output.warning("frameworks not supported yet in new CMakeDeps generator") + + if info.libs: + if len(info.libs) != 1: + raise ConanException(f"New CMakeDeps only allows 1 lib per component:\n" + f"{self._conanfile}: {info.libs}") + assert info.location, "info.location missing for .libs, it should have been deduced" + location = self._path(info.location, pkg_folder, pkg_folder_var) + link_location = self._path(info.link_location, pkg_folder, pkg_folder_var) \ + if info.link_location else None + lib_type = "SHARED" if info.type is PackageType.SHARED else \ + "STATIC" if info.type is PackageType.STATIC else None + assert lib_type, f"Unknown package type {info.type}" + target["type"] = lib_type + target["location"] = location + target["link_location"] = link_location + link_languages = info.languages or self._conanfile.languages or [] + link_languages = ["CXX" if c == "C++" else c for c in link_languages] + target["link_languages"] = link_languages + + return target + + def _add_root_lib_target(self, libs, pkg_name, cpp_info): + """ + Addd a new pkgname::pkgname INTERFACE target that depends on default_components or + on all other library targets (not exes) + It will not be added if there exists already a pkgname::pkgname target. + """ + root_target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + root_target_name = root_target_name or f"{pkg_name}::{pkg_name}" + if libs and root_target_name not in libs: + # Add a generic interface target for the package depending on the others + if cpp_info.default_components is not None: + all_requires = [] + for defaultc in cpp_info.default_components: + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, + defaultc) + comp_name = target_name or f"{pkg_name}::{defaultc}" + all_requires.append(comp_name) + all_requires = " ".join(all_requires) + else: + all_requires = " ".join(libs.keys()) + libs[root_target_name] = {"type": "INTERFACE", + "requires": all_requires} + + def _get_exes(self, cpp_info, pkg_name, pkg_folder, pkg_folder_var): + exes = {} + + if cpp_info.has_components: + assert not cpp_info.exe, "Package has components and exe" + assert not cpp_info.libs, "Package has components and libs" + for name, comp in cpp_info.components.items(): + if comp.exe or comp.type is PackageType.APP: + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile, + name) + target = target_name or f"{pkg_name}::{name}" + exe_location = self._path(comp.location, pkg_folder, pkg_folder_var) + exes[target] = exe_location + else: + if cpp_info.exe: + assert not cpp_info.libs, "Package has exe and libs" + assert cpp_info.location, "Package has exe and no location" + target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile) + target = target_name or f"{pkg_name}::{pkg_name}" + exe_location = self._path(cpp_info.location, pkg_folder, pkg_folder_var) + exes[target] = exe_location + + return exes + + def _get_dependencies(self): + """ transitive dependencies Filenames for find_dependency() + """ + # Build requires are already filtered by the get_transitive_requires + transitive_reqs = self._cmakedeps.get_transitive_requires(self._conanfile) + # FIXME: Hardcoded CONFIG + ret = {self._cmakedeps.get_cmake_filename(r): "CONFIG" for r in transitive_reqs.values()} + return ret + + @staticmethod + def _path(p, pkg_folder, pkg_folder_var): + def escape(p_): + return p_.replace("$", "\\$").replace('"', '\\"') + + p = p.replace("\\", "/") + if os.path.isabs(p): + if p.startswith(pkg_folder): + rel = p[len(pkg_folder):].lstrip("/") + return f"${{{pkg_folder_var}}}/{escape(rel)}" + return escape(p) + return f"${{{pkg_folder_var}}}/{escape(p)}" + + @staticmethod + def _escape_cmake_string(values): + return " ".join(v.replace("\\", "\\\\").replace('$', '\\$').replace('"', '\\"') + for v in values) + + @property + def _template(self): + # TODO: Check why not set_property instead of target_link_libraries + return textwrap.dedent("""\ + {%- macro config_wrapper(config, value) -%} + {% if config -%} + $<$:{{value}}> + {%- else -%} + {{value}} + {%- endif %} + {%- endmacro -%} + set({{pkg_folder_var}} "{{pkg_folder}}") + + # Dependencies finding + include(CMakeFindDependencyMacro) + + {% for dep, dep_find_mode in dependencies.items() %} + if(NOT {{dep}}_FOUND) + find_dependency({{dep}} REQUIRED {{dep_find_mode}}) + endif() + {% endfor %} + + ################# Libs information ############## + {% for lib, lib_info in libs.items() %} + #################### {{lib}} #################### + if(NOT TARGET {{ lib }}) + message(STATUS "Conan: Target declared imported {{lib_info["type"]}} library '{{lib}}'") + add_library({{lib}} {{lib_info["type"]}} IMPORTED) + endif() + {% if lib_info.get("includedirs") %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES + {{config_wrapper(config, lib_info["includedirs"])}}) + {% endif %} + {% if lib_info.get("defines") %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS + {{config_wrapper(config, lib_info["defines"])}}) + {% endif %} + {% if lib_info.get("cxxflags") %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS + $<$:{{config_wrapper(config, lib_info["cxxflags"])}}>) + {% endif %} + {% if lib_info.get("cflags") %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_COMPILE_OPTIONS + $<$:{{config_wrapper(config, lib_info["cflags"])}}>) + {% endif %} + {% if lib_info.get("sharedlinkflags") %} + {% set linkflags = config_wrapper(config, lib_info["sharedlinkflags"]) %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_OPTIONS + "$<$,SHARED_LIBRARY>:{{linkflags}}>" + "$<$,MODULE_LIBRARY>:{{linkflags}}>") + {% endif %} + {% if lib_info.get("exelinkflags") %} + {% set exeflags = config_wrapper(config, lib_info["exelinkflags"]) %} + set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_OPTIONS + "$<$,EXECUTABLE>:{{exeflags}}>") + {% endif %} + + {% if lib_info.get("link_languages") %} + get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES) + {% for lang in lib_info["link_languages"] %} + if(NOT "{{lang}}" IN_LIST _languages) + message(SEND_ERROR + "Target {{lib}} has {{lang}} linkage but {{lang}} not enabled in project()") + endif() + set_property(TARGET {{lib}} APPEND PROPERTY + IMPORTED_LINK_INTERFACE_LANGUAGES_{{config}} {{lang}}) + {% endfor %} + {% endif %} + {% if lib_info.get("location") %} + set_property(TARGET {{lib}} APPEND PROPERTY IMPORTED_CONFIGURATIONS {{config}}) + set_target_properties({{lib}} PROPERTIES IMPORTED_LOCATION_{{config}} + "{{lib_info["location"]}}") + {% endif %} + {% if lib_info.get("link_location") %} + set_target_properties({{lib}} PROPERTIES IMPORTED_IMPLIB_{{config}} + "{{lib_info["link_location"]}}") + {% endif %} + {% if lib_info.get("requires") %} + target_link_libraries({{lib}} INTERFACE {{lib_info["requires"]}}) + {% endif %} + {% if lib_info.get("system_libs") %} + target_link_libraries({{lib}} INTERFACE {{lib_info["system_libs"]}}) + {% endif %} + + {% endfor %} + + ################# Global variables for try compile and legacy ############## + {% for prefix in additional_variables_prefixes %} + set({{ prefix }}_VERSION_STRING "{{ version }}") + {% if include_dirs is not none %} + set({{ prefix }}_INCLUDE_DIRS "{{ include_dirs }}" ) + set({{ prefix }}_INCLUDE_DIR "{{ include_dirs }}" ) + {% endif %} + {% if libraries is not none %} + set({{ prefix }}_LIBRARIES {{ libraries }} ) + {% endif %} + {% if definitions is not none %} + set({{ prefix }}_DEFINITIONS {{ definitions}} ) + {% endif %} + {% endfor %} + + ################# Exes information ############## + {% for exe, location in exes.items() %} + #################### {{exe}} #################### + if(NOT TARGET {{ exe }}) + message(STATUS "Conan: Target declared imported executable '{{exe}}' {{context}}") + add_executable({{exe}} IMPORTED) + else() + get_property(_context TARGET {{exe}} PROPERTY CONAN_CONTEXT) + if(NOT $${_context} STREQUAL "{{context}}") + message(STATUS "Conan: Exe {{exe}} was already defined in ${_context}") + get_property(_configurations TARGET {{exe}} PROPERTY IMPORTED_CONFIGURATIONS) + message(STATUS "Conan: Exe {{exe}} defined configurations: ${_configurations}") + foreach(_config ${_configurations}) + set_property(TARGET {{exe}} PROPERTY IMPORTED_LOCATION_${_config}) + endforeach() + set_property(TARGET {{exe}} PROPERTY IMPORTED_CONFIGURATIONS) + endif() + endif() + set_property(TARGET {{exe}} APPEND PROPERTY IMPORTED_CONFIGURATIONS {{config}}) + set_target_properties({{exe}} PROPERTIES IMPORTED_LOCATION_{{config}} "{{location}}") + set_property(TARGET {{exe}} PROPERTY CONAN_CONTEXT "{{context}}") + {% endfor %} + """) diff --git a/conan/tools/cmake/cmakedeps2/targets.py b/conan/tools/cmake/cmakedeps2/targets.py new file mode 100644 index 00000000000..2b934f82502 --- /dev/null +++ b/conan/tools/cmake/cmakedeps2/targets.py @@ -0,0 +1,47 @@ +import textwrap + +import jinja2 +from jinja2 import Template + + +class TargetsTemplate2: + """ + FooTargets.cmake + """ + def __init__(self, cmakedeps, conanfile): + self._cmakedeps = cmakedeps + self._conanfile = conanfile + + def content(self): + t = Template(self._template, trim_blocks=True, lstrip_blocks=True, + undefined=jinja2.StrictUndefined) + return t.render(self._context) + + @property + def filename(self): + f = self._cmakedeps.get_cmake_filename(self._conanfile) + return f"{f}Targets.cmake" + + @property + def _context(self): + filename = self._cmakedeps.get_cmake_filename(self._conanfile) + ret = {"ref": str(self._conanfile.ref), + "filename": filename} + return ret + + @property + def _template(self): + return textwrap.dedent("""\ + message(STATUS "Configuring Targets for {{ ref }}") + + # Load information for each installed configuration. + file(GLOB _target_files "${CMAKE_CURRENT_LIST_DIR}/{{filename}}-Targets-*.cmake") + foreach(_target_file IN LISTS _target_files) + include("${_target_file}") + endforeach() + + file(GLOB _build_files "${CMAKE_CURRENT_LIST_DIR}/{{filename}}-TargetsBuild-*.cmake") + foreach(_build_file IN LISTS _build_files) + include("${_build_file}") + endforeach() + """) diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py index ca8a85e383b..e19b113a90f 100644 --- a/conan/tools/cmake/toolchain/blocks.py +++ b/conan/tools/cmake/toolchain/blocks.py @@ -520,6 +520,10 @@ def to_apple_archs(conanfile): class FindFiles(Block): template = textwrap.dedent("""\ # Define paths to find packages, programs, libraries, etc. + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") + message(STATUS "Conan toolchain: Including CMakeDeps generated conan_find_paths.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/conan_cmakedeps_paths.cmake") + else() {% if find_package_prefer_config %} set(CMAKE_FIND_PACKAGE_PREFER_CONFIG {{ find_package_prefer_config }}) @@ -578,6 +582,7 @@ class FindFiles(Block): set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE "BOTH") endif() {% endif %} + endif() """) def _runtime_dirs_value(self, dirs): diff --git a/conans/model/build_info.py b/conans/model/build_info.py index 4505075e6a1..4b4ef9f334b 100644 --- a/conans/model/build_info.py +++ b/conans/model/build_info.py @@ -79,6 +79,8 @@ def __init__(self, set_defaults=False): self._sharedlinkflags = None # linker flags self._exelinkflags = None # linker flags self._objects = None # linker flags + self._exe = None # application executable, only 1 allowed, following CPS + self._languages = None self._sysroot = None self._requires = None @@ -119,9 +121,11 @@ def serialize(self): "sysroot": self._sysroot, "requires": self._requires, "properties": self._properties, + "exe": self._exe, # single exe, incompatible with libs "type": self._type, "location": self._location, - "link_location": self._link_location + "link_location": self._link_location, + "languages": self._languages } @staticmethod @@ -131,6 +135,14 @@ def deserialize(contents): setattr(result, f"_{field}", value) return result + def clone(self): + # Necessary below for exploding a cpp_info.libs = [lib1, lib2] into components + result = _Component() + for k, v in vars(self).items(): + if k.startswith("_"): + setattr(result, k, copy.copy(v)) + return result + @property def includedirs(self): if self._includedirs is None: @@ -258,6 +270,14 @@ def libs(self): def libs(self, value): self._libs = value + @property + def exe(self): + return self._exe + + @exe.setter + def exe(self, value): + self._exe = value + @property def type(self): return self._type @@ -282,6 +302,14 @@ def link_location(self): def link_location(self, value): self._link_location = value + @property + def languages(self): + return self._languages + + @languages.setter + def languages(self, value): + self._languages = value + @property def defines(self): if self._defines is None: @@ -453,7 +481,9 @@ def relocate(el): def parsed_requires(self): return [r.split("::", 1) if "::" in r else (None, r) for r in self.requires] - def deduce_cps(self, pkg_type): + def deduce_locations(self, pkg_type): + if self._exe: # exe is a new field, it should have the correct location + return if self._location or self._link_location: if self._type is None or self._type is PackageType.HEADER: raise ConanException("Incorrect cpp_info defining location without type or header") @@ -461,13 +491,16 @@ def deduce_cps(self, pkg_type): if self._type not in [None, PackageType.SHARED, PackageType.STATIC, PackageType.APP]: return - # Recipe didn't specify things, need to auto deduce - libdirs = [x.replace("\\", "/") for x in self.libdirs] - bindirs = [x.replace("\\", "/") for x in self.bindirs] + if len(self.libs) == 0: + return if len(self.libs) != 1: raise ConanException("More than 1 library defined in cpp_info.libs, cannot deduce CPS") + # Recipe didn't specify things, need to auto deduce + libdirs = [x.replace("\\", "/") for x in self.libdirs] + bindirs = [x.replace("\\", "/") for x in self.bindirs] + # TODO: Do a better handling of pre-defined type libname = self.libs[0] static_patterns = [f"{libname}.lib", f"{libname}.a", f"lib{libname}.a"] @@ -519,6 +552,7 @@ class CppInfo: def __init__(self, set_defaults=False): self.components = defaultdict(lambda: _Component(set_defaults)) + self.default_components = None self._package = _Component(set_defaults) def __getattr__(self, attr): @@ -526,19 +560,22 @@ def __getattr__(self, attr): return getattr(self._package, attr) def __setattr__(self, attr, value): - if attr in ("components", "_package", "_aggregated"): + if attr in ("components", "default_components", "_package", "_aggregated"): super(CppInfo, self).__setattr__(attr, value) else: setattr(self._package, attr, value) def serialize(self): ret = {"root": self._package.serialize()} + if self.default_components: + ret["default_components"] = self.default_components for component_name, info in self.components.items(): ret[component_name] = info.serialize() return ret def deserialize(self, content): self._package = _Component.deserialize(content.pop("root")) + self.default_components = content.get("default_components") for component_name, info in content.items(): self.components[component_name] = _Component.deserialize(info) return self @@ -678,3 +715,37 @@ def required_components(self): # Then split the names ret = [r.split("::") if "::" in r else (None, r) for r in ret] return ret + + def deduce_full_cpp_info(self, conanfile): + pkg_type = conanfile.package_type + + result = CppInfo() # clone it + + if self.libs and len(self.libs) > 1: # expand in multiple components + ConanOutput().warning(f"{conanfile}: The 'cpp_info.libs' contain more than 1 library. " + "Define 'cpp_info.components' instead.") + assert not self.components, f"{conanfile} cpp_info shouldn't have .libs and .components" + for lib in self.libs: + c = _Component() # Do not do a full clone, we don't need the properties + c.type = self.type # This should be a string + c.includedirs = self.includedirs + c.libdirs = self.libdirs + c.libs = [lib] + result.components[f"_{lib}"] = c + + common = self._package.clone() + common.libs = [] + common.type = str(PackageType.HEADER) # the type of components is a string! + common.requires = list(result.components.keys()) + (self.requires or []) + + result.components["_common"] = common + else: + result._package = self._package.clone() + result.default_components = self.default_components + result.components = {k: v.clone() for k, v in self.components.items()} + + result._package.deduce_locations(pkg_type) + for comp in result.components.values(): + comp.deduce_locations(pkg_type) + + return result diff --git a/conans/model/conf.py b/conans/model/conf.py index cdbf0cb55cc..ebc41bc5885 100644 --- a/conans/model/conf.py +++ b/conans/model/conf.py @@ -80,6 +80,7 @@ "tools.cmake.cmake_layout:build_folder": "(Experimental) Allow configuring the base folder of the build for local builds", "tools.cmake.cmake_layout:test_folder": "(Experimental) Allow configuring the base folder of the build for test_package", "tools.cmake:cmake_program": "Path to CMake executable", + "tools.cmake.cmakedeps:new": "Use the new CMakeDeps generator", "tools.cmake:install_strip": "Add --strip to cmake.install()", "tools.deployer:symlinks": "Set to False to disable deployers copying symlinks", "tools.files.download:retry": "Number of retries in case of failure when downloading", diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py new file mode 100644 index 00000000000..e2c3a84bea9 --- /dev/null +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new.py @@ -0,0 +1,1104 @@ +import os +import platform +import re +import textwrap + +import pytest + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.assets.sources import gen_function_h, gen_function_cpp +from conan.test.utils.tools import TestClient + + +new_value = "will_break_next" + + +@pytest.mark.tool("cmake") +class TestExes: + @pytest.mark.parametrize("tool_requires", [False, True]) + def test_exe(self, tool_requires): + conanfile = textwrap.dedent(r""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake + + class Test(ConanFile): + name = "mytool" + version = "0.1" + package_type = "application" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeToolchain" + exports_sources = "*" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.exe = "mytool" + self.cpp_info.set_property("cmake_target_name", "MyTool::myexe") + self.cpp_info.location = os.path.join("bin", "mytool") + """) + main = textwrap.dedent(""" + #include + #include + + int main() { + std::cout << "Mytool generating out.c!!!!!" << std::endl; + std::ofstream f("out.c"); + } + """) + c = TestClient() + c.run("new cmake_exe -d name=mytool -d version=0.1") + c.save({"conanfile.py": conanfile, + "src/main.cpp": main}) + c.run("create .") + + requires = "tool_requires" if tool_requires else "requires" + consumer = textwrap.dedent(f""" + from conan import ConanFile + from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout + class Consumer(ConanFile): + settings = "os", "compiler", "arch", "build_type" + {requires} = "mytool/0.1" + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.generate() + + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(consumer C) + + find_package(mytool) + add_custom_command(OUTPUT out.c COMMAND MyTool::myexe) + add_library(myLib out.c) + """) + c.save({"conanfile.py": consumer, + "CMakeLists.txt": cmake}, clean_first=True) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported executable 'MyTool::myexe'" in c.out + assert "Mytool generating out.c!!!!!" in c.out + + def test_exe_components(self): + conanfile = textwrap.dedent(r""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake + + class Test(ConanFile): + name = "mytool" + version = "0.1" + package_type = "application" + settings = "os", "arch", "compiler", "build_type" + generators = "CMakeToolchain" + exports_sources = "*" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.components["my1exe"].exe = "mytool1" + self.cpp_info.components["my1exe"].set_property("cmake_target_name", "MyTool::my1exe") + self.cpp_info.components["my1exe"].location = os.path.join("bin", "mytool1") + self.cpp_info.components["my2exe"].exe = "mytool2" + self.cpp_info.components["my2exe"].set_property("cmake_target_name", "MyTool::my2exe") + self.cpp_info.components["my2exe"].location = os.path.join("bin", "mytool2") + """) + main = textwrap.dedent(""" + #include + #include + + int main() {{ + std::cout << "Mytool{number} generating out{number}.c!!!!!" << std::endl; + std::ofstream f("out{number}.c"); + }} + """) + cmake = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + cmake_minimum_required(VERSION 3.15) + project(proj CXX) + + add_executable(mytool1 src/main1.cpp) + add_executable(mytool2 src/main2.cpp) + + install(TARGETS mytool1 DESTINATION "." RUNTIME DESTINATION bin) + install(TARGETS mytool2 DESTINATION "." RUNTIME DESTINATION bin) + """) + c = TestClient() + c.save({"conanfile.py": conanfile, + "CMakeLists.txt": cmake, + "src/main1.cpp": main.format(number=1), + "src/main2.cpp": main.format(number=2) + }) + c.run("create .") + + consumer = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMakeDeps, CMakeToolchain, CMake, cmake_layout + class Consumer(ConanFile): + settings = "os", "compiler", "arch", "build_type" + tool_requires = "mytool/0.1" + + def generate(self): + deps = CMakeDeps(self) + deps.generate() + tc = CMakeToolchain(self) + tc.generate() + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(consumer C) + + find_package(mytool) + add_custom_command(OUTPUT out1.c COMMAND MyTool::my1exe) + add_custom_command(OUTPUT out2.c COMMAND MyTool::my2exe) + add_library(myLib out1.c out2.c) + """) + c.save({"conanfile.py": consumer, + "CMakeLists.txt": cmake}, clean_first=True) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported executable 'MyTool::my1exe'" in c.out + assert "Mytool1 generating out1.c!!!!!" in c.out + assert "Conan: Target declared imported executable 'MyTool::my2exe'" in c.out + assert "Mytool2 generating out2.c!!!!!" in c.out + + +@pytest.mark.tool("cmake") +class TestLibs: + def test_libs(self, matrix_client): + c = matrix_client + c.run("new cmake_lib -d name=app -d version=0.1 -d requires=matrix/1.0") + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::matrix'" in c.out + + @pytest.mark.parametrize("shared", [False, True]) + def test_libs_transitive(self, transitive_libraries, shared): + c = transitive_libraries + c.run("new cmake_lib -d name=app -d version=0.1 -d requires=engine/1.0") + shared = "-o engine/*:shared=True" if shared else "" + c.run(f"build . {shared} -c tools.cmake.cmakedeps:new={new_value}") + if shared: + assert "matrix::matrix" not in c.out # It is hidden as static behind the engine + assert "Conan: Target declared imported SHARED library 'engine::engine'" in c.out + else: + assert "Conan: Target declared imported STATIC library 'matrix::matrix'" in c.out + assert "Conan: Target declared imported STATIC library 'engine::engine'" in c.out + + def test_multilevel_shared(self): + # TODO: make this shared fixtures in conftest for multi-level shared testing + c = TestClient(default_server_user=True) + c.run("new cmake_lib -d name=matrix -d version=0.1") + c.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + c.save({}, clean_first=True) + c.run("new cmake_lib -d name=engine -d version=0.1 -d requires=matrix/0.1") + c.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + c.save({}, clean_first=True) + c.run("new cmake_lib -d name=gamelib -d version=0.1 -d requires=engine/0.1") + c.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + c.save({}, clean_first=True) + c.run("new cmake_exe -d name=game -d version=0.1 -d requires=gamelib/0.1") + c.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + assert "matrix/0.1: Hello World Release!" + assert "engine/0.1: Hello World Release!" + assert "gamelib/0.1: Hello World Release!" + assert "game/0.1: Hello World Release!" + + # Make sure it works downloading to another cache + c.run("upload * -r=default -c") + c.run("remove * -c") + + c2 = TestClient(servers=c.servers) + c2.run("new cmake_exe -d name=game -d version=0.1 -d requires=gamelib/0.1") + c2.run(f"create . -o *:shared=True -c tools.cmake.cmakedeps:new={new_value}") + + assert "matrix/0.1: Hello World Release!" + assert "engine/0.1: Hello World Release!" + assert "gamelib/0.1: Hello World Release!" + assert "game/0.1: Hello World Release!" + + +class TestLibsLinkageTraits: + def test_linkage_shared_static(self): + """ + the static library is skipped + """ + c = TestClient() + c.run("new cmake_lib -d name=matrix -d version=0.1") + c.run(f"create . -c tools.cmake.cmakedeps:new={new_value} -tf=") + + c.save({}, clean_first=True) + c.run("new cmake_lib -d name=engine -d version=0.1 -d requires=matrix/0.1") + c.run(f"create . -o engine/*:shared=True -c tools.cmake.cmakedeps:new={new_value} -tf=") + + c.save({}, clean_first=True) + c.run("new cmake_exe -d name=game -d version=0.1 -d requires=engine/0.1") + c.run(f"create . -o engine/*:shared=True -c tools.cmake.cmakedeps:new={new_value} " + "-c tools.compilation:verbosity=verbose") + assert re.search(r"Skipped binaries(\s*)matrix/0.1", c.out) + assert "matrix/0.1: Hello World Release!" + assert "engine/0.1: Hello World Release!" + assert "game/0.1: Hello World Release!" + + +@pytest.mark.tool("cmake") +class TestLibsComponents: + def test_libs_components(self, matrix_client_components): + """ + explicit usage of components + """ + c = matrix_client_components + + # TODO: Check that find_package(.. COMPONENTS nonexisting) fails + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + add_executable(app src/app.cpp) + # standard one, and a custom cmake_target_name one + target_link_libraries(app PRIVATE matrix::module MatrixHeaders) + """) + app_cpp = textwrap.dedent(""" + #include "module.h" + #include "headers.h" + int main() { module(); headers();} + """) + c.save({"CMakeLists.txt": cmake, + "src/app.cpp": app_cpp}) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out + if platform.system() == "Windows": + c.run_command(r".\build\Release\app.exe") + assert "Matrix headers __cplusplus: __cplusplus2014" in c.out + + def test_libs_components_default(self, matrix_client_components): + """ + Test that the default components are used when no component is specified + """ + c = matrix_client_components + + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + + app_cpp = textwrap.dedent(""" + #include "module.h" + int main() { module();} + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE matrix::matrix) + """) + c.save({"src/app.cpp": app_cpp, + "CMakeLists.txt": cmake}) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out + assert "Conan: Target declared imported INTERFACE library 'MatrixHeaders'" in c.out + + def test_libs_components_default_error(self, matrix_client_components): + """ + Same as above, but it fails, because headers is not in the default components + """ + c = matrix_client_components + + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + + app_cpp = textwrap.dedent(""" + #include "module.h" + #include "headers.h" + int main() { module();} + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE matrix::matrix) + """) + c.save({"src/app.cpp": app_cpp, + "CMakeLists.txt": cmake}) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}", assert_error=True) + assert "Error in build() method, line 35" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE matrix::matrix MatrixHeaders) + """) + c.save({"CMakeLists.txt": cmake}) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Running CMake.build()" in c.out # Now it doesn't fail + + def test_libs_components_transitive(self, matrix_client_components): + """ + explicit usage of components + matrix::module -> matrix::vector + + engine::bots -> engine::physix + engine::physix -> matrix::vector + engine::world -> engine::physix, matrix::module + """ + c = matrix_client_components + + from conan.test.assets.sources import gen_function_h + bots_h = gen_function_h(name="bots") + from conan.test.assets.sources import gen_function_cpp + bots_cpp = gen_function_cpp(name="bots", includes=["bots", "physix"], calls=["physix"]) + physix_h = gen_function_h(name="physix") + physix_cpp = gen_function_cpp(name="physix", includes=["physix", "vector"], calls=["vector"]) + world_h = gen_function_h(name="world") + world_cpp = gen_function_cpp(name="world", includes=["world", "physix", "module"], + calls=["physix", "module"]) + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake + from conan.tools.files import copy + + class Engine(ConanFile): + name = "engine" + version = "1.0" + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeToolchain" + exports_sources = "src/*", "CMakeLists.txt" + + requires = "matrix/1.0" + generators = "CMakeDeps", "CMakeToolchain" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.components["bots"].libs = ["bots"] + self.cpp_info.components["bots"].includedirs = ["include"] + self.cpp_info.components["bots"].libdirs = ["lib"] + self.cpp_info.components["bots"].requires = ["physix"] + + self.cpp_info.components["physix"].libs = ["physix"] + self.cpp_info.components["physix"].includedirs = ["include"] + self.cpp_info.components["physix"].libdirs = ["lib"] + self.cpp_info.components["physix"].requires = ["matrix::vector"] + + self.cpp_info.components["world"].libs = ["world"] + self.cpp_info.components["world"].includedirs = ["include"] + self.cpp_info.components["world"].libdirs = ["lib"] + self.cpp_info.components["world"].requires = ["physix", "matrix::module"] + """) + + cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + cmake_minimum_required(VERSION 3.15) + project(matrix CXX) + + find_package(matrix CONFIG REQUIRED) + + add_library(physix src/physix.cpp) + add_library(bots src/bots.cpp) + add_library(world src/world.cpp) + + target_link_libraries(physix PRIVATE matrix::vector) + target_link_libraries(bots PRIVATE physix) + target_link_libraries(world PRIVATE physix matrix::module) + + set_target_properties(bots PROPERTIES PUBLIC_HEADER "src/bots.h") + set_target_properties(physix PROPERTIES PUBLIC_HEADER "src/physix.h") + set_target_properties(world PROPERTIES PUBLIC_HEADER "src/world.h") + install(TARGETS physix bots world) + """) + c.save({"src/physix.h": physix_h, + "src/physix.cpp": physix_cpp, + "src/bots.h": bots_h, + "src/bots.cpp": bots_cpp, + "src/world.h": world_h, + "src/world.cpp": world_cpp, + "CMakeLists.txt": cmakelists, + "conanfile.py": conanfile}) + c.run("create .") + + c.save({}, clean_first=True) + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=engine/1.0") + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(engine CONFIG REQUIRED) + + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE engine::bots) + + install(TARGETS app) + """) + app_cpp = textwrap.dedent(""" + #include "bots.h" + int main() { bots();} + """) + c.save({"CMakeLists.txt": cmake, + "src/app.cpp": app_cpp}) + c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::module'" in c.out + assert "Conan: Target declared imported INTERFACE library 'matrix::matrix'" in c.out + assert "Conan: Target declared imported STATIC library 'engine::bots'" in c.out + assert "Conan: Target declared imported STATIC library 'engine::physix'" in c.out + assert "Conan: Target declared imported STATIC library 'engine::world'" in c.out + assert "Conan: Target declared imported INTERFACE library 'engine::engine'" in c.out + + assert "bots: Release!" in c.out + assert "physix: Release!" in c.out + assert "vector: Release!" in c.out + + def test_libs_components_multilib(self): + """ + cpp_info.libs = ["lib1", "lib2"] + """ + c = TestClient() + + from conan.test.assets.sources import gen_function_h + vector_h = gen_function_h(name="vector") + from conan.test.assets.sources import gen_function_cpp + vector_cpp = gen_function_cpp(name="vector", includes=["vector"]) + module_h = gen_function_h(name="module") + module_cpp = gen_function_cpp(name="module", includes=["module"]) + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake + + class Matrix(ConanFile): + name = "matrix" + version = "1.0" + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeToolchain" + exports_sources = "src/*", "CMakeLists.txt" + + generators = "CMakeDeps", "CMakeToolchain" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.set_property("cmake_target_name", "MyMatrix::MyMatrix") + self.cpp_info.libs = ["module", "vector"] + """) + + cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + cmake_minimum_required(VERSION 3.15) + project(matrix CXX) + + add_library(module src/module.cpp) + add_library(vector src/vector.cpp) + + set_target_properties(vector PROPERTIES PUBLIC_HEADER "src/vector.h") + set_target_properties(module PROPERTIES PUBLIC_HEADER "src/module.h") + install(TARGETS module vector) + """) + c.save({"src/module.h": module_h, + "src/module.cpp": module_cpp, + "src/vector.h": vector_h, + "src/vector.cpp": vector_cpp, + "CMakeLists.txt": cmakelists, + "conanfile.py": conanfile}) + c.run("create .") + + c.save({}, clean_first=True) + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + + find_package(matrix CONFIG REQUIRED) + + add_executable(app src/app.cpp) + target_link_libraries(app PRIVATE MyMatrix::MyMatrix) + + install(TARGETS app) + """) + app_cpp = textwrap.dedent(""" + #include "vector.h" + #include "module.h" + int main() { vector();module();} + """) + c.save({"CMakeLists.txt": cmake, + "src/app.cpp": app_cpp}) + c.run(f"create . -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'matrix::_vector'" in c.out + assert "Conan: Target declared imported STATIC library 'matrix::_module'" in c.out + assert "Conan: Target declared imported INTERFACE library 'MyMatrix::MyMatrix'" in c.out + assert "matrix::matrix" not in c.out + + assert "vector: Release!" in c.out + assert "module: Release!" in c.out + assert "vector: Release!" in c.out + + +@pytest.mark.tool("cmake") +class TestHeaders: + def test_header_lib(self, matrix_client): + c = matrix_client + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.files import copy + class EngineHeader(ConanFile): + name = "engine" + version = "1.0" + requires = "matrix/1.0" + exports_sources = "*.h" + settings = "compiler" + def package(self): + copy(self, "*.h", src=self.source_folder, dst=self.package_folder) + def package_id(self): + self.info.clear() + def package_info(self): + self.cpp_info.defines = ["MY_MATRIX_HEADERS_DEFINE=1", + "MY_MATRIX_HEADERS_DEFINE2=1"] + # Few flags to cover that CMakeDeps doesn't crash with them + if self.settings.compiler == "msvc": + self.cpp_info.cxxflags = ["/Zc:__cplusplus"] + self.cpp_info.cflags = ["/Zc:__cplusplus"] + self.cpp_info.system_libs = ["ws2_32"] + else: + self.cpp_info.system_libs = ["m", "dl"] + # Just to verify CMake don't break + if self.settings.compiler == "gcc": + self.cpp_info.sharedlinkflags = ["-z now", "-z relro"] + self.cpp_info.exelinkflags = ["-z now", "-z relro"] + """) + engine_h = textwrap.dedent(""" + #pragma once + #include + #include "matrix.h" + #ifndef MY_MATRIX_HEADERS_DEFINE + #error "Fatal error MY_MATRIX_HEADERS_DEFINE not defined" + #endif + #ifndef MY_MATRIX_HEADERS_DEFINE2 + #error "Fatal error MY_MATRIX_HEADERS_DEFINE2 not defined" + #endif + void engine(){ std::cout << "Engine!" < + + #ifndef MY_MATRIX_HEADERS_{version}_DEFINE + #error "Fatal error MY_MATRIX_HEADERS_{version}_DEFINE not defined" + #endif + void engine(){{ std::cout << "Engine {version}!" < + #include + #include "protobuf.h" + + int main() { + protobuf(); + #ifdef NDEBUG + std::cout << "Protoc RELEASE generating out.c!!!!!" << std::endl; + #else + std::cout << "Protoc DEBUG generating out.c!!!!!" << std::endl; + #endif + std::ofstream f("out.c"); + } + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(protobuf CXX) + + add_library(protobuf src/protobuf.cpp) + add_executable(protoc src/main.cpp) + target_link_libraries(protoc PRIVATE protobuf) + set_target_properties(protobuf PROPERTIES PUBLIC_HEADER "src/protobuf.h") + + install(TARGETS protoc protobuf) + """) + c = TestClient() + c.save({"conanfile.py": conanfile, + "CMakeLists.txt": cmake, + "src/protobuf.h": gen_function_h(name="protobuf"), + "src/protobuf.cpp": gen_function_cpp(name="protobuf", includes=["protobuf"]), + "src/main.cpp": main}) + c.run("export .") + + consumer = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + class Consumer(ConanFile): + settings = "os", "compiler", "arch", "build_type" + requires = "protobuf/0.1" + generators = "CMakeToolchain", "CMakeDeps" + + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + self.run(os.path.join(self.cpp.build.bindir, "myapp")) + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(consumer CXX) + + find_package(MyProtobuf CONFIG REQUIRED) + add_custom_command(OUTPUT out.c COMMAND Protobuf::Protocompile) + add_executable(myapp myapp.cpp out.c) + target_link_libraries(myapp PRIVATE protobuf::protobuf) + get_target_property(imported_configs Protobuf::Protocompile IMPORTED_CONFIGURATIONS) + message(STATUS "Protoc imported configurations: ${imported_configs}!!!") + """) + myapp = textwrap.dedent(""" + #include + #include "protobuf.h" + + int main() { + protobuf(); + std::cout << "MyApp" << std::endl; + } + """) + c.save({"conanfile.py": consumer, + "CMakeLists.txt": cmake, + "myapp.cpp": myapp}, clean_first=True) + return c + + def test_requires(self, protobuf): + c = protobuf + c.run(f"build . --build=missing -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out + assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out + assert "Protoc RELEASE generating out.c!!!!!" in c.out + assert 'Protoc imported configurations: RELEASE!!!' in c.out + + def test_both(self, protobuf): + consumer = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + class Consumer(ConanFile): + settings = "os", "compiler", "arch", "build_type" + requires = "protobuf/0.1" + tool_requires = "protobuf/0.1" + generators = "CMakeToolchain", "CMakeDeps" + + def layout(self): + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + """) + c = protobuf + c.save({"conanfile.py": consumer}) + c.run("build . -s:h build_type=Debug --build=missing " + f"-c tools.cmake.cmakedeps:new={new_value}") + + assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out + assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out + assert "Protoc RELEASE generating out.c!!!!!" in c.out + assert "protobuf: Release!" in c.out + assert "protobuf: Debug!" not in c.out + assert 'Protoc imported configurations: RELEASE!!!' in c.out + + cmd = "./build/Debug/myapp" if platform.system() != "Windows" else r"build\Debug\myapp" + c.run_command(cmd) + assert "protobuf: Debug!" in c.out + assert "protobuf: Release!" not in c.out + + c.run("build . --build=missing " + f"-c tools.cmake.cmakedeps:new={new_value}") + + assert "Conan: Target declared imported STATIC library 'protobuf::protobuf'" in c.out + assert "Conan: Target declared imported executable 'Protobuf::Protocompile'" in c.out + assert "Protoc RELEASE generating out.c!!!!!" in c.out + assert "protobuf: Release!" in c.out + assert "protobuf: Debug!" not in c.out + assert 'Protoc imported configurations: RELEASE!!!' in c.out + + cmd = "./build/Release/myapp" if platform.system() != "Windows" else r"build\Release\myapp" + c.run_command(cmd) + assert "protobuf: Debug!" not in c.out + assert "protobuf: Release!" in c.out + + +@pytest.mark.tool("cmake", "3.23") +class TestConfigs: + @pytest.mark.skipif(platform.system() != "Windows", reason="Only MSVC multi-conf") + def test_multi_config(self, matrix_client): + c = matrix_client + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + c.run("install . -s build_type=Debug --build=missing " + f"-c tools.cmake.cmakedeps:new={new_value}") + + c.run_command("cmake --preset conan-default") + c.run_command("cmake --build --preset conan-release") + c.run_command("cmake --build --preset conan-debug") + + c.run_command("build\\Release\\app") + assert "matrix/1.0: Hello World Release!" in c.out + assert "app/0.1: Hello World Release!" in c.out + c.run_command("build\\Debug\\app") + assert "matrix/1.0: Hello World Debug!" in c.out + assert "app/0.1: Hello World Debug!" in c.out + + def test_cross_config(self, matrix_client): + # Release dependencies, but compiling app in Debug + c = matrix_client + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + c.run(f"install . -s &:build_type=Debug -c tools.cmake.cmakedeps:new={new_value}") + + # With modern CMake > 3.26 not necessary set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) + find_package(matrix CONFIG REQUIRED) + + add_executable(app src/app.cpp src/main.cpp) + target_link_libraries(app PRIVATE matrix::matrix) + """) + c.save({"CMakeLists.txt": cmake}) + + preset = "conan-default" if platform.system() == "Windows" else "conan-debug" + c.run_command(f"cmake --preset {preset}") + c.run_command("cmake --build --preset conan-debug") + + c.run_command(os.path.join("build", "Debug", "app")) + assert "matrix/1.0: Hello World Release!" in c.out + assert "app/0.1: Hello World Debug!" in c.out + + @pytest.mark.skipif(platform.system() == "Windows", reason="This doesn't work in MSVC") + def test_cross_config_implicit(self, matrix_client): + # Release dependencies, but compiling app in Debug, without specifying it + c = matrix_client + c.run("new cmake_exe -d name=app -d version=0.1 -d requires=matrix/1.0") + c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + + # With modern CMake > 3.26 not necessary set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(app CXX) + set(CMAKE_MAP_IMPORTED_CONFIG_DEBUG Release) + find_package(matrix CONFIG REQUIRED) + + add_executable(app src/app.cpp src/main.cpp) + target_link_libraries(app PRIVATE matrix::matrix) + """) + + c.save({"CMakeLists.txt": cmake}) + # Now we can force the Debug build, even if dependencies are Release + c.run_command("cmake . -DCMAKE_BUILD_TYPE=Debug -B build " + "-DCMAKE_PREFIX_PATH=build/Release/generators") + c.run_command("cmake --build build") + c.run_command("./build/app") + assert "matrix/1.0: Hello World Release!" in c.out + assert "app/0.1: Hello World Debug!" in c.out + + +@pytest.mark.tool("cmake", "3.23") +class TestCMakeTry: + + def test_check_c_source_compiles(self, matrix_client): + """ + https://github.com/conan-io/conan/issues/12012 + """ + c = matrix_client # it brings the "matrix" package dependency pre-built + + consumer = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMakeDeps + class PkgConan(ConanFile): + settings = "os", "arch", "compiler", "build_type" + requires = "matrix/1.0" + generators = "CMakeToolchain", + def generate(self): + deps = CMakeDeps(self) + deps.set_property("matrix", "cmake_additional_variables_prefixes", ["MyMatrix"]) + deps.generate() + """) + + cmakelist = textwrap.dedent("""\ + cmake_minimum_required(VERSION 3.15) + project(Hello LANGUAGES CXX) + + find_package(matrix CONFIG REQUIRED) + include(CheckCXXSourceCompiles) + + set(CMAKE_REQUIRED_INCLUDES ${MyMatrix_INCLUDE_DIRS}) + set(CMAKE_REQUIRED_LIBRARIES ${MyMatrix_LIBRARIES}) + check_cxx_source_compiles("#include + int main(void) { matrix();return 0; }" IT_COMPILES) + """) + + c.save({"conanfile.py": consumer, + "CMakeLists.txt": cmakelist}, clean_first=True) + c.run(f"install . -c tools.cmake.cmakedeps:new={new_value}") + + preset = "conan-default" if platform.system() == "Windows" else "conan-release" + c.run_command(f"cmake --preset {preset} ") + assert "Performing Test IT_COMPILES - Success" in c.out diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_cpp_linkage.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_cpp_linkage.py new file mode 100644 index 00000000000..0932a77fa35 --- /dev/null +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_cpp_linkage.py @@ -0,0 +1,54 @@ +import platform +import textwrap +import pytest + + +new_value = "will_break_next" + + +@pytest.mark.skipif(platform.system() == "Windows", reason="Windows doesn't fail to link") +def test_auto_cppstd(matrix_c_interface_client): + c = matrix_c_interface_client + # IMPORTANT: This must be a C and CXX CMake project!! + consumer = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(myapp C CXX) + + find_package(matrix REQUIRED) + + add_executable(app app.c) + target_link_libraries(app PRIVATE matrix::matrix) + """) + + conanfile = textwrap.dedent("""\ + import os + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + + class Recipe(ConanFile): + settings = "os", "compiler", "build_type", "arch" + package_type = "application" + generators = "CMakeToolchain", "CMakeDeps" + requires = "matrix/0.1" + + def layout(self): + cmake_layout(self) + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + self.run(os.path.join(self.cpp.build.bindir, "app"), env="conanrun") + """) + app = textwrap.dedent(""" + #include "matrix.h" + int main(){ + matrix(); + return 0; + } + """) + c.save({"conanfile.py": conanfile, + "CMakeLists.txt": consumer, + "app.c": app}, clean_first=True) + c.run(f"build . -c tools.cmake.cmakedeps:new={new_value}") + assert "Hello Matrix!" in c.out diff --git a/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py new file mode 100644 index 00000000000..f3c04e165f3 --- /dev/null +++ b/test/functional/toolchains/cmake/cmakedeps/test_cmakedeps_new_paths.py @@ -0,0 +1,97 @@ +import re +import textwrap + +import pytest + +from conan.test.assets.genconanfile import GenConanfile +from conan.test.utils.tools import TestClient + +new_value = "will_break_next" + + +@pytest.fixture +def client(): + c = TestClient() + pkg = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + import os + class Pkg(ConanFile): + settings = "build_type", "os", "arch", "compiler" + requires = "dep/0.1" + generators = "CMakeDeps", "CMakeToolchain" + def layout(self): # Necessary to force config files in another location + cmake_layout(self) + def build(self): + cmake = CMake(self) + cmake.configure(variables={"CMAKE_FIND_DEBUG_MODE": "ON"}) + """) + cmake = textwrap.dedent(""" + cmake_minimum_required(VERSION 3.15) + project(pkgb LANGUAGES NONE) + find_package(dep CONFIG REQUIRED) + """) + c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"), + "pkg/conanfile.py": pkg, + "pkg/CMakeLists.txt": cmake}) + return c + + +@pytest.mark.tool("cmake") +def test_cmake_generated(client): + c = client + c.run("create dep") + c.run(f"build pkg -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan toolchain: Including CMakeDeps generated conan_find_paths.cmake" in c.out + assert "Conan: Target declared imported INTERFACE library 'dep::dep'" in c.out + + +@pytest.mark.tool("cmake") +def test_cmake_in_package(client): + c = client + # same, but in-package + dep = textwrap.dedent(""" + import os + from conan import ConanFile + from conan.tools.files import save + class Pkg(ConanFile): + name = "dep" + version = "0.1" + + def package(self): + content = 'message(STATUS "Hello from dep dep-Config.cmake!!!!!")' + save(self, os.path.join(self.package_folder, "cmake", "dep-config.cmake"), content) + def package_info(self): + self.cpp_info.set_property("cmake_find_mode", "none") + self.cpp_info.builddirs = ["cmake"] + """) + + c.save({"dep/conanfile.py": dep}) + c.run("create dep") + c.run(f"build pkg -c tools.cmake.cmakedeps:new={new_value}") + assert "Conan toolchain: Including CMakeDeps generated conan_find_paths.cmake" in c.out + assert "Hello from dep dep-Config.cmake!!!!!" in c.out + + +class TestRuntimeDirs: + + def test_runtime_lib_dirs_multiconf(self): + client = TestClient() + app = GenConanfile().with_requires("dep/1.0").with_generator("CMakeDeps")\ + .with_settings("build_type") + client.save({"lib/conanfile.py": GenConanfile(), + "dep/conanfile.py": GenConanfile("dep").with_requires("onelib/1.0", + "twolib/1.0"), + "app/conanfile.py": app}) + client.run("create lib --name=onelib --version=1.0") + client.run("create lib --name=twolib --version=1.0") + client.run("create dep --version=1.0") + + client.run(f'install app -s build_type=Release -c tools.cmake.cmakedeps:new={new_value}') + client.run(f'install app -s build_type=Debug -c tools.cmake.cmakedeps:new={new_value}') + + contents = client.load("app/conan_cmakedeps_paths.cmake") + pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)" + runtime_lib_dirs = re.search(pattern_lib_dirs, contents).group(1) + assert "" in runtime_lib_dirs + assert "" in runtime_lib_dirs diff --git a/test/functional/toolchains/conftest.py b/test/functional/toolchains/conftest.py index e1f294b62a6..b81671e870f 100644 --- a/test/functional/toolchains/conftest.py +++ b/test/functional/toolchains/conftest.py @@ -1,8 +1,10 @@ import os import shutil +import textwrap import pytest +from conan.test.assets.sources import gen_function_h, gen_function_cpp from conan.test.utils.test_files import temp_folder from conan.test.utils.tools import TestClient @@ -48,3 +50,188 @@ def transitive_libraries(_transitive_libraries): c.cache_folder = os.path.join(temp_folder(), ".conan2") shutil.copytree(_transitive_libraries.cache_folder, c.cache_folder) return c + + +@pytest.fixture(scope="session") +def _matrix_client_components(): + """ + 2 components, different than the package name + """ + c = TestClient() + headers_h = textwrap.dedent(""" + #include + #ifndef MY_MATRIX_HEADERS_DEFINE + #error "Fatal error MY_MATRIX_HEADERS_DEFINE not defined" + #endif + void headers(){ std::cout << "Matrix headers: Release!" << std::endl; + #if __cplusplus + std::cout << " Matrix headers __cplusplus: __cplusplus" << __cplusplus << std::endl; + #endif + } + """) + vector_h = gen_function_h(name="vector") + vector_cpp = gen_function_cpp(name="vector", includes=["vector"]) + module_h = gen_function_h(name="module") + module_cpp = gen_function_cpp(name="module", includes=["module", "vector"], calls=["vector"]) + + conanfile = textwrap.dedent(""" + from conan import ConanFile + from conan.tools.cmake import CMake + + class Matrix(ConanFile): + name = "matrix" + version = "1.0" + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeToolchain" + exports_sources = "src/*", "CMakeLists.txt" + + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + + def package(self): + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.default_components = ["vector", "module"] + + self.cpp_info.components["headers"].includedirs = ["include/headers"] + self.cpp_info.components["headers"].set_property("cmake_target_name", "MatrixHeaders") + self.cpp_info.components["headers"].defines = ["MY_MATRIX_HEADERS_DEFINE=1"] + # Few flags to cover that CMakeDeps doesn't crash with them + if self.settings.compiler == "msvc": + self.cpp_info.components["headers"].cxxflags = ["/Zc:__cplusplus"] + self.cpp_info.components["headers"].cflags = ["/Zc:__cplusplus"] + self.cpp_info.components["headers"].system_libs = ["ws2_32"] + else: + self.cpp_info.components["headers"].system_libs = ["m"] + # Just to verify CMake don't break + self.cpp_info.sharedlinkflags = ["-z now", "-z relro"] + self.cpp_info.exelinkflags = ["-z now", "-z relro"] + + self.cpp_info.components["vector"].libs = ["vector"] + self.cpp_info.components["vector"].includedirs = ["include"] + self.cpp_info.components["vector"].libdirs = ["lib"] + + self.cpp_info.components["module"].libs = ["module"] + self.cpp_info.components["module"].includedirs = ["include"] + self.cpp_info.components["module"].libdirs = ["lib"] + self.cpp_info.components["module"].requires = ["vector"] + """) + + cmakelists = textwrap.dedent(""" + set(CMAKE_CXX_COMPILER_WORKS 1) + set(CMAKE_CXX_ABI_COMPILED 1) + cmake_minimum_required(VERSION 3.15) + project(matrix CXX) + + add_library(vector src/vector.cpp) + add_library(module src/module.cpp) + add_library(headers INTERFACE) + target_link_libraries(module PRIVATE vector) + + set_target_properties(headers PROPERTIES PUBLIC_HEADER "src/headers.h") + set_target_properties(module PROPERTIES PUBLIC_HEADER "src/module.h") + set_target_properties(vector PROPERTIES PUBLIC_HEADER "src/vector.h") + install(TARGETS vector module) + install(TARGETS headers PUBLIC_HEADER DESTINATION include/headers) + """) + c.save({"src/headers.h": headers_h, + "src/vector.h": vector_h, + "src/vector.cpp": vector_cpp, + "src/module.h": module_h, + "src/module.cpp": module_cpp, + "CMakeLists.txt": cmakelists, + "conanfile.py": conanfile}) + c.run("create .") + return c + + +@pytest.fixture() +def matrix_client_components(_matrix_client_components): + c = TestClient() + c.cache_folder = os.path.join(temp_folder(), ".conan2") + shutil.copytree(_matrix_client_components.cache_folder, c.cache_folder) + return c + + +@pytest.fixture(scope="session") +def _matrix_c_interface_client(): + c = TestClient() + matrix_h = textwrap.dedent("""\ + #pragma once + #ifdef __cplusplus + extern "C" { + #endif + void matrix(); + #ifdef __cplusplus + } + #endif + """) + matrix_cpp = textwrap.dedent("""\ + #include "matrix.h" + #include + #include + void matrix(){ + std::cout<< std::string("Hello Matrix!") < + $ + ) + set_target_properties(matrix PROPERTIES PUBLIC_HEADER "include/matrix.h") + install(TARGETS matrix EXPORT matrixConfig) + export(TARGETS matrix + NAMESPACE matrix:: + FILE "${CMAKE_CURRENT_BINARY_DIR}/matrixConfig.cmake" + ) + install(EXPORT matrixConfig + DESTINATION "${CMAKE_INSTALL_PREFIX}/matrix/cmake" + NAMESPACE matrix:: + ) + """) + conanfile = textwrap.dedent("""\ + from conan import ConanFile + from conan.tools.cmake import CMake, cmake_layout + class Recipe(ConanFile): + name = "matrix" + version = "0.1" + settings = "os", "compiler", "build_type", "arch" + package_type = "static-library" + generators = "CMakeToolchain" + exports_sources = "CMakeLists.txt", "src/*", "include/*" + languages = "C++" + def build(self): + cmake = CMake(self) + cmake.configure() + cmake.build() + def layout(self): + cmake_layout(self) + def package(self): + cmake = CMake(self) + cmake.install() + def package_info(self): + self.cpp_info.libs = ["matrix"] + """) + c.save({"include/matrix.h": matrix_h, + "src/matrix.cpp": matrix_cpp, + "conanfile.py": conanfile, + "CMakeLists.txt": cmake}) + c.run("create .") + return c + + +@pytest.fixture() +def matrix_c_interface_client(_matrix_c_interface_client): + c = TestClient() + c.cache_folder = os.path.join(temp_folder(), ".conan2") + shutil.copytree(_matrix_c_interface_client.cache_folder, c.cache_folder) + return c