diff --git a/conan/internal/graph/graph.py b/conan/internal/graph/graph.py index 02bd723534d..d541b30a347 100644 --- a/conan/internal/graph/graph.py +++ b/conan/internal/graph/graph.py @@ -152,7 +152,8 @@ def propagate_downstream(self, require, node, visibility_conflicts, src_node=Non d = self.dependants[0] down_require = d.require.transform_downstream(self.conanfile.package_type, require, - node.conanfile.package_type) + node.conanfile.package_type, + d.src.conanfile) if down_require is None: return @@ -204,7 +205,8 @@ def check_downstream_exists(self, require): # TODO: Implement an optimization where the requires is checked against a graph global # print(" Lets check_downstream one more") down_require = dependant.require.transform_downstream(self.conanfile.package_type, - require, None) + require, None, + dependant.src.conanfile) if down_require is None: # print(" No need to check downstream more") diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py index 374b16bf88a..3d8a1fa10bb 100644 --- a/conan/internal/model/conf.py +++ b/conan/internal/model/conf.py @@ -26,7 +26,8 @@ If the policy 'required_conan_version>=version' is defined, different behaviors can be enabled: - If required_conan_version>=2.28, bugfix https://github.com/conan-io/conan/pull/19705 for transitive static libraries package_id - If required_conan_version>=2.28, bugfix https://github.com/conan-io/conan/pull/19849 for VirtualBuildEnv bindir path propagation based on requirement run trait - - If required_conan_version>=2.28, https://github.com/conan-io/conan/pull/19286 defaults the new 'consistent' trait to True for the host context, even when 'visible=False'""" + - If required_conan_version>=2.28, https://github.com/conan-io/conan/pull/19286 defaults the new 'consistent' trait to True for the host context, even when 'visible=False' + - If required_conan_version>=2.30, bugfix https://github.com/conan-io/conan/pull/20073 for propagation of the 'transitive_header' trait""" BUILT_IN_CONFS = { "core:required_conan_version": "Raise if current version does not match the defined range.", diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index 611384e1813..76118e2e472 100644 --- a/conan/internal/model/requires.py +++ b/conan/internal/model/requires.py @@ -1,7 +1,7 @@ from conan.errors import ConanException from conan.internal.model.pkg_type import PackageType from conan.api.model import RecipeReference -from conan.internal.model.version_range import VersionRange +from conan.internal.model.version_range import VersionRange, required_conan_version_policy class Requirement: @@ -286,7 +286,7 @@ def aggregate(self, other): self.package_id_mode = other.package_id_mode self.required_nodes.update(other.required_nodes) - def transform_downstream(self, pkg_type, require, dep_pkg_type): + def transform_downstream(self, pkg_type, require, dep_pkg_type, source): """ consumer ---self---> foo ---require---> bar \\ -------------------????-------------------- / @@ -353,7 +353,10 @@ def transform_downstream(self, pkg_type, require, dep_pkg_type): if require.transitive_headers is not None: downstream_require.headers = require.headers and require.transitive_headers if self.transitive_headers is not None: - downstream_require.transitive_headers = self.transitive_headers + transitive_propagation = required_conan_version_policy(source, "2.29.9") + downstream_require.transitive_headers = (self.transitive_headers + if not transitive_propagation else + self.transitive_headers and require.transitive_headers) if require.transitive_libs is not None: downstream_require.libs = require.libs and require.transitive_libs diff --git a/test/integration/graph/core/graph_manager_base.py b/test/integration/graph/core/graph_manager_base.py index b1e54480cb2..796d2e103f4 100644 --- a/test/integration/graph/core/graph_manager_base.py +++ b/test/integration/graph/core/graph_manager_base.py @@ -68,7 +68,7 @@ class Alias(ConanFile): self._cache_recipe(ref, conanfile) @staticmethod - def recipe_consumer(reference=None, requires=None, build_requires=None): + def recipe_consumer(reference=None, requires=None, build_requires=None, shared=None): path = temp_folder() path = os.path.join(path, "conanfile.py") conanfile = GenConanfile() @@ -81,6 +81,8 @@ def recipe_consumer(reference=None, requires=None, build_requires=None): if build_requires: for r in build_requires: conanfile.with_build_requires(r) + if shared is not None: + conanfile.with_shared_option(shared) save(path, str(conanfile)) return path diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py index 4146e41d712..23f4f837156 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -1,4 +1,5 @@ import pytest +import os from conan.internal.graph.graph_error import GraphMissingError, GraphLoopError, GraphConflictError from conan.errors import ConanException @@ -19,7 +20,10 @@ def _check_transitive(node, transitive_deps): assert v1.require.libs is v2[2], f"{v1.node}!=expected {v2[0]} ({v2[2]}) libs" assert v1.require.build is v2[3], f"{v1.node}!=expected {v2[0]} ({v2[3]}) build" assert v1.require.run is v2[4], f"{v1.node}!=expected {v2[0]} ({v2[4]}) run" - assert len(v2) <= 5 + if len(v2) > 5: + assert v1.require.transitive_headers is v2[5], f"{v1.node}!=expected {v2[0]} ({v2[5]}) transitive_headers" + if len(v2) > 6: + assert v1.require.transitive_libs is v2[6], f"{v1.node}!=expected {v2[0]} ({v2[6]}) transitive_libs" class TestLinear(GraphManagerTest): @@ -199,6 +203,150 @@ def test_transitive_all_shared_transitive_headers_libs(self): (liba, True, True, False, True)]) _check_transitive(libb, [(liba, True, True, False, True)]) + @pytest.mark.parametrize("version", [None, ">=2.30-dev"]) + @pytest.mark.parametrize("shared", [True, False]) + def test_simple_transitive_headers_chain(self, shared, version): + # consumer -> libd -> libc - transitive_headers=True -> libb - > liba -> lib0 + if version: + with open(os.path.join(self.cache_folder, "global.conf"), "w") as f: + f.write(f'core:policies=["required_conan_version{version}"]') + self.recipe_cache("lib0/0.1", option_shared=shared) + liba = GenConanfile().with_requirement("lib0/0.1").with_shared_option(shared) + self.recipe_conanfile("liba/0.1", liba) + + libb = GenConanfile().with_requirement("liba/0.1").with_shared_option(shared) + self.recipe_conanfile("libb/0.1", libb) + + libc = GenConanfile() + libc.with_requirement("libb/0.1", transitive_headers=True).with_shared_option(shared) + self.recipe_conanfile("libc/0.1", libc) + + libd = GenConanfile().with_requirement("libc/0.1").with_shared_option(shared) + self.recipe_conanfile("libd/0.1", libd) + + consumer = self.recipe_consumer("consumer/0.1", ["libd/0.1"]) + deps_graph = self.build_consumer(consumer) + + consumer = deps_graph.root + libd = consumer.edges[0].dst + libc = libd.edges[0].dst + libb = libc.edges[0].dst + liba = libb.edges[0].dst + lib0 = liba.edges[0].dst + + # node, headers, lib, build, run (transitive_headers, transitive_libs) + _check_transitive(consumer, [ + (libd, True, True, False, shared, None, None), + (libc, False, not shared, False, shared, None, None), + (libb, False, not shared, False, shared, None, None), + (liba, False, not shared, False, shared, None, None), + (lib0, False, not shared, False, shared, None, None), + ]) + + _check_transitive(libd, [ + (libc, True, True, False, shared, None, None), + (libb, True, not shared, False, shared, None, None), + (liba, False, not shared, False, shared, None, None), + (lib0, False, not shared, False, shared, None, None), + ]) + + _check_transitive(libc, [ + (libb, True, True, False, shared, True, None), + # Having the transitive_headers trait propagated without the fix + # does not create a new package id in this case, it only cases about the headers trait + (liba, False, not shared, False, shared, None if version else True, None), + (lib0, False, not shared, False, shared, None if version else True, None), + ]) + + @pytest.mark.parametrize("version", [None, ">=2.30-dev"]) + @pytest.mark.parametrize("shared", [True, False]) + @pytest.mark.parametrize("liba_first", [True, False]) + @pytest.mark.parametrize("transitive", [True, False, None]) + def test_transitive_headers_duplicate_different_diamond(self, version, shared, liba_first, transitive): + # libd -> libc - > liba + # \ - transitive_headers=True -> libb -> liba + self.recipe_cache("liba/0.1", option_shared=shared) + libb = GenConanfile().with_requirement("liba/0.1") + libb.with_shared_option(shared) + self.recipe_conanfile("libb/0.1", libb) + + libc = GenConanfile() + if liba_first: + libc.with_requirement("liba/0.1").with_requirement("libb/0.1", transitive_headers=transitive) + else: + libc.with_requirement("libb/0.1", transitive_headers=transitive).with_requirement("liba/0.1") + + libc.with_shared_option(shared) + if version: + libc = str(libc) + f"\nrequired_conan_version = '{version}'" + + self.recipe_conanfile("libc/0.1", libc) + + consumer = self.recipe_consumer("libd/0.1", ["libc/0.1"]) + deps_graph = self.build_consumer(consumer) + + assert 4 == len(deps_graph.nodes) + libd = deps_graph.root + libc = libd.edges[0].dst + if liba_first: + liba = libc.edges[0].dst + libb = libc.edges[1].dst + else: + liba = libc.edges[1].dst + libb = libc.edges[0].dst + + self._check_node(libd, "libd/0.1", deps=[libc]) + self._check_node(libc, "libc/0.1#123", deps=[libb, liba], dependents=[libd]) + self._check_node(libb, "libb/0.1#123", deps=[liba], dependents=[libc]) + self._check_node(liba, "liba/0.1#123", dependents=[libb, libc]) + + # # node, headers, lib, build, run, (transitive_headers, transitive_libs) + _check_transitive(libd, [ + (libc, True, True, False, shared, None, None), + (libb, bool(transitive), not shared, False, shared, None, None), + # Header does not depend on the transitive flag of libb + # when the fix is applied + (liba, False if version else bool(transitive), not shared, False, shared, None, None), + ]) + _check_transitive(libc, [ + (libb, True, True, False, shared, transitive, None), + (liba, True, True, False, shared, None if version else (True if transitive else None), None), + ]) + + @pytest.mark.parametrize("version", [None, ">=2.30-dev"]) + def test_static_shared_transitive_chain(self, version): + # consumer -> shared1 - transitive_libs = True -> static2 -> static3 + # Consumer needs static2 and static3 in all cases + if version: + with open(os.path.join(self.cache_folder, "global.conf"), "w") as f: + f.write(f'core:policies=["required_conan_version{version}"]') + self.recipe_cache("static3/0.1", option_shared=False) + self.recipe_cache("static2/0.1", option_shared=False, requires=["static3/0.1"]) + shared1 = GenConanfile().with_requirement("static2/0.1", transitive_headers=True) + shared1.with_shared_option(True) + self.recipe_conanfile("shared1/0.1", shared1) + + consumer = self.recipe_consumer("consumer/0.1", ["shared1/0.1"]) + deps_graph = self.build_consumer(consumer) + + assert 4 == len(deps_graph.nodes) + consumer = deps_graph.root + shared1 = consumer.edges[0].dst + static2 = shared1.edges[0].dst + static3 = static2.edges[0].dst + + self._check_node(consumer, "consumer/0.1", deps=[shared1]) + self._check_node(shared1, "shared1/0.1#123", deps=[static2], dependents=[consumer]) + self._check_node(static2, "static2/0.1#123", deps=[static3], dependents=[shared1]) + self._check_node(static3, "static3/0.1#123", dependents=[static2]) + + # # node, headers, lib, build, run, (transitive_headers, transitive_libs) + _check_transitive(consumer, [ + (shared1, True, True, False, True, None, None), + (static2, True, False, False, False, None, None), + (static3, False, False, False, False, None, None), + ]) + def test_middle_shared_up_static(self): # app -> libb0.1 (shared) -> liba0.1 (static) self.recipe_cache("liba/0.1", option_shared=False) @@ -376,12 +524,16 @@ def test_direct_header_only(self): # node, headers, lib, build, run _check_transitive(app, [(liba, True, False, False, False)]) - def test_header_only(self): + @pytest.mark.parametrize("app_shared", [True, False, None]) + @pytest.mark.parametrize("libb_shared", [True, False, None]) + def test_header_only(self, app_shared, libb_shared): # app -> libb0.1 -> liba0.1 (header_only) self.recipe_conanfile("liba/0.1", GenConanfile().with_package_type("header-library")) libb = GenConanfile().with_requirement("liba/0.1") + if libb_shared is not None: + libb.with_shared_option(libb_shared) self.recipe_conanfile("libb/0.1", libb) - consumer = self.recipe_consumer("app/0.1", ["libb/0.1"]) + consumer = self.recipe_consumer("app/0.1", ["libb/0.1"], shared=app_shared) deps_graph = self.build_consumer(consumer) @@ -395,9 +547,13 @@ def test_header_only(self): self._check_node(liba, "liba/0.1#123", dependents=[libb]) # node, headers, lib, build, run - _check_transitive(app, [(libb, True, True, False, False), - (liba, False, False, False, False)]) - _check_transitive(libb, [(liba, True, False, False, False)]) + _check_transitive(app, [ + (libb, True, True, False, bool(libb_shared), None, None), + (liba, False, False, False, False, None, None) + ]) + _check_transitive(libb, [ + (liba, True, False, False, False, None, None) + ]) def test_header_only_with_transitives(self): # app -> liba0.1(header) -> libb0.1 (static) @@ -421,12 +577,16 @@ def test_header_only_with_transitives(self): self._check_node(libb, "libb/0.1#123", dependents=[liba]) self._check_node(libc, "libc/0.1#123", dependents=[liba]) - # node, headers, lib, build, run - _check_transitive(app, [(liba, True, False, False, False), - (libb, True, True, False, False), - (libc, True, True, False, True)]) - _check_transitive(liba, [(libb, True, True, False, False), - (libc, True, True, False, True)]) + # node, headers, lib, build, run, transitive_headers, transitive_libs + _check_transitive(app, [ + (liba, True, False, False, False, None, None), + (libb, True, True, False, False, None, None), + (libc, True, True, False, True, None, None) + ]) + _check_transitive(liba, [ + (libb, True, True, False, False, True, True), + (libc, True, True, False, True, True, True) + ]) def test_multiple_header_only_with_transitives(self): # app -> libd0.1(header) -> liba0.1(header) -> libb0.1 (static)