diff --git a/cep-XXXX.md b/cep-XXXX.md new file mode 100644 index 00000000..9a1e9b29 --- /dev/null +++ b/cep-XXXX.md @@ -0,0 +1,188 @@ +# CEP XXXX - Version strings and their ordering + + + + + + + + + +
Title Version strings and their ordering
Status Draft
Author(s) Jaime Rodríguez-Guerra <jaime.rogue@gmail.com>
Created Sep 26, 2025
Updated Sep 26, 2025
Discussion N/A
Implementation 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
+ +## 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. + +> 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: + +```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/). + + + +[RFC2119]: https://www.ietf.org/rfc/rfc2119.txt