Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CMakeDeps/CMakeToolchain: Several improvements for open issues #9455

Merged
merged 11 commits into from Sep 1, 2021
46 changes: 32 additions & 14 deletions conan/tools/cmake/cmakedeps/cmakedeps.py
@@ -1,4 +1,5 @@
import os
from fnmatch import fnmatch

from conan.tools._check_build_profile import check_using_build_profile
from conan.tools.cmake.cmakedeps.templates.config import ConfigTemplate
Expand All @@ -11,6 +12,11 @@
from conans.util.files import save


FIND_MODE_MODULE = "module"
FIND_MODE_CONFIG = "config"
FIND_MODE_NONE = "none"
FIND_MODE_BOTH = "both"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious why a "both" option?
AFAIK official CMake are modules (And there are a few projects the provide their own) and most project install targets + follow https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html which are your config packages

but I've not come across a project that does both 🤔 with the old generators it was the costume who decided what to install but now it's defined by the recipe... we'll need to be more careful to define it correctly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both cover the case where the library has a module in cmake, typical FindXXX.cmake but also the author's provided config (irrespective if we package it or not). In that case, you don't know how the consumer will expect to consume the package, maybe find_package(XXX MODULE) maybe find_package(xxx CONFIG) but we want Conan to be as "ready" as possible to consume any conan center package. So CMakeDeps will generate both. Note that the config namespace and target name should be declared typically following the author's one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, I understand the motivation but I am not aware of any examples.

There are a few project out there that provide their own module files even though the upstream project provides config files. I wonder if this bonus feature will help there 🤔


class CMakeDeps(object):

def __init__(self, conanfile):
Expand Down Expand Up @@ -75,26 +81,38 @@ def content(self):
if dep.is_build_context and dep.ref.name not in self.build_context_activated:
continue

if dep.new_cpp_info.get_property("skip_deps_file", "CMakeDeps"):
cmake_find_mode = dep.new_cpp_info.get_property("cmake_find_mode", "CMakeDeps") or FIND_MODE_CONFIG
cmake_find_mode = cmake_find_mode.lower()
# Skip from the requirement
if cmake_find_mode == FIND_MODE_NONE:
# Skip the generation of config files for this node, it will be located externally
continue

if cmake_find_mode in (FIND_MODE_CONFIG, FIND_MODE_BOTH):
self._generate_files(require, dep, ret, find_module_mode=False)

if cmake_find_mode in (FIND_MODE_MODULE, FIND_MODE_BOTH):
self._generate_files(require, dep, ret, find_module_mode=True)

return ret

def _generate_files(self, require, dep, ret, find_module_mode):
if not find_module_mode:
config_version = ConfigVersionTemplate(self, require, dep)
ret[config_version.filename] = config_version.render()

data_target = ConfigDataTemplate(self, require, dep)
ret[data_target.filename] = data_target.render()
data_target = ConfigDataTemplate(self, require, dep, find_module_mode)
ret[data_target.filename] = data_target.render()

target_configuration = TargetConfigurationTemplate(self, require, dep)
ret[target_configuration.filename] = target_configuration.render()
target_configuration = TargetConfigurationTemplate(self, require, dep, find_module_mode)
ret[target_configuration.filename] = target_configuration.render()

targets = TargetsTemplate(self, require, dep)
ret[targets.filename] = targets.render()
targets = TargetsTemplate(self, require, dep, find_module_mode)
ret[targets.filename] = targets.render()

config = ConfigTemplate(self, require, dep)
# Check if the XXConfig.cmake exists to keep the first generated configuration
# to only include the build_modules from the first conan install. The rest of the
# file is common for the different configurations.
if not os.path.exists(config.filename):
ret[config.filename] = config.render()
return ret
config = ConfigTemplate(self, require, dep, find_module_mode)
# Check if the XXConfig.cmake exists to keep the first generated configuration
# to only include the build_modules from the first conan install. The rest of the
# file is common for the different configurations.
if not os.path.exists(config.filename):
ret[config.filename] = config.render()
62 changes: 34 additions & 28 deletions conan/tools/cmake/cmakedeps/templates/__init__.py
Expand Up @@ -7,22 +7,23 @@

class CMakeDepsFileTemplate(object):

def __init__(self, cmakedeps, require, conanfile):
def __init__(self, cmakedeps, require, conanfile, find_module_mode=False):
self.cmakedeps = cmakedeps
self.require = require
self.conanfile = conanfile
self.find_module_mode = find_module_mode

@property
def pkg_name(self):
return self.conanfile.ref.name + self.suffix

@property
def target_namespace(self):
return get_target_namespace(self.conanfile) + self.suffix
return self.get_target_namespace(self.conanfile) + self.suffix

@property
def file_name(self):
return get_file_name(self.conanfile) + self.suffix
return get_file_name(self.conanfile, self.find_module_mode) + self.suffix

@property
def suffix(self):
Expand Down Expand Up @@ -74,29 +75,34 @@ def arch(self):
def config_suffix(self):
return "_{}".format(self.configuration.upper()) if self.configuration else ""

def get_target_namespace(self):
return get_target_namespace(self.conanfile)

def get_file_name(self):
return get_file_name(self.conanfile)


def get_target_namespace(req):
ret = req.new_cpp_info.get_property("cmake_target_name", "CMakeDeps")
if not ret:
ret = req.cpp_info.get_name("cmake_find_package_multi", default_name=False)
return ret or req.ref.name


def get_component_alias(req, comp_name):
if comp_name not in req.new_cpp_info.components:
# foo::foo might be referencing the root cppinfo
if req.ref.name == comp_name:
return get_target_namespace(req)
raise ConanException("Component '{name}::{cname}' not found in '{name}' "
"package requirement".format(name=req.ref.name, cname=comp_name))
ret = req.new_cpp_info.components[comp_name].get_property("cmake_target_name", "CMakeDeps")
if not ret:
ret = req.cpp_info.components[comp_name].get_name("cmake_find_package_multi",
default_name=False)
return ret or comp_name
return get_file_name(self.conanfile, find_module_mode=self.find_module_mode)

def get_target_namespace(self, req):
if self.find_module_mode:
ret = req.new_cpp_info.get_property("cmake_module_target_name", "CMakeDeps")
if ret:
return ret

ret = req.new_cpp_info.get_property("cmake_target_name", "CMakeDeps")
lasote marked this conversation as resolved.
Show resolved Hide resolved
if not ret:
ret = req.cpp_info.get_name("cmake_find_package_multi", default_name=False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No fallback to "cmake_find_package" for module?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been greping conan-center-index and the name is always set for both of them:

        self.cpp_info.names["cmake_find_package"] = "Crc32c"
        self.cpp_info.names["cmake_find_package_multi"] = "Crc32c"

So the fallback looks enough. We introduce this fallback mostly for Conan center.

return ret or req.ref.name

def get_component_alias(self, req, comp_name):
if comp_name not in req.new_cpp_info.components:
# foo::foo might be referencing the root cppinfo
if req.ref.name == comp_name:
return self.get_target_namespace(req)
raise ConanException("Component '{name}::{cname}' not found in '{name}' "
"package requirement".format(name=req.ref.name, cname=comp_name))
if self.find_module_mode:
ret = req.new_cpp_info.components[comp_name].get_property("cmake_module_target_name",
"CMakeDeps")
if ret:
return ret
ret = req.new_cpp_info.components[comp_name].get_property("cmake_target_name", "CMakeDeps")
if not ret:
ret = req.cpp_info.components[comp_name].get_name("cmake_find_package_multi",
default_name=False)
return ret or comp_name
32 changes: 26 additions & 6 deletions conan/tools/cmake/cmakedeps/templates/config.py
Expand Up @@ -14,31 +14,51 @@ class ConfigTemplate(CMakeDepsFileTemplate):

@property
def filename(self):
if self.file_name == self.file_name.lower():
return "{}-config.cmake".format(self.file_name)
if self.find_module_mode:
lasote marked this conversation as resolved.
Show resolved Hide resolved
return "Find{}.cmake".format(self.file_name)
else:
return "{}Config.cmake".format(self.file_name)
if self.file_name == self.file_name.lower():
return "{}-config.cmake".format(self.file_name)
else:
return "{}Config.cmake".format(self.file_name)

@property
def context(self):
return {"file_name": self.file_name,
targets_include = "" if not self.find_module_mode else "module-"
targets_include += "{}Targets.cmake".format(self.file_name)
return {"is_module": self.find_module_mode,
"version": self.conanfile.ref.version,
"file_name": self.file_name,
"pkg_name": self.pkg_name,
"config_suffix": self.config_suffix,
"target_namespace": self.target_namespace,
"check_components_exist": self.cmakedeps.check_components_exist}
"check_components_exist": self.cmakedeps.check_components_exist,
"targets_include_file": targets_include}

@property
def template(self):
return textwrap.dedent("""\
########## MACROS ###########################################################################
#############################################################################################

# Requires CMake > 3.15
if(${CMAKE_VERSION} VERSION_LESS "3.15")
message(FATAL_ERROR "The 'CMakeDeps' generator only works with CMake >= 3.15")
endif()

{% if is_module %}
include(FindPackageHandleStandardArgs)
set({{ pkg_name }}_FOUND 1)
set({{ pkg_name }}_VERSION "{{ version }}")

find_package_handle_standard_args({{ pkg_name }}
REQUIRED_VARS {{ pkg_name }}_VERSION
VERSION_VAR {{ pkg_name }}_VERSION)
mark_as_advanced({{ pkg_name }}_FOUND {{ pkg_name }}_VERSION)
{% endif %}

include(${CMAKE_CURRENT_LIST_DIR}/cmakedeps_macros.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/{{ file_name }}Targets.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/{{ targets_include_file }})
include(CMakeFindDependencyMacro)

foreach(_DEPENDENCY {{ '${' + pkg_name + '_FIND_DEPENDENCY_NAMES' + '}' }} )
Expand Down
17 changes: 8 additions & 9 deletions conan/tools/cmake/cmakedeps/templates/target_configuration.py
@@ -1,7 +1,6 @@
import textwrap

from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate, get_component_alias, \
get_target_namespace
from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate

"""

Expand All @@ -14,8 +13,9 @@ class TargetConfigurationTemplate(CMakeDepsFileTemplate):

@property
def filename(self):
return "{}Target-{}.cmake".format(self.file_name,
self.cmakedeps.configuration.lower())
name = "" if not self.find_module_mode else "module-"
lasote marked this conversation as resolved.
Show resolved Hide resolved
name += "{}-Target-{}.cmake".format(self.file_name, self.cmakedeps.configuration.lower())
return name

@property
def context(self):
Expand Down Expand Up @@ -147,7 +147,7 @@ def get_required_components_names(self):
ret = []
sorted_comps = self.conanfile.new_cpp_info.get_sorted_components()
for comp_name, comp in sorted_comps.items():
ret.append(get_component_alias(self.conanfile, comp_name))
ret.append(self.get_component_alias(self.conanfile, comp_name))
ret.reverse()
return ret

Expand All @@ -163,16 +163,15 @@ def get_deps_targets_names(self):
for dep_name, component_name in self.conanfile.new_cpp_info.required_components:
if not dep_name:
# Internal dep (no another component)
dep_name = get_target_namespace(self.conanfile)
req = self.conanfile
else:
req = self.conanfile.dependencies.host[dep_name]
dep_name = get_target_namespace(req)

component_name = get_component_alias(req, component_name)
dep_name = self.get_target_namespace(req)
component_name = self.get_component_alias(req, component_name)
ret.append("{}::{}".format(dep_name, component_name))
elif self.conanfile.dependencies.direct_host:
# Regular external "conanfile.requires" declared, not cpp_info requires
ret = ["{p}::{p}".format(p=get_target_namespace(r))
ret = ["{p}::{p}".format(p=self.get_target_namespace(r))
for r in self.conanfile.dependencies.direct_host.values()]
return ret
20 changes: 10 additions & 10 deletions conan/tools/cmake/cmakedeps/templates/target_data.py
@@ -1,8 +1,7 @@
import os
import textwrap

from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate, get_component_alias, \
get_target_namespace
from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate
from conan.tools.cmake.utils import get_file_name

"""
Expand All @@ -16,7 +15,8 @@ class ConfigDataTemplate(CMakeDepsFileTemplate):

@property
def filename(self):
data_fname = "{}-{}".format(self.file_name, self.configuration.lower())
data_fname = "" if not self.find_module_mode else "module-"
data_fname += "{}-{}".format(self.file_name, self.configuration.lower())
if self.arch:
data_fname += "-{}".format(self.arch)
data_fname += "-data.cmake"
Expand Down Expand Up @@ -122,14 +122,14 @@ def get_required_components_cpp(self):
if "::" in require: # Points to a component of a different package
pkg, cmp_name = require.split("::")
req = self.conanfile.dependencies.direct_host[pkg]
public_comp_deps.append("{}::{}".format(get_target_namespace(req),
get_component_alias(req, cmp_name)))
public_comp_deps.append("{}::{}".format(self.get_target_namespace(req),
self.get_component_alias(req, cmp_name)))
else: # Points to a component of same package
public_comp_deps.append("{}::{}".format(self.target_namespace,
get_component_alias(self.conanfile,
require)))
self.get_component_alias(self.conanfile,
require)))
deps_cpp_cmake.public_deps = " ".join(public_comp_deps)
component_rename = get_component_alias(self.conanfile, comp_name)
component_rename = self.get_component_alias(self.conanfile, comp_name)
ret.append((component_rename, deps_cpp_cmake))
ret.reverse()
return ret
Expand All @@ -143,9 +143,9 @@ def _get_dependency_filenames(self):
for dep_name, _ in self.conanfile.new_cpp_info.required_components:
if dep_name and dep_name not in ret: # External dep
req = direct_host[dep_name]
ret.append(get_file_name(req))
ret.append(get_file_name(req, self.find_module_mode))
elif direct_host:
ret = [get_file_name(r) for r in direct_host.values()]
ret = [get_file_name(r, self.find_module_mode) for r in direct_host.values()]

return ret

Expand Down
17 changes: 13 additions & 4 deletions conan/tools/cmake/cmakedeps/templates/targets.py
Expand Up @@ -13,21 +13,30 @@ class TargetsTemplate(CMakeDepsFileTemplate):

@property
def filename(self):
return "{}Targets.cmake".format(self.file_name)
name = "" if not self.find_module_mode else "module-"
name += self.file_name + "Targets.cmake"
return name

@property
def context(self):
data_pattern = "${_DIR}/" if not self.find_module_mode else "${_DIR}/module-"
data_pattern += "{}-*-data.cmake".format(self.file_name)

target_pattern = "" if not self.find_module_mode else "module-"
target_pattern += "{}-Target-*.cmake".format(self.file_name)

ret = {"pkg_name": self.pkg_name,
"target_namespace": self.target_namespace,
"file_name": self.file_name}
"data_pattern": data_pattern,
"target_pattern": target_pattern}
return ret

@property
def template(self):
return textwrap.dedent("""\
# Load the debug and release variables
get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
file(GLOB DATA_FILES "${_DIR}/{{ file_name }}-*-data.cmake")
file(GLOB DATA_FILES "{{data_pattern}}")

foreach(f ${DATA_FILES})
include(${f})
Expand All @@ -48,7 +57,7 @@ def template(self):

# Load the debug and release library finders
get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
file(GLOB CONFIG_FILES "${_DIR}/{{ file_name }}Target-*.cmake")
file(GLOB CONFIG_FILES "${_DIR}/{{ target_pattern }}")

foreach(f ${CONFIG_FILES})
include(${f})
Expand Down