Skip to content
54 changes: 44 additions & 10 deletions conan/tools/cmake/cmakeconfigdeps/target_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ def _requires(self, info, components):
continue
dep_target = self._cmakedeps.get_property("cmake_target_name", d)
dep_target = dep_target or f"{d.ref.name}::{d.ref.name}"
link_feature = self._cmakedeps.get_property("cmake_link_feature", d)
link = not (pkg_type is PackageType.SHARED and d.package_type is PackageType.SHARED)
result[dep_target] = link
result[dep_target] = {
"link": link,
"link_feature": link_feature
}
return result

for required_pkg, required_comp in requires:
Expand All @@ -70,7 +74,12 @@ def _requires(self, info, components):
dep_target = dep_target or f"{pkg_name}::{required_comp}"
link = not (pkg_type is PackageType.SHARED and
dep_comp.type is PackageType.SHARED)
result[dep_target] = link
link_feature = self._cmakedeps.get_property("cmake_link_feature", self._conanfile,
required_comp)
result[dep_target] = {
"link": link,
"link_feature": link_feature
}
else: # Different package
try:
dep = transitive_reqs[required_pkg]
Expand Down Expand Up @@ -102,8 +111,12 @@ def _requires(self, info, components):

dep_target = self._cmakedeps.get_property("cmake_target_name", dep, comp)
dep_target = dep_target or default_target
link_feature = self._cmakedeps.get_property("cmake_link_feature", dep, comp)

result[dep_target] = link
result[dep_target] = {
"link": link,
"link_feature": link_feature
}
return result

@property
Expand Down Expand Up @@ -195,6 +208,7 @@ def _get_cmake_lib(self, info, components, pkg_folder, pkg_folder_var, comp_name
comp_name=comp_name, check_type=list) or []
sources = [self._path(source, pkg_folder, pkg_folder_var) for source in info.sources]
target = {"type": "INTERFACE",
"comp_name": comp_name,
"includedirs": includedirs,
"defines": defines,
"requires": requires,
Expand Down Expand Up @@ -264,9 +278,19 @@ def _add_root_lib_target(self, libs, pkg_name, cpp_info):
target_name = self._cmakedeps.get_property("cmake_target_name", self._conanfile,
defaultc)
comp_name = target_name or f"{pkg_name}::{defaultc}"
all_requires[comp_name] = True # It is an interface, full link
link_feature = self._cmakedeps.get_property("cmake_link_feature", self._conanfile,
defaultc)
all_requires[comp_name] = {
"link": True, # It is an interface, full link
"link_feature": link_feature
}
else:
all_requires = {k: True for k in libs.keys()}
all_requires = {k: {
"link": True,
"link_feature": self._cmakedeps.get_property("cmake_link_feature", self._conanfile,
v.get("comp_name"))
}
for k, v in libs.items()}
# This target might have an alias, so we need to check it
cmake_target_aliases = self._get_aliases()
libs[root_target_name] = {"type": "INTERFACE",
Expand Down Expand Up @@ -407,16 +431,26 @@ def _template(self):

{% if lib_info.get("requires") %}
# Information of transitive dependencies
{% for require_target, link in lib_info["requires"].items() %}
# Requirement {{require_target}} => Full link: {{link}}
{% for require_target, link_info in lib_info["requires"].items() %}

{% if link %}
# Requirement {{lib}} -> {{require_target}} (Full link: {{link_info["link"]}})
{% if link_info["link"] %}
{% if link_info["link_feature"] %}
# Link feature: {{link_info["link_feature"]}}
if(CMAKE_VERSION VERSION_LESS "3.24")
message(FATAL_ERROR "The 'CMakeConfigDeps' generator LINK_FEATURE property only works with CMake >= 3.24")
endif()
{% endif %}
# set property allows to append, and lib_info[requires] will iterate
set_property(TARGET {{lib}} APPEND PROPERTY INTERFACE_LINK_LIBRARIES
{% if link_info["link_feature"] %}
"$<LINK_LIBRARY:{{link_info["link_feature"]}},{{config_wrapper(config, require_target)}}>")
Copy link
Member Author

Choose a reason for hiding this comment

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

This would need a version check?

Copy link
Member

Choose a reason for hiding this comment

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

what is the minimum CMake that supports this?

Copy link
Member Author

Choose a reason for hiding this comment

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

3.24 as per https://cmake.org/cmake/help/latest/variable/CMAKE_LINK_LIBRARY_USING_FEATURE.html, which is lower than CMakeConfigDeps so we're ok if I'm not mistaken?

{% else %}
"{{config_wrapper(config, require_target)}}")
{% endif %}
{% else %}
if(${CMAKE_VERSION} VERSION_LESS "3.27")
message(FATAL_ERROR "The 'CMakeToolchain' generator only works with CMake >= 3.27")
if(CMAKE_VERSION VERSION_LESS "3.27")
message(FATAL_ERROR "The 'CMakeConfigDeps' generator COMPILE_ONLY expression only works with CMake >= 3.27")
endif()
# If the headers trait is not there, this will do nothing
target_link_libraries({{lib}} INTERFACE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,28 @@ def test_transitive_headers(self, shared):
c.run(f"build . -o *:shared={shared} -c tools.cmake.cmakedeps:new={new_value}")
# it works

@pytest.mark.tool("cmake", "3.27")
def test_link_features(self):
tc = TestClient()
tc.run("new cmake_lib -d name=matrix -d version=0.1")
conanfile = tc.load("conanfile.py")
# So that the custom link feature works
conanfile += (" self.cpp_info.set_property('cmake_extra_variables', "
"{'CMAKE_LINK_LIBRARY_USING_MYFET_SUPPORTED': True,"
"'CMAKE_LINK_LIBRARY_USING_MYFET': '<LIBRARY>'})\n")
# And set this library to use the custom link feature
conanfile += " self.cpp_info.set_property('cmake_link_feature', 'MYFET')"
tc.save({"conanfile.py": conanfile})
tc.run(f"create -c tools.cmake.cmakedeps:new={new_value}")

tc.save({}, clean_first=True)
tc.run("new cmake_lib -d name=lib -d version=0.1 -d requires=matrix/0.1")
tc.run(f"create -c tools.cmake.cmakedeps:new={new_value}")
test_build_folder = tc.created_test_build_folder("lib/0.1")
test_generators_folder = os.path.join("test_package", test_build_folder, "generators")
libs_targets = tc.load(os.path.join(test_generators_folder, "lib-Targets-release.cmake"))
assert '"$<LINK_LIBRARY:MYFET,$<$<CONFIG:RELEASE>:matrix::matrix>>"' in libs_targets


@pytest.mark.tool("cmake")
class TestLibsComponents:
Expand Down
112 changes: 110 additions & 2 deletions test/integration/toolchains/cmake/cmakedeps2/test_cmakedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ def package_info(self):
targets = c.load("libtool-Targets-release.cmake")
# The libtool shouldn't depend on the automake::automake target
assert "automake::automake" not in targets
assert "# Requirement automake::mylibapp => Full link: True" in targets
assert "# Requirement libtool::libtool -> automake::mylibapp (Full link: True)" in targets
assert "$<$<CONFIG:RELEASE>:automake::mylibapp>" in targets

def test_requires_from_library_component(self):
Expand Down Expand Up @@ -656,4 +656,112 @@ def package_info(self):
client.run(f"install --requires=pkg/0.1 -g CMakeDeps -c tools.cmake.cmakedeps:new={new_value}")
target = client.load("pkg-Targets-release.cmake")
assert 'add_library(pkg::base INTERFACE IMPORTED)' in target
assert "# Requirement pkg::base => Full link: True" in target
assert "# Requirement pkg::comp -> pkg::base (Full link: True)" in target


class TestLinkFeatures:
def test_link_info_global_cpp_info(self):
tc = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
name = "pkg"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"

def package_info(self):
self.cpp_info.set_property("cmake_link_feature", "MYFET")
""")
tc.save({"conanfile.py": conanfile})
tc.run("create")

dep = textwrap.dedent("""
from conan import ConanFile
class Dep(ConanFile):
name = "dep"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"
requires = "pkg/1.0"
""")
tc.save({"conanfile.py": dep})
tc.run("create")
tc.run("install --requires=dep/1.0 -g CMakeConfigDeps")
# The requirement should propagate the link feature info
target = tc.load("dep-Targets-release.cmake")
assert "# Requirement dep::dep -> pkg::pkg (Full link: True)\n# Link feature: MYFET" in target

def test_link_info_local_component_from_interface(self):
tc = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
name = "pkg"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"

def package_info(self):
self.cpp_info.components["compA"].set_property("cmake_link_feature", "MYFET")
""")
tc.save({"conanfile.py": conanfile})
tc.run("create")
tc.run("install --requires=pkg/1.0 -g CMakeConfigDeps")
targets = tc.load("pkg-Targets-release.cmake")
# The interface library created as a global target should have the requirement
assert "# Requirement pkg::pkg -> pkg::compA (Full link: True)\n# Link feature: MYFET" in targets

def test_link_info_local_component_to_component_require(self):
tc = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
name = "pkg"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"

def package_info(self):
self.cpp_info.components["compA"].set_property("cmake_link_feature", "MYFET")
self.cpp_info.components["compB"].requires = ["compA"]
""")
tc.save({"conanfile.py": conanfile})
tc.run("create")
tc.run("install --requires=pkg/1.0 -g CMakeConfigDeps")
targets = tc.load("pkg-Targets-release.cmake")
# The component requirement should have the link feature info
assert "# Requirement pkg::compB -> pkg::compA (Full link: True)\n# Link feature: MYFET" in targets

def test_link_info_lib_to_component_require(self):
tc = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
name = "pkg"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"

def package_info(self):
self.cpp_info.components["compA"].set_property("cmake_link_feature", "MYFET")
""")
tc.save({"conanfile.py": conanfile})
tc.run("create")

dep = textwrap.dedent("""
from conan import ConanFile
class Dep(ConanFile):
name = "dep"
version = "1.0"
settings = "os", "compiler", "build_type", "arch"
requires = "pkg/1.0"

def package_info(self):
self.cpp_info.requires = ["pkg::compA"]
""")
tc.save({"conanfile.py": dep})
tc.run("create")
tc.run("install --requires=dep/1.0 -g CMakeConfigDeps")
targets = tc.load("dep-Targets-release.cmake")
# The requirement should have the link feature info
assert "# Requirement dep::dep -> pkg::compA (Full link: True)\n# Link feature: MYFET" in targets
Loading