Generic "transitive behavior" in CMake Package Configuration File (find_dependency)
Asked Answered
M

1

16

I'm trying to enhance my CMake convention-over-configuration framework at the moment. Each of my C++ components (i.e. CMake project) is built via that framework and the framework is already capable to create a CMake Package Configuration File using the configure_package_config_file() command.

The following (minimal) template file PackageConfig.cmake.in (v1) is used by the framework.

@PACKAGE_INIT@

include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]")
check_required_components("@PROJECT_NAME@")

Everything works fine if a component Foo built and installed with that approach is used by another component Bar with the find_package(<package> CONFIG) command (as long as the correct directory paths pointing to the installed CMake Package Configuration File of Foo is set via the CLI).

But (of course) problems occur if A itself has one or more dependencies. Using the current approach, B has to find_package() each of the dependencies of A itself. That means that transitive dependencies are currently not reported to the component requiring a dependency. Obviously this is not what I want to achieve.

After some googleing I've learned about the find_dependency() command, which has been created to solve the problem mentioned:

It is designed to be used in a Package Configuration File (<package>Config.cmake). find_dependency forwards the correct parameters for QUIET and REQUIRED which were passed to the original find_package() call. Any additional arguments specified are forwarded to find_package().

So far so good, but wait... I have to explicitly set each dependency name and its version again? I've already done that when declaring the dependencies in the CMakeLists.txt! How can I create a reusable and generic Package Configuration File using that approach? I can't identify any solution to that problem at the moment, other than explicitly listing all dependencies (together with their version) in the CMakeLists.txt and passing that list to the PackageConfig.cmake.in.

Example: Untested PackageConfig.cmake.in (v2):

@PACKAGE_INIT@

include(CMakeFindDependencyMacro)

# TODO(wolters): How-to implement this with a generic approach? Would the
# following work? Does a better solution do the problem exist?
#
# 1. Add the following to `CMakeLists.txt`:
#    list(APPEND target_dependencies "Baz 1.2.3")
#    list(APPEND target_dependencies "Example 0.9.0")
# 2. "Pass" the variable `target_dependencies` to the
#    `configure_package_config_file` command.
# 3. Add the following code to the CMake package config file.

foreach(dependency "@target_dependencies@")
  find_dependency(${dependency})
endforeach()

include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]")
check_required_components("@PROJECT_NAME@")

Though this feels odd and I do not (yet) know if it works at all (but it should, theoretically).

So my question is: How can I generically implement transitive behavior for a CMake package configuration file.

Last but not least: I'm using the latest stable CMake version 3.9.4.

Mussulman answered 19/10, 2017 at 18:42 Comment(6)
This is a really good question, I am working on the same issue here. At the moment I am just adding the dependencies by hand. I wish that cmake provided some way to do this. But I guess unless the find_package call is linked to the library target in such a way that the dependency target is (e.g. target_link_libraries(foo PUBLIC bar::bar)) there is no way to track anything.Cassino
It get's even more tricky when you want to find only a partial component of your library, but different components have different dependencies. E.g. you shouldn't need to require the dependencies of all components of the library, if you're only requesting one component of the library.Cassino
I think the fundamental problem here is that there is no formal link between imported targets and the package that provided them. The developer of a library package might need targets (components) of a third-party package that the eventual consumer will not need, or even use ExternalProject_Add() to get a special version with debugging/tracing/profiling enabled, whereas the consumer will want to link with the standard version.Ostap
And as an addendum to all the above: things could be even worse if a dependency is a generator expression (e.g., dependency has to be added for a particular platform, compiler, result of some test (e.g., clock_gettime may require librt for older glibc), or just a project's build option). @Ostap 100% right -- this is a big and damn complex problem. And also things get worse when some dependency (an imported target) do not belongs to any result of find_package but was added e.g., by pkg_check_modules() or "manually" imported. <TBC>Swiss
The other side is a "package" version -- the content of a found "package" (inter- components/package dependencies) may vary depending on the package/product version.Swiss
I tried to write an automatic dependency tracker for about 4 years and end up with my own genex interpreter implemented in CMake %) -- and that was my big mistake %) Finally, the "manual" way of specifying package/component dependencies much easier than tons of triky and overcomplicated CMake code! (which is very fragile among other flaws)Swiss
P
3

This is too long for a comment, so I'm posting as an answer...

As a first cut, you could compute the list of loaded packages before calling configure_package_config_file in a variable called PACKAGE_DEPENDENCIES. First adjust your template like so:

@PACKAGE_INIT@

include(CMakeFindDependencyMacro)
@PACKAGE_DEPENDENCIES@

include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]")
check_required_components("@PROJECT_NAME@")

Then, to compute PACKAGE_DEPENDENCIES, you could use a snippet like the following:

set(PACKAGE_DEPENDENCIES "")
get_property(packages GLOBAL PROPERTY PACKAGES_FOUND)
foreach (pkg IN LISTS packages)
  get_property(is_transitive GLOBAL PROPERTY _CMAKE_${pkg}_TRANSITIVE_DEPENDENCY)
  if (is_transitive)
    continue()
  elseif (${pkg}_VERSION)
    string(APPEND PACKAGE_DEPENDENCIES "find_dependency(${pkg} ${${pkg}_VERSION})\n")
  else ()
    string(APPEND PACKAGE_DEPENDENCIES "find_dependency(${pkg})\n")
  endif()
endforeach ()

You would then be ready to call configure_package_config_file.

There are a number of ways this approach is brittle, however:

  1. The global PACKAGES_FOUND property includes all packages, even those found recursively via find_dependency, which is most likely not what you want. Filtering it with the undocumented, internal property _CMAKE_<PKG>_TRANSITIVE_DEPENDENCY (as I do here) might break without warning or policy in a newer CMake version. It also relies on packages calling find_dependency as opposed to find_package.
  2. Packages loaded via Find module (as opposed to a config file) might not have a version set. Broken config files (i.e. those without a PackageConfigVersion.cmake file) might also lack version information.

One resolution to (1) might be to only apply this behavior to packages using your convention-over-configuration framework (e.g. by setting a special global property). Similar story with (2).

Prune answered 18/8, 2021 at 20:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.