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

2

31

We have a number of Git repositories, some containing our own code and some containing slightly modified third-party library code. A simplified dependency graph looks like this:

  executable_A
    |     |
    |     v
    |  library_B
    |     |
    v     v
   library_C

So the executable has two dependencies on library_C, one direct and one transitive. I am hoping to tie this all together using Git submodules and CMake, so a simplified directory structure looks like this:

executable_A/
  CMakeListst.txt
  library_B/
    CMakeLists.txt
    library_C/
      CMakeLists.txt
  library_C/
    CMakeLists.txt

As you can see, the library_C repository is included as a submodule twice. Let's assume that both submodules are pointing at the same commit (any ideas about how to enforce that would be welcome, but are not the topic of this question).

We're using add_subdirectory, target_link_libraries and target_include_directories to manage these interdependencies. Pretty standard.

The problem is that CMake doesn't like it if you create a target with the same name twice, so it complains:

CMake Error at library_C/CMakeLists.txt:13 (add_library):
add_library cannot create target "library_C" because another target with the same name already exists. The existing target is a static library created in source directory ".../library_B/library_C".
See documentation for policy CMP0002 for more details.

I'd rather not remove the direct dependency of executable_A on library_C, because the fact that it is pulled in via library_B is an implementation detail of library_B that should not be relied on. Moreover, this approach will break down as soon as we add another dependency like executable_A --> library_D --> library_C.

(This question is the closest I could find, but is a bit more general and remains unanswered anyway.)

Beira answered 23/3, 2017 at 14:6 Comment(2)
Common approach is to check whether some project-specific target exists (if(TARGET library_C)) before stepping into the project with add_subdirectory().Sweptback
@Sweptback if(NOT TARGET library_c) then, of course. Sounds like a viable approach. Care to post it as an answer?Beira
S
22

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()
Sweptback answered 23/3, 2017 at 19:19 Comment(2)
Hey Tsyvarev, is it also possible to make multiple target. The problem I have with this approach is versioning. What happens if project A depends on version 1.0.0 of library C and library B depends on version 1.0.1 of library C. I mean you could always choose the highest version, but ideally each project should require the specified version. Do you have any idea how this would be possible?Xerophyte
It is very unlikely, that you may have in the build tree the same project but of different versions. Targets names are rarely changed when the project evolves, so both versions of the project would have the same target names, which cannot be handled by CMake. For avoid target name clashing, you may have pre-built versions of the external project, or use ExternalProject_Add for build them.Sweptback
S
3

CMake 3.10 and later now support:

include_guard(GLOBAL)

Analogous to #pragma once in c++.

A great book on cmake that explained so much to me is "Professional CMAKE, A Practical Guide" by Craig Scott. Available online only at: [https://crascit.com/professional-cmake/][1]

I don't Craig, but his book is a great service to cmake newcomers like myself.

Sickler answered 7/11, 2019 at 19:30 Comment(1)
Good to know that this exists! I don't think it would have solved this particular problem though, because the submodule is present twice in the source tree. CMAKE_CURRENT_LIST_FILE would be different for each incarnation.Beira

© 2022 - 2024 — McMap. All rights reserved.