Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 305
Florian Wolters Avatar asked Oct 19 '17 18:10

Florian Wolters


1 Answers

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).

like image 88
Alex Reinking Avatar answered Oct 14 '22 09:10

Alex Reinking