Installing/exporting projects that conditionally use optional dependencies

I’m starting to get the hang of writing install and export rules for my CMake projects, but I’ve got a couple of questions about using external dependencies in projects:

First – If I use a custom find module for an external dependency, I get the error target X needs target Y which is not in any export set. Does this mean that if I’m writing find modules for third party packages, and I want projects that use these find modules to be installable, the find module should define an export set for its targets?

Secondly, a more specific use case: I’ve got a header-only math library that wraps various other platform-specific libraries (like Apple vDSP, Intel IPP, MIPP, etc). The idea is that ideally, at CMake configure-time, CMake should detect which underlying library to use based on the target system and what it can find locally. This works perfectly when building from source in the local project tree, but when trying to cmake --install this library, I again get complaints about dependency targets not being in export sets. But regardless of the answer to my first question above, this use case brings up another question: does CMake support use cases where usage requirements may differ on the machine where a package has been installed than where it was built? Is it possible to create logic that says, for this library, in all cases, if IPP can be found on the system, then link against it, but if not, use a fallback implementation?

I think the crux of question #2 is, can targets be installed with dependencies that are not resolved (or even known) until configure time on the target machine…

No, just find_dependency the same thing from your -config.cmake file. You may need to ship these module files and modify CMAKE_MODULE_PATH before calling it though. If you do modify the variable, I recommend resetting it afterwards.

Yes; use generator expressions to detect these things and conditionalize everything.

During the build, sure. But once installed, the use of IPP is kind of set in stone, is it not?

Those would need to be made available before finding your package. It seems like the easiest way to do this may involve something like always installing all targets, but making them “inert” if the platform says they’re not viable. There’s no real way to make variables affect these things though that I can think of off the top of my head. Setting properties on consuming targets is probably the best way.

2 Likes

I always install() all of my custom Find modules into the EXPORTED configuration dir, whether they’re necessary on the platform / for the build or not. Then, my PackageNameConfig.cmake.in template uses find_dependency() to discover any of those requirements.

Many won’t be used because, like Ben (er… other-Ben) said, the decision on whether a dependency is used is typically cemented at build time — either the library was linked-with / compiled-to-use the dependency, or not. So, for those dependencies, I define NEED_WHATEVER variables and reference them in my config template:

@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR})
if (@NEED_ASIO@)
  find_dependency(ASIO)
endif()
if (@NEED_ALSA@)
  find_dependency(ALSA)
endif()
  • NEED_ALSA will always be true on Linux, false everywhere else. (All of the NEED_* variables are explicitly defined one way or the other in all cases.)
  • NEED_ASIO may or may not be true on Windows or macOS, always false on Linux. It’s discovered by the FindASIO.cmake module I’ve included with the config, but whether or not it’s necessary has already been decided when the package is built.

So the actual installed PackageNameConfig.cmake file will get generated with literal if(TRUE) and if(FALSE) conditionals around the find_dependency() calls.

(And yes, I should be doing this, instead of just messing with CMAKE_MODULE_PATH and leaving it that way:)

1 Like

Thanks for the clarification!

So is it correct that any possible usage of find_package() within a project must be echoed by a find_dependency() in the -config.cmake file, and not just required dependencies? The docs for find_dependency() say that it “forwards the REQUIRED flag from the original find_package() call”, so it seems like there’s no way to use this macro to express an optional dependency, because this would be controlled by the caller of the find_package() that loaded the -config.cmake file in question, right?

Could you elaborate on this? What about packages that provide CMake modules that you want to be include-able after calling find_package(MyPackage)?

I know about the $<TARGET_NAME_IF_EXISTS:tgt> generator expression, but how would you say, if this target doesn’t exist, then try this other target? Would you have to do a string comparison to see if $<TARGET_NAME_IF_EXISTS:tgt> evaluates to an empty string?

Yes, I think I was confusing myself due to the fact that the library in question is header-only. You’re right though, once it’s been installed, I can/should require whatever dependencies it was built with.

@ferdnyc I like your setup with the NEED_dependency variables, that seems like a good solution for optionally searching for certain dependencies. I still wonder how to express in a -config.cmake file that failing to find a dependency is not an error…

I recommend doing if (@NEED_ASIO@) # NEED_ASIO in the code so that when reading the configured file, it has some way to trace back how it got in this state.

Conditionally call find_dependency :slight_smile: .

I think it better to just include it for them, but this should be explicit IMO. Keep a separate directory for consumable modules and add it in a way that is preserved and only temporarily add your Find module directory.

You can also call find_package yourself. find_dependency just does some “magic” for the REQUIRED and QUIET flags for you. Other than that, it’s just a normal find_package call. You can see how SMTK does it here.

3 Likes

Simple enough :sweat_smile:

Interesting. I suppose that I can restore the module path to its previous state after my -config.cmake file returns, but export a MY_PACKAGE_MODULE_PATH variable that consuming projects can then choose to append to their module path (or not).

I mean, I guess it depends what’s in there, or what you think they might want to only-conditionally include.

  • If it’s the set of Find modules you packaged with the configuration for its dependencies, that’s a moot point because the discovery will already be done by the time any path variable is defined. But you should just go ahead and always add that directory to the path (temporarily) when running find_dependency() / find_package(), anyway. CMake will pick up any system-installed config-file packages first, and only fall back to the Find module if there’s no match. So, a redundant Find module is not generally a problem.1

    If a parent project wants to override your dependency discovery, all it has to do is preemptively find_package() the dependency first, before the find_package() that imports your config — the result will be cached and the find_*() call in your config will become a no-op. The parent project can even supply their own Find module that overrides your bundled one, as long as they find_package() the module’s name before your config gets a chance to. I’ve had to do that with some config-file packages that were doing weird things with the dependencies they discovered and imported.

  • If they’re .cmake files that define CMake macros or functions the parent project may want to call, I’m with Ben — I’d just include them from the config. If we’re talking about dependencies here, there isn’t likely to be that much bundled in the form of cmake code, right? Name the functions/macros something sensible that isn’t going to conflict, prefixed with your package name or whatever, and call it a day.

  • If they’re .cmake files that set variables or modify the build environment, consider whether that couldn’t better be expressed as targets / properties on targets that are defined by the config, instead.

    You can pack a lot of customization into IMPORTED INTERFACE targets, especially if they then use generator expressions to further customize their properties. And since all IMPORTED targets are optional, the parent project just has to use a target_link_libraries() call on their own targets to apply whichever they choose.

  • But if it’s the rare type of package that supplies optional includes in the form of CMake modules, like KDE’s ECM (Extra CMake Modules), then… yeah, they just define variables to hold their file paths, and the documentation leads off:

    To use ECM, add the following to your CMakeLists.txt:

    find_package(ECM REQUIRED NO_MODULE)
    set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
    

    (note that you may want to append ${ECM_MODULE_PATH} to CMAKE_MODULE_PATH rather than discarding the existing value). You can then just include the modules you require, or use find_package() as needed. For example:

    include(ECMInstallIcons)
    

Notes

  1. (Though, a redundant Find module can cause issues in other ways, as I discovered the hard way when one of our libraries had an out-of-date bundled FindPython.cmake that was failing to discover current Python releases. The CMake-provided Find module in the system install had no problems at all. I have no idea why that module was there, but just deleting it from the repo solved the entire problem. Lesson: Only provide Find modules for dependencies when you really have to.)
1 Like