Repository: conan-io/conan

Issue:
Duplicated libraries in the build order returned by conan info
Using conan 1.6.0 on ubuntu 16.04.

Here is the dependency graph : 
![graph](https://user-images.githubusercontent.com/13416648/43128924-75a8da14-8f33-11e8-9261-9e67aae4ba93.png)

And the output returned by `conan info` (I added the notation "(Private)" when it is the case) :
[conan_info.txt](https://github.com/conan-io/conan/files/2222887/conan_info.txt)

When calling `conan info`with the `-bo` options to get what we have to rebuild in our graph, it outputs the following : 
Command: `conan info . -bo Concepts/1.0.0@common/stable -bo PackageG/0.1.0@common/stable`
Returns: `[Concepts/1.0.0@common/stable, Concepts/1.0.0@common/stable, Concepts/1.0.0@common/stable], [Math/0.3.0@common/stable, Math/0.3.0@common/stable, Math/0.3.0@common/stable], [PackageC/0.0.1@common/stable, PackageG/0.1.0@common/stable, PackageN/1.0.0@common/stable]`

As you can see, some packages are duplicated, hence in our CI theses packages are built multiple times unnecessarily.

If needed, I can provide the recipes.


Discussion:
Ouch, I made another test, and it gets even worse, the build order is completly broken.

Here is the command on this graph:
`conan info . -bo Catch/2.2.3@common/stable`

Here is the output:
`[Catch/2.2.3@common/stable, Catch/2.2.3@common/stable, Catch/2.2.3@common/stable], [Catch/2.2.3@common/stable, Catch/2.2.3@common/stable, Catch/2.2.3@common/stable, Catch/2.2.3@common/stable, Concepts/1.0.0@common/stable, Concepts/1.0.0@common/stable, Concepts/1.0.0@common/stable], [Algorithms/0.1.0@common/stable, Catch/2.2.3@common/stable, Catch/2.2.3@common/stable, Catch/2.2.3@common/stable, Catch/2.2.3@common/stable, Math/0.3.0@common/stable, Math/0.3.0@common/stable, Math/0.3.0@common/stable], [Algorithms/0.1.0@common/stable, PackageC/0.0.1@common/stable, PackageG/0.1.0@common/stable, PackageN/1.0.0@common/stable]`

If I remove the duplicates to see better:
`[Catch/2.2.3@common/stable], [Catch/2.2.3@common/stable, Concepts/1.0.0@common/stable], [Algorithms/0.1.0@common/stable, Catch/2.2.3@common/stable, Math/0.3.0@common/stable], [Algorithms/0.1.0@common/stable, PackageC/0.0.1@common/stable, PackageG/0.1.0@common/stable, PackageN/1.0.0@common/stable]`

In the first group, packages are ok.
In the second group, Catch shouldn't appear as it is required by Concepts, moreover it has already been built in the first group.
The same goes for the third.
The fourth is ok.
Hi @dkgs 

Yes, it seems there is an issue with the latest changes in the graph model, now that it is able to represent the build requires in it. I'll check what can be done asap, thanks very much for reporting.

Resolving patch:
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -34,6 +34,17 @@ def __init__(self, conan_ref, conanfile):
         self.binary_remote = None
         self.build_require = False
 
+    def partial_copy(self):
+        result = Node(self.conan_ref, self.conanfile)
+        result.dependants = set()
+        result.dependencies = []
+        result.binary = self.binary
+        result.recipe = self.recipe
+        result.remote = self.remote
+        result.binary_remote = self.binary_remote
+        result.build_require = self.build_require
+        return result
+
     def add_edge(self, edge):
         if edge.src == self:
             if edge not in self.dependencies:
@@ -218,9 +229,46 @@ def _inverse_closure(self, references):
             current = new_current
         return closure
 
+    def collapse_graph(self):
+        """Computes and return a new graph, that doesn't have duplicated nodes with the same
+        PackageReference. This is the case for build_requires and private requirements
+        """
+        result = DepsGraph()
+        result.add_node(self.root.partial_copy())
+        unique_nodes = {}  # {PackageReference: Node (result, unique)}
+        nodes_map = {self.root: result.root}  # {Origin Node: Result Node}
+        # Add the nodes, without repetition. THe "node.partial_copy()" copies the nodes
+        # without Edges
+        for node in self.nodes:
+            if not node.conan_ref:
+                continue
+            package_ref = PackageReference(node.conan_ref, node.conanfile.info.package_id())
+            if package_ref not in unique_nodes:
+                result_node = node.partial_copy()
+                result.add_node(result_node)
+                unique_nodes[package_ref] = result_node
+            else:
+                result_node = unique_nodes[package_ref]
+            nodes_map[node] = result_node
+
+        # Compute the new edges of the graph
+        for node in self.nodes:
+            result_node = nodes_map[node]
+            for dep in node.dependencies:
+                src = result_node
+                dst = nodes_map[dep.dst]
+                result.add_edge(src, dst, dep.private)
+            for dep in node.dependants:
+                src = nodes_map[dep.src]
+                dst = result_node
+                result.add_edge(src, dst, dep.private)
+
+        return result
+
     def build_order(self, references):
-        levels = self.inverse_levels()
-        closure = self._inverse_closure(references)
+        new_graph = self.collapse_graph()
+        levels = new_graph.inverse_levels()
+        closure = new_graph._inverse_closure(references)
         result = []
         for level in reversed(levels):
             new_level = [n.conan_ref for n in level if (n in closure and n.conan_ref)]


Test patch:
diff --git a/conans/test/command/info_test.py b/conans/test/command/info_test.py
--- a/conans/test/command/info_test.py
+++ b/conans/test/command/info_test.py
@@ -379,7 +379,51 @@ def build_order_test(self):
                          load(os.path.join(self.client.current_folder, "file.json")))
 
         self.client.run("info Hello1/0.1@lasote/stable -bo=Hello0/0.1@lasote/stable --json")
-        self.assertIn('{"groups": [["Hello0/0.1@lasote/stable"], ["Hello1/0.1@lasote/stable"]]}', self.client.user_io.out)
+        self.assertIn('{"groups": [["Hello0/0.1@lasote/stable"], ["Hello1/0.1@lasote/stable"]]}',
+                      self.client.out)
+
+    def build_order_build_requires_test(self):
+        # https://github.com/conan-io/conan/issues/3267
+        client = TestClient()
+        conanfile = """from conans import ConanFile
+class AConan(ConanFile):
+    pass
+    """
+        client.save({"conanfile.py": conanfile})
+        client.run("create . tool/0.1@user/channel")
+        client.run("create . dep/0.1@user/channel")
+        conanfile = conanfile + 'requires = "dep/0.1@user/channel"'
+        client.save({"conanfile.py": conanfile})
+        client.run("export . Pkg/0.1@user/channel")
+        client.run("export . Pkg2/0.1@user/channel")
+        client.save({"conanfile.txt": "[requires]\nPkg/0.1@user/channel\nPkg2/0.1@user/channel",
+                     "myprofile": "[build_requires]\ntool/0.1@user/channel"}, clean_first=True)
+        client.run("info . -pr=myprofile -bo=tool/0.1@user/channel")
+        self.assertIn("[tool/0.1@user/channel], [Pkg/0.1@user/channel, Pkg2/0.1@user/channel]",
+                      client.out)
+
+    def build_order_privates_test(self):
+        # https://github.com/conan-io/conan/issues/3267
+        client = TestClient()
+        conanfile = """from conans import ConanFile
+class AConan(ConanFile):
+    pass
+    """
+        client.save({"conanfile.py": conanfile})
+        client.run("create . tool/0.1@user/channel")
+        conanfile_dep = conanfile + 'requires = "tool/0.1@user/channel"'
+        client.save({"conanfile.py": conanfile_dep})
+        client.run("create . dep/0.1@user/channel")
+        conanfile_pkg = conanfile + 'requires = ("dep/0.1@user/channel", "private"),'
+        client.save({"conanfile.py": conanfile_pkg})
+        client.run("export . Pkg/0.1@user/channel")
+        client.run("export . Pkg2/0.1@user/channel")
+        client.save({"conanfile.txt": "[requires]\nPkg/0.1@user/channel\nPkg2/0.1@user/channel"},
+                    clean_first=True)
+        client.run("info . -bo=tool/0.1@user/channel")
+        self.assertIn("[tool/0.1@user/channel], [dep/0.1@user/channel], "
+                      "[Pkg/0.1@user/channel, Pkg2/0.1@user/channel]",
+                      client.out)
 
     def diamond_build_order_test(self):
         self.client = TestClient()