-
Notifications
You must be signed in to change notification settings - Fork 30
CEP XXXX: "Version strings and their ordering" #132
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
base: main
Are you sure you want to change the base?
Changes from all commits
44748f0
f97a0ba
a6c1cc5
8857fcd
aa46546
7421f14
5f07fdc
4673a80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| # CEP XXXX - Version strings and their ordering | ||
|
|
||
| <table> | ||
| <tr><td> Title </td><td> Version strings and their ordering </td> | ||
| <tr><td> Status </td><td> Draft </td></tr> | ||
| <tr><td> Author(s) </td><td> Jaime Rodríguez-Guerra <[email protected]></td></tr> | ||
| <tr><td> Created </td><td> Sep 26, 2025 </td></tr> | ||
| <tr><td> Updated </td><td> Sep 26, 2025 </td></tr> | ||
| <tr><td> Discussion </td><td> N/A </td></tr> | ||
| <tr><td> Implementation </td><td> https://github.com/conda/conda/blob/6614653b1d9bdbffcef55e338d3220daed70c7f8/conda/models/version.py#L52, https://github.com/conda/rattler/blob/rattler-v0.37.4/crates/rattler_conda_types/src/version/mod.rs#L141 </td></tr> | ||
| </table> | ||
|
|
||
| ## Abstract | ||
|
|
||
| This CEP describes version strings as used in the conda ecosystem, and their ordering. | ||
|
|
||
| ## Motivation | ||
|
|
||
| The motivation of this CEP is mostly informative, but will also try to clarify some ambiguous details that should be homogenized across existing implementations. | ||
|
|
||
| ## Specification | ||
|
|
||
| The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", | ||
| "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as | ||
| described in [RFC2119][RFC2119] when, and only when, they appear in all capitals, as shown here. | ||
|
|
||
| More specifically, violations of a MUST or MUST NOT rule MUST result in an error. Violations of the | ||
| rules specified by any of the other all-capital terms MAY result in a warning, at discretion of the | ||
| implementation. | ||
|
|
||
| ### Version strings | ||
|
|
||
| [CEP 26](./cep-0026.md) only discussed the type of characters that can be part of a version string, and its maximum length: | ||
|
|
||
| > [...] version strings MUST only consist of digits, periods, lowercase ASCII letters, underscores, plus symbols, and exclamation marks. The maximum length of a version string MUST NOT exceed 64 characters. | ||
|
|
||
| The present CEP _extends_ these rules with additional constraints: | ||
|
|
||
| - Version strings MUST be composed of alphanumeric characters `[A-Za-z0-9]`, separated into segments by periods `.` and underscores `_`. | ||
| - The first segment MUST start with a digit. | ||
| - Empty segments (i.e. two consecutive dots, a leading/trailing underscore) MUST NOT be allowed. | ||
| - All segments SHOULD start with a digit for clarity. | ||
| - A single epoch number (a positive integer followed by `!`) MAY prefix the rest of the string. | ||
| - A single local version string MAY be added at the end, separated by a plus symbol `+`. | ||
|
|
||
| ### Ordering | ||
|
|
||
| Before being compared, version strings MUST be parsed into a list of segments (with each segment being a list of components) as follows: | ||
|
|
||
| - They are first split into _epoch_, _main version_, and _local version_ at `!` and `+` respectively. | ||
| - If there is no `!`, the epoch is set to `0`. | ||
| - If there is no `+`, the local version is empty. | ||
| - The main version part is then split into components at `.` and `_`. | ||
| - Each component is split again into consecutive runs of numerals and non-numerals. | ||
| - Subcomponents containing only numerals are converted to integers. | ||
| - Strings are converted to lowercase, with special treatment for `dev` and `post`. | ||
| - When a component starts with a letter, the fill value `0` is inserted before the letter. | ||
| - The epoch and main version segments are concatenated. | ||
| - The same is repeated for the local version part, and stored as a separate list of segments. | ||
|
|
||
| For example: | ||
|
|
||
| ```python | ||
| >>> parse("1.2g.beta15.rc") | ||
| [[0], [1], [2, 'g'], [0, 'beta', 15], [0, 'rc']], [] | ||
| >>> parse("1!2.15.1_ALPHA") | ||
| [[1], [2], [15], [1, 'alpha']], [] | ||
| >>> parse("1!2.15.1_alpha+1.2.3h123") | ||
| [[1], [2], [15], [1], [0, 'alpha']], [[1], [2], [3, 'h', 123]] | ||
| ``` | ||
|
|
||
| The resulting list of components MUST be compared as follows: | ||
|
|
||
| - Integers are compared numerically. | ||
| - Strings are compared lexicographically, case-insensitive. The substring `dev` is always smaller. | ||
| - Strings are considered smaller than integers, except for `post`, which is always greater. | ||
| - When a component has no correspondent, the missing component is assumed to be `0`. | ||
| - Local versions are only compared when the main versions are identical. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: if we could find some example of how local versions are used out in the wild
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found these in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added it in a new Examples section. |
||
|
|
||
| > Warning: | ||
| > Pre-releases markers are sensitive to leading zeros and periods. While `"1.1.0" == "1.1.0.0" == | ||
| > "1.1"`, the rule "When a component starts with a letter, the fill value `0` is inserted" results | ||
| > in `"1.1.0rc" == "1.1.rc" > "1.1rc"`. See [conda#12568](https://github.com/conda/conda/issues/12568). | ||
|
|
||
| ## Rationale | ||
|
|
||
| - The first segment of a version must start with a digit to avoid confusing situations like `v13.2.1` being smaller than `3.1.0`. See [conda/conda#14255](https://github.com/conda/conda/issues/14255). | ||
| - The `dev` substring is handled differently to allow `dev` pre-releases to sort before alphas, betas and release candidates. | ||
| - The `post` substring is handled differently to allow `post` releases to sort after any equivalent final release. | ||
| - Missing components are treated like `0` to allow equivalences like `'1.1' == '1.1.0'`. | ||
| - The `0` fill value is used in components starting with letters to keep numbers and strings in phase, resulting in `'1.1.a1' == '1.1.0a1'`. | ||
|
|
||
| ## Backwards compatibility | ||
|
|
||
| This CEP _extends_ [CEP 26](./cep-0026.md) with more details about version strings. | ||
|
|
||
| `conda` has historically allowed versions to start with a non-numeric character. This is now forbidden to avoid issues like `v10.4.5` being interpreted as smaller than `3.3.2`. | ||
|
|
||
| ## Examples | ||
|
|
||
| The ordering specification results in the following versions sorted in this way: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could use a comment/example of how non-empty local versions are always ordered after (right?!) the ones with empty local versions. |
||
|
|
||
| ```sh | ||
| 0.4 | ||
| == 0.4.0 | ||
| < 0.4.1.rc | ||
| == 0.4.1.RC # case-insensitive comparison | ||
| < 0.4.1 | ||
| < 0.5a1 | ||
| < 0.5b3 | ||
| < 0.5C1 # case-insensitive comparison | ||
| < 0.5 | ||
| < 0.9.6 | ||
| < 0.960923 | ||
| < 1.0 | ||
| < 1.1dev1 # special case ``dev`` | ||
| < 1.1a1 | ||
| < 1.1.0dev1 # special case ``dev`` | ||
| == 1.1.dev1 # 0 is inserted before string | ||
| < 1.1.a1 | ||
| < 1.1.0rc1 | ||
| < 1.1.0 | ||
| == 1.1.0.0 | ||
| == 1.1 | ||
| < 1.1.0post1 # special case ``post`` | ||
| == 1.1.post1 # 0 is inserted before string | ||
| < 1.1post1 # special case ``post`` | ||
| < 1996.07.12 | ||
| < 1!0.4.1 # epoch increased | ||
| < 1!3.1.1.6 | ||
| < 2!0.4.1 # epoch increased again | ||
| ``` | ||
|
|
||
| Local versions are not very common but there are some examples: | ||
|
|
||
| ```text | ||
| $ conda search "*[version='*+*']" | ||
| Loading channels: done | ||
| # Name Version Build Channel | ||
| py-sirius-ms 2.1+sirius6.0.3 pyhd8ed1ab_0 conda-forge | ||
| py-sirius-ms 2.1+sirius6.0.4 pyhd8ed1ab_0 conda-forge | ||
| py-sirius-ms 2.1+sirius6.0.5 pyhd8ed1ab_0 conda-forge | ||
| py-sirius-ms 2.1+sirius6.0.6 pyhd8ed1ab_0 conda-forge | ||
| py-sirius-ms 2.1+sirius6.0.7 pyhd8ed1ab_0 conda-forge | ||
| py-sirius-ms 2.1+sirius6.0.7 pyhd8ed1ab_1 conda-forge | ||
| py-sirius-ms 3.0+sirius6.1.0 pyhd8ed1ab_0 conda-forge | ||
| py-sirius-ms 3.0.1+sirius6.1.0 pyhd8ed1ab_0 conda-forge | ||
| py-sirius-ms 3.1+sirius6.1.1 pyhd8ed1ab_0 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.4 r44h57928b3_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.4 r44h694c41f_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.4 r44ha770c72_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.5 r44h57928b3_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.5 r44h694c41f_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.5 r44ha770c72_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.6 r44h57928b3_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.6 r44h694c41f_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.6 r44ha770c72_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.7 r44h57928b3_0 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.7 r44h57928b3_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.7 r44h694c41f_0 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.7 r44h694c41f_1 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.7 r44ha770c72_0 conda-forge | ||
| r-sirius-ms 2.1+sirius6.0.7 r44ha770c72_1 conda-forge | ||
| r-sirius-ms 3.0.1+sirius6.1.0 r44h57928b3_0 conda-forge | ||
| r-sirius-ms 3.0.1+sirius6.1.0 r44h694c41f_0 conda-forge | ||
| r-sirius-ms 3.0.1+sirius6.1.0 r44ha770c72_0 conda-forge | ||
| r-sirius-ms 3.1+sirius6.1.1 r44h57928b3_0 conda-forge | ||
| r-sirius-ms 3.1+sirius6.1.1 r44h694c41f_0 conda-forge | ||
| r-sirius-ms 3.1+sirius6.1.1 r44ha770c72_0 conda-forge | ||
| r-sirius-ms 3.1+sirius6.1.1 r45h57928b3_1 conda-forge | ||
| r-sirius-ms 3.1+sirius6.1.1 r45h694c41f_1 conda-forge | ||
| r-sirius-ms 3.1+sirius6.1.1 r45ha770c72_1 conda-forge | ||
| typst-test 0.0.0.post105+699b871 h6e96688_0 conda-forge | ||
| typst-test 0.0.0.post105+699b871 h6e96688_1 conda-forge | ||
| typst-test 0.0.0.post106+2b4e689 h6e96688_0 conda-forge | ||
| ``` | ||
|
|
||
| ## References | ||
|
|
||
| - [`conda 25.7.x` docs on Version Ordering](https://docs.conda.io/projects/conda/en/25.7.x/user-guide/concepts/pkg-specs.html#version-ordering). | ||
|
|
||
| ## Copyright | ||
|
|
||
| All CEPs are explicitly [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). | ||
|
|
||
| <!-- links --> | ||
|
|
||
| [RFC2119]: https://www.ietf.org/rfc/rfc2119.txt | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that strings and number can never be compared, because of this:
So, starting at 0, even indices will always contain a number and odd always a string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I think you are right here 🤔 I wonder if this bit in the docs didn't get updated once they introduced the "fill value" solution.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, given this script
We get:
See how
devis special cased to uppercase (every other string gets lowercased!), andpostgets converted toinf. I guess this is a hack to make it work with Python's default ordering (uppercase sorts before lowercase, andinfis always larger). However, we do see a case here where letters and numbers need to be compared (albeit after internal conversions, so I don't think the rule applies and I guess we can remove it from the spec? But it doesn't hurt to keep it either).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think both the
DEVandinfhere should be considered implementation details.In mamba this is handled with a custom comparison function for literals (it checks a few special cases and then defers to string comparison). In both C++ (mamba) and Rust (rattler), comparing a number and a string is not the "naive" thing to do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also surprised about this. Pretty sure we don't have this behaviour in mamba. Should this be part of the spec? what should be the rule?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That first
[0]is the omitted0!epoch, which is covered in the spec, I think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha yes sorry, I got confused (the epoch is stored separately in mamba). I thought it was the first segment and that there was some sort of rule that "post" cannot be in the first segment...