Repository: conan-io/conan

Issue:
[bug] semver fails to resolve metadata field correctly
### Environment details

* Operating System+version: Ubuntu 22.04
* Compiler+version: N/A
* Conan version: 2.0.17
* Python version: 3.10.8


### Steps to reproduce

1. Have a package with a version number followed by a "metadata (i.e. preceded by +)" field containing a number. For example: ```1.0.0+2```. NOTE: There cannot be a "larger" version available, such as ```1.0.0+3``` if you want to reproduce this.
2. in a conanfile, require ```>=``` this version. For example: ```self.requires("package/[>=1.0.0+2]")```
3. Try to resolve dependencies. Conan will fail with: ```ERROR: Package 'package/[>=1.0.0+2]' not resolved: Version range '>=1.0.0+2' from requirement 'package/[>=1.0.0+2]' [etc, etc...] ```

I can verify that this should not be the case by running:
```
>>> from conans.model.version import Version
>>> Version("1.0.0+2") >= Version("1.0.0+2")
True
>>> Version("1.0.0+2") >= Version("1.0.0+3")
False
```

Note also that if I set the metadata number to one LESS than the exisiting version, it will be able to find it! If I set ```self.requires("package/[>=1.0.0+1]")``` it will be able to find version ```1.0.0+2```.

This seems like a bug to me?

FYI: we use this field to hold  "build number" in case we run another build without changing the version. It increments after each build. It's useful to us to be able to specify that we should not use a build which is older than some build number.

### Logs

_No response_


Discussion:
Hi @antoinebardoz thanks a lot for the report.
This seems to be an issue with a somewhat recent change to how Conan handles prereleases and version ranges, where it's not expecting build metadata in the range.

I've opened https://github.com/conan-io/conan/pull/15653 to track the fix, but I anticipate that some more investigation is still needed on our part, will keep you posted :)



Patch:
diff --git a/conans/model/version_range.py b/conans/model/version_range.py
index 4360758a109..97778f68cf6 100644
--- a/conans/model/version_range.py
+++ b/conans/model/version_range.py
@@ -12,7 +12,7 @@ def __init__(self, operator, version):
         self.display_version = version
 
         value = str(version)
-        if (operator == ">=" or operator == "<") and "-" not in value:
+        if (operator == ">=" or operator == "<") and "-" not in value and version.build is None:
             value += "-"
         self.version = Version(value)
 


Test patch:
diff --git a/conans/test/unittests/model/version/test_version_comparison.py b/conans/test/unittests/model/version/test_version_comparison.py
index 27f52f75ab3..da480377edf 100644
--- a/conans/test/unittests/model/version/test_version_comparison.py
+++ b/conans/test/unittests/model/version/test_version_comparison.py
@@ -59,6 +59,13 @@
      ("0.0.1-3-1", "0.0.1"),
      ("0.0.1-3-1", "0.0.1-4"),
      ("0.0.1-3", "0.0.1-3-1"),  # 3 is less than 3-1, pure alphanumeric ID "3-1"
+     # build + prerelease
+     ("1.1-pre.1", "1.1+b.1"),
+     ("1.1-pre.1", "1.1-pre.1+b.1"),
+     ("1.1-pre.1+b.1", "1.1-pre.1+b.2"),
+     ("1.1+b.1", "1.2-pre.1+b.2"),
+     ("1.0.0-+2", "1.0.0+2"),  # Note it is not the number, but the prerelease
+     ("1.0.0-+2", "1.0.0+1"),  # Even if the build metadata is lower the first one is pre-release
      # cci. schema used in CCI
      ("cci.20230101", "cci.20240101"),
      ]
diff --git a/conans/test/unittests/model/version/test_version_range.py b/conans/test/unittests/model/version/test_version_range.py
index 3d45847a245..263caf65f82 100644
--- a/conans/test/unittests/model/version/test_version_range.py
+++ b/conans/test/unittests/model/version/test_version_range.py
@@ -35,9 +35,19 @@
                            ["1.0", "1.2", "3.3"],  ["1-pre.1", "1.5-a1", "3.3-a1"]],
     ['^1.1.2',  [[['>=', '1.1.2-'], ['<', '2.0.0-']]], ["1.2.3"],  ["1.2.0-alpha1", "2.0.0-alpha1"]],
     ['~1.1.2',  [[['>=', '1.1.2-'], ['<', '1.2.0-']]], ["1.1.3"],  ["1.1.3-alpha1", "1.2.0-alpha1"]],
-    ['>=2.0-pre.0 <3', [[['>=', '2.0-pre.0'], ['<', '3-']]], ["2.1"], ["2.0-pre.1","2.0-alpha.1"]],
+    ['>=2.0-pre.0 <3', [[['>=', '2.0-pre.0'], ['<', '3-']]], ["2.1"], ["2.0-pre.1", "2.0-alpha.1"]],
+    # Build metadata
+    ['>=1.0.0+2', [[['>=', '1.0.0+2']]], ["1.0.0+2", "1.0.0+3"], ["1.0.0+1"]],
+    ['>=1.0.0', [[['>=', '1.0.0-']]], ["1.0.0+2", "1.0.0+3"], []],
+    # Build metadata and pre-releases
+    ['>=1.0.0-pre.1+2', [[['>=', '1.0.0-pre.1+2']]], ["1.0.0+1", "1.0.0+2"], ["1.0.0-pre.1+3"]], # excluded 1+3 because is a pre-release!
+    ['>=1.0.0-pre.1+2, include_prerelease=True', [[['>=', '1.0.0-pre.1+2']]], ["1.0.0+1", "1.0.0+2", "1.0.0-pre.1+3"], ["1.0.0-pre.1+1"]],
+    ['<1.0.1-pre.1+2', [[['<', '1.0.1-pre.1+2']]], ["1.0.0+1", "1.0.0+2"], ["1.0.0-pre.1+2"]],
+    ['<1.0.1-pre.1+2, include_prerelease=True', [[['<', '1.0.1-pre.1+2']]], ["1.0.0+1", "1.0.0+2", "1.0.1-pre.1+1"], ["1.0.1-pre.1+2"]],
     # Or explicitly
-    ['>=2.0-pre.0, include_prerelease', [[['>=', '2.0-pre.0']]], ["2.1", "2.0-pre.1"], ["1.5"]]
+    ['>=2.0-pre.0, include_prerelease', [[['>=', '2.0-pre.0']]], ["2.1", "2.0-pre.1"], ["1.5"]],
+    # Build metadata
+    ['>=1.0.0+2', [[['>=', '1.0.0+2']]], ["1.0.0+2", "1.0.0+3"], ["1.0.0+1"]],
 ]