Propagation of FetchContent targets when installing

Hi there,

I’ve run into this issue I don’t quite understand and was hoping someone could help clarify.

I have a simple header-only library (LibA) which is used by a static library (LibB) via FetchContent. I would like to install LibB and the have an application (App) bring it in via find_package.

The problem I’ve run into is App can find LibB, but it complains that:

Found package configuration file:

    <path>/<to>/install/lib/cmake/LibB/LibB-config.cmake

  but it set LibB_FOUND to FALSE so package "LibB" is
  considered to be NOT FOUND.  Reason given by package:

The following imported targets are referenced, but are missing: LibA::LibA.

When I run the install command it does look like the files for LibA are installed as well, so I’m not sure what I’m missing to get this to work as expected.

LibB references LibA in its target_link_libraries command (so it can be built on its own) but it seems this isn’t exported (whereas if it had also been brought in by find_package it would have been propagated as expected).

Is there an extra step I’m missing somewhere or is this simply not possible?

Thank you very much for your time,

Tom

Here’s an approximation of the CMakeLists.txt files for reference

# LibA

cmake_minimum_required(VERSION 3.15)
project(LibA LANGUAGES CXX)

add_library(${PROJECT_NAME} INTERFACE)
target_include_directories(
    ${PROJECT_NAME}
    INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>/include)

# install (if required)
target_include_directories(
    ${PROJECT_NAME}
    INTERFACE $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
install(
    TARGETS ${PROJECT_NAME}
    EXPORT ${PROJECT_NAME}-config)
install(
    EXPORT ${PROJECT_NAME}-config
    NAMESPACE ${PROJECT_NAME}::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(
    DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/${PROJECT_NAME}/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME})
# LibB

cmake_minimum_required(VERSION 3.15)
project(LibB LANGUAGES CXX)

include(FetchContent)
FetchContent_Declare(
    LibA
    GIT_REPOSITORY https://github.com/me/LibA.git)

add_library(${PROJECT_NAME})
target_sources(${PROJECT_NAME} PRIVATE src/LibB.cpp)
target_include_directories(
    ${PROJECT_NAME}
    PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/>)
target_link_libraries(${PROJECT_NAME} PUBLIC LibA)

# install (if required)
target_include_directories(
    ${PROJECT_NAME} INTERFACE $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
install(
    TARGETS ${PROJECT_NAME}
    EXPORT ${PROJECT_NAME}-config
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(
    EXPORT ${PROJECT_NAME}-config
    NAMESPACE ${PROJECT_NAME}::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(
    DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/include/${PROJECT_NAME}/
    DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME})
# App

cmake_minimum_required(VERSION 3.15)
project(App LANGUAGES CXX)

find_package(LibB REQUIRED CONFIG)

add_executable(${PROJECT_NAME})
target_sources(
    ${PROJECT_NAME} PRIVATE main.cpp)
target_link_libraries(
    ${PROJECT_NAME} PRIVATE LibB::LibB)
1 Like

This is the heart of your problem:

# LibB

install(
    EXPORT ${PROJECT_NAME}-config
    NAMESPACE ${PROJECT_NAME}::
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})

Using your export file directly as your <projName>-config.cmake file is a common trap. It is fine for very simple projects that have no dependencies, but as you’ve discovered, once you have dependencies this no longer works. The pattern I normally recommend is to manually write your package’s config file and have it include() the export file(s) that you generate. You can then pull in your dependencies in your manually written package config file. For example:

libb-config.cmake:

include(CMakeFindDependencyMacro)
find_dependency(LibA)
include(${CMAKE_CURRENT_LIST_DIR}/LibB_Targets.cmake) 

The install command in your LibB project then installs to the name used above instead of to the config file directly (you also have to install the above file too):

install(
    FILES libb-config.cmake
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})

install(
    EXPORT ${PROJECT_NAME}-config
    NAMESPACE ${PROJECT_NAME}::
    FILE ${PROJECT_NAME}_Targets.cmake   # <----------
    DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})

(Since I think you’ve mentioned that you have my book previously, see Section 26.7.1: Config Files For CMake Projects which talks about this topic. For those who don’t have the book, you can download the slides from my CppCon 2019 talk which covers aspects of this and there are some bonus slides at the end which sketch out the steps in CMake code)

Note also how the above shows that using ${PROJECT_NAME} for something whose name can’t actually change without breaking things is a bit of an ant-pattern. The manually written libb-config.cmake file can’t use ${PROJECT_NAME} and anyone depending on your package is going to expect the package name to never change. It’s usually clearer to just use that name directly in the places you’ve used it in your example (your code will be more readable too).

2 Likes

Hi @craig.scott!

Thank you so much for your detailed response! I really appreciate it :slightly_smiling_face: I think I understand now (I’m going to have to re-read it a couple of times and map things back to my concrete use case). Hopefully I should be able to get things working by adding a new -config.cmake file as you suggest.

I do have your excellent book yes, I’ll make sure to check out the section you recommend (I’d re-read the FetchContent section and sections after but hadn’t been able to find what I was after unfortunately :see_no_evil:)

Thanks again and I’ll report back when things are working, hopefully with a link to the projects (might be tomorrow now as I’m on UK time).

Much appreciated!

Tom

@craig.scott One thing I wanted to clarify is should libb-config.cmake appear at the root of the project alongside CMakeLists.txt? I’ve just tried hooking things up now but when I run the install command I see…

-- Installing: <path/to/install>/lib/cmake/libb/libb_Targets.cmake
-- Installing: <path/to/install>/lib/cmake/libb/libb_Targets-noconfig.cmake

Instead of…

-- Installing: <path/to/install>/lib/cmake/libb/libb-config.cmake
-- Installing: <path/to/install>/lib/cmake/libb/libb-config-noconfig.cmake

My example is slightly more complicated than the example (the dependency chain contains a few more FetchContent commands) so I might need to fix things for earlier projects too…

Hmm I’m afraid I’m having real trouble getting that to work, it seems like the installed file ends with _Targets not -config if I do that so App can’t find the dependency LibB at all. Is there something else I need to do with a -config.in file? I don’t see any reference to _Targets in your book either. If you have any other suggestions I’d be very grateful to hear. Thank you!

I updated my previous reply - you have to install the libb-config.cmake file too. The book uses examples that have two export sets, not just one, so the exports to look for there are MyProj_Runtime and MyProj_Development. The book also uses the ProjNameConfig.cmake convention rather than projname-config.cmake, but either would be fine.

1 Like

Thank you very much for the updated example @craig.scott, I’ve now got things working using this approach :grinning_face_with_smiling_eyes:

One alternative approach I found is instead of writing and installing the libb-config.cmake file it’s possible to add find_dependency (or just find_package) with the name of the missing dependency (in this case LibA) to the App CMakeLists.txt file and things also work (example below).

From a consumer (application developer) standpoint the version you outline is better as you just have to use find_package on the library you want, and it must have its dependencies exported correctly. But from a producer (library developer) standpoint having to write the additional -config.cmake file is a little bit of a pain. I’m curious if there’s a way to essentially ‘export’ the dependency directly from the CMakeLists.txt file itself of LibB? No worries if not I was just wondering :slightly_smiling_face:

Thank you again very much for your help! I’ll aim to get things tidied up and will include a link to the libraries as an example when they’re done :+1:

All the best,

Tom

Update: Also I wanted to check this is also not technically required if everything in the dependency chain is found using find_package as opposed to FetchContent (or using add_submodule directly). I think that’s where my main confusion came from where it felt like it should work the same… but things are a bit different. Is that assumption correct? Thanks!

cmake_minimum_required(VERSION 3.15)
project(App LANGUAGES CXX)

include(CMakeFindDependencyMacro)
find_dependency(LibA) # this also works

find_package(LibB REQUIRED CONFIG)

add_executable(${PROJECT_NAME})
target_sources(
    ${PROJECT_NAME} PRIVATE main.cpp)
target_link_libraries(
    ${PROJECT_NAME} PRIVATE LibB::LibB)

Hi there,

I wanted to send an update with a link to the real-world projects I was having this issue with (which are now all working thanks to your help @craig.scott! :grinning_face_with_smiling_eyes:)

This is the base project (a header-only library with no dependencies - a simple little toy math library of mine):

I have another incredibly simple header-only library that uses as to provide a simple free-look camera:

And I’ve built an experimental/prototype controller for camera behaviours (a static library):

Finally, it gets used by a little sample graphics application I made using SDL, bgfx and Dear ImGui:

All the CMake logic that I use I’ve bundled up into a little helper file I can pull in with FetchContent:

I’ve provided the ability for a user to pass the project name and the config name separately per your recommendation. It’s probably not perfect but it cuts out a ton of boilerplate from my projects for now which is nice!

I’ll see if I can write this up soon as a reference for others who might run into the same issue in future.

Thanks again!

Tom

Also on one last somewhat unrelated note… why in the CMake docs does it say that using CMAKE_INSTALL_PREFIX with INSTALL_INTERFACE is wrong (it says it will not be relocatable - I might need to update my code if this is the case).

This is the link to the relevant section: https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html#creating-relocatable-packages

Thanks!

CMAKE_INSTALL_PREFIX is expected to be an absolute path, so if you force this to be your install location, it means you are hard-coding an absolute path for the install, which by definition makes it not relocatable. If you want your package to be relocatable, its install location should be a relative path and it will be treated as relative to whatever the install location is selected to be at install time (that will be CMAKE_INSTALL_PREFIX by default, but it can also be redirected by the user or whatever is driving the install - CPack explicitly overrides the install location when creating packages, for example).

1 Like

I see, I think I understand :stuck_out_tongue_closed_eyes:

Thanks again for all your help! I hope the examples were of some interest.

All the best,

Tom

I realized I was confusing CMAKE_INSTALL_INCLUDEDIR (and other CMAKE_INSTALL_<dir> variants) with CMAKE_INSTALL_PREFIX (More info here and here so I do think my install scripts are correct :+1: