Version Specifiers in Maven 3.x

Categories: Java

Introduction

Each maven artifact has a version-specifier. Sadly, the actual format of this version-specifier is not properly described in any of the official Maven documentation. There aren’t any reliable sources on the internet either. And worst of all, the supported format and its meaning (particularly how versions compare/sort) differs between Maven releases.

Hopefully the description below clarifies the behaviour of version-specifiers in Maven 3.x. The information is primarily derived from reading the source code, although this jira issue has some useful links. The official documentation is rather vague on the topic - and in one place actually describes the old 2.x comparison behaviour.

In particular, the available information is very vague about which version-specifiers sort before (are “older than”) others.

Versioning

A version-specifier has the following form:

  component(-component)*

where a component is one of:

  • semantic-version
  • string

A semantic-version component is of form major(.minor(.patch)), eg 1.2.3 or 4.0. A single digit is also a semantic-version, eg “4” is equivalent to 4.0.0.

A string component is any sequence of characters excluding whitespace and dash.

Here are some simple and more complex examples:

  • 1.2-alpha1
  • 1.2.3-beta-2-rc
  • 1.2.1-alpha-3.2-SNAPSHOT
  • 4.0-SNAPSHOT-rc2
  • 5.0-GA

Note that SNAPSHOT is simply a string component, and is not limited to occurring at the end of the version-specifier.

Comparing Versions

Comparison of two version-specifiers simply compares components pairwise from left to right, until two components are found which are not equal.

When a version-specifier is compared to one that is shorter in length (has fewer components), then the shorter is effectively “padded with nulls”, eg comparing “1.2-alpha-1-rc-snapshot” (5 components) to “1.3-alpha2” (2 components) is equivalent to comparing it against “1.3-alpha2-null-null-null”.

Comparisons of semantic-version components works in the obvious manner; if the major-values are identical, then the minor-values are compared, and if needed the patch-values. A missing value (null) is equivalent to zero, eg “3” is equal to “3.0.0”.

Semantic versions always come “before” strings, eg “9” comes before (is older than) “alpha” or “5a”.

Comparisons of string components is not quite so simple. Comparison is case-insensitive, and strings are sorted in the following order from “lowest version” (oldest) to “highest version” (newest):

  • snapshot
  • alpha
  • beta
  • milestone
  • rc (abbreviation for release-candidate) and cr (abbreviation for candidate-release)
  • (empty-string or null), “final”, “ga” (abbreviation for general-availability)
  • sp (abbreviation for service-pack)
  • (other-string, ie any other value)

Thus 1.0-SNAPSHOT < 1.0-beta-4-SNAPSHOT < 1.0-RC-2 < 1.0 < 1.0-sp-6 < 1.0-FizzBuzz-3.

Comparing two strings which are not any of the above special values (ie which are in category other-string) is done alphabetically, eg “aaa” comes before (is older than) “aab”.

An implication of this sorting-criteria is that custom strings can never sort before the official release (as alpha/beta/etc do). Something like “1.0-pre1” is not a “pre-release” version; it is newer than “1.0” because “pre1” is an “other string” which sorts after empty-string/null.

In addition:

  • any string of form “a{N}” is equivalent to “alpha-{N}”
  • any string of form “b{N}” is equivalent to “beta-{N}”
  • any string of form “m{N}” is equivalent to “milestone-{N}”

Tricky Cases

If a component in the version-specifier does not match the requirements for a “semantic version”, then it is treated as a string. In particular, “1.2.3.4” is simply a string; it has too many numerical components. It will therefore sort after (is considered newer-than) “1.10.3.4”. It is also newer than “2.0”, because “2.0” is really a semantic-version which sorts before (is older than) any string. Similarly, “1.2.3.RELEASE” is just a single string component, and will be compared to other version-specifiers of the same form using the string-component-comparison rules.

The version-specifiers “1.0-foo1” and “1.0-foo-1” are quite different; the first has two components (semantic-version, string) while the second has threee components (semantic-version, string, semantic-version).

String Sorting Examples

  • “1.0-alpha” is older than “1.0-foo” because “alpha” comes before other-string
  • “1.0-alpha” is older than “1.0” because “alpha” comes before empty-string/null
  • “1.0-SNAPSHOT” is older than “1.0” because “snapshot” comes before empty-string/null
  • “1.0” is older than “1.0-foo” because empty-string/null comes before other-string.

  • “1.0-alpha-2” is older than “1.0-alpha-15” because semantic-version (numeric) 2 comes before semantic-version (numeric) 15
  • “1.0-foo15” is older than “1.0-foo2” because string “foo15” comes before string “foo2”
  • “1.0-snapshot” is older than “1.0-beta-snapshot”, because “snapshot” comes before “beta”.
  • “1.0” comes before “1.0-1” because null/zero-integer comes before integer 1

  • “1.0-m1” is older than “1.0-c1” because “milestone” comes before other-string (“c1”).
  • “1.0-m2” is older than “1.0-m15” because “m2” is equivalent to “milestone-2” and (numeric) 2 comes before (numeric) 15.

Other Notes

Internally, a semantic-version is treated as a list of 1..3 integer components. The code that stores and compares versions doesn’t actually have a datastructure for “semantic version”, just integer, string, and list-of-component. However the code that parses the original version-specifier does recognise things like “1.2.3” and represents it as a “list-typed” component holding a list of integers. This parsing logic matches a maximum of 3 numeric fields; anything more is a string.

When the maven “deploy” operation is performed, any component named “snapshot” is replaced by the current timestamp.