Repository: conan-io/conan

Issue:
[bug] Error in workspace graph with several build_requires consuming same package
Changelog: (Feature | Fix | Bugfix): Describe here your pull request
Docs: https://github.com/conan-io/docs/pull/XXXX

Reproduce issue in #5612


Discussion:

Resolving patch:
diff --git a/conans/client/graph/graph_manager.py b/conans/client/graph/graph_manager.py
--- a/conans/client/graph/graph_manager.py
+++ b/conans/client/graph/graph_manager.py
@@ -211,15 +211,21 @@ def _get_recipe_build_requires(conanfile):
 
         return conanfile.build_requires
 
-    def _recurse_build_requires(self, graph, builder, binaries_analyzer, check_updates, update,
-                                build_mode, remotes, profile_build_requires, recorder,
+    def _recurse_build_requires(self, graph, subgraph, builder, binaries_analyzer, check_updates,
+                                update, build_mode, remotes, profile_build_requires, recorder,
                                 processed_profile, graph_lock, apply_build_requires=True):
+        """
+        :param graph: This is the full dependency graph with all nodes from all recursions
+        :param subgraph: A partial graph of the nodes that need to be evaluated and expanded
+            at this recursion. Only the nodes belonging to this subgraph will get their package_id
+            computed, and they will resolve build_requires if they need to be built from sources
+        """
 
-        binaries_analyzer.evaluate_graph(graph, build_mode, update, remotes)
+        binaries_analyzer.evaluate_graph(subgraph, build_mode, update, remotes)
         if not apply_build_requires:
             return
 
-        for node in graph.ordered_iterate():
+        for node in subgraph.ordered_iterate():
             # Virtual conanfiles doesn't have output, but conanfile.py and conanfile.txt do
             # FIXME: To be improved and build a explicit model for this
             if node.recipe == RECIPE_VIRTUAL:
@@ -249,8 +255,8 @@ def _recurse_build_requires(self, graph, builder, binaries_analyzer, check_updat
                                                          package_build_requires.values(),
                                                          check_updates, update, remotes,
                                                          processed_profile, graph_lock)
-                self._recurse_build_requires(subgraph, builder, binaries_analyzer, check_updates,
-                                             update, build_mode,
+                self._recurse_build_requires(graph, subgraph, builder, binaries_analyzer,
+                                             check_updates, update, build_mode,
                                              remotes, profile_build_requires, recorder,
                                              processed_profile, graph_lock)
                 graph.nodes.update(subgraph.nodes)
@@ -259,8 +265,8 @@ def _recurse_build_requires(self, graph, builder, binaries_analyzer, check_updat
                 subgraph = builder.extend_build_requires(graph, node, new_profile_build_requires,
                                                          check_updates, update, remotes,
                                                          processed_profile, graph_lock)
-                self._recurse_build_requires(subgraph, builder, binaries_analyzer, check_updates,
-                                             update, build_mode,
+                self._recurse_build_requires(graph, subgraph, builder, binaries_analyzer,
+                                             check_updates, update, build_mode,
                                              remotes, {}, recorder,
                                              processed_profile, graph_lock)
                 graph.nodes.update(subgraph.nodes)
@@ -277,7 +283,7 @@ def _load_graph(self, root_node, check_updates, update, build_mode, remotes,
         binaries_analyzer = GraphBinariesAnalyzer(self._cache, self._output,
                                                   self._remote_manager)
 
-        self._recurse_build_requires(graph, builder, binaries_analyzer, check_updates, update,
+        self._recurse_build_requires(graph, graph, builder, binaries_analyzer, check_updates, update,
                                      build_mode, remotes,
                                      profile_build_requires, recorder, processed_profile,
                                      graph_lock,


Test patch:
diff --git a/conans/test/functional/build_requires/build_requires_test.py b/conans/test/functional/build_requires/build_requires_test.py
--- a/conans/test/functional/build_requires/build_requires_test.py
+++ b/conans/test/functional/build_requires/build_requires_test.py
@@ -1,10 +1,12 @@
 import os
+import textwrap
 import unittest
 
 from parameterized.parameterized import parameterized
 
+from conans.model.ref import ConanFileReference
 from conans.paths import CONANFILE
-from conans.test.utils.tools import TestClient
+from conans.test.utils.tools import TestClient, GenConanfile
 from conans.util.files import load
 from conans.test.utils.conanfile import TestConanFile
 
@@ -379,3 +381,40 @@ class package(ConanFile):
         client.save({"conanfile.py": consumer})
         client.run("install . --build=missing -o Pkg:someoption=3")
         self.assertIn("first/0.0.0@lasote/stable: Coverage: True", client.out)
+
+    def failed_assert_test(self):
+        # https://github.com/conan-io/conan/issues/5685
+        client = TestClient()
+        client.save({"conanfile.py": GenConanfile()})
+        client.run("export . common/1.0@test/test")
+
+        req = textwrap.dedent("""
+            from conans import ConanFile
+            class BuildReqConan(ConanFile):
+                requires = "common/1.0@test/test"
+            """)
+        client.save({"conanfile.py": req})
+        client.run("export . req/1.0@test/test")
+        client.run("export . build_req/1.0@test/test")
+
+        build_req_req = textwrap.dedent("""
+            from conans import ConanFile
+            class BuildReqConan(ConanFile):
+                requires = "common/1.0@test/test"
+                build_requires = "build_req/1.0@test/test"
+        """)
+        client.save({"conanfile.py": build_req_req})
+        client.run("export . build_req_req/1.0@test/test")
+
+        consumer = textwrap.dedent("""
+                    [requires]
+                    req/1.0@test/test
+                    [build_requires]
+                    build_req_req/1.0@test/test
+                """)
+        client.save({"conanfile.txt": consumer}, clean_first=True)
+        client.run("install . --build=missing")
+        # This used to assert and trace, now it works
+        self.assertIn("conanfile.txt: Applying build-requirement: build_req_req/1.0@test/test",
+                      client.out)
+
diff --git a/conans/test/functional/workspace/graph_test.py b/conans/test/functional/workspace/graph_test.py
new file mode 100644
--- /dev/null
+++ b/conans/test/functional/workspace/graph_test.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+import textwrap
+import unittest
+
+from jinja2 import Template
+
+from conans.test.utils.tools import TestClient
+
+
+class WorkspaceTest(unittest.TestCase):
+    conanfile = Template(textwrap.dedent("""
+        from conans import ConanFile
+
+        class Library(ConanFile):
+            name = "{{ name }}"
+            version = "{{ version }}"
+            {% if br %}build_requires = "{{ br }}"{% endif %}
+            {% if req %}requires = "{{ req }}"{% endif %}
+    """))
+
+    def test_graph_failure(self):
+        # https://github.com/conan-io/conan/issues/5612
+        client = TestClient()
+
+        client.save({"fmt/conanfile.py": self.conanfile.render(name="fmt", version="version")})
+
+        client.save({"libA/conanfile.py": self.conanfile.render(name="libA", version="version",
+                                                                req="fmt/version@user/channel")})
+        client.save({"libB/conanfile.py": self.conanfile.render(name="libB", version="version",
+                                                                req="fmt/version@user/channel",
+                                                                br="libA/version@user/channel")})
+        client.save({"libC/conanfile.py": self.conanfile.render(name="libC", version="version",
+                                                                req="fmt/version@user/channel",
+                                                                br="libB/version@user/channel")})
+
+        client.run("create fmt fmt/version@user/channel")
+
+        client.run("create libA libA/version@user/channel")
+        client.run("create libB libB/version@user/channel")
+        client.run("create libC libC/version@user/channel")
+
+        conanws = textwrap.dedent("""
+                    editables:
+                      libA/version@user/channel:
+                        path: libA
+                      libB/version@user/channel:
+                        path: libB
+                      libC/version@user/channel:
+                        path: libC
+        
+                    workspace_generator: cmake
+                    root: libC/version@user/channel
+                """)
+        client.save({"conanws.yml": conanws})
+        client.run("workspace install .")