diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py index 5da864e5bb7..fab4f0fa112 100644 --- a/conans/client/graph/graph_builder.py +++ b/conans/client/graph/graph_builder.py @@ -232,6 +232,17 @@ def _resolved_system_tool(node, require, profile_build, profile_host, resolve_pr return d, ConanFile(str(d)), RECIPE_SYSTEM_TOOL, None def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock): + if require.ref.version == "": + if not require.build or require.visible: + raise ConanException(f"{node.ref} require '{require.ref}': 'host_version' can only " + "be used for non-visible tool_requires") + req = Requirement(require.ref, headers=True, libs=True, visible=True) + transitive = node.transitive_deps.get(req) + if transitive is None: + raise ConanException(f"{node.ref} require '{require.ref}': didn't find a matching " + "host dependency") + require.ref.version = transitive.require.ref.version + if graph_lock is not None: # Here is when the ranges and revisions are resolved graph_lock.resolve_locked(node, require, self._resolve_prereleases) diff --git a/conans/model/requires.py b/conans/model/requires.py index dc832e0ffb1..a705e010cb7 100644 --- a/conans/model/requires.py +++ b/conans/model/requires.py @@ -392,10 +392,11 @@ class BuildRequirements: def __init__(self, requires): self._requires = requires - def __call__(self, ref, package_id_mode=None, visible=False, run=None, options=None): + def __call__(self, ref, package_id_mode=None, visible=False, run=None, options=None, + override=None): # TODO: Check which arguments could be user-defined self._requires.build_require(ref, package_id_mode=package_id_mode, visible=visible, run=run, - options=options) + options=options, override=override) class ToolRequirements: @@ -485,7 +486,7 @@ def __call__(self, str_ref, **kwargs): self._requires[req] = req def build_require(self, ref, raise_if_duplicated=True, package_id_mode=None, visible=False, - run=None, options=None): + run=None, options=None, override=None): """ Represent a generic build require, could be a tool, like "cmake" or a bundle of build scripts. @@ -501,7 +502,7 @@ def build_require(self, ref, raise_if_duplicated=True, package_id_mode=None, vis # FIXME: This raise_if_duplicated is ugly, possibly remove ref = RecipeReference.loads(ref) req = Requirement(ref, headers=False, libs=False, build=True, run=run, visible=visible, - package_id_mode=package_id_mode, options=options) + package_id_mode=package_id_mode, options=options, override=override) if raise_if_duplicated and self._requires.get(req): raise ConanException("Duplicated requirement: {}".format(ref)) diff --git a/conans/test/integration/build_requires/build_requires_test.py b/conans/test/integration/build_requires/build_requires_test.py index 206da0c84d9..abcb75f8cad 100644 --- a/conans/test/integration/build_requires/build_requires_test.py +++ b/conans/test/integration/build_requires/build_requires_test.py @@ -1,3 +1,4 @@ +import json import os import platform import textwrap @@ -747,3 +748,148 @@ def requirements(self): c.assert_listed_require({"dep/1.0": "Cache"}) c.run("create consumer --build-require") assert "dep/1.0" not in c.out + + +class TestBuildTrackHost: + + def test_overriden_host_but_not_build(self): + """ + Making the ``tool_requires(..., visible=True)`` works, and allows overriding, but + propagates the build-requirement to protobuf/protoc down the graph, and VirtualBuildEnv + will put ``protoc`` from it in the PATH. Not a problem in majority of cases, but not the + cleanest + """ + c = TestClient() + pkg = textwrap.dedent(""" + from conan import ConanFile + class ProtoBuf(ConanFile): + name = "pkg" + version = "0.1" + def requirements(self): + self.requires("protobuf/1.0") + def build_requirements(self): + self.tool_requires("protobuf/1.0", visible=True) + """) + c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), + "pkg/conanfile.py": pkg, + "app/conanfile.py": GenConanfile().with_requires("pkg/0.1") + .with_requirement("protobuf/1.1", override=True) + .with_build_requirement("protobuf/1.1", + override=True)}) + c.run("create protobuf --version=1.0") + c.run("create protobuf --version=1.1") + c.run("create pkg") + c.run("install app") + c.assert_listed_require({"protobuf/1.1": "Cache"}) + c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) + + def test_overriden_host_version(self): + """ + Make the tool_requires follow the regular require with the expression "" + """ + c = TestClient() + pkg = textwrap.dedent(""" + from conan import ConanFile + class ProtoBuf(ConanFile): + name = "pkg" + version = "0.1" + def requirements(self): + self.requires("protobuf/1.0") + def build_requirements(self): + self.tool_requires("protobuf/") + """) + c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), + "pkg/conanfile.py": pkg, + "app/conanfile.py": GenConanfile().with_requires("pkg/0.1") + .with_requirement("protobuf/1.1", override=True)}) + c.run("create protobuf --version=1.0") + c.run("create protobuf --version=1.1") + c.run("create pkg") + c.run("install pkg") # make sure it doesn't crash + c.run("install app") + c.assert_listed_require({"protobuf/1.1": "Cache"}) + c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) + # verify locks work + c.run("lock create app") + lock = json.loads(c.load("app/conan.lock")) + build_requires = lock["build_requires"] + assert len(build_requires) == 1 + assert "protobuf/1.1" in build_requires[0] + # lock can be used + c.run("install app --lockfile=app/conan.lock") + c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) + + def test_overriden_host_version_version_range(self): + """ + same as above, but using version ranges instead of overrides + """ + c = TestClient() + pkg = textwrap.dedent(""" + from conan import ConanFile + class ProtoBuf(ConanFile): + name = "pkg" + version = "0.1" + def requirements(self): + self.requires("protobuf/[*]") + def build_requirements(self): + self.tool_requires("protobuf/") + """) + c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), + "pkg/conanfile.py": pkg, + "app/conanfile.py": GenConanfile().with_requires("pkg/0.1")}) + c.run("create protobuf --version=1.0") + c.run("create pkg") + c.run("install pkg") # make sure it doesn't crash + c.run("install app") + c.assert_listed_require({"protobuf/1.0": "Cache"}) + c.assert_listed_require({"protobuf/1.0": "Cache"}, build=True) + + c.run("create protobuf --version=1.1") + c.run("install pkg") # make sure it doesn't crash + c.run("install app") + c.assert_listed_require({"protobuf/1.1": "Cache"}) + c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) + # verify locks work + c.run("lock create app") + lock = json.loads(c.load("app/conan.lock")) + build_requires = lock["build_requires"] + assert len(build_requires) == 1 + assert "protobuf/1.1" in build_requires[0] + # lock can be used + c.run("install app --lockfile=app/conan.lock") + c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True) + + def test_track_host_error_nothost(self): + """ + if no host requirement is defined, it will be an error + """ + c = TestClient() + pkg = textwrap.dedent(""" + from conan import ConanFile + class ProtoBuf(ConanFile): + name = "protobuf" + def build_requirements(self): + self.tool_requires("protobuf/") + """) + + c.save({"pkg/conanfile.py": pkg}) + c.run("install pkg", assert_error=True) + assert "ERROR: protobuf/None require 'protobuf/': " \ + "didn't find a matching host dependency" in c.out + + def test_track_host_errors_trait(self): + """ + It is not possible to make host_version visible too + """ + c = TestClient() + pkg = textwrap.dedent(""" + from conan import ConanFile + class ProtoBuf(ConanFile): + name = "protobuf" + def requirements(self): + self.tool_requires("other/", visible=True) + """) + c.save({"pkg/conanfile.py": pkg}) + c.run("install pkg", assert_error=True) + assert "ERROR: protobuf/None require 'other/': 'host_version' " \ + "can only be used for non-visible tool_requires" in c.out