CMake及其如何寻找其他项目及其依赖项

104
想象一下以下的情景: 项目A 是一个共享库,它有几个依赖项 (LibALibBLibC)。 项目B 是一个可执行文件,它依赖于 项目A,因此需要所有 项目A 的依赖项才能构建。
此外,这两个项目都使用 CMake 进行构建,并且不应该需要通过 'install' 目标来安装 项目A,以便 项目B 使用它,因为这可能会给开发人员带来麻烦。
使用 CMake 解决这些依赖关系的最佳方法是什么? 理想的解决方案应尽可能简单 (但不要过于简单),并需要最少的维护。

3
为了未来的繁荣:http://www.cmake.org/Wiki/CMake/Tutorials/How_to_create_a_ProjectConfig.cmake_file - blockchaindev
1
那个教程似乎没有解释如何处理导出外部库依赖项。唯一链接的库是由该项目构建的库。我需要知道如何告诉B项目,A项目需要各种外部库,因此这些库需要添加到B项目的链接步骤中。 - Ben Farmer
如果你是个 PC 玩家,实际上你应该尝试一下 Linux 或者 Linux 子系统。这个平台最好的地方就在于 Linux 会为你安装所有依赖项。更妙的是,它会建议你缺少哪些依赖项,并提供 Sudo apt-get install mydependencies 的安装方式。非常简单易懂。 - Juniar
@Juniar,我同意这确实可以简化和优化很多事情。但是会让软件部署成为一场噩梦。我更喜欢将所有东西打包在一起,作为我的软件的全部组件进行部署(即使可能会部分重复某些库)。更不用说维护问题了。每个盒子都会有独特的库集(在一定程度上)。 - OpalApps
@OpalApps,依赖项可能安装在不同的路径和目录上,但您仍然可以在编译时添加这些依赖项,或配置/包含外部路径。它们不会全部安装在一个路径上,但是“sudo apt-get install”确实会安装在特定的目录中,只需切换它们即可。 - Juniar
3个回答

172

很简单。这是我脑海中的示例:

最高级别的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)

components/B中的CMakeLists.txt

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)

components/C中的CMakeLists.txt

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)

components/A中的CMakeLists.txt

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)

components/Executable中的CMakeLists.txt

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)
为了让内容更加明确,这里是相应的源代码树结构:
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

这里有许多可以调整/定制或更改以满足特定需求的点,但这至少能帮助你入门。

注意:我已经在几个中型和大型项目中成功地使用了这种结构。


18
你真是个“明星”!你帮我摆脱了长达一天的巨大头痛。非常感谢你。 - rsacchettini
3
我很好奇,如果我从“可执行文件”目录调用CMake,它是否会编译成功?还是说我必须始终从项目的根目录进行编译? - Ahmet Ipkin
2
我认为它确实有一个小缺点,即在每个项目中两次定义包含目录(一次用于 set(...INCLUDE_DIRS,一次用于 include_directories()),这使得维护变得困难(需要始终记住在两个位置添加新的包含依赖项)。您可以使用 get_property(...PROPERTY INCLUDE_DIRECTORIES) 查询它们。 - pseyfert
2
这真的很棒,但是当A、B和C完全是独立的项目时,如何实现相同的功能呢?也就是说,我想构建A并导出它自己的ProjectConfig.cmake文件,然后在B中使用find_package在系统上找到A,以某种方式获取A所依赖的所有库的列表,以便在构建B时链接它们。 - Ben Farmer
2
@Ben Farmer,这确实是一个更复杂的主题,值得撰写一篇详细的文章来解释。我从来没有耐心在这里概述它。事实上,这就是我管理项目的方式,因为这实际上是CMake的最终(专业)用途和意图。为了管理所有这些,我有自己的CMake框架,可以在幕后处理很多事情。例如,您可以尝试构建我的任何一个C++ HacksC++ Firewall玩具项目。 - Alexander Shukaev
显示剩余5条评论

20

Alexander Shukaev有一个良好的开端,但还有许多事情可以做得更好:

  1. 不要使用include_directories。至少要使用target_include_directories。然而,如果使用导入目标,则可能甚至不需要这样做。
  2. 使用导入目标。以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
    )
    

    这会处理包括目录、库等在内的依赖项。如果您在B头文件中使用了Boost,则应使用PUBLIC而不是PRIVATE,这些依赖关系将被中转地添加到取决于B的任何内容中。

    不要使用文件匹配(除非您使用3.12)。直到最近,文件匹配仅在配置时起作用,因此如果您添加文件并构建,它无法检测到更改,直到您显式地重新生成项目为止。但是,如果您直接列出文件并尝试构建,则应识别配置已过期并在构建步骤中自动重新生成。

    这里有一部好的讲座(YouTube:C++Now 2017: Daniel Pfeifer “Effective CMake"),介绍了一种包管理器的思想,使根级CMake能够使用find_packagesubdirectory,尽管我一直试图采用这种思想,并且正在使用像你那样的目录结构,但遇到了很大的问题。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接