add_subdirectory vs ExternalProject_Add vs include(path/to/ProjectConfig.cmake)

Hello all,

I’m having trouble distinguishing the best way to include dependencies. Ultimately I would like to be able to do find my dependencies in the CMAKE_INSTALL_PREFIX and make sure to have reproducible builds and not use system libraries. This means I will compile a lot of dependencies my self. I am wondering if this is a valid pattern below?

find_package(PackageName)
if (NOT PackageName_FOUND)
  # build package HERE
  find_package(PackageName REQUIRED)
endif()

# ...

target_link_libraries(prj PackageName::Libraries)

Or should I make my own FindPacakge.cmake files per each dependency and have this cmake script look in the install path for the library and build it if it doesn’t exist from inside this FindPackage script itself? I’m not sure whats best approach or what is latest way to do this.

And finally, what is the best way to build an external project? add_subdirectory if it supports cmake? ExternalProject_Add() if it doesn’t support cmake? Or include path/to/project/Config.cmake if it exists?

This is a great, general question. There are multiple ways to approach this, each with their own tradeoffs. The way I understand it is the following (I would greatly appreciate clarity/correction from others too):

  • add_subdirectory: can be used if the other project is a Git submodule or similar of your main project. Intermingles scope like a conventional subdirectory
  • FetchContent: instead of a Git submodule, use Git or cURL or similar to download other Cmake project. Intermingles scope as well–better for external projects that you control or understand thoroughly
  • ExternalProject: Can build other project (autotools, makefile, etc) without mingling scope. Requires extra code in your project to declare libraries from the other project.

I do follow like your pattern:

find_package(Foo)
if(NOT Foo_FOUND)
  # fetchcontent, external project, or git submodule update
  # if external project, additional code to specify targets from that project
endif()

I have a few types of examples. This one uses two externalProjects to build Zlib and HDF5, if they’re not found: h5fortran/CMakeLists.txt at main · geospace-code/h5fortran (github.com)

Another example using Git submodule: p4est/CMakeLists.txt at prev3-develop · cburstedde/p4est (github.com)

FetchContent if lapack not found: scalapack/lapack.cmake at master · scivision/scalapack (github.com)

These examples could be further optimized, but give examples of each in somewhat widely used projects.

Thanks for guidance and some examples.

I notice the hdfortran project uses GLOBAL when declaring it’s library targets. Is this required in order for adjacent ExternalProject’s to be able to also link to these targets? Or is global serving a different purpose? The docs say

The target name has scope in the directory in which it is created and below, but the GLOBAL option extends visibility. It may be referenced like any target built within the project.

But it’s not explicit what they mean by within the project in this context when you are linking together a bunch of projects!

Also - because ExternalProject_Add runs at build time, not at initial cmake configure time, and because I have multiple projects that are are maintained by me (that i control) but are External Projects relative to my current project because these other dependencies maintained by me are also used elsewhere, and if these external projects maintained by me are using this pattern:

find_package(Foo)
if(NOT Foo_FOUND)
  # fetchcontent, external project, or git submodule update
  # if external project, additional code to specify targets from that project
endif()

Then this means i have multiple external projects trying to look for the same dependency multiple times and none of them would find_package if it doesn’t already exist in the install directory already.

I got around this by using the “find or build” logic, by building the package using execute_process which will run at cmake instantiating time. When I do it this way, then the first project who needs the dependency will first detect it’s not there and build it synchronously. Then adjacent projects will find it using find_package.

Long story short, is using execute_process instead of ExternalProject_Add also a valid way to build a dependecy? When there is so many ways to skin a cat i guess it can be a good thing or a bad thing. I want to make sure i am not too much in the weeds.