diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py index a9015d9284b..243d6e309fd 100644 --- a/conans/client/graph/graph_builder.py +++ b/conans/client/graph/graph_builder.py @@ -404,6 +404,18 @@ def _config_node(node, down_ref, down_options): def _resolve_recipe(self, current_node, dep_graph, requirement, check_updates, update, remotes, profile, graph_lock, original_ref=None): + + ref = requirement.ref + if ref.version == "": + if not requirement.build_require: + raise ConanException(f"{current_node.ref} uses '' in requires, " + "it is only allowed in tool_requires") + transitive = current_node._public_deps.get(ref.name, context="host") + if transitive is None: + raise ConanException( + f"{current_node.ref} require '{ref}': didn't find a matching host dependency") + requirement.ref = transitive.ref + try: result = self._proxy.get_recipe(requirement.ref, check_updates, update, remotes, self._recorder) diff --git a/conans/model/ref.py b/conans/model/ref.py index ae57366b040..9361a68ef58 100644 --- a/conans/model/ref.py +++ b/conans/model/ref.py @@ -153,7 +153,7 @@ def validate_name(name, reference_token=None): @staticmethod def validate_version(version, pkg_name): ConanName.validate_string(version) - if version == "*": + if version == "*" or version == "": return if ConanName._validation_pattern.match(version) is None: if ( diff --git a/conans/test/integration/build_requires/build_requires_test.py b/conans/test/integration/build_requires/build_requires_test.py index 48579d65181..7df07c4f9fc 100644 --- a/conans/test/integration/build_requires/build_requires_test.py +++ b/conans/test/integration/build_requires/build_requires_test.py @@ -1,7 +1,9 @@ +import json import os import textwrap import unittest +import pytest from parameterized.parameterized import parameterized from conans.model.ref import ConanFileReference @@ -527,3 +529,102 @@ class BuildReqConan(ConanFile): client.save({"conanfile.txt": consumer}, clean_first=True) client.run("install . --build=missing") assert "Applying build-requirement: build_req/1.0@test/test" in client.out + + +class TestBuildTrackHost: + + @pytest.mark.parametrize("build_profile", [False, True]) + def test_overriden_host_version(self, build_profile): + """ + 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 1.0@") + c.run("create protobuf 1.1@") + build_profile = "-pr:b default" if build_profile else "" + c.run(f"create pkg {build_profile}") + c.run(f"install pkg {build_profile}") # make sure it doesn't crash + c.run(f"install app {build_profile}") + assert "protobuf/1.1" in c.out + assert "protobuf/1.0:" not in c.out + + # verify locks work + c.run(f"lock create app/conanfile.py {build_profile}") + lock = c.load("conan.lock") + assert "protobuf/1.1" in lock + assert "protobuf/1.0" not in lock + # lock can be used + c.run(f"install app --lockfile=conan.lock") + assert "protobuf/1.1" in c.out + assert "protobuf/1.0:" not in c.out + + @pytest.mark.parametrize("build_profile", [False, True]) + def test_overriden_host_version_version_range(self, build_profile): + """ + same as above, but using version ranges instead of overrides + """ + build_profile = "-pr:b default" if build_profile else "" + c = TestClient() + c.save({"protobuf/conanfile.py": GenConanfile("protobuf"), + "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requirement("protobuf/[*]") + .with_build_requirement("protobuf/"), + "app/conanfile.py": GenConanfile().with_requires("pkg/0.1")}) + c.run("create protobuf 1.0@") + c.run(f"create pkg {build_profile}") + c.run(f"install pkg {build_profile}") # make sure it doesn't crash + c.run(f"install app {build_profile}") + assert "protobuf/1.0" in c.out + assert "protobuf/1.1" not in c.out + + c.run("create protobuf 1.1@") + c.run(f"install pkg {build_profile}") # make sure it doesn't crash + c.run(f"install app {build_profile}") + assert "protobuf/1.1" in c.out + assert "protobuf/1.0" not in c.out + + # verify locks work + c.run(f"lock create app/conanfile.py {build_profile}") + lock = c.load("conan.lock") + assert "protobuf/1.1" in lock + assert "protobuf/1.0" not in lock + # lock can be used + c.run("install app --lockfile=conan.lock") + assert "protobuf/1.1" in c.out + assert "protobuf/1.0:" not in c.out + + @pytest.mark.parametrize("build_profile", [False, True]) + def test_track_host_error_nothost(self, build_profile): + """ + if no host requirement is defined, it will be an error + """ + build_profile = "-pr:b default" if build_profile else "" + c = TestClient() + c.save({"conanfile.py": + GenConanfile("pkg").with_build_requirement("protobuf/")}) + c.run(f"install . {build_profile}", assert_error=True) + assert "'protobuf/': didn't find a matching host dependency" in c.out + + @pytest.mark.parametrize("build_profile", [False, True]) + def test_track_host_error_wrong_context(self, build_profile): + """ + it can only be used by tool_requires, not regular requires + """ + build_profile = "-pr:b default" if build_profile else "" + c = TestClient() + c.save({"conanfile.py": GenConanfile("pkg").with_requirement("protobuf/")}) + c.run(f"install . {build_profile}", assert_error=True) + assert "uses '' in requires" in c.out