From 626b50f58efe9c1c0fe831e15afa86cdb3c2db8c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 27 Jan 2025 14:02:19 +0100 Subject: [PATCH] fix incubating CMakeDeps replace_requires (#17566) * fix incubating CMakeDeps replace_requires * fix tests * possible fix * add PkgConfigDeps check * Fixed bug using the replace_requires mechanism in PkgConfigDeps. Some variables renames (clearer names) * More tests * Minor changes --------- Co-authored-by: Francisco Ramirez de Anton --- conan/internal/model/cpp_info.py | 2 +- conan/internal/model/dependencies.py | 8 +- conan/tools/cmake/__init__.py | 2 +- .../cmake/cmakedeps2/target_configuration.py | 4 +- conan/tools/gnu/pkgconfigdeps.py | 25 +- .../graph/test_replace_requires.py | 312 ++++++++++++++++++ 6 files changed, 338 insertions(+), 15 deletions(-) diff --git a/conan/internal/model/cpp_info.py b/conan/internal/model/cpp_info.py index af66f87e6c6..05f9af4c2a4 100644 --- a/conan/internal/model/cpp_info.py +++ b/conan/internal/model/cpp_info.py @@ -822,7 +822,7 @@ def deduce_full_cpp_info(self, conanfile): 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.type = self.type c.includedirs = self.includedirs c.libdirs = self.libdirs c.bindirs = self.bindirs diff --git a/conan/internal/model/dependencies.py b/conan/internal/model/dependencies.py index a1b0d3882e6..22353419eab 100644 --- a/conan/internal/model/dependencies.py +++ b/conan/internal/model/dependencies.py @@ -130,7 +130,7 @@ def transitive_requires(self, other): for k, v in self._data.items(): for otherk, otherv in other._data.items(): if v == otherv: - data[k] = v + data[otherk] = v # Use otherk to respect original replace_requires return ConanFileDependencies(data) @property @@ -182,6 +182,8 @@ def get_transitive_requires(consumer, dependency): # The build dependencies cannot be transitive in generators like CMakeDeps, # even if users make them visible pkg_deps = dependency.dependencies.filter({"direct": True, "build": False}) - result = consumer.dependencies.transitive_requires(pkg_deps) - result = result.filter({"skip": False}) + # First we filter the skipped dependencies + result = consumer.dependencies.filter({"skip": False}) + # and we keep those that are really dependencies of the current package + result = result.transitive_requires(pkg_deps) return result diff --git a/conan/tools/cmake/__init__.py b/conan/tools/cmake/__init__.py index 73c8a835b08..e57f14a6587 100644 --- a/conan/tools/cmake/__init__.py +++ b/conan/tools/cmake/__init__.py @@ -7,7 +7,7 @@ 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" + "'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 diff --git a/conan/tools/cmake/cmakedeps2/target_configuration.py b/conan/tools/cmake/cmakedeps2/target_configuration.py index 0cb24590de9..be3967ffdc0 100644 --- a/conan/tools/cmake/cmakedeps2/target_configuration.py +++ b/conan/tools/cmake/cmakedeps2/target_configuration.py @@ -67,10 +67,12 @@ def _requires(self, info, components): # It must be the interface pkgname::pkgname target assert required_pkg == required_comp comp = None + default_target = f"{dep.ref.name}::{dep.ref.name}" # replace_requires else: comp = required_comp + default_target = f"{required_pkg}::{required_comp}" dep_target = self._cmakedeps.get_property("cmake_target_name", dep, comp) - dep_target = dep_target or f"{required_pkg}::{required_comp}" + dep_target = dep_target or default_target result.append(dep_target) return result diff --git a/conan/tools/gnu/pkgconfigdeps.py b/conan/tools/gnu/pkgconfigdeps.py index 5ef67d55db0..205b2d8728e 100644 --- a/conan/tools/gnu/pkgconfigdeps.py +++ b/conan/tools/gnu/pkgconfigdeps.py @@ -182,7 +182,7 @@ def package_info(self): continue # If the dependency is not in the transitive, might be skipped else: # For instance, dep == "hello/1.0" and req == "hello::cmp1" -> hello == hello req_conanfile = self._dep - comp_name = self._get_component_name(req_conanfile, comp_ref_name) + comp_name = self._get_component_name(req_conanfile, pkg_ref_name, comp_ref_name) if not comp_name: pkg_name = self._get_package_name(req_conanfile) # Creating a component name with namespace, e.g., dep-comp1 @@ -203,13 +203,13 @@ def _components_info(self): for comp_ref_name, cpp_info in self._dep.cpp_info.get_sorted_components().items(): # At first, let's check if we have defined some components requires, e.g., "dep::cmp1" comp_requires_names = self._get_cpp_info_requires_names(cpp_info) - comp_name = self._get_component_name(self._dep, comp_ref_name) + comp_name = self._get_component_name(self._dep, pkg_name, comp_ref_name) if not comp_name: comp_name = self._get_name_with_namespace(pkg_name, comp_ref_name) comp_description = f"Conan component: {comp_name}" else: comp_description = f"Conan component: {pkg_name}-{comp_name}" - comp_aliases = self._get_component_aliases(self._dep, comp_ref_name) + comp_aliases = self._get_component_aliases(self._dep, pkg_name, comp_ref_name) comp_version = (self.get_property("component_version", self._dep, comp_ref_name) or self.get_property("system_package_version", self._dep, comp_ref_name) or self._dep.ref.version) @@ -314,10 +314,14 @@ def _get_package_aliases(self, dep): pkg_aliases = self.get_property("pkg_config_aliases", dep, check_type=list) return pkg_aliases or [] - def _get_component_aliases(self, dep, comp_name): + def _get_component_aliases(self, dep, pkg_name, comp_name): + # TODO: LET'S DEPRECATE ALL THE ALIASES MECHANISM!! if comp_name not in dep.cpp_info.components: - # foo::foo might be referencing the root cppinfo - if dep.ref.name == comp_name: + # Either foo::foo might be referencing the root cpp_info + if (dep.ref.name == comp_name or + # Or a "replace_require" is used and cpp_info.requires is the root one, e.g., + # zlib/*: zlib-ng/*, and self.cpp_info.requires = ["zlib::zlib"] + (dep.ref.name != pkg_name and pkg_name == comp_name)): return self._get_package_aliases(dep) raise ConanException("Component '{name}::{cname}' not found in '{name}' " "package requirement".format(name=dep.ref.name, @@ -329,10 +333,13 @@ def _get_package_name(self, dep): pkg_name = self.get_property("pkg_config_name", dep) or dep.ref.name return f"{pkg_name}{self._suffix}" - def _get_component_name(self, dep, comp_name): + def _get_component_name(self, dep, pkg_name, comp_name): if comp_name not in dep.cpp_info.components: - # foo::foo might be referencing the root cppinfo - if dep.ref.name == comp_name: + # Either foo::foo might be referencing the root cpp_info + if (dep.ref.name == comp_name or + # Or a "replace_require" is used and cpp_info.requires is the root one, e.g., + # zlib/*: zlib-ng/*, and self.cpp_info.requires = ["zlib::zlib"] + (dep.ref.name != pkg_name and pkg_name == comp_name)): return self._get_package_name(dep) raise ConanException("Component '{name}::{cname}' not found in '{name}' " "package requirement".format(name=dep.ref.name, diff --git a/test/integration/graph/test_replace_requires.py b/test/integration/graph/test_replace_requires.py index c27d729c5c3..dbde54b949e 100644 --- a/test/integration/graph/test_replace_requires.py +++ b/test/integration/graph/test_replace_requires.py @@ -375,3 +375,315 @@ def package_info(self): # There are actually 2 dependencies, pointing to the same node assert "libepoxy/0.1: DEP: opengl: libgl" in c.out assert "libepoxy/0.1: DEP: egl: libgl" in c.out + + +class TestReplaceRequiresTransitiveGenerators: + """ Generators are incorrectly managing replace_requires + # https://github.com/conan-io/conan/issues/17557 + """ + + @pytest.mark.parametrize("diamond", [True, False]) + def test_no_components(self, diamond): + c = TestClient() + zlib_ng = textwrap.dedent(""" + from conan import ConanFile + class ZlibNG(ConanFile): + name = "zlib-ng" + version = "0.1" + package_type = "static-library" + def package_info(self): + self.cpp_info.libs = ["zlib"] + self.cpp_info.type = "static-library" + self.cpp_info.location = "lib/zlib.lib" + self.cpp_info.set_property("cmake_file_name", "ZLIB") + self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") + self.cpp_info.set_property("pkg_config_name", "ZLIB") + """) + openssl = textwrap.dedent(""" + from conan import ConanFile + class openssl(ConanFile): + name = "openssl" + version = "0.1" + package_type = "static-library" + requires = "zlib/0.1" + def package_info(self): + self.cpp_info.libs = ["crypto"] + self.cpp_info.type = "static-library" + self.cpp_info.location = "lib/crypto.lib" + self.cpp_info.requires = ["zlib::zlib"] + """) + zlib = '"zlib/0.1"' if diamond else "" + conanfile = textwrap.dedent(f""" + from conan import ConanFile + class App(ConanFile): + name = "app" + version = "0.1" + settings = "build_type" + requires = "openssl/0.1", {zlib} + package_type = "application" + generators = "CMakeDeps", "PkgConfigDeps" + """) + profile = textwrap.dedent(""" + [settings] + build_type = Release + + [replace_requires] + zlib/0.1: zlib-ng/0.1 + """) + c.save({"zlibng/conanfile.py": zlib_ng, + "openssl/conanfile.py": openssl, + "app/conanfile.py": conanfile, + "profile": profile}) + + c.run("create zlibng") + c.run("create openssl -pr=profile") + c.run("install app -pr=profile -c tools.cmake.cmakedeps:new=will_break_next") + assert "zlib/0.1: zlib-ng/0.1" in c.out + + pc_content = c.load("app/ZLIB.pc") + assert 'Libs: -L"${libdir}" -lzlib' in pc_content + pc_content = c.load("app/openssl.pc") + assert 'Requires: ZLIB' in pc_content + + cmake = c.load("app/ZLIB-Targets-release.cmake") + assert "add_library(ZLIB::ZLIB STATIC IMPORTED)" in cmake + + cmake = c.load("app/openssl-Targets-release.cmake") + assert "find_dependency(ZLIB REQUIRED CONFIG)" in cmake + assert "add_library(openssl::openssl STATIC IMPORTED)" in cmake + assert "set_target_properties(openssl::openssl PROPERTIES INTERFACE_LINK_LIBRARIES\n" \ + ' "$<$:ZLIB::ZLIB>")' in cmake + + @pytest.mark.parametrize("diamond", [True, False]) + def test_openssl_components(self, diamond): + c = TestClient() + zlib_ng = textwrap.dedent(""" + from conan import ConanFile + class ZlibNG(ConanFile): + name = "zlib-ng" + version = "0.1" + package_type = "static-library" + def package_info(self): + self.cpp_info.libs = ["zlib"] + self.cpp_info.type = "static-library" + self.cpp_info.location = "lib/zlib.lib" + self.cpp_info.set_property("cmake_file_name", "ZLIB") + self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") + self.cpp_info.set_property("pkg_config_name", "ZLIB") + """) + openssl = textwrap.dedent(""" + from conan import ConanFile + class openssl(ConanFile): + name = "openssl" + version = "0.1" + package_type = "static-library" + requires = "zlib/0.1" + def package_info(self): + self.cpp_info.components["crypto"].libs = ["crypto"] + self.cpp_info.components["crypto"].type = "static-library" + self.cpp_info.components["crypto"].location = "lib/crypto.lib" + self.cpp_info.components["crypto"].requires = ["zlib::zlib"] + """) + zlib = '"zlib/0.1"' if diamond else "" + conanfile = textwrap.dedent(f""" + from conan import ConanFile + class App(ConanFile): + name = "app" + version = "0.1" + settings = "build_type" + requires = "openssl/0.1", {zlib} + package_type = "application" + generators = "CMakeDeps", "PkgConfigDeps" + """) + profile = textwrap.dedent(""" + [settings] + build_type = Release + + [replace_requires] + zlib/0.1: zlib-ng/0.1 + """) + c.save({"zlibng/conanfile.py": zlib_ng, + "openssl/conanfile.py": openssl, + "app/conanfile.py": conanfile, + "profile": profile}) + + c.run("create zlibng") + c.run("create openssl -pr=profile") + c.run("install app -pr=profile -c tools.cmake.cmakedeps:new=will_break_next") + assert "zlib/0.1: zlib-ng/0.1" in c.out + + pc_content = c.load("app/ZLIB.pc") + assert 'Libs: -L"${libdir}" -lzlib' in pc_content + pc_content = c.load("app/openssl-crypto.pc") + assert 'Requires: ZLIB' in pc_content + + cmake = c.load("app/ZLIB-Targets-release.cmake") + assert "add_library(ZLIB::ZLIB STATIC IMPORTED)" in cmake + + cmake = c.load("app/openssl-Targets-release.cmake") + assert "find_dependency(ZLIB REQUIRED CONFIG)" in cmake + assert "add_library(openssl::crypto STATIC IMPORTED)" in cmake + assert "set_target_properties(openssl::crypto PROPERTIES INTERFACE_LINK_LIBRARIES\n" \ + ' "$<$:ZLIB::ZLIB>")' in cmake + + @pytest.mark.parametrize("diamond", [True, False]) + @pytest.mark.parametrize("explicit_requires", [True, False]) + def test_zlib_components(self, diamond, explicit_requires): + c = TestClient() + zlib_ng = textwrap.dedent(""" + from conan import ConanFile + class ZlibNG(ConanFile): + name = "zlib-ng" + version = "0.1" + package_type = "static-library" + def package_info(self): + self.cpp_info.components["myzlib"].libs = ["zlib"] + self.cpp_info.components["myzlib"].type = "static-library" + self.cpp_info.components["myzlib"].location = "lib/zlib.lib" + self.cpp_info.set_property("cmake_file_name", "ZLIB") + self.cpp_info.components["myzlib"].set_property("pkg_config_name", "ZLIB") + self.cpp_info.components["myzlib"].set_property("cmake_target_name", + "ZLIB::ZLIB") + """) + openssl = textwrap.dedent(f""" + from conan import ConanFile + class openssl(ConanFile): + name = "openssl" + version = "0.1" + package_type = "static-library" + requires = "zlib/0.1" + def package_info(self): + self.cpp_info.libs = ["crypto"] + self.cpp_info.type = "static-library" + self.cpp_info.location = "lib/crypto.lib" + if {explicit_requires}: + self.cpp_info.requires = ["zlib::zlib"] + """) + zlib = '"zlib/0.1"' if diamond else "" + conanfile = textwrap.dedent(f""" + from conan import ConanFile + class App(ConanFile): + name = "app" + version = "0.1" + settings = "build_type" + requires = "openssl/0.1", {zlib} + package_type = "application" + generators = "CMakeDeps", "PkgConfigDeps" + """) + profile = textwrap.dedent(""" + [settings] + build_type = Release + + [replace_requires] + zlib/0.1: zlib-ng/0.1 + """) + c.save({"zlibng/conanfile.py": zlib_ng, + "openssl/conanfile.py": openssl, + "app/conanfile.py": conanfile, + "profile": profile}) + + c.run("create zlibng") + c.run("create openssl -pr=profile") + c.run("install app -pr=profile -c tools.cmake.cmakedeps:new=will_break_next") + assert "zlib/0.1: zlib-ng/0.1" in c.out + + pc_content = c.load("app/zlib-ng.pc") + assert 'Requires: ZLIB' in pc_content + pc_content = c.load("app/ZLIB.pc") + assert 'Libs: -L"${libdir}" -lzlib' in pc_content + pc_content = c.load("app/openssl.pc") + assert 'Requires: zlib-ng' in pc_content + + cmake = c.load("app/ZLIB-Targets-release.cmake") + assert "add_library(ZLIB::ZLIB STATIC IMPORTED)" in cmake + + cmake = c.load("app/openssl-Targets-release.cmake") + assert "find_dependency(ZLIB REQUIRED CONFIG)" in cmake + assert "add_library(openssl::openssl STATIC IMPORTED)" in cmake + # It should access the generic zlib-ng target + assert "set_target_properties(openssl::openssl PROPERTIES INTERFACE_LINK_LIBRARIES\n" \ + ' "$<$:zlib-ng::zlib-ng>")' in cmake + + @pytest.mark.parametrize("diamond", [True, False]) + @pytest.mark.parametrize("package_requires", [False, True]) + def test_both_components(self, diamond, package_requires): + c = TestClient() + zlib_ng = textwrap.dedent(""" + from conan import ConanFile + class ZlibNG(ConanFile): + name = "zlib-ng" + version = "0.1" + package_type = "static-library" + def package_info(self): + self.cpp_info.components["myzlib"].libs = ["zlib"] + self.cpp_info.components["myzlib"].type = "static-library" + self.cpp_info.components["myzlib"].location = "lib/zlib.lib" + self.cpp_info.set_property("cmake_file_name", "ZLIB") + self.cpp_info.components["myzlib"].set_property("pkg_config_name", "ZLIB") + self.cpp_info.components["myzlib"].set_property("cmake_target_name", + "ZLIB::ZLIB") + """) + openssl = textwrap.dedent(f""" + from conan import ConanFile + class openssl(ConanFile): + name = "openssl" + version = "0.1" + package_type = "static-library" + requires = "zlib/0.1" + def package_info(self): + self.cpp_info.components["crypto"].libs = ["crypto"] + self.cpp_info.components["crypto"].type = "static-library" + self.cpp_info.components["crypto"].location = "lib/crypto.lib" + if {package_requires}: + self.cpp_info.components["crypto"].requires = ["zlib::zlib"] + else: + self.cpp_info.components["crypto"].requires = ["zlib::myzlib"] + """) + zlib = '"zlib/0.1"' if diamond else "" + conanfile = textwrap.dedent(f""" + from conan import ConanFile + class App(ConanFile): + name = "app" + version = "0.1" + settings = "build_type" + requires = "openssl/0.1", {zlib} + package_type = "application" + generators = "CMakeDeps", "PkgConfigDeps" + """) + profile = textwrap.dedent(""" + [settings] + build_type = Release + + [replace_requires] + zlib/0.1: zlib-ng/0.1 + """) + c.save({"zlibng/conanfile.py": zlib_ng, + "openssl/conanfile.py": openssl, + "app/conanfile.py": conanfile, + "profile": profile}) + + c.run("create zlibng") + c.run("create openssl -pr=profile") + c.run("install app -pr=profile -c tools.cmake.cmakedeps:new=will_break_next") + assert "zlib/0.1: zlib-ng/0.1" in c.out + + pc_content = c.load("app/zlib-ng.pc") + assert 'Requires: ZLIB' in pc_content + pc_content = c.load("app/ZLIB.pc") + assert 'Libs: -L"${libdir}" -lzlib' in pc_content + pc_content = c.load("app/openssl-crypto.pc") + assert f'Requires: {"zlib-ng" if package_requires else "ZLIB"}' in pc_content + + cmake = c.load("app/ZLIB-Targets-release.cmake") + assert "add_library(ZLIB::ZLIB STATIC IMPORTED)" in cmake + + cmake = c.load("app/openssl-Targets-release.cmake") + assert "find_dependency(ZLIB REQUIRED CONFIG)" in cmake + assert "add_library(openssl::crypto STATIC IMPORTED)" in cmake + if package_requires: + # The generic package requirement uses the package name zlib-ng + assert "set_target_properties(openssl::crypto PROPERTIES INTERFACE_LINK_LIBRARIES\n" \ + ' "$<$:zlib-ng::zlib-ng>")' in cmake + else: + assert "set_target_properties(openssl::crypto PROPERTIES INTERFACE_LINK_LIBRARIES\n" \ + ' "$<$:ZLIB::ZLIB>")' in cmake