CMake 并查找其他项目及其依赖项

设想下面的场景: Project A是一个共享库,它有几个依赖项(LibALibBLibC)。Project B是一个依赖于 project A的可执行文件,因此也需要构建所有 Project A的依赖项。

此外,这两个项目都是使用 CMake构建的,为了让 Project B使用它,Project A不应该需要安装(通过“ install”目标) ,因为这可能会成为开发人员的一个麻烦。

使用 CMake解决这些依赖关系的最佳方法是什么?理想的解决方案应该尽可能简单(尽管不会更简单) ,并且只需要最少的维护。

52603 次浏览

Easy. Here is the example from the top of my head:

The top level CMakeLists.txt:

cmake_minimum_required(VERSION 2.8.10)


# You can tweak some common (for all subprojects) stuff here. For example:


set(CMAKE_DISABLE_IN_SOURCE_BUILD ON)
set(CMAKE_DISABLE_SOURCE_CHANGES  ON)


if ("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
message(SEND_ERROR "In-source builds are not allowed.")
endif ()


set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_COLOR_MAKEFILE   ON)


# Remove 'lib' prefix for shared libraries on Windows
if (WIN32)
set(CMAKE_SHARED_LIBRARY_PREFIX "")
endif ()


# When done tweaking common stuff, configure the components (subprojects).
# NOTE: The order matters! The most independent ones should go first.
add_subdirectory(components/B) # B is a static library (depends on Boost)
add_subdirectory(components/C) # C is a shared library (depends on B and external XXX)
add_subdirectory(components/A) # A is a shared library (depends on C and B)


add_subdirectory(components/Executable) # Executable (depends on A and C)

CMakeLists.txt in components/B:

cmake_minimum_required(VERSION 2.8.10)


project(B C CXX)


find_package(Boost
1.50.0
REQUIRED)


file(GLOB CPP_FILES source/*.cpp)


include_directories(${Boost_INCLUDE_DIRS})


add_library(${PROJECT_NAME} STATIC ${CPP_FILES})


# Required on Unix OS family to be able to be linked into shared libraries.
set_target_properties(${PROJECT_NAME}
PROPERTIES POSITION_INDEPENDENT_CODE ON)


target_link_libraries(${PROJECT_NAME})


# Expose B's public includes (including Boost transitively) to other
# subprojects through cache variable.
set(${PROJECT_NAME}_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/include
${Boost_INCLUDE_DIRS}
CACHE INTERNAL "${PROJECT_NAME}: Include Directories" FORCE)

CMakeLists.txt in components/C:

cmake_minimum_required(VERSION 2.8.10)


project(C C CXX)


find_package(XXX REQUIRED)


file(GLOB CPP_FILES source/*.cpp)


add_definitions(${XXX_DEFINITIONS})


# NOTE: Boost's includes are transitively added through B_INCLUDE_DIRS.
include_directories(${B_INCLUDE_DIRS}
${XXX_INCLUDE_DIRS})


add_library(${PROJECT_NAME} SHARED ${CPP_FILES})


target_link_libraries(${PROJECT_NAME} B
${XXX_LIBRARIES})


# Expose C's definitions (in this case only the ones of XXX transitively)
# to other subprojects through cache variable.
set(${PROJECT_NAME}_DEFINITIONS ${XXX_DEFINITIONS}
CACHE INTERNAL "${PROJECT_NAME}: Definitions" FORCE)


# Expose C's public includes (including the ones of C's dependencies transitively)
# to other subprojects through cache variable.
set(${PROJECT_NAME}_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/include
${B_INCLUDE_DIRS}
${XXX_INCLUDE_DIRS}
CACHE INTERNAL "${PROJECT_NAME}: Include Directories" FORCE)

CMakeLists.txt in components/A:

cmake_minimum_required(VERSION 2.8.10)


project(A C CXX)


file(GLOB CPP_FILES source/*.cpp)


# XXX's definitions are transitively added through C_DEFINITIONS.
add_definitions(${C_DEFINITIONS})


# NOTE: B's and Boost's includes are transitively added through C_INCLUDE_DIRS.
include_directories(${C_INCLUDE_DIRS})


add_library(${PROJECT_NAME} SHARED ${CPP_FILES})


# You could need `${XXX_LIBRARIES}` here too, in case if the dependency
# of A on C is not purely transitive in terms of XXX, but A explicitly requires
# some additional symbols from XXX. However, in this example, I assumed that
# this is not the case, therefore A is only linked against B and C.
target_link_libraries(${PROJECT_NAME} B
C)


# Expose A's definitions (in this case only the ones of C transitively)
# to other subprojects through cache variable.
set(${PROJECT_NAME}_DEFINITIONS ${C_DEFINITIONS}
CACHE INTERNAL "${PROJECT_NAME}: Definitions" FORCE)


# Expose A's public includes (including the ones of A's dependencies
# transitively) to other subprojects through cache variable.
set(${PROJECT_NAME}_INCLUDE_DIRS ${PROJECT_SOURCE_DIR}/include
${C_INCLUDE_DIRS}
CACHE INTERNAL "${PROJECT_NAME}: Include Directories" FORCE)

CMakeLists.txt in components/Executable:

cmake_minimum_required(VERSION 2.8.10)


project(Executable C CXX)


file(GLOB CPP_FILES source/*.cpp)


add_definitions(${A_DEFINITIONS})


include_directories(${A_INCLUDE_DIRS})


add_executable(${PROJECT_NAME} ${CPP_FILES})


target_link_libraries(${PROJECT_NAME} A C)

To make it clear, here is the corresponding source tree structure:

Root of the project
├───components
│   ├───Executable
│   │   ├───resource
│   │   │   └───icons
│   │   ├───source
|   |   └───CMakeLists.txt
│   ├───A
│   │   ├───include
│   │   │   └───A
│   │   ├───source
|   |   └───CMakeLists.txt
│   ├───B
│   │   ├───include
│   │   │   └───B
│   │   ├───source
|   |   └───CMakeLists.txt
│   └───C
│       ├───include
│       │   └───C
│       ├───source
|       └───CMakeLists.txt
└───CMakeLists.txt

There are many points where this could be tweaked/customized or changed to satisfy certain needs, but this should at least get you started.

NOTE: I've successfully employed this structure in several medium-sized and large projects.

This can also be done using the CMake Cache mechanism to achieve the same (i.e. sharing of project-specific variables):

set(VAR "value" CACHE INTERNAL "")

Refer to Stack Overflow question How to share variables between different CMake files.

Alexander Shukaev's got a great start, but there are a number of things that could be done better:

  1. Don't use include_directories. At the very least, use target_include_directories. However, you probably don't even need to do that if you use the imported targets.
  2. Use the imported targets. Example for Boost:

    find_package(Boost 1.56 REQUIRED COMPONENTS
    date_time filesystem iostreams)
    add_executable(foo foo.cc)
    target_link_libraries(foo
    PRIVATE
    Boost::date_time
    Boost::filesystem
    Boost::iostreams
    )
    

    This takes care of the include directories, libraries, etc. If you used Boost in your headers in B, then instead of PRIVATE, use PUBLIC, and these dependencies would be transitively added to whatever depends on B.

  3. Don't use file globing (unless you use 3.12). Until very recently, file globbing only works during configuration time, so if you add files and build, it cannot detect the changes until you explicitly regenerate the project. However if you list the files directly, and attempt to build, it should recognize the configuration is out of date and regenerate automatically in the build step.

There's good talk here (YouTube): C++Now 2017: Daniel Pfeifer “Effective CMake"

Which covers a package manager idea that allows your root level CMake to work with find_package OR subdirectory, though, I've been trying to adopt the ideology of this and am having big problems with using find_package for everything and having a directory structure like yours.