[CMake] Modular build system for repository with libraries shared between multiple targets.

Mateusz Zych mte.zych at gmail.com
Sun Aug 12 08:45:39 EDT 2018


Hi, I'm trying to understand what's the recommended way of
implementing CMake build system in the following repository:

repo
|
|___ util
|    |___ include
|    |    |___ util
|    |         |___ util.h
|    |
|    |___ source
|    |    |___ util.cpp
|    |
|    |___ CMakeLists.txt
|
|___ tool
|    |___ include
|    |    |___ tool
|    |         |___ tool.h
|    |
|    |___ source
|    |    |___ tool.cpp
|    |
|    |___ CMakeLists.txt
|
|___ app
     |___ app.cpp
     |
     |___ CMakeLists.txt

Essentially this repo contains static library util, shared library tool and
executable app.

Both executable app and shared library tool depend on static library util,
but they don't depend on each other.
This can be represented by dependency graph like so:

app   tool
 |    |
 |    |
 V    V
  util

The reason why static library util is placed in the root of repo,
is the fact that it's shared by both executable app and shared library tool.

Writing //repo/util/CMakeLists.txt is very easy:

cmake_minimum_required(VERSION 3.11)

*project(util)*

add_library(util STATIC)
target_sources(util PRIVATE include/util/util.h
                                  source/util.cpp)
target_include_directories(util PUBLIC include)


However, implementing //repo/tool/CMakeLists.txt

cmake_minimum_required(VERSION 3.11)

*# no project definition*


add_library(tool SHARED)
target_sources(tool PRIVATE include/tool/tool.h
                                  source/tool.cpp)
target_include_directories(tool PUBLIC include)
target_link_libraries(tool PRIVATE util)


and //repo/app/CMakeLists.txt is more challenging.

cmake_minimum_required (VERSION 3.11)

*# no project definition*

add_executable(app)
target_sources(app PRIVATE app.cpp)
target_link_libraries(app PRIVATE util)

I understand CMake project as a set of targets, which are standalone (can
be built by themselves).

Source: https://stackoverflow.com/questions/26878379/in-
cmake-what-is-a-project

This meas that I can define project util

consisting
of one target (static library util),
because target util doesn't have any dependencies
, meaning it's standalone.
Unfortunately defining project
tool
 and project app is problematic,
because
executable
app and shared library
tool
do d
epend on static library
util
.

I found two solutions to this issue, but neither seems "correct" to me:

1. Create //repo/CMakeLists.txt and define project repo which will add util
, tool and app as sub-directories.


cmake_minimum_required(VERSION 3.11)


project(repo)

add_subdirectory(util)

add_subdirectory(tool)

add_subdirectory(app)


This is the cleanest solution, but it has a serious drawback of not
defining project app and project tool,
which could be built independently from each other.


I thought that I will be able to mitigate this issue by
defining multiple projects in //repo/CMakeLists.txt each with its own
separate set of targets.


cmake_minimum_required(VERSION 3.11)

*project(tool)*

*add_subdirectory(util)*

*add_subdirectory(tool)*


*project(app)*

*add_subdirectory(util)*

*add_subdirectory(app)*


project(repo)

add_subdirectory(util)

add_subdirectory(tool)

add_subdirectory(app)


T
his is not possible, since one CMakeLists.txt can define only single
project and
calling add_subdirectory() twice on the same directory is an error.
I believe these constraints arise from the fact that
*CMake projects are not providing any scoping / grouping mechanism for
targets.*

Source: https://cmake.org/pipermail/cmake/2009-June/030188.html


It is possible to create W/A mitigating this issue,
by using if(), elseif() and else() commands in //repo/CMakeLists.txt,
but this solution looks to me like an awful hack.

cmake_minimum_required(VERSION 3.11)


*if(TOOL)*

project(tool)

add_subdirectory(util)

add_subdirectory(tool)

*elseif(APP)*

project(app)

add_subdirectory(util)

add_subdirectory(app)

*else()*

project(repo)

add_subdirectory(util)

add_subdirectory(tool)

add_subdirectory(app)

*endif()*


Making matters worse this approach requires global handling of
sub-directories in a project,
meaning that knowledge of the whole dependency graph is required to add
sub-directories in correct order.

This breaks down the most desirable properties of modern CMake - modularity
and isolation of targets.


2. Define project as a self-containing set of targets by adding
sub-directories were they are required.


In this approach //repo/tool/CMakeLists.txt would add upper directory util
by defining not only source directory, but also binary directory.

cmake_minimum_required(VERSION 3.11)


*project(tool)*

*add_subdirectory(../util util)*


add_library(tool SHARED)

target_sources(tool PRIVATE include/tool/tool.h

                                  source/tool.cpp)

target_include_directories(tool PUBLIC include)

target_link_libraries(tool PRIVATE util)


Using the exact same technique //repo/app/CMakeLists.txt would also add
upper directory util.


cmake_minimum_required (VERSION 3.11)


*project(app)*

*add_subdirectory(../util util)*


add_executable(app)

target_sources(app PRIVATE app.cpp)

target_link_libraries(app PRIVATE util)


This approach allows each project to be stand-alone,
because all sub-directories required by targets directly defined by that
project are added directly in place.


This is huge advantage compared to the first solution,

because project is responsible only for adding sub-directories,

which are required by direct dependencies of
targets defined
directly
in that project.


All sub-directories required for satisfying project's indirect dependencies
will be added automatically by projects defining targets,
which are direct dependencies of
targets defined
directly
in that project.

For example, if target util would depend on target foo,
then in project util sub-directory foo would be added to satisfy that
dependency.

cmake_minimum_required(VERSION 3.11)


project(util)

*add_subdirectory(../foo foo)*


add_library(util STATIC)

target_sources(util PRIVATE include/util/util.h

                                  source/util.cpp)

target_include_directories(util PUBLIC include)

* target_link_libraries(util PRIVATE foo)*


So in case of app project util sub-directory would be added first,
then foo sub-directory would be added by the util project.
Effectively adding sub-directory will create a cascade of
add_subdirectory() commands,

meaning that sub-directories would be added transitively.

This approach doesn't require knowledge of the whole dependency graph -
just direct dependencies are needed to be known.

Now targets can be modular and isolated, which is what modern CMake is all
about.



Unfortunately this approach has major flaw - it doesn't allow sharing
libraries between targets.
It's a trap giving false illusion, that knowledge of whole dependency graph
is not required.

Consider adding repo project in //repo/CMakeLists.txt.

cmake_minimum_required(VERSION 3.11)


*project(repo)*


*add_subdirectory(tool)*

*add_subdirectory(app)*


This will immediately fail,
because directory util will be added twice - first time by project tool and
second time by project app.
Essentially in this approach projects cannot share libraries,
because it is required to  control globally how sub-directories are added
to make sure that they are added exactly once.


Considering that both solutions feels like complete failures,
what is recommended way of implementing CMake build system in described
repository?

Am I missing something or CMake legitimately lacks mechanisms for handling
this case properly?
Honestly it's hard to believe that CMake would lack support for this case,
considering this is such a widely used and flexible build system.

I would really appreciate help with solving this issue. :-)
Thank you, Mateusz Zych
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://cmake.org/pipermail/cmake/attachments/20180812/8cb34203/attachment-0001.html>


More information about the CMake mailing list