Repository: conan-io/conan

Issue:
Conan prioritizes indirect dependency over direct dependency
I'm having the following component tree:

* LibConsumer uses LibCore/1.0.0+abc@user/channel
* MyComp uses LibConsumer and LibCore/1.0.0+xyz@user/channel

conanfile.py in MyComp:

```
...
    def requirements(self):
        self.requires( "LibConsumer/1.0.0@user/channel") #indirectly uses LibCore/1.0.0+abc@user/channel
        self.requires( "LibCore/1.0.0+xyz@user/channel")
...
```

When running a `conan install` on MyComp Conan resolves LibCore to `LibCore/1.0.0+abc@user/channel`. This is probably due to LibConsumer being first in the list of requires and the SemVer of both LibCore packages not being different. When I swap the requires-statement to the following Conan resolves LibCore to `LibCore/1.0.0+xyz@user/channel`:

```
...
    def requirements(self):
        self.requires( "LibCore/1.0.0+xyz@user/channel")
        self.requires( "LibConsumer/1.0.0@user/channel")
...
```

I would have expected Conan to always prefer the direct dependencies over the indirect dependencies, thus always resolving to `LibCore/1.0.0+xyz@user/channel` because it has explicitly been named. I know that there isn't a difference between from a SemVer viewpoint, but there could be reasons why someone put the one over the other into the conanfile.

Is there a way to enforce using the direct deps over the indirect ones? Should this be changed by default?




Discussion:
I've been able to reproduce. The reason is that Conan compares the versions and by definition, the `Build metadata` (+xyz) is discarded, so it considers both references equal. Nevertheless, if the reference is not the same (and the package either) I think this behavior is not correct.
I would try to propose a fix but I'm a bit afraid it can break something else.

Right now it breaks our builds at least :/

Sometimes our CI rebuilds a package and changes a dependency (we're using aliases). The version of the package hasn't changed, but it is "something else" than before. Therefore we add the Git SHA to the metadata. Sometimes a project wants to use a specific version of a package, down to the metadata level.
We could fix this by increasing the version number everytime we make a change. But that'd be quite complex. The CI would need to increase the version number automatically, recommit this into the repo etc.

Would it be possible to fix this with a config value? Something like `preferExplicitRequirements = True` or `doNotIgnoreSemverMetadata = True`?
I'm not saying this is not going to be fixed, I'm investigating right now, I'll update asap.

Resolving patch:
diff --git a/conans/model/requires.py b/conans/model/requires.py
--- a/conans/model/requires.py
+++ b/conans/model/requires.py
@@ -146,7 +146,6 @@ def update(self, down_reqs, output, own_ref, down_ref):
 
                     output.warn(msg)
                     req.ref = other_ref
-
             new_reqs[name] = req
         return new_reqs
 
diff --git a/conans/model/version.py b/conans/model/version.py
--- a/conans/model/version.py
+++ b/conans/model/version.py
@@ -143,7 +143,12 @@ def get_el(a_list, index):
         equals = all(get_el(other.as_list, ind) == get_el(self.as_list, ind)
                      for ind in range(0, max(len(other.as_list), len(self.as_list))))
         if equals:
-            return 0
+            if self.build == other.build:
+                return 0
+            if self.build > other.build:
+                return -1
+            else:
+                return 1
 
         # Check greater than or less than
         other_list = other.as_list


Test patch:
diff --git a/conans/test/functional/command/create_test.py b/conans/test/functional/command/create_test.py
--- a/conans/test/functional/command/create_test.py
+++ b/conans/test/functional/command/create_test.py
@@ -6,7 +6,7 @@
 
 from conans.client import tools
 from conans.model.ref import ConanFileReference, PackageReference
-from conans.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID
+from conans.test.utils.tools import TestClient, NO_SETTINGS_PACKAGE_ID, GenConanfile
 from conans.util.files import load
 
 
@@ -38,6 +38,30 @@ def package_info(self):
         cmake = client.load("conanbuildinfo.cmake")
         self.assertIn("set(CONAN_LIBS LibB LibA ${CONAN_LIBS})", cmake)
 
+    def can_override_even_versions_with_build_metadata_test(self):
+        # https://github.com/conan-io/conan/issues/5900
+
+        client = TestClient()
+        client.save({"conanfile.py":
+                    GenConanfile().with_name("libcore").with_version("1.0+abc")})
+        client.run("create .")
+        client.save({"conanfile.py":
+                    GenConanfile().with_name("libcore").with_version("1.0+xyz")})
+        client.run("create .")
+
+        client.save({"conanfile.py":
+                    GenConanfile().with_name("intermediate").
+                    with_version("1.0").with_require_plain("libcore/1.0+abc")})
+        client.run("create .")
+
+        client.save({"conanfile.py":
+                    GenConanfile().with_name("consumer").
+                    with_version("1.0").with_require_plain("intermediate/1.0").
+                    with_require_plain("libcore/1.0+xyz")})
+        client.run("create .")
+        self.assertIn("WARN: intermediate/1.0: requirement libcore/1.0+abc "
+                      "overridden by consumer/1.0 to libcore/1.0+xyz", client.out)
+
     def transitive_same_name_test(self):
         # https://github.com/conan-io/conan/issues/1366
         client = TestClient()
diff --git a/conans/test/unittests/model/version_test.py b/conans/test/unittests/model/version_test.py
--- a/conans/test/unittests/model/version_test.py
+++ b/conans/test/unittests/model/version_test.py
@@ -55,6 +55,14 @@ def simple_test(self):
         self.assertFalse(Version("4.0.0.1") == "4")
         self.assertTrue(Version("4.0.0.1") >= "4")
 
+    def test_build_metadata_is_not_equal(self):
+        # https://github.com/conan-io/conan/issues/5900
+        self.assertNotEqual(Version("4.0.0+abc"), Version("4.0.0+xyz"))
+        # Shouldn't be an "official" order for build metadata, but as they cannot be equal
+        # the order is alphabetic
+        self.assertTrue(Version("4.0.0+abc") > Version("4.0.0+xyz"))
+        self.assertTrue(Version("4.0.0+xyz") < Version("4.0.0+abc"))
+
     def text_test(self):
         v1 = Version("master+build2")
         self.assertEqual(v1.major(), "master")