Best practices for dependency management in a nested library / superproject setup (FetchContent vs. find_package)

Hello,

My team and I really like the versatility of FetchContent and we use it almost everywhere. We’ve built up a fair number of internal libraries to increase development speed, so things feel like Lego blocks now. That’s great, but it’s also brought me to a lot of problems with dependency handling. I see many projects using very different approaches (variables, custom functions, etc.) and I just can’t figure out what the best practice is supposed to be.

The main goal for us is not only to make things stable, but also to require the bare minimum work from the end user: ideally either FetchContent or find_package (and that’s it), unless they explicitly want to override something.

For context: we’re using CMake 3.30.

I have a fairly complex dependency tree that looks something like this:

  • LibA, LibB – external libraries (third-party).
  • LibC – external library that depends on LibA and LibB (not ours).
  • LibD – our library, depends on LibC and LibB.
  • LibE – our standalone library (no dependencies).
  • LibF – our library, depends on LibD.
  • exec – final executable, links against LibD, LibE, LibF.

My questions are:

  1. Where is it okay to use FetchContent, and where is it not advised?

    • Should FetchContent only appear at the top-level superproject (exec), or is it acceptable inside libraries (like LibD) to pull in their dependencies?
  2. Is it possible to make this work without introducing options/variables in each subproject?

    • For example, can I just write each library using find_package for its deps, and then let a superproject override everything with FetchContent && OVERRIDE_FIND_PACKAGE, ?
  3. What would be the needed order?

    • If I want to use FetchContent in the superproject for everything, do I need to declare and make available A, B, C before D and F, etc.?
  4. Export sets and collisions.

    • I often see errors like:
install(EXPORT ...) includes target "foo" which requires target "bar" that is not in any export set

the most apperent it is when building grpc using FetchContent for instance they go off after setting set(libname_ENABLE_INSTALL OFF)

  1. Handling SO name conflicts.
    • If both my project and the superproject rely on (say) LibB as a .so with the same SONAME/symbols, what’s the right way to avoid collisions?
      • Should I never export third-party libraries from my package along with my package?
      • Should I let the superproject always own the third-party deps (No FetchContentin the lib)?
      • Or is it okay to re-export them, then what do i do if my exec wants other version always not forget updating the lib along the exec?

In short:

  • We want stability and a simple user experience (just find_package or FetchContent, nothing more). If it’s possible. For instance setting up lib to use gRPC caused so much errors and fiddling with vars it uses, using set or option with Forcing cache to fix them, and those may not show up for a really long time.
  • But I’m really confused about what is considered “best practice” for balancing find_package, FetchContent, and install(EXPORT …).
  • Sometimes things work fine for months, then break with the smallest environment change.

Thanks for the help in advance