find_package: Use deterministic search order by default

Historically, find_package() does not guarantee the order in which directories
matching a search path containing a glob expression are processed in - the
"first valid package" will be selected if there are multiple candidates. In such
cases, which package is chosen is completely random and can change, potentially
leading to build failures and reproducibility issues. This is rather unexpected
and confusing for developers.

Now that CMake has bumped its major version, it's a good time to change default
sort order and direction could be changed to natural sorting with a descending
order. That will result in the newest version of a library being picked in case
there are multiple ones available.
This commit is contained in:
Moritz Haase
2025-06-13 13:41:50 +02:00
parent bababf5579
commit 61d8fae116
7 changed files with 83 additions and 40 deletions

View File

@@ -581,12 +581,12 @@ Paths are searched in the order described above. The first viable package
configuration file found is used, even if a newer version of the package
resides later in the list of search paths.
For search paths which contain glob expressions (``*``), the order in which
directories matching the glob are searched is unspecified unless the
:variable:`CMAKE_FIND_PACKAGE_SORT_ORDER` variable is set. This variable,
along with the :variable:`CMAKE_FIND_PACKAGE_SORT_DIRECTION` variable,
determines the order in which CMake considers glob matches. For example, if
the file system contains the package configuration files
For search paths which contain glob expressions (``*``), directories matching
the glob are searched in natural, descending order by default. This behavior
can be overridden by setting variables :variable:`CMAKE_FIND_PACKAGE_SORT_ORDER`
and :variable:`CMAKE_FIND_PACKAGE_SORT_DIRECTION` accordingly. Those variables
determine the order in which CMake considers glob matches. For example, if the
file system contains the package configuration files
::
@@ -594,21 +594,21 @@ the file system contains the package configuration files
<prefix>/example-1.10/example-config.cmake
<prefix>/share/example-2.0/example-config.cmake
it is unspecified (when the aforementioned variables are unset) whether
``find_package(example)`` will find ``example-1.2`` or ``example-1.10``
(assuming that both are viable), but ``find_package`` will *not* find
``example-2.0``, because one of the other two will be found first.
then ``find_package(example)`` will (when the aforementioned variables are
unset) pick ``example-1.10`` (assuming both ``example-1.2`` and ``example-1.10``
are viable). Note however that ``find_package`` will *not* find ``example-2.0``,
because one of the other two will be found first.
To control the order in which ``find_package`` searches directories that match
a glob expression, use :variable:`CMAKE_FIND_PACKAGE_SORT_ORDER` and
:variable:`CMAKE_FIND_PACKAGE_SORT_DIRECTION`.
For instance, to cause the above example to select ``example-1.10``,
For instance, to cause the above example to select ``example-1.2``,
one can set
.. code-block:: cmake
set(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL)
set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
set(CMAKE_FIND_PACKAGE_SORT_DIRECTION ASC)
before calling ``find_package``.
@@ -624,6 +624,15 @@ before calling ``find_package``.
and ``<prefix>/<name>.framework/Versions/*/Resources/CMake``. In previous
versions of CMake, this order was unspecified.
.. versionchanged:: 4.2
When encountering multiple viable matches, ``find_package`` now picks the
one with the most recent version by default. In previous versions of CMake,
the result was unspecified. Accordingly, the default of
:variable:`CMAKE_FIND_PACKAGE_SORT_ORDER` has changed from ``NONE`` to
``NATURAL`` and :variable:`CMAKE_FIND_PACKAGE_SORT_DIRECTION`
now defaults to ``DEC`` (descending) instead of ``ASC`` (ascending).
.. include:: include/FIND_XXX_ROOT.rst
.. include:: include/FIND_XXX_ORDER.rst

View File

@@ -3,16 +3,21 @@ CMAKE_FIND_PACKAGE_SORT_DIRECTION
.. versionadded:: 3.7
.. versionchanged:: 4.2
The default sort direction has changed from ``DEC`` to ``ASC``.
The sorting direction used by :variable:`CMAKE_FIND_PACKAGE_SORT_ORDER`.
It can assume one of the following values:
``ASC``
Default. Ordering is done in ascending mode.
Ordering is done in ascending mode.
The lowest folder found will be tested first.
``DEC``
Ordering is done in descending mode.
Default. Ordering is done in descending mode.
The highest folder found will be tested first.
If :variable:`CMAKE_FIND_PACKAGE_SORT_ORDER` is not set or is set to ``NONE``
this variable has no effect.
If :variable:`CMAKE_FIND_PACKAGE_SORT_ORDER` is set to ``NONE`` this variable
has no effect.

View File

@@ -3,39 +3,40 @@ CMAKE_FIND_PACKAGE_SORT_ORDER
.. versionadded:: 3.7
.. versionchanged:: 4.2
The default sort order has changed from ``NONE`` to ``NATURAL``.
The default order for sorting directories which match a search path containing
a glob expression found using :command:`find_package`. It can assume one of
the following values:
``NONE``
Default. No attempt is done to sort directories.
No attempt is done to sort directories.
The first valid package found will be selected.
``NAME``
Sort directories lexicographically before searching.
``NATURAL``
Sort directories using natural order (see ``strverscmp(3)`` manual),
Default. Sort directories using natural order (see ``strverscmp(3)`` manual),
i.e. such that contiguous digits are compared as whole numbers.
Natural sorting can be employed to return the highest version when multiple
versions of the same library are available to be found by
:command:`find_package`. For example suppose that the following libraries
have package configuration files on disk, in a directory of the same name,
with all such directories residing in the same parent directory:
Natural sorting is employed by default to return the highest version when
multiple versions of the same library are available to be found by
:command:`find_package`. For example suppose that the following libraries have
package configuration files on disk, in a directory of the same name, with all
such directories residing in the same parent directory:
* libX-1.1.0
* libX-1.2.9
* libX-1.2.10
* ``libX-1.1.0``
* ``libX-1.2.9``
* ``libX-1.2.10``
By setting ``NATURAL`` order we can select the one with the highest
version number ``libX-1.2.10``.
.. code-block:: cmake
set(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL)
find_package(libX CONFIG)
The default order of ``NATURAL`` will select the one with the highest version
number, i.e. ``libX-1.2.10``.
The sort direction can be controlled using the
:variable:`CMAKE_FIND_PACKAGE_SORT_DIRECTION` variable
(by default descending, e.g. lib-B will be tested before lib-A).
:variable:`CMAKE_FIND_PACKAGE_SORT_DIRECTION` variable (by default descending,
i.e. ``libX-1.2`` will be tested before ``libX-1.0`` and ``lib-B`` will be
tested before ``lib-A``).

View File

@@ -334,10 +334,10 @@ private:
class FlushDebugBufferOnExit;
/*! the selected sortOrder (None by default)*/
SortOrderType SortOrder = None;
/*! the selected sortDirection (Asc by default)*/
SortDirectionType SortDirection = Asc;
/*! the selected sortOrder (Natural by default)*/
SortOrderType SortOrder = Natural;
/*! the selected sortDirection (Dec by default)*/
SortDirectionType SortDirection = Dec;
struct ConfigFileInfo
{

View File

@@ -117,7 +117,7 @@ foreach(p ${PACKAGES})
endforeach()
# Enable framework and bundle searching. Make sure bundles are found
# before unix-syle packages.
# before unix-style packages.
set(CMAKE_FIND_FRAMEWORK LAST)
set(CMAKE_FIND_APPBUNDLE FIRST)
@@ -584,6 +584,17 @@ endif()
unset(SortLib_VERSION)
set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
# Expected to default to 'NATURAL' and 'DEC'
unset(CMAKE_FIND_PACKAGE_SORT_ORDER)
unset(CMAKE_FIND_PACKAGE_SORT_DIRECTION)
FIND_PACKAGE(SortLib CONFIG)
IF (NOT "${SortLib_VERSION}" STREQUAL "3.10.1")
message(SEND_ERROR "FIND_PACKAGE_SORT_ORDER Default! ${SortLib_VERSION}")
endif()
unset(SortLib_VERSION)
set(SortFramework_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
SET(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
SET(CMAKE_FIND_PACKAGE_SORT_DIRECTION ASC)

View File

@@ -90,6 +90,7 @@ foreach(CMAKE_FIND_PACKAGE_SORT_DIRECTION IN ITEMS "" ASC Bogus)
unset(SortLib_VERSION)
endforeach()
set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
set(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL)
set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
@@ -100,6 +101,7 @@ endif()
set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
unset(SortLib_VERSION)
set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
FIND_PACKAGE(SortLib 4.0 CONFIG)
IF (NOT "${SortLib_VERSION}" STREQUAL "4.0.0")
@@ -107,6 +109,18 @@ IF (NOT "${SortLib_VERSION}" STREQUAL "4.0.0")
endif()
unset(SortLib_VERSION)
set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
# Expected to default to 'NATURAL' and 'DEC'
unset(CMAKE_FIND_PACKAGE_SORT_ORDER)
unset(CMAKE_FIND_PACKAGE_SORT_DIRECTION)
FIND_PACKAGE(SortLib CONFIG)
IF (NOT "${SortLib_VERSION}" STREQUAL "3.10.1")
message(SEND_ERROR "FIND_PACKAGE_SORT_ORDER Default! ${SortLib_VERSION}")
endif()
unset(SortLib_VERSION)
set(SortFramework_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
set(CMAKE_FIND_PACKAGE_SORT_DIRECTION ASC)
@@ -117,6 +131,7 @@ endif()
set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
unset(SortFramework_VERSION)
set(SortFramework_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
set(CMAKE_FIND_PACKAGE_SORT_ORDER NATURAL)
set(CMAKE_FIND_PACKAGE_SORT_DIRECTION DEC)
@@ -127,6 +142,7 @@ endif()
set(SortLib_DIR "" CACHE FILEPATH "Wipe out find results for testing." FORCE)
unset(SortFramework_VERSION)
unset(CMAKE_FIND_PACKAGE_SORT_ORDER)
unset(CMAKE_FIND_PACKAGE_SORT_DIRECTION)

View File

@@ -6,6 +6,7 @@ list(INSERT CMAKE_PREFIX_PATH 0
set(CMAKE_FIND_DEBUG_MODE 1)
# Stable sorting for predictable behaviors.
set(CMAKE_FIND_PACKAGE_SORT_ORDER NAME)
set(CMAKE_FIND_PACKAGE_SORT_DIRECTION ASCENDING)
# Unset search variables for more predictable output.
unset(CMAKE_FRAMEWORK_PATH)