diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py index b87894edfb1..a50b47541ef 100644 --- a/conans/client/graph/graph.py +++ b/conans/client/graph/graph.py @@ -65,6 +65,7 @@ def __init__(self, ref, conanfile, context, recipe=None, path=None, test=False): self.error = None self.cant_build = False # It will set to a str with a reason if the validate_build() fails self.should_build = False # If the --build or policy wants to build this binary + self.build_allowed = False def __lt__(self, other): """ diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py index a65c074c993..7cb8097b64a 100644 --- a/conans/client/graph/graph_binaries.py +++ b/conans/client/graph/graph_binaries.py @@ -162,6 +162,7 @@ def _evaluate_node(self, node, build_mode, remotes, update): if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile): node.should_build = True + node.build_allowed = True node.binary = BINARY_BUILD if not node.cant_build else BINARY_INVALID if (node.binary in (BINARY_BUILD, BINARY_MISSING) and node.conanfile.info.invalid and @@ -370,27 +371,39 @@ def evaluate_graph(self, deps_graph, build_mode, lockfile, remotes, update, buil @staticmethod def _skip_binaries(graph): required_nodes = set() + # Aggregate all necessary starting nodes required_nodes.add(graph.root) for node in graph.nodes: - if node.binary not in (BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_EDITABLE) \ - and node is not graph.root: - continue - # The nodes that are directly required by this one to build correctly - deps_required = set(d.node for d in node.transitive_deps.values() if d.require.files) - - # second pass, transitive affected. Packages that have some dependency that is required - # cannot be skipped either. In theory the binary could be skipped, but build system - # integrations like CMakeDeps rely on find_package() to correctly find transitive deps - indirect = (d.node for d in node.transitive_deps.values() - if any(t.node in deps_required for t in d.node.transitive_deps.values())) - deps_required.update(indirect) - - # Third pass, mark requires as skippeable - for dep in node.transitive_deps.values(): - dep.require.skip = dep.node not in deps_required - - # Finally accumulate for marking binaries as SKIP download - required_nodes.update(deps_required) + if node.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_EDITABLE): + if not node.build_allowed: # Only those that are forced to build, not only "missing" + required_nodes.add(node) + + root_nodes = required_nodes.copy() + while root_nodes: + new_root_nodes = set() + for node in root_nodes: + # The nodes that are directly required by this one to build correctly + deps_required = set(d.node for d in node.transitive_deps.values() if d.require.files) + + # second pass, transitive affected. Packages that have some dependency that is required + # cannot be skipped either. In theory the binary could be skipped, but build system + # integrations like CMakeDeps rely on find_package() to correctly find transitive deps + indirect = (d.node for d in node.transitive_deps.values() + if any(t.node in deps_required for t in d.node.transitive_deps.values())) + deps_required.update(indirect) + + # Third pass, mark requires as skippeable + for dep in node.transitive_deps.values(): + dep.require.skip = dep.node not in deps_required + + # Finally accumulate all needed nodes for marking binaries as SKIP download + news_req = [r for r in deps_required + if r.binary in (BINARY_BUILD, BINARY_EDITABLE_BUILD, BINARY_EDITABLE) + if r not in required_nodes] # Avoid already expanded before + new_root_nodes.update(news_req) # For expanding the next iteration + required_nodes.update(deps_required) + + root_nodes = new_root_nodes for node in graph.nodes: if node not in required_nodes and node.conanfile.conf.get("tools.graph:skip_binaries", diff --git a/conans/test/integration/build_requires/build_requires_test.py b/conans/test/integration/build_requires/build_requires_test.py index d2bdcc39453..7c30f7a380e 100644 --- a/conans/test/integration/build_requires/build_requires_test.py +++ b/conans/test/integration/build_requires/build_requires_test.py @@ -1,6 +1,7 @@ import json import os import platform +import re import textwrap import unittest @@ -969,3 +970,21 @@ def test_overriden_host_version_transitive_deps(self): # lock can be used c.run("install app --lockfile=app/conan.lock") c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) + + +def test_build_missing_build_requires(): + c = TestClient() + c.save({"tooldep/conanfile.py": GenConanfile("tooldep", "0.1"), + "tool/conanfile.py": GenConanfile("tool", "0.1").with_tool_requires("tooldep/0.1"), + "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_tool_requires("tool/0.1"), + "app/conanfile.py": GenConanfile().with_requires("pkg/0.1")}) + c.run("create tooldep") + c.run("create tool") + c.run("create pkg") + c.run("remove tool*:* -c") + c.run("install app") + assert "- Build" not in c.out + assert re.search(r"Skipped binaries(\s*)tool/0.1, tooldep/0.1", c.out) + c.run("install app --build=missing") + assert "- Build" not in c.out + assert re.search(r"Skipped binaries(\s*)tool/0.1, tooldep/0.1", c.out) diff --git a/conans/test/integration/build_requires/profile_build_requires_test.py b/conans/test/integration/build_requires/profile_build_requires_test.py index 09d71b40bf9..a0529a043f7 100644 --- a/conans/test/integration/build_requires/profile_build_requires_test.py +++ b/conans/test/integration/build_requires/profile_build_requires_test.py @@ -58,6 +58,10 @@ def _create(self, client): client.run("export . --user=lasote --channel=stable") def test_profile_requires(self): + """ + cli -(tool-requires)-> tool/0.1 + \\--(requires)->mylib/0.1 -(tool_requires)->tool/0.1 (skipped) + """ client = TestClient() self._create(client)