CMake ${Project_VERSION} variables considered harmful.
Summary
When a project calls find_package(Foo)
, the current best practice (AIUI) is for FooConfig.cmake
to return one or more targets that specify Foo's interface. However, version information is different. The current convention, implemented by find_package
itself, is to define Foo_VERSION
variables in the caller's scope.
Since targets are defined at a higher scope than variables, this means that an important component of a package's interface (the version metadata) may no longer be present when a package's targets are visible:
function(find_foo)
# Foo provides:
# - A Foo::Foo target (at directory scope)
# - A Foo_VERSION variable (at function scope)
find_package(Foo)
endfunction()
find_foo()
if (TARGET Foo::Foo) # Ok, target Foo::Foo is defined.
if (${Foo_VERSION} VERSION_LESS ${Bar_VERSION}) # !! Foo_VERSION is not visible!
uh_oh()
endif()
endif()
Real-life implications
Say package Baz
depends on packages Foo
and Bar
, which both depend on the package Kramble
:
- Baz calls
find_package(Foo)
which callsfind_package(Kramble)
inside of a function. - Baz calls
find_package(Bar)
, which contains:
if (NOT TARGET Kramble)
find_package(Kramble REQUIRED)
endif()
if ("${Kramble_VERSION}" VERSION_LESS "1.0")
uh_oh()
endif()
Now Bar is incorrectly configured because of how Foo implemented its config.
Which project is at fault?
- Foo had a complex dependency chain managed by a set of cmake functions and used a standard technique to find Kramble.
- Bar detected that Kramble was already found, and simply used the metadata defined when a package is found.
- Baz found two dependencies using standard techniques.
- Kramble defined its configuration using modern targets and the standard CMake way of specifying versions.
None of these action were unreasonable, IMO. The issue is that the standard method of providing version information is incompatible with returning targets from find_package
.
Possible Solution
This problem would be solved by providing a consistent, canonical way to embed a target's version information directly into the target itself. A new TARGET_VERSION
and/or PACKAGE_VERSION
target property is a straight-forward implementation of this:
function(find_foo)
# Foo provides:
# - A Foo::Foo target (at directory scope) with a TARGET_VERSION property
find_package(Foo)
endfunction()
find_foo()
if (TARGET Foo::Foo)
target_get_property(foo_ver Foo::Foo TARGET_VERSION)
if (${foo_ver} VERSION_LESS ${Bar_VERSION}) # OK!
hurray()
endif()
endif()
Possible Alternatives
There exists a VERSION
target property already, but this may not be ideal for reuse here. It already has semantic meaning related to library versioning that is not considered applicable to all targets. For example, attempt to set (or get(!)) the VERSION
target property from an INTERFACE_LIBRARY
will trigger a hard error at present. It may be an unacceptable footgun to have a target property that is canonical in some versions of CMake but a hard error (only for some target types) in other versions.
Concerns
Since find_package
does not know about the targets produced by a find module, using a property would put the burden of setting the property on the target's author.
This could get especially burdensome if the full version metadata is expected to be set as properties (version, major, minor, patch, tweak, components). Ideally only the single, full version string would be stored, and a cmake utility function is added to parse out the derived metadata only when a user needs them. This would improve conformance by simplifying the convention.