Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[bug] version range selection from pre-release order #17631

Open
mrjoel opened this issue Jan 25, 2025 · 3 comments · May be fixed by #17654
Open

[bug] version range selection from pre-release order #17631

mrjoel opened this issue Jan 25, 2025 · 3 comments · May be fixed by #17654
Assignees

Comments

@mrjoel
Copy link
Contributor

mrjoel commented Jan 25, 2025

Describe the bug

In Conan 2 the version range selection criteria with pre-release versions differs from Conan 1. That's not itself problematic, however it appears that the Conan 2 behavior is incorrect with regards to SemVer semantics. Based on my investigation I'm suggesting that the bug is both in code and the docs.

Specifically, SemVer 2.0 11.3 stipulates that

When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version

Example: 1.0.0-alpha < 1.0.0.

We use a package with automatic version assignment based on git for development snapshots of dependencies. We add pre-release tags to indicate development towards the next anticipated release, but since it's developmental we don't want to have the final tag be a suitable version. (We may also add reved pre-release tags several times as an indicator for when compatibility boundaries occur within the development cycle).

As noted on the version ranges page, pre-release versions can be nuanced. I'm also checking my expectations, but there does seem to be an issue here. I don't make the suggestion lightly; notably, I'll acknowledge that I'm also suggesting that semver-checker is also wrong when comparing pre-release versions by strict less-than operator (including for the specific example in SemVer 11.3).

The following two example files illustrate my expectation. The dependency works in Conan 1, but Conan 2 doesn't match on the strictly less than operator.

from conan import ConanFile

class PackageA(ConanFile):
    name = "packagea"
    version = "2.1.0-0.dev+123.g0abcdef"
from conan import ConanFile, conan_version

class PackageB(ConanFile):
    name = "packageb"
    version = "1.0.0"

    def requirements(self):
        prerelease = "include_prereleases" if conan_version.major==2 else "include_prerelease=True"
        self.requires(
            # The full expression I actually want, but doesn't work.
            #f"packagea/[>=2.1.0-0.dev <2.1.0 || 2.0.0, {prerelease}]"

            # The OR expression part doesn't seem to be relevant.
            f"packagea/[>=2.1.0-0.dev <2.1.0, {prerelease}]"

            # Using <= instead of < does strangely allow selection of the pre-release package.
            #f"packagea/[>=2.1.0-0.dev <=2.1.0, {prerelease}]"
        )

As for documentation, the following pages appear to have inconsistencies/inaccuracies.

  • version ranges page

    The green important admonition early in the page is correct when it states

    "So 1.1-alpha.1 is older than 1.1, not newer."

    However, when discussing the include_prerelease option later in the page, it seems incorrect to state that for the example expression requires = "pkg/[>1 <2, include_prerelease]" that 1.0-pre.1 should be included, or that 2.0-pre1 should be excluded. Strictly speaking, 1.0-pre.1 is earlier than >1 should allow, and 2.0-pre1 is definitely earlier (less) than the full 2.0.0 release implied by <2.

  • requires attribute

    Similarly, the pre-releases table of examples is not as I'd expect either.

    • For [>=1.0 <2], the value 1.0.0-pre.1 should not be included, while 2.0-pre.1 should be
    • For [<3.2.1], the value 3.2.1-pre.1 should be included

How to reproduce it

No response

@memsharded memsharded self-assigned this Jan 26, 2025
@memsharded
Copy link
Member

Hi @mrjoel

This is something that we thoroughly discussed and iterated, based on user feedback and expectations.
At the moment it doesn't seem a bug, but expected design:

For [>=1.0 <2], the value 1.0.0-pre.1 should not be included, while 2.0-pre.1 should be

The logic is that 1.0.0-pre.1 is a 1.0 release. It is not a 0.9 one, but a 1.0 one

For [<3.2.1], the value 3.2.1-pre.1 should be included

Likewise, the 3.2.1-pre.1 is also a 3.2.1 release.

So the logic to include and exclude them is correct.

I'll also like to clarify that in Conan 2, hardcoding the resolution to prereleases in requires is not the most recommended approach, and an new conf core.version_ranges:resolve_prereleases (see https://docs.conan.io/2/devops/versioning/resolve_prereleases.html#handling-version-ranges-and-pre-releases)

I'll give an example that makes this more evident:

  • A team has a project with some dependency to one of their packages mypkg/[>=1.0 <2.0]
  • They happily release patches and minors, that are API compatible and move forward to the mypkg/1.3.8 for example
  • When they are not sure about some API compatible but possibly behavior breaking releases like mypkg/1.4, they create prereleases like mypkg/1.4-pre.1
  • The conf core.version_ranges:resolve_prereleases is activated whenever the CI or developers want to test these prereleases
  • Now the team starts to work in an API breaking major release, because they need to move forward a big change in mypkg
  • They start to work in that version, it will take time, so they start to create alpha prereleases like mypkg/2.0-alpha.1
  • If these pre-releases are automatically accepted by the mypkg/[>=1.0 <2.0] range, that will break all the consumers that were depending on mypkg
  • Even if they explicitly said "we don't want to depend on the next API breaking major releases 2.0 (<2.0, strictly lower than 2.0)

Then, it was concluded that even if 2.0-alpha < 2.0 in strict version ordering, the version ranges like mypkg/[>=1.0 <2.0] are semantically equivalent to mypkg/[>=1.0- <2.0-], that is, including prereleases in the lower bound for >=, and excluding prereleases in the upper bound for strictly lower than < bounds.

@mrjoel
Copy link
Contributor Author

mrjoel commented Jan 28, 2025

The logic is that 1.0.0-pre.1 is a 1.0 release. It is not a 0.9 one, but a 1.0 one.

That seems somewhat unfortunate and non-intuitive from a SemVer perspective (1.0[.0] is a specific version, not a collection of multiple versions), but appears to be at least mostly self-consistent. Unless I'm missing something, however, that seems to prevent any range expression distinction based on pre-release values? For example, it appears that one can't write an expression that allows 2.1.0-beta or later, while not enabling usage of 2.1.0-alpha versions.

I'd expect to be able to do something like:

from conan import ConanFile

class PackageC(ConanFile):
    name = "packagec"
    version = "2.1.0-beta"
class PackageD(ConanFile):
    name = "packaged"
    version = "1.0.0"

    def requirements(self):
        self.requires(
            # What I might really want to write, assuming the "alpha is a 2.1.0 release" logic.
            #"packagec/[=2.1.0, include_prereleases]"

            # Using a ranged comparator seems required to permit selection.
            "packagec/[<=2.1.0, include_prereleases]"

            # But - only for less-than-equal, not greater-than-equal?!?
            #"packagec/[=>2.1.0, include_prereleases]"

            # Can't set pre-release lower bound though.
            #"packagec/[>=2.1.0-beta <=2.1.0, include_prereleases]"

            # At best, something approximate like so, but also undesirably includes e.g., -alpha.
            #"packagec/[>2.0.99 <=2.1.0, include_prereleases]"

            # Also works (still not excluding -alpha), although quite
            # non intuitively if expecting a total ordering.
            "packagec/[>=2.1.0 <=2.1.0, include_prereleases]"
        )

This has been important for us with our internal dependencies (detailed below) to more strongly signal to a developer that no compatible version is available and needs to be updated from a remote.

Since that is the case, I'd suggest that at least a documentation update would be important in order to clarify those semantics of range expression comparisons for pre-release and build metadata version sections. In particular, based on my updated understanding, it would be important to clarify the following (adjusted for correctness as needed):

  1. Most importantly, clarify that comparators do not operate on the full SemVer value, but only on the "version core" or base version. I find this especially important given the other existing note about how Conan does specifically consider the +build field in ordering or pre-releases (that behavior is definitely appreciated).
  2. the include_prerelease option controls whether or not pre-release versions of a package in the cache are considered as available in comparisons (i.e., 2.1.0 will be in the set of available versions if at least one 2.1.0-pre package is present)
  3. once the base version has been determined, if include_prerelease is enabled, the latest pre-release available for the determined base version is always selected

I'd further suggest that it would still be greatly beneficial to have finer control within a given pre-release range for a single base version, specifically the ability to have comparators do full (pedantic?) SemVer ordering. If it were added, it would seem to need to be an additional range expression option (strict_compare) or some such for compatibility with the Conan 2 logic above.

I'll also like to clarify that in Conan 2, hardcoding the resolution to prereleases in requires is not the most recommended approach, and an new conf core.version_ranges:resolve_prereleases.

I've seen that and looked into it, however it seems to only enable an all-or-nothing approach (there isn't anything like a [resolve_prereleases] profile to do based on reference pattern matching). We'd like the flexibility to only enable pre-releases of our dependency package when needed, and not other pre-release packages, possibly from other concurrent projects a developer may be working on at the same time.

The pattern we use for this is to have a intermediate meta package which serves as the primary source of requirements. The actual product source conanfile only depends on a matching version of that single dependency meta package. We allow each to develop concurrently, and use git tags to highlight incompatibility points (API changes, etc.) within a development cycle. Both the meta package version as well as the range expression is dynamically determined based on the particular git commit being used.

If a developer is building a development version of the product from git, then we want to be able to specify including prerelease versions for the dependency metapackage, but not other packages, including packages for other products that may also be in the local cache. To do this, we regularly rev dependencies within a development cycle, but only update the dependency when incompatible changes are made, for which we rev the first prerelease as a quasi API compatibility flag within the dev cycle. For example, if moving from using packageb/1.5.9 to packageb/2.0.0 with incompatible changes, we'd rev the providing and dependent git tags to change from 2.1.0-0.dev+123.g0abcdef to 2.1.0-1.dev+124.g0bbbbbb. We leverage some simple logic to transform git-describe to a semantic version, including pre-release and build portions. By doing this, all package versions are readily tracable back to git commit from which they originated, and the proper version is dynamically determined by each of the recipes (only permitting usage of pre-release dependency package if build a pre-release product source tree).

I illustrate below a possible flow within a given development cycle which is enabled by this. It illustrates the pattern - we in no way always have all or even most of the transitions in each cycle, but having the structure to be able to increment when and as needed has been quite beneficial.

change type dependency range product-dependencies package version
release, no pre-release deps desired 2.0.0 2.0.0
start dev towards next planned release; no incompatible changes from prior release [>= 2.1.0-0.dev <2.1.0 || 2.0.0, include_prereleases] 2.1.0-0.dev+10.g0abcdef
update some dependencies in compatible manner; latest version available is used, update not required (no change) 2.1.0-0.dev+20.g0111111
make incompatible change (e.g., API) [>= 2.1.0-1.dev <2.1.0, include_prereleases] 2.1.0-1.dev+30.g0333333
release alpha with no compatible changes (no change) 2.1.0-alpha+40.g0444444
release beta with incompatible change [>= 2.1.0-beta <2.1.0, include_prereleases] 2.1.0-beta+30.g0555555
release, no pre-release deps desired 2.1.0 2.1.0

@memsharded memsharded linked a pull request Jan 28, 2025 that will close this issue
@memsharded
Copy link
Member

Hi @mrjoel

For example, it appears that one can't write an expression that allows 2.1.0-beta or later, while not enabling usage of 2.1.0-alpha versions.

Not sure what you mean, I can add these cases to the test suite, and it works:

# Limits
    ['>=2.1.0-beta', False, ["2.1.0"], ["2.1.0-beta", "2.1.0-alpha"]],
    ['>=2.1.0-beta', True, ["2.1.0-beta", "2.1.0-gamma"], ["2.1.0-alpha"]],
])
def test_range_prereleases_conf(version_range, resolve_prereleases, versions_in, versions_out):

Where versions_in are accepted versions inside the range and versions_out are rejected versions outside of the range.

As you can see, if not using prereleases, 2.1.0 is accepted, but all prereleases are rejected.
But if enabling prereleases, 2.1.0-beta and higher like gamma are accepted, but alpha is rejected.

I am adding this case in the test in PR, just in case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants