From 70c283b42971302b79b20934e90046daaa6991c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 10 Jun 2026 14:13:28 +0200 Subject: [PATCH 01/13] transitive_headers chained diamond test --- .../graph/core/graph_manager_test.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py index 4146e41d712..bde2f53d9a8 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -199,6 +199,106 @@ 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("shared", [True]) + def test_simple_transitive_headers_chain(self, shared): + # consumer -> libd -> libc - transitive_headers=True -> libb - > liba -> lib0 + 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 + _check_transitive(consumer, [ + (libd, True, True, False, shared), + (libc, False, not shared, False, shared), + (libb, False, not shared, False, shared), + (liba, False, not shared, False, shared), + (lib0, False, not shared, False, shared), + ]) + + _check_transitive(libd, [ + (libc, True, True, False, shared), + (libb, True, not shared, False, shared), + (liba, False, not shared, False, shared), + (lib0, False, not shared, False, shared), + ]) + + _check_transitive(libc, [ + (libb, True, True, False, shared), + (liba, False, not shared, False, shared), + (lib0, False, not shared, False, shared), + ]) + + @pytest.mark.parametrize("shared", [True, False]) + @pytest.mark.parametrize("liba_first", [True, False]) + @pytest.mark.parametrize("transitive", [True, False]) + def test_transitive_headers_duplicate_different_diamond(self, shared, liba_first, transitive): + # libd -> libc - (faulty) - > 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) + 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 + _check_transitive(libd, [ + (libc, True, True, False, shared), + (libb, transitive, not shared, False, shared), + # Fails, currently depends on transitive as libb does + (liba, False, not shared, False, shared), + ]) + _check_transitive(libc, [ + (libb, True, True, False, shared), + (liba, True, True, False, shared), + ]) + def test_middle_shared_up_static(self): # app -> libb0.1 (shared) -> liba0.1 (static) self.recipe_cache("liba/0.1", option_shared=False) From aab29cf5afb5f5a81faddd1fe26e58f8d36ed9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 10 Jun 2026 20:57:39 +0200 Subject: [PATCH 02/13] Guard transitive_{libs,headers} propagation on chained value --- conan/internal/model/requires.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index 611384e1813..f713e4230b8 100644 --- a/conan/internal/model/requires.py +++ b/conan/internal/model/requires.py @@ -353,12 +353,12 @@ 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 + downstream_require.transitive_headers = self.transitive_headers and require.transitive_headers if require.transitive_libs is not None: downstream_require.libs = require.libs and require.transitive_libs if self.transitive_libs is not None: - downstream_require.transitive_libs = self.transitive_libs + downstream_require.transitive_libs = self.transitive_libs and require.transitive_libs downstream_require.consistent = require.consistent and self.consistent From dad72ca68b2dd3b7d12a4af521305bccf6767c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Thu, 11 Jun 2026 15:18:11 +0200 Subject: [PATCH 03/13] Check for transitive traits where necessary --- .../graph/core/graph_manager_test.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py index bde2f53d9a8..65d98fe4173 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -19,7 +19,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[1]}) transitive_headers" + if len(v2) > 6: + assert v1.require.transitive_libs is v2[6], f"{v1.node}!=expected {v2[0]} ({v2[2]}) transitive_libs" class TestLinear(GraphManagerTest): @@ -199,7 +202,7 @@ 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("shared", [True]) + @pytest.mark.parametrize("shared", [True, False]) def test_simple_transitive_headers_chain(self, shared): # consumer -> libd -> libc - transitive_headers=True -> libb - > liba -> lib0 self.recipe_cache("lib0/0.1", option_shared=shared) @@ -226,7 +229,7 @@ def test_simple_transitive_headers_chain(self, shared): liba = libb.edges[0].dst lib0 = liba.edges[0].dst - # node, headers, lib, build, run + # node, headers, lib, build, run (transitive_headers, transitive_libs) _check_transitive(consumer, [ (libd, True, True, False, shared), (libc, False, not shared, False, shared), @@ -289,14 +292,14 @@ def test_transitive_headers_duplicate_different_diamond(self, shared, liba_first # # node, headers, lib, build, run _check_transitive(libd, [ - (libc, True, True, False, shared), - (libb, transitive, not shared, False, shared), + (libc, True, True, False, shared, None, None), + (libb, bool(transitive), not shared, False, shared, None, None), # Fails, currently depends on transitive as libb does - (liba, False, not shared, False, shared), + (liba, False, not shared, False, shared, None, None), ]) _check_transitive(libc, [ - (libb, True, True, False, shared), - (liba, True, True, False, shared), + (libb, True, True, False, shared, transitive, None), + (liba, True, True, False, shared, None, None), ]) def test_middle_shared_up_static(self): From 465d322c2994cadab38890107d99d9849940aecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Thu, 11 Jun 2026 23:31:37 +0200 Subject: [PATCH 04/13] Static chain test broken by transitive shared --- .../graph/core/graph_manager_test.py | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py index 65d98fe4173..a9e782dd2c1 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -246,16 +246,16 @@ def test_simple_transitive_headers_chain(self, shared): ]) _check_transitive(libc, [ - (libb, True, True, False, shared), - (liba, False, not shared, False, shared), - (lib0, False, not shared, False, shared), + (libb, True, True, False, shared, True, None), + (liba, False, not shared, False, shared, None, None), + (lib0, False, not shared, False, shared, None, None), ]) @pytest.mark.parametrize("shared", [True, False]) @pytest.mark.parametrize("liba_first", [True, False]) - @pytest.mark.parametrize("transitive", [True, False]) + @pytest.mark.parametrize("transitive", [True, False, None]) def test_transitive_headers_duplicate_different_diamond(self, shared, liba_first, transitive): - # libd -> libc - (faulty) - > liba + # libd -> libc - > liba # \ - transitive_headers=True -> libb -> liba self.recipe_cache("liba/0.1", option_shared=shared) libb = GenConanfile().with_requirement("liba/0.1") @@ -290,11 +290,11 @@ def test_transitive_headers_duplicate_different_diamond(self, shared, liba_first 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 + # # 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), - # Fails, currently depends on transitive as libb does + # Header does not depend on the transitive flag of libb (liba, False, not shared, False, shared, None, None), ]) _check_transitive(libc, [ @@ -302,6 +302,36 @@ def test_transitive_headers_duplicate_different_diamond(self, shared, liba_first (liba, True, True, False, shared, None, None), ]) + def test_static_shared_transitive_chain(self): + # consumer -> shared1 - transitive_libs = True -> static2 -> static3 + # Consumer needs static2 and static3 in all cases + 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) From 572388f6cf4864fe9d18d3a6d572443eab3d0bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Thu, 11 Jun 2026 23:32:31 +0200 Subject: [PATCH 05/13] transitive_libs should not be propagated --- conan/internal/model/requires.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index f713e4230b8..e910f331fd8 100644 --- a/conan/internal/model/requires.py +++ b/conan/internal/model/requires.py @@ -358,7 +358,7 @@ def transform_downstream(self, pkg_type, require, dep_pkg_type): if require.transitive_libs is not None: downstream_require.libs = require.libs and require.transitive_libs if self.transitive_libs is not None: - downstream_require.transitive_libs = self.transitive_libs and require.transitive_libs + downstream_require.transitive_libs = self.transitive_libs downstream_require.consistent = require.consistent and self.consistent From 3d5587ea05eed3f512b4c93de9e891a7dc5740da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 15 Jun 2026 11:58:32 +0200 Subject: [PATCH 06/13] Policy for transitive_header propagation --- conan/internal/graph/graph.py | 6 ++- conan/internal/model/requires.py | 7 +-- .../graph/core/graph_manager_test.py | 47 ++++++++++++------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/conan/internal/graph/graph.py b/conan/internal/graph/graph.py index 02bd723534d..a1efbf0a256 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, + consumer_conanfile=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, + consumer_conanfile=dependant.src.conanfile) if down_require is None: # print(" No need to check downstream more") diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index e910f331fd8..80e9ddaa6d5 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, consumer_conanfile): """ consumer ---self---> foo ---require---> bar \\ -------------------????-------------------- / @@ -353,7 +353,8 @@ 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 and require.transitive_headers + transitive_propagation = required_conan_version_policy(consumer_conanfile, "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_test.py b/test/integration/graph/core/graph_manager_test.py index a9e782dd2c1..e4d82f13e5c 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 @@ -20,9 +21,9 @@ def _check_transitive(node, transitive_deps): 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" if len(v2) > 5: - assert v1.require.transitive_headers is v2[5], f"{v1.node}!=expected {v2[0]} ({v2[1]}) transitive_headers" + 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[2]}) transitive_libs" + assert v1.require.transitive_libs is v2[6], f"{v1.node}!=expected {v2[0]} ({v2[6]}) transitive_libs" class TestLinear(GraphManagerTest): @@ -202,9 +203,13 @@ 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]) @pytest.mark.parametrize("shared", [True, False]) - def test_simple_transitive_headers_chain(self, shared): + 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) @@ -231,30 +236,33 @@ def test_simple_transitive_headers_chain(self, shared): # node, headers, lib, build, run (transitive_headers, transitive_libs) _check_transitive(consumer, [ - (libd, True, True, False, shared), - (libc, False, not shared, False, shared), - (libb, False, not shared, False, shared), - (liba, False, not shared, False, shared), - (lib0, False, not shared, False, shared), + (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), - (libb, True, not shared, False, shared), - (liba, False, not shared, False, shared), - (lib0, False, not shared, False, shared), + (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), - (liba, False, not shared, False, shared, None, None), - (lib0, False, not shared, False, shared, None, 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, shared, liba_first, transitive): + 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) @@ -269,10 +277,12 @@ def test_transitive_headers_duplicate_different_diamond(self, shared, liba_first 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) @@ -295,11 +305,12 @@ def test_transitive_headers_duplicate_different_diamond(self, shared, liba_first (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 - (liba, False, not shared, False, shared, None, None), + # 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, None), + (liba, True, True, False, shared, None if version else (True if transitive else None), None), ]) def test_static_shared_transitive_chain(self): From ab59dcbc4a1b3fe9c95f216025e1def454efd745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 15 Jun 2026 12:01:09 +0200 Subject: [PATCH 07/13] Readbility --- conan/internal/model/requires.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index 80e9ddaa6d5..f873999f19b 100644 --- a/conan/internal/model/requires.py +++ b/conan/internal/model/requires.py @@ -354,7 +354,9 @@ def transform_downstream(self, pkg_type, require, dep_pkg_type, consumer_conanfi downstream_require.headers = require.headers and require.transitive_headers if self.transitive_headers is not None: transitive_propagation = required_conan_version_policy(consumer_conanfile, "2.29.9") - downstream_require.transitive_headers = self.transitive_headers if not transitive_propagation else self.transitive_headers and require.transitive_headers + 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 From 2f37abc572dd8f2f644c242bde3d35eb44e75576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 15 Jun 2026 12:04:27 +0200 Subject: [PATCH 08/13] Add policy documentation --- conan/internal/model/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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.", From 724433998d96160f84708474bd800ef872ee7b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 15 Jun 2026 12:13:16 +0200 Subject: [PATCH 09/13] No change in static_shared transitive chain after fix --- test/integration/graph/core/graph_manager_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py index e4d82f13e5c..4cf9c35a76e 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -313,9 +313,13 @@ def test_transitive_headers_duplicate_different_diamond(self, version, shared, l (liba, True, True, False, shared, None if version else (True if transitive else None), None), ]) - def test_static_shared_transitive_chain(self): + @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) From 76eff489c2fbce98df01c3a3035e228ec93e6a2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 17 Jun 2026 17:57:54 +0200 Subject: [PATCH 10/13] Add fixed check for one of the new test cases --- test/integration/graph/core/graph_manager_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py index 4cf9c35a76e..b5925e63d1f 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -203,7 +203,7 @@ 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]) + @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 From 957ea732dcc925775d6474aed8d4f24a4c5f6886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Wed, 17 Jun 2026 18:00:48 +0200 Subject: [PATCH 11/13] Better namin g --- conan/internal/graph/graph.py | 4 ++-- conan/internal/model/requires.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conan/internal/graph/graph.py b/conan/internal/graph/graph.py index a1efbf0a256..d541b30a347 100644 --- a/conan/internal/graph/graph.py +++ b/conan/internal/graph/graph.py @@ -153,7 +153,7 @@ def propagate_downstream(self, require, node, visibility_conflicts, src_node=Non down_require = d.require.transform_downstream(self.conanfile.package_type, require, node.conanfile.package_type, - consumer_conanfile=d.src.conanfile) + d.src.conanfile) if down_require is None: return @@ -206,7 +206,7 @@ def check_downstream_exists(self, require): # print(" Lets check_downstream one more") down_require = dependant.require.transform_downstream(self.conanfile.package_type, require, None, - consumer_conanfile=dependant.src.conanfile) + dependant.src.conanfile) if down_require is None: # print(" No need to check downstream more") diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py index f873999f19b..76118e2e472 100644 --- a/conan/internal/model/requires.py +++ b/conan/internal/model/requires.py @@ -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, consumer_conanfile): + def transform_downstream(self, pkg_type, require, dep_pkg_type, source): """ consumer ---self---> foo ---require---> bar \\ -------------------????-------------------- / @@ -353,7 +353,7 @@ def transform_downstream(self, pkg_type, require, dep_pkg_type, consumer_conanfi if require.transitive_headers is not None: downstream_require.headers = require.headers and require.transitive_headers if self.transitive_headers is not None: - transitive_propagation = required_conan_version_policy(consumer_conanfile, "2.29.9") + 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) From 6e74bc3353846298489f4e3604648a421230df66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Mon, 22 Jun 2026 13:50:59 +0200 Subject: [PATCH 12/13] More tests for the test --- .../graph/core/graph_manager_test.py | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/test/integration/graph/core/graph_manager_test.py b/test/integration/graph/core/graph_manager_test.py index b5925e63d1f..60e292b8a75 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -543,9 +543,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, False, 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) @@ -569,12 +573,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) From a159e1e44b6f7c8d91826a79242507bdbcbd898a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abril=20Rinc=C3=B3n=20Blanco?= Date: Fri, 26 Jun 2026 11:47:20 +0200 Subject: [PATCH 13/13] More checks for transitive headers --- test/integration/graph/core/graph_manager_base.py | 4 +++- test/integration/graph/core/graph_manager_test.py | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) 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 60e292b8a75..23f4f837156 100644 --- a/test/integration/graph/core/graph_manager_test.py +++ b/test/integration/graph/core/graph_manager_test.py @@ -524,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) @@ -544,7 +548,7 @@ def test_header_only(self): # node, headers, lib, build, run _check_transitive(app, [ - (libb, True, True, False, False, None, None), + (libb, True, True, False, bool(libb_shared), None, None), (liba, False, False, False, False, None, None) ]) _check_transitive(libb, [