How to Handle a Transitive Dependency Conflict Using Git Submodules and Cmake

How to handle a transitive dependency conflict using Git submodules and CMake?

There are several approaches for detect and discard inclusion of the project, which has already be included in some other parts of the main project.

Check project's target existence

The simplest pattern for single inclusion of subproject is checking existence of some subproject's target:

# When include 'C' subproject
if(NOT TARGET library_C)
add_subdirectory(C)
endif()

(Here we assume that project C defines target library_C.)

After such conditional inclusion all subproject's targets and functions will be immediately available for the caller with garantee.

It is better to use this pattern in all places (in executable_A and library_B). Such a way changing order of library_B and library_C in executable_A doesn't break correctness.

This pattern can be reworked for use by subproject itself:

# At the beginning of 'C' project
cmake_minimum_required(...)
if(TARGET library_C)
return() # The project has already been built.
endif()

project(C)
...

Check project existence

When a project is created, CMake defines several variables for it, and <PROJECT-NAME>_BINARY_DIR is among them. Note, that this variable is cached, so when cmake is called the second time (e.g. if some of CMakeLists.txt has been changed), the variable exists at the very beginning.

# When include 'C' subproject
if(NOT C_BINARY_DIR # Check that the subproject has never been included
OR C_BINARY_DIR STREQUAL "${CMAKE_CURRENT_BINARY_DIR}/C" # Or has been included by us.
)
add_subdirectory(C)
endif()

This pattern can be reworked for use by subproject itself:

# At the beginning of 'C' project
cmake_minimum_required(...)
if(NOT C_BINARY_DIR # Check that the project has never been created
OR C_BINARY_DIR STREQUAL "${CMAKE_CURRENT_BINARY_DIR}" # Or has been created by us.
project(C)
else()
return() # The project has already been built
endif()

CMake build git submodules and its dependencies

Your mistake adding B as a dependency to RootProject where A needs it.

Solution

In the following directory structure:

RootProject
|__ .git
|__ CMakeLists.txt
|__ src
| |__ main.c
| |__ ...
|
|__ vendor
|__ A
|__ .git
|__ CMakeLists.txt
|__ src
|__ ...
  • RootProject depends on A.
  • A is a library that has no dependencies.

If you want to add B as a dependency to A, you would treat A as completely separate project from RootProject and add B directly to A.

The directory structure of A becomes:

A
|__ .git
|__ CMakeLists.txt
|__ src
| |__ ...
|
|__ vendor
|__ B
|__ .git
|__ CMakeLists.txt
|__ src
|__ ...

Using git submodules to "manage" the dependencies:

# Adding `B` as a dependency to `A`
cd vendor/A
mkdir -p vendor
cd vendor
git submodule add https://github.com/username/B.git

This will add B as a dependency to A, and not to RootProject.

Now simply add the following to vendor/A/CMakeLists.txt:

add_subdirectory("vendor/B")
# ......
target_link_libraries(A PUBLIC B)

Also note that each of A, B and RootProject must have a top level CMakeLists.txt in order for add_subdirectory to work.

See also:

  • add_subdirectory
  • find_package
  • Git submodules

CMake and using git-submodule for dependence projects

You can check whether the target a already exists before calling add_subdirectory:

if (NOT TARGET a)
add_subdirectory(ProjectA)
endif ()

so that it only adds the subdirectory once for your whole CMake project.

CMake: How to tell where transitive dependency is coming from?

You can parse dot file generated by graphviz and extract details which you want. Below is sample python script to do that.

import pydot
import sys

graph = pydot.graph_from_dot_file(sys.argv[1])
result = {}

for g in graph:
# print(g)
for node in g.get_node_list():
if node.get("label") != None:
result[node.get("label")] = []

for edge in g.get_edges():
result[g.get_node(edge.get_source())[0].get("label")].append(g.get_node(edge.get_destination())[0].get("label"))

for r in result:
print(r+":"+",".join(result[r]))

You can also add this script to run from cmake as custom target, so you can call it from you build system. You can find sample cmake project here

Cmake : several projects using custom target to make test

So, is there a way to define a custom target locally to a cmake project for example ? or is there a clever way to do what I would like to do ? (which seems quite natural)

For you are working with custom targets, probably ALLOW_DUPLICATE_CUSTOM_TARGETS can do the job.

The documentation is quite clear indeed:

Allow duplicate custom targets to be created.

If you go a bit further you find that:

The property allows multiple add_custom_target command calls in different directories to specify the same target name.

Unfortunately it has also some limitations:

[...] setting this property will cause non-Makefile generators to produce an error and refuse to generate the project.

Read carefully the documentation (see the link above) to know if it's suitable for your purposes.

cmake transitive usage: propagate libs but keep headers private

This is exactly what PRIVATE linking does. It propagates libraries and keeps headers private.

target_link_libraries(mylib PRIVATE koko)

CMake Dependency Management

Quick update: It seems that Hunter has a git submodule mode: https://hunter.readthedocs.io/en/latest/user-guides/hunter-user/git-submodule.html

Thus it actually fits my use case perfectly.

Have this is in your CMakeLists.txt

include(your/local/path/to/HunterGate.cmake)
HunterGate(
URL "https://github.com/cpp-pm/hunter/archive/v0.23.320.tar.gz"
SHA1 "9b4e732afd22f40482c11ad6342f7d336634226f"
LOCAL
)

hunter_add_package(mysubmodule_lib)
find_package(mysubmodule_lib CONFIG REQUIRED)

Have this at cmake/Hunter/config.cmake in your project:

hunter_config(mysubmodule_lib GIT_SUBMODULE your/path/to/mysubmodule_lib VERSION 1.2.3)


Related Topics



Leave a reply



Submit