Repository: conan-io/conan
Pull request #17931 (2025-03-11 09:13:12)

fix build-profile self-loop when version ranges
Changelog: Bugfix: Avoid self-requirement and loop when a ``[tool_requires]`` in the build profile depends on itself with a version range.
Docs: Omit


Close https://github.com/conan-io/conan/issues/17930


Patch:
diff --git a/conan/internal/model/requires.py b/conan/internal/model/requires.py
index f5e5aae6f..b5fc44c63 100644
--- a/conan/internal/model/requires.py
+++ b/conan/internal/model/requires.py
@@ -174,7 +174,7 @@ class Requirement:
         or None if it is not an expression
         """
         version = repr(self.ref.version)
-        if version.startswith("[") and version.endswith("]"):
+        if version[0] == "[" and version[-1] == "]":
             return VersionRange(version[1:-1])
 
     @property
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
index af016a0e8..b4c6ed365 100644
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -20,7 +20,7 @@ from conan.api.model import RecipeReference
 from conan.internal.model.requires import Requirement
 
 
-class DepsGraphBuilder(object):
+class DepsGraphBuilder:
 
     def __init__(self, proxy, loader, resolver, cache, remotes, update, check_update, global_conf):
         self._proxy = proxy
@@ -197,8 +197,16 @@ class DepsGraphBuilder(object):
         for pattern, tool_requires in profile.tool_requires.items():
             if ref_matches(ref, pattern, is_consumer=conanfile._conan_is_consumer):  # noqa
                 for tool_require in tool_requires:  # Do the override
-                    if str(tool_require) == str(ref):  # FIXME: Ugly str comparison
-                        continue  # avoid self-loop of build-requires in build context
+                    # Check if it is a self-loop of build-requires in build context and avoid it
+                    if ref and tool_require.name == ref.name and tool_require.user == ref.user and \
+                            tool_require.channel == ref.channel:
+                        if tool_require.version == ref.version:
+                            continue
+                        # Also avoid it if the version is in the range
+                        version_range = repr(tool_require.version)
+                        if version_range[0] == "[" and version_range[-1] == "]":
+                            if ref.version.in_range(version_range[1:-1]):
+                                continue
                     node.conanfile.requires.tool_require(tool_require.repr_notime(),
                                                          raise_if_duplicated=False)
 


Test patch:
diff --git a/test/integration/build_requires/profile_build_requires_test.py b/test/integration/build_requires/profile_build_requires_test.py
index 1d7c9c4a3..946ce389f 100644
--- a/test/integration/build_requires/profile_build_requires_test.py
+++ b/test/integration/build_requires/profile_build_requires_test.py
@@ -254,3 +254,20 @@ def test_tool_requires_revision_profile():
     c.run("graph info app -pr:b=build_profile --build=*")
     assert f"tool/0.1#{rev1}" in c.out
     assert rev2 not in c.out
+
+
+def test_tool_requires_version_range_loop():
+    # https://github.com/conan-io/conan/issues/17930
+    c = TestClient(light=True)
+    build_profile = textwrap.dedent("""\
+        [settings]
+        os=Linux
+        [tool_requires]
+        tool/[>=1.0 <2]
+        """)
+    c.save({"tool/conanfile.py": GenConanfile("tool", "1.1"),
+            "app/conanfile.py": GenConanfile("app", "0.1").with_tool_requires("tool/1.1"),
+            "build_profile": build_profile})
+    c.run("create tool")
+    c.run("install app -pr:b=build_profile")
+    assert "tool/1.1" in c.out  # It is skipped
diff --git a/test/unittests/model/version/test_version_comparison.py b/test/unittests/model/version/test_version_comparison.py
index a0d6341b9..0b8e475e0 100644
--- a/test/unittests/model/version/test_version_comparison.py
+++ b/test/unittests/model/version/test_version_comparison.py
@@ -1,7 +1,6 @@
 import pytest
 
 from conan.internal.model.version import Version
-from conan.internal.model.version_range import VersionRange
 
 v = [("1", "2"),
      ("1.0", "1.1"),