How to use FetchContent correctly for building external dependencies

Hi, I am trying to use FetchContent to build and statically link against a third party dependency in my c++ project.

I am hitting the following problems:

  1. Install directives from the dependency are also added to my project, which causes the dependency libs to be installed. I circumvented this by using “FetchContent_Populate” and add_subdirectory( … EXCLUDE_FROM_ALL).

  2. add_test directives from the dependencies are also added to my project, which causes ctest to also run all tests of the dependency. This fails however, as “EXCLUDE_FROM_ALL” from the above workaround caused the tests to not build - duh. I circumvented this by adding a dependency from my own tests to the dependency test executable.

  3. Settings like “CMAKE_RUNTIME_OUTPUT_DIRECTORY” apply also to the dependency, which then causes cmake to not find the test executable of the dependency when running ctest. I circumvented this, by adding “RUNTIME_OUTPUT_DIRECTORY” as a target property to all my executables.

In general I am having the feeling of building a house of cards with lots of circumventions.

Is FetchContent designed to handle external code dependencies? If so how to use it correctly?

Is there a possibility to use FetchContent in a more isolated way. I.e. Do not add tests and install directives, just expose the targets to link against?

What are the best practices here?

Some dependencies don’t expect to be added to a parent project and can be less convenient to pull in with FetchContent. Install rules and tests are common areas where this shows up. There are some things you can potentially do to reduce the inconvenience, depending on the situation.

For (1), you can sometimes work around the install problems by making better use of install components. If the dependencies don’t use components with their install() commands, you can change the default component just before you pull the dependency in by setting the CMAKE_INSTALL_DEFAULT_COMPONENT_NAME variable. Then in your main project, omit that component from the set of components you want to include in your packages (take a look at the CPACK_COMPONENTS_ALL variable).

Dealing with tests is harder. If you use the method above to handle the install problems, then you will avoid the need for EXCLUDE_FROM_ALL and then you should be able to avoid the dependency problems you ran into. The dependency’s tests will still show up and be included in the main projects tests, but that is hard to avoid without the dependency providing some sort of switch to turn off adding tests. A well-structured project should ideally provide a project-specific CMake variable that can be passed down to turn tests on or off, and it should default to true if the dependency is being built on its own and false if it is not the top level project.

The output directory can be handled by unsetting CMAKE_RUNTIME_OUTPUT_DIRECTORY just before you pull in the dependency (you can restore its value again after pulling in the dependency if something later in that CMakeLists.txt file still needs id).

As stated at the beginning, FetchContent relies on the dependency projects you pull in being structured in a way that supports them not being the top level project. If you’re in control of all those dependencies (as is often the case in company environments), it can work very well. If you’re pulling in many arbitrary dependencies from the open source world, your experiences can vary widely. Some projects take care and behave well, others are downright terrible and have little interest in improving the situation. For the latter, some people maintain their own fork or set of patches to address shortcomings in such dependencies, but you would have to decide what path makes the most sense for your situation.

1 Like

I agree with Craig’s comments and for your type of application I would consider ExternalProject. In general, I default to using ExternalProject unless I truly need something at configure time–then I use FetchContent, which saves me from refactoring my CMake project into a superbuild.

Thank you for your very helpful answers.

I think with Craig’s hints, I can already reduce my pain a little bit and at least work around the issues in a more clean way.

For my next projects I will have a closer look at ExternalProject or maybe something like Conan. I guess there are some other problems with this approach (finding libs, configuring build, etc.).

For context: In my case I am trying to integrate gRPC into my application.

Great! A common project layout I have is where my library needs external libraries out of my control. First I find_package and if they’re not found or fail check_source_compiles() test, I use ExternalProject to build them.
Example: h5fortran will build ZLIB and HDF5 via ExternalProject if broken or not found.