0

I maintain 3 static libraries: a, b and c, that use CMake. They are located in separate git repositories and can be standalone.

  • a is a very basic library, that has no dependencies.

  • b depends on a. It includes a as a submodule and adds it with add_subdirectory.

  • c depends on both of a and b. It includes them as submodules and adds with add_subdirectory.

So, the structure of c repo is as follows:

/c (repository root)
|
+-- CMakeLists.txt (add_subdirectory(a), add_subdirectory(b))
|
+--/a (git submodule)
|  |
|  +-- CMakeLists.txt (add_library(a))
|
+--/b (git submodule)
   |
   +-- CMakeLists.txt (add_subdirectory(a))
   |
   +--/a (git submodule added recursively)
      |
      +-- CMakeLists.txt (add_library(a))

We can see, that a is duplicated: add_library(a) is called twice and cause an error about duplicated target.

I can wrap every add_subdirectory into if (NOT TARGET ...), but it seems to be improper solution to the problem. I could also just make c use a from b, but I don't want c to "know about" b's internals and dependencies.

I saw here on SO some advice not to use add_subdirectory on projects, that can be included into another ones, but can't figure out alternative way.

To sum up: what is the right way to deal with such dependencies using CMake? The main restriction is that b must be standalone, i.e. I want to build it without building c.

Moreover, I don't want to "install" these libs. If I would build b, then a must be built once just before b. If I would build c, then a and b must be built once just before c.

starball
  • 20,030
  • 7
  • 43
  • 238
Georgy Firsov
  • 286
  • 1
  • 13

2 Answers2

3

I suppose you could "play it fair" and make neither B nor C assume that they can add A without conflict.

What you suggested with if (NOT TARGET ...) would work. You could also check if something like <PROJECT-NAME_SOURCE_DIR> is defined- assuming that each of A, B, and C are their own projects in the CMake sense of the word.


But actually, if you're using git subdirectories for them anyway, you might as well look into switching to use FetchContent- in which case the call to FetchContent_MakeAvailable will only do things if the thing to fetch hasn't already been fetched. In this case, you would FetchContent_Declare A in B, and A and B in C, and do the same for FetchContent_MakeAvailable. Make sure to heed the guidance in the docs which says:

The FetchContent_Declare() function records the options that describe how to populate the specified content. If such details have already been recorded earlier in this project (regardless of where in the project hierarchy), this and all later calls for the same content <name> are ignored. This "first to record, wins" approach is what allows hierarchical projects to have parent projects override content details of child projects.

[...]

Projects should aim to declare the details of all dependencies they might use before they call FetchContent_MakeAvailable() for any of them. This ensures that if any of the dependencies are also sub-dependencies of one or more of the others, the main project still controls the details that will be used (because it will declare them first before the dependencies get a chance to).

starball
  • 20,030
  • 7
  • 43
  • 238
  • It seems to be what I'm looking for, I'll read about `FetchContent` in more details and hit "answered" button afterwards. Moreover, I found `ExternalProject`. May it be a solution too? – Georgy Firsov Jul 08 '23 at 07:40
  • 1
    @GeorgyFirsov FetchContent and ExternalProject are quite different in usage. For one thing, the first fetches at configure time and the other fetches at build time. With the first, fetched projects are part of the "host" buildsystem, and with the second, they are built more "separately" (separate config, need to install and import the targets, etc.). – starball Jul 08 '23 at 07:47
  • got it. Is it correct understanding of `FetchContent` "on high level": I use `FetchContent_Declare` and `FetchContent_MakeAvailable` for project `a` in `b` and the same things for `a` and `b` in `c`? In that case `a` and `b` will be added once and I can just use their targets without any `add_subdirectoty`? – Georgy Firsov Jul 08 '23 at 07:59
  • @GeorgyFirsov see the edit I just made. – starball Jul 08 '23 at 08:15
1

A good way to use a dependency inside your project without specifying the exact details of where the dependency comes from is to use find_package.

This allows you to replace the logic later without modifying the code and also allows for the user to overwrite the way the dependency is added.

cmake_modules/FindA.cmake

if (NOT TARGET ...)
   add_subdirectory(...)
endif()

set(A_FOUND 1)

CMakeLists.txt

# provide a source of the module to serve as fallback, in case the user hasn't already provided one
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake_modules)

find_package(A REQUIRED)

... use the lib here ...

This allows the user to set CMAKE_MODULE_PATH adding a directory containing an alternative FindA.cmake which could e.g. could refer to cmake configuraton files installed alongside the lib on the system.

alt_cmake_modules/FindA.cmake

set(_A_REQUIRED_OPT)
if (A_FIND_REQUIRED)
    set(_A_REQUIRED_OPT REQUIRED)
endif()
find_package(A CONFIG ${_A_REQUIRED_OPT})

Cmake configuration

cmake -D "CMAKE_MODULE_PATH=$(pwd)/alt_cmake_modules" -S ... -B ...

You could also easily combine this with the suggestion with the approach presented in starball's answer.

fabian
  • 80,457
  • 12
  • 86
  • 114