Fun with CMake version ranges

Since release 3.19 CMake allows to specify a range in find_package() calls:

find_package(Dummy minVersion...maxVersion)

Although currently not much find modules support version ranges one might want to set the upper end to a maximum version the code is compatible with. This can be seen as a safeguard against breaking changes in an unknown future major version.

Let say we have a package “Dummy” whose major versions 1 and 2 are known to work with our own code. One might be tempted to specify the version range like the following:

find_package(Dummy 1...2)

This works well with 1.0.0, 1.5.0 or 2.0.0, but will break with 2.0.1 or 2.1.0.

Instead use the following syntax to include major versions 1 and 2:

find_package(Dummy 1...<3)

But why so?

Let’s have a look at FindPackageHandleStandardArgs.cmake at the function find_package_check_version which most find modules use:

    if ((${package}_FIND_VERSION_RANGE_MIN STREQUAL "INCLUDE"
          AND version VERSION_GREATER_EQUAL ${package}_FIND_VERSION_MIN)
        AND ((${package}_FIND_VERSION_RANGE_MAX STREQUAL "INCLUDE"
            AND version VERSION_LESS_EQUAL ${package}_FIND_VERSION_MAX)
          OR (${package}_FIND_VERSION_RANGE_MAX STREQUAL "EXCLUDE"
            AND version VERSION_LESS ${package}_FIND_VERSION_MAX)))

The version check uses regular if clauses with the syntax for version tests. Taking a closer look to the documentation reveals that the version check is done component-wise (major.minor.patch) and ommitted components are treated as zero. So writing

find_package(Dummy 1...2)

in the same as

find_package(Dummy 1.0.0...2.0.0)

Now its clear and logical that this will match 2.0.0, but not 2.1.0. See this github repo for an example.

However it still feels a bit unnatural as other version range implementations handle major version ranges differently. node-semver e.g. which is used among the nodejs eco system includes “2.1.0” in the version range “1 – 2”:

const semver = require('semver');

a = semver.satisfies('2.0.0', '1 - 2');
b = semver.satisfies('2.1.0', '1 - 2');
c = semver.satisfies('3.0.0', '1 - 2');

console.log(a); // true
console.log(b); // true
console.log(c); // false