Repository: conan-io/conan
Pull request: Add different ref tracking syntax for  `<host_version>`
Changelog: Feature: Add tracking syntax in `<host_version>` for different references.
Docs: https://github.com/conan-io/docs/pull/3480

Close https://github.com/conan-io/conan/issues/15265

I don't like the current approach, so opening the PR so that I can get some ideas from the team


Problem statement:
[feature] <host_version>: ability to reference a dependency of a different name
### What is your suggestion?

To cover cases where we have:
* self.requires()
* self.tool_requires()

Against a pair of references that could be separate components of the same tool - e.g. `libgettext` vs `gettext`, or an hypothetical `libprotobuf` vs `protobuf-compiler`, when we need a 1:1 version matching.





### Have you read the CONTRIBUTING guide?

- [ ] I've read the CONTRIBUTING guide

Hints from issue discussion:
Some of the syntaxes I recall we originally discussed was
```
self.tool_requires("pkg/<host_version(foo)>")
self.tool_requires("pkg/<host_version:foo>")
```

I personally like the second one a bit better, but happy to hear other opnions

Commit message: Add remote input to build-map command

Patch to review:
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
index 27d8449d82c..e513cd05bde 100644
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -1,5 +1,6 @@
 import copy
 import os
+import re
 from collections import deque
 
 from conans.client.conanfile.configure import run_configure_method
@@ -13,6 +14,7 @@
 from conans.errors import ConanException
 from conans.model.conan_file import ConanFile
 from conans.model.options import Options
+from conans.model.package_ref import PkgReference
 from conans.model.recipe_ref import RecipeReference, ref_matches
 from conans.model.requires import Requirement
 
@@ -270,14 +272,21 @@ def _resolve_replace_requires(self, node, require, profile_build, profile_host,
             break  # First match executes the alternative and finishes checking others
 
     def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock):
-        if require.ref.version == "<host_version>":
+        require_version = str(require.ref.version)
+        if require_version == "<host_version>" or require_version.startswith("<host_version") and require_version.endswith(">"):
             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)
+            tracking_ref = require_version.split(':')
+            ref = require.ref
+            if len(tracking_ref) > 1:
+                temp = RecipeReference.loads(str(node.ref))
+                temp.name = tracking_ref[1][:-1]  # Remove the trailing >
+                ref = temp
+            req = Requirement(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 "
+                raise ConanException(f"{node.ref} require '{ref}': didn't find a matching "
                                      "host dependency")
             require.ref.version = transitive.require.ref.version
 
diff --git a/conans/test/integration/build_requires/build_requires_test.py b/conans/test/integration/build_requires/build_requires_test.py
index 2f09809a6fe..d47306d8e0a 100644
--- a/conans/test/integration/build_requires/build_requires_test.py
+++ b/conans/test/integration/build_requires/build_requires_test.py
@@ -512,6 +512,28 @@ def test_overriden_host_version_transitive_deps(self):
         c.run("install app --lockfile=app/conan.lock")
         c.assert_listed_require({"protobuf/1.1": "Cache"}, build=True)
 
+    @pytest.mark.parametrize("host_version, assert_error, assert_msg", [
+        ("libgettext>", False, "gettext/0.2#d9f9eaeac9b6e403b271f04e04149df2"),
+        # Error cases, just checking that we fail gracefully - no tracebacks
+        ("libgettext", True, "Package 'gettext/<host_version:libgettext' not resolved"),
+        (":>", True, "app/1.0 require '/1.0': didn't find a matching host dependency"),
+        (">", True, "app/1.0 require '/1.0': didn't find a matching host dependency"),
+        (":", True, " Package 'gettext/<host_version::' not resolved"),
+        ("", True, "Package 'gettext/<host_version:' not resolved: No remote defined")
+    ])
+    def test_host_version_different_ref(self, host_version, assert_error, assert_msg):
+        tc = TestClient(light=True)
+        tc.save({"gettext/conanfile.py": GenConanfile("gettext"),
+                 "libgettext/conanfile.py": GenConanfile("libgettext"),
+                 "app/conanfile.py": GenConanfile("app", "1.0").with_requires("libgettext/[>0.1]")
+                                                   .with_tool_requirement(f"gettext/<host_version:{host_version}")})
+        tc.run("create libgettext --version=0.2")
+        tc.run("create gettext --version=0.1 --build-require")
+        tc.run("create gettext --version=0.2 --build-require")
+
+        tc.run("create app", assert_error=assert_error)
+        assert assert_msg in tc.out
+
 
 def test_build_missing_build_requires():
     c = TestClient()


Code review comments:
diff_hunk: @@ -270,14 +272,21 @@ def _resolve_replace_requires(self, node, require, profile_build, profile_host,
             break  # First match executes the alternative and finishes checking others
 
     def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock):
-        if require.ref.version == "<host_version>":
+        require_version = str(require.ref.version)
+        if require_version == "<host_version>" or require_version.startswith("<host_version") and require_version.endswith(">"):
             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)
+            tracking_ref = require_version.split(':')
+            ref = require.ref
+            if len(tracking_ref) > 1:
+                temp = RecipeReference.loads(str(node.ref))
+                temp.name = tracking_ref[1][:-1]  # Remove the trailing >
+                ref = temp
path: conans/client/graph/graph_builder.py
text: @user1:
```suggestion
                ref = RecipeReference.loads(str(node.ref))
                ref.name = tracking_ref[1][:-1]  # Remove the trailing >
```

diff_hunk: @@ -270,14 +272,21 @@ def _resolve_replace_requires(self, node, require, profile_build, profile_host,
             break  # First match executes the alternative and finishes checking others
 
     def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock):
-        if require.ref.version == "<host_version>":
+        require_version = str(require.ref.version)
+        if require_version == "<host_version>" or require_version.startswith("<host_version") and require_version.endswith(">"):
path: conans/client/graph/graph_builder.py
text: @user1:
```suggestion
        if require_version.startswith("<host_version") and require_version.endswith(">"):
```
good enough of a check?

diff_hunk: @@ -270,14 +272,21 @@ def _resolve_replace_requires(self, node, require, profile_build, profile_host,
             break  # First match executes the alternative and finishes checking others
 
     def _create_new_node(self, node, require, graph, profile_host, profile_build, graph_lock):
-        if require.ref.version == "<host_version>":
+        require_version = str(require.ref.version)
+        if require_version == "<host_version>" or require_version.startswith("<host_version") and require_version.endswith(">"):
             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)
+            tracking_ref = require_version.split(':')
path: conans/client/graph/graph_builder.py
text: @user1:
```suggestion
            tracking_ref = require_version.split(':', 1)
```