From aa8b1139459054ec24ca44769426f455890466c6 Mon Sep 17 00:00:00 2001 From: Carlos Zoido Date: Tue, 29 Nov 2022 14:57:03 +0100 Subject: [PATCH] Set some CMakeDeps properties from consumer (#12609) * draft * change arg order * fix name * minor changes * minor changes * change dict syntax * fix get * wip * remove or * restore or * invalidate upstream * change syntax * change suffix --- conan/tools/cmake/cmakedeps/cmakedeps.py | 41 +++- .../cmake/cmakedeps/templates/__init__.py | 12 +- .../cmake/cmakedeps/templates/target_data.py | 10 +- conan/tools/cmake/utils.py | 24 --- conans/client/generators/markdown.py | 6 +- .../cmake/cmakedeps/test_cmakedeps.py | 199 ++++++++++++++++++ 6 files changed, 252 insertions(+), 40 deletions(-) diff --git a/conan/tools/cmake/cmakedeps/cmakedeps.py b/conan/tools/cmake/cmakedeps/cmakedeps.py index 0306575cbb9..75751841c2c 100644 --- a/conan/tools/cmake/cmakedeps/cmakedeps.py +++ b/conan/tools/cmake/cmakedeps/cmakedeps.py @@ -32,6 +32,7 @@ def __init__(self, conanfile): # Enable/Disable checking if a component target exists or not self.check_components_exist = False + self._properties = {} def generate(self): # FIXME: Remove this in 2.0 @@ -76,7 +77,7 @@ def content(self): if dep.is_build_context and dep.ref.name not in self.build_context_activated: continue - cmake_find_mode = dep.cpp_info.get_property("cmake_find_mode") + 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() # Skip from the requirement @@ -112,3 +113,41 @@ def _generate_files(self, require, dep, ret, find_module_mode): # file is common for the different configurations. if not os.path.exists(config.filename): ret[config.filename] = config.render() + + def set_property(self, dep, prop, value, build_context=False): + 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): + dep_name = dep.ref.name + build_suffix = "&build" if str( + dep_name) in self.build_context_activated and dep.context == "build" else "" + dep_comp = f"{str(dep_name)}::{comp_name}" if comp_name else f"{str(dep_name)}" + try: + return self._properties[f"{dep_comp}{build_suffix}"][prop] + except KeyError: + return dep.cpp_info.get_property(prop) if not comp_name else dep.cpp_info.components[ + comp_name].get_property(prop) + + def get_cmake_package_name(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() diff --git a/conan/tools/cmake/cmakedeps/templates/__init__.py b/conan/tools/cmake/cmakedeps/templates/__init__.py index 05415e983c1..7e11bd8618a 100644 --- a/conan/tools/cmake/cmakedeps/templates/__init__.py +++ b/conan/tools/cmake/cmakedeps/templates/__init__.py @@ -1,7 +1,6 @@ import jinja2 from jinja2 import Template -from conan.tools.cmake.utils import get_cmake_package_name from conans.errors import ConanException @@ -23,7 +22,7 @@ def root_target_name(self): @property def file_name(self): - return get_cmake_package_name(self.conanfile, module_mode=self.generating_module) + self.suffix + return self.cmakedeps.get_cmake_package_name(self.conanfile, module_mode=self.generating_module) + self.suffix @property def suffix(self): @@ -85,10 +84,10 @@ def _get_target_default_name(req, component_name="", suffix=""): def get_root_target_name(self, req, suffix=""): if self.generating_module: - ret = req.cpp_info.get_property("cmake_module_target_name") + ret = self.cmakedeps.get_property("cmake_module_target_name", req) if ret: return ret - ret = req.cpp_info.get_property("cmake_target_name") + ret = self.cmakedeps.get_property("cmake_target_name", req) return ret or self._get_target_default_name(req, suffix=suffix) def get_component_alias(self, req, comp_name): @@ -99,10 +98,11 @@ def get_component_alias(self, req, comp_name): raise ConanException("Component '{name}::{cname}' not found in '{name}' " "package requirement".format(name=req.ref.name, cname=comp_name)) if self.generating_module: - ret = req.cpp_info.components[comp_name].get_property("cmake_module_target_name") + ret = self.cmakedeps.get_property("cmake_module_target_name", req, comp_name=comp_name) if ret: return ret - ret = req.cpp_info.components[comp_name].get_property("cmake_target_name") + ret = self.cmakedeps.get_property("cmake_target_name", req, comp_name=comp_name) + # If we don't specify the `cmake_target_name` property for the component it will # fallback to the pkg_name::comp_name, it wont use the root cpp_info cmake_target_name # property because that is also an absolute name (Greetings::Greetings), it is not a namespace diff --git a/conan/tools/cmake/cmakedeps/templates/target_data.py b/conan/tools/cmake/cmakedeps/templates/target_data.py index f2285859292..61d249de3f7 100644 --- a/conan/tools/cmake/cmakedeps/templates/target_data.py +++ b/conan/tools/cmake/cmakedeps/templates/target_data.py @@ -4,7 +4,7 @@ from conan.tools.cmake.cmakedeps import FIND_MODE_NONE, FIND_MODE_CONFIG, FIND_MODE_MODULE, \ FIND_MODE_BOTH from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate -from conan.tools.cmake.utils import get_cmake_package_name, get_find_mode + """ foo-release-x86_64-data.cmake @@ -189,9 +189,9 @@ def _get_dependency_filenames(self): for dep_name, _ in self.conanfile.cpp_info.required_components: if dep_name and dep_name not in ret: # External dep req = direct_host[dep_name] - ret.append(get_cmake_package_name(req)) + ret.append(self.cmakedeps.get_cmake_package_name(req)) elif direct_host: - ret = [get_cmake_package_name(r, self.generating_module) for r in direct_host.values()] + ret = [self.cmakedeps.get_cmake_package_name(r, self.generating_module) for r in direct_host.values()] return ret @@ -201,8 +201,8 @@ def _get_dependencies_find_modes(self): return ret deps = self.conanfile.dependencies.filter({"build": False, "visible": True, "direct": True}) for dep in deps.values(): - dep_file_name = get_cmake_package_name(dep, self.generating_module) - find_mode = get_find_mode(dep) + dep_file_name = self.cmakedeps.get_cmake_package_name(dep, self.generating_module) + find_mode = self.cmakedeps.get_find_mode(dep) default_value = "NO_MODULE" if not self.generating_module else "MODULE" values = { FIND_MODE_NONE: "", diff --git a/conan/tools/cmake/utils.py b/conan/tools/cmake/utils.py index cceea99bfe9..0a99e28b900 100644 --- a/conan/tools/cmake/utils.py +++ b/conan/tools/cmake/utils.py @@ -6,27 +6,3 @@ def is_multi_configuration(generator): return False return "Visual" in generator or "Xcode" in generator or "Multi-Config" in generator - -def get_cmake_package_name(conanfile, module_mode=None): - """Get the name of the file for the find_package(XXX)""" - # This is used by CMakeToolchain/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 get_find_mode(conanfile) in [FIND_MODE_MODULE, FIND_MODE_BOTH]: - ret = conanfile.cpp_info.get_property("cmake_module_file_name") - if ret: - return ret - ret = conanfile.cpp_info.get_property("cmake_file_name") - return ret or conanfile.ref.name - - -def get_find_mode(conanfile): - """ - :param conanfile: conanfile of the requirement - :return: "none" or "config" or "module" or "both" or "config" when not set - """ - tmp = conanfile.cpp_info.get_property("cmake_find_mode") - if tmp is None: - return "config" - return tmp.lower() diff --git a/conans/client/generators/markdown.py b/conans/client/generators/markdown.py index 1776b11d0d1..f855662bebb 100644 --- a/conans/client/generators/markdown.py +++ b/conans/client/generators/markdown.py @@ -6,9 +6,7 @@ from jinja2 import Environment from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps -from conan.tools.cmake.cmakedeps.templates import ( - CMakeDepsFileTemplate, - get_cmake_package_name as cmake_get_file_name) +from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate from conan.tools.gnu.pkgconfigdeps import ( _get_component_name as pkgconfig_get_component_name, _get_name_with_namespace as pkgconfig_get_name_with_namespace, @@ -444,7 +442,7 @@ def read_pkg_file(filename): cmake_variables = { 'global_target_name': requirement.cpp_info.get_property('cmake_target_name') or "{0}::{0}".format(requirement.ref.name), 'component_alias': cmake_component_alias, - 'file_name': cmake_get_file_name(requirement) + 'file_name': cmake_deps.get_cmake_package_name(requirement) } pkgconfig_component_alias = { diff --git a/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py b/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py index 5e147de5182..15d07a2e95f 100644 --- a/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py +++ b/conans/test/integration/toolchains/cmake/cmakedeps/test_cmakedeps.py @@ -187,3 +187,202 @@ def package_info(self): assert r"spaces=me you" in deps assert r"foobar=bazbuz" in deps assert r"answer=42" in deps + + +def test_dependency_props_from_consumer(): + client = TestClient(path_with_spaces=False) + bar = textwrap.dedent(r''' + from conan import ConanFile + class FooConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + def package_info(self): + self.cpp_info.set_property("cmake_find_mode", "both") + self.cpp_info.components["component1"].requires = [] + ''') + + foo = textwrap.dedent(r''' + from conan import ConanFile + from conan.tools.cmake import CMakeDeps, cmake_layout + class FooConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + requires = "bar/1.0" + def layout(self): + cmake_layout(self) + def generate(self): + deps = CMakeDeps(self) + {set_find_mode} + deps.set_property("bar", "cmake_file_name", "custom_bar_file_name") + deps.set_property("bar", "cmake_module_file_name", "custom_bar_module_file_name") + deps.set_property("bar", "cmake_target_name", "custom_bar_target_name") + deps.set_property("bar", "cmake_module_target_name", "custom_bar_module_target_name") + deps.set_property("bar::component1", "cmake_target_name", "custom_bar_component_target_name") + deps.generate() + ''') + + set_find_mode = """ + deps.set_property("bar", "cmake_find_mode", {find_mode}) + """ + + client.save({"foo.py": foo.format(set_find_mode=""), "bar.py": bar}, clean_first=True) + + module_file = os.path.join(client.current_folder, "build", "generators", "module-custom_bar_module_file_nameTargets.cmake") + components_module = os.path.join(client.current_folder, "build", "generators", "custom_bar_file_name-Target-release.cmake") + config_file = os.path.join(client.current_folder, "build", "generators", "custom_bar_file_nameTargets.cmake") + + # uses cmake_find_mode set in bar: both + client.run("create bar.py bar/1.0@") + client.run("install foo.py foo/1.0@") + assert os.path.exists(module_file) + assert os.path.exists(config_file) + module_content = client.load(module_file) + assert "add_library(custom_bar_module_target_name INTERFACE IMPORTED)" in module_content + config_content = client.load(config_file) + assert "add_library(custom_bar_target_name INTERFACE IMPORTED)" in config_content + components_module_content = client.load(components_module) + assert "add_library(bar_custom_bar_component_target_name_DEPS_TARGET INTERFACE IMPORTED)" in components_module_content + + client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'none'")), + "bar.py": bar}, clean_first=True) + client.run("create bar.py bar/1.0@") + client.run("install foo.py foo/1.0@") + assert not os.path.exists(module_file) + assert not os.path.exists(config_file) + + client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'module'")), + "bar.py": bar}, clean_first=True) + client.run("create bar.py bar/1.0@") + client.run("install foo.py foo/1.0@") + assert os.path.exists(module_file) + assert not os.path.exists(config_file) + + client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'config'")), + "bar.py": bar}, clean_first=True) + client.run("create bar.py bar/1.0@") + client.run("install foo.py foo/1.0@") + assert not os.path.exists(module_file) + assert os.path.exists(config_file) + + +def test_props_from_consumer_build_context_activated(): + client = TestClient(path_with_spaces=False) + bar = textwrap.dedent(r''' + from conan import ConanFile + class FooConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + def package_info(self): + self.cpp_info.set_property("cmake_find_mode", "both") + self.cpp_info.components["component1"].requires = [] + ''') + + foo = textwrap.dedent(r''' + from conan import ConanFile + from conan.tools.cmake import CMakeDeps, cmake_layout + class FooConan(ConanFile): + settings = "os", "compiler", "build_type", "arch" + requires = "bar/1.0" + tool_requires = "bar/1.0" + def layout(self): + cmake_layout(self) + def generate(self): + deps = CMakeDeps(self) + deps.build_context_activated = ["bar"] + deps.build_context_suffix = {{"bar": "_BUILD"}} + {set_find_mode} + + deps.set_property("bar", "cmake_file_name", "custom_bar_file_name") + deps.set_property("bar", "cmake_module_file_name", "custom_bar_module_file_name") + deps.set_property("bar", "cmake_target_name", "custom_bar_target_name") + deps.set_property("bar", "cmake_module_target_name", "custom_bar_module_target_name") + deps.set_property("bar::component1", "cmake_target_name", "custom_bar_component_target_name") + + deps.set_property("bar", "cmake_file_name", "custom_bar_build_file_name", build_context=True) + deps.set_property("bar", "cmake_module_file_name", "custom_bar_build_module_file_name", build_context=True) + deps.set_property("bar", "cmake_target_name", "custom_bar_build_target_name", build_context=True) + deps.set_property("bar", "cmake_module_target_name", "custom_bar_build_module_target_name", build_context=True) + deps.set_property("bar::component1", "cmake_target_name", "custom_bar_build_component_target_name", build_context=True) + + deps.generate() + ''') + + set_find_mode = """ + deps.set_property("bar", "cmake_find_mode", {find_mode}) + deps.set_property("bar", "cmake_find_mode", {find_mode}, build_context=True) + """ + + client.save({"foo.py": foo.format(set_find_mode=""), "bar.py": bar}, clean_first=True) + + module_file = os.path.join(client.current_folder, "build", "generators", + "module-custom_bar_module_file_nameTargets.cmake") + components_module = os.path.join(client.current_folder, "build", "generators", + "custom_bar_file_name-Target-release.cmake") + config_file = os.path.join(client.current_folder, "build", "generators", + "custom_bar_file_nameTargets.cmake") + + module_file_build = os.path.join(client.current_folder, "build", "generators", + "module-custom_bar_build_module_file_name_BUILDTargets.cmake") + components_module_build = os.path.join(client.current_folder, "build", "generators", + "custom_bar_build_file_name_BUILD-Target-release.cmake") + config_file_build = os.path.join(client.current_folder, "build", "generators", + "custom_bar_build_file_name_BUILDTargets.cmake") + + # uses cmake_find_mode set in bar: both + client.run("create bar.py bar/1.0@ -pr:h=default -pr:b=default") + client.run("install foo.py foo/1.0@ -pr:h=default -pr:b=default") + assert os.path.exists(module_file) + assert os.path.exists(config_file) + assert os.path.exists(module_file_build) + assert os.path.exists(config_file_build) + + module_content = client.load(module_file) + assert "add_library(custom_bar_module_target_name INTERFACE IMPORTED)" in module_content + config_content = client.load(config_file) + assert "add_library(custom_bar_target_name INTERFACE IMPORTED)" in config_content + + module_content_build = client.load(module_file_build) + assert "add_library(custom_bar_build_module_target_name INTERFACE IMPORTED)" in module_content_build + config_content_build = client.load(config_file_build) + assert "add_library(custom_bar_build_target_name INTERFACE IMPORTED)" in config_content_build + + components_module_content = client.load(components_module) + assert "add_library(bar_custom_bar_component_target_name_DEPS_TARGET INTERFACE IMPORTED)" in components_module_content + + components_module_content_build = client.load(components_module_build) + assert "add_library(bar_BUILD_custom_bar_build_component_target_name_DEPS_TARGET INTERFACE IMPORTED)" in components_module_content_build + + client.save( + {"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'none'")), "bar.py": bar}, + clean_first=True) + client.run("create bar.py bar/1.0@ -pr:h=default -pr:b=default") + client.run("install foo.py foo/1.0@ -pr:h=default -pr:b=default") + assert not os.path.exists(module_file) + assert not os.path.exists(config_file) + assert not os.path.exists(module_file_build) + assert not os.path.exists(config_file_build) + + client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'module'")), + "bar.py": bar}, clean_first=True) + client.run("create bar.py bar/1.0@ -pr:h=default -pr:b=default") + client.run("install foo.py foo/1.0@ -pr:h=default -pr:b=default") + assert os.path.exists(module_file) + assert not os.path.exists(config_file) + assert os.path.exists(module_file_build) + assert not os.path.exists(config_file_build) + + client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="'config'")), + "bar.py": bar}, clean_first=True) + client.run("create bar.py bar/1.0@ -pr:h=default -pr:b=default") + client.run("install foo.py foo/1.0@ -pr:h=default -pr:b=default") + assert not os.path.exists(module_file) + assert os.path.exists(config_file) + assert not os.path.exists(module_file_build) + assert os.path.exists(config_file_build) + + # invalidate upstream property setting a None, will use config that's the default + client.save({"foo.py": foo.format(set_find_mode=set_find_mode.format(find_mode="None")), + "bar.py": bar}, clean_first=True) + client.run("create bar.py bar/1.0@ -pr:h=default -pr:b=default") + client.run("install foo.py foo/1.0@ -pr:h=default -pr:b=default") + assert not os.path.exists(module_file) + assert os.path.exists(config_file) + assert not os.path.exists(module_file_build) + assert os.path.exists(config_file_build)