如何在我的 CMake 项目中使用需要先安装的库?

How to use libraries within my CMake project that need to be installed first?

我的 CMake 构建系统有问题。有 CMakeLists.txt 文件定义 运行 次或库或使用 ExternalProjects_Add() 下载和构建外部代码。由于存在依赖关系,这些项目必须相互寻找。现在我想在顶层有一个 CMakeLists.txt 来一次构建所有这些。为了找到一个项目,必须安装。但是查找项目已经在配置时在 CMake 中完成。

repository
├─project
│ ├─game (Depends on engine, uses EngineConfig.cmake once installed)
│ │ ├─CMakeLists.txt
│ │ ├─include
│ │ ├─src
│ │ └─textures
│ ├─engine (Depends on boost, uses built-in FindBoost.cmake)
│ │ ├─CMakeLists.txt
│ │ ├─include
│ │ └─src
│ ├─boost (Not the source code, just an ExternalProject_Add call)
│ : └─CMakeLists.txt
│
├─build
│ ├─game
│ ├─engine
│ ├─boost (Source will be downloaded and built here)
│ : ├─download
│   ├─source
│   :
│
├─install
│ ├─game
│ │ ├─bin
│ │ └─textures
│ ├─engine
│ │ ├─include
│ │ │ └─engine
│ │ │   ├─EngineConfig.cmake (Needed to find the library)
│ │ │   :
│ │ │
│ │ └─lib
│ ├─boost (Layout is up to the external library)
│ : └─ ...
│
└─CMakeLists.txt (Calls add_subdirectory for all inside the project folder)

运行 每个项目的 CMake 进程: 使用 execute_process(${CMAKE_COMMAND} ...),我可以在配置时配置和构建每个项目。但是,这意味着我在编辑代码后总是必须 运行 CMake,并且无法从 IDE 我生成的项目文件中进行编译。

链接到 CMake 目标: 运行为所有外部库设置一个 CMake 进程是可以的,因为我不处理它们。可以通过使用目标名称调用 target_link_libraries() 来使用我自己的库。但是,链接是不够的。我的库包括外部库的目录。这些也必须提供给使用项目。

如何在我的 CMake 项目中使用需要先安装的库?

engine 项目导出库时,您需要指定其包含目录。下面的代码是对 http://www.cmake.org/cmake/help/v3.0/manual/cmake-packages.7.html#creating-packages 中提供的示例的简化。调整路径以使用安装前缀 install/engine 构建和安装 engine 组件。

engine/CMakeLists.txt:

...
install(TARGETS engine EXPORT engineTargets
    DESTINATION lib
    INCLUDES DESTINATION include
)

set(ConfigPackageLocation lib/cmake/engine)

install(EXPORT engineTargets
    FILE EngineTargets.cmake
    DESTINATION ${ConfigPackageLocation}
)

install(FILES cmake/EngineConfig.cmake
    DESTINATION ${ConfigPackageLocation}
)

engine/cmake/EngineConfig.cmake:

include("${CMAKE_CURRENT_LIST_DIR}/EngineTargets.cmake")

这提供了导出目标的接口。所以当它被可执行文件链接时,可执行文件得到正确的 INCLUDE_DIRECTORIES 属性:

CMakeLists.txt:

# Need for `find_package` to find `EngineConfig.cmake`.
set(CMAKE_PREFIX_PATH <path-pointed-to-install/engine>)

game/CMakeLists.txt:

find_package(Engine)
add_executable(game ...)
target_link_libraries(game engine)

您可以将您的项目分为三组:

  1. 您未在此超级项目中处理的外部依赖项
  2. 您正在处理的项目太复杂而无法将它们添加为子目录,例如目标太多或其他原因。 (您的示例中似乎没有这样的项目。)
  3. 您正在处理的项目:这些将作为超级项目的子目录添加。

在配置超级项目之前,您需要配置、构建和安装组#1 和#2 中的项目:

  • 您可以在 运行 超级项目的 CMakeLists.txt 之前完成,例如,从 shell-script
  • 或者,正如您提到的,在超级项目的 CMakeLists.txt 中,使用 execute_process(${CMAKE_COMMAND} ...)。您可以使用适当的 find_package(... QUIET) 命令的结果有条件地执行此操作。

您需要决定第 3 组中的项目,如 engine 将仅用于将它们用作子目录的项目,或者您打算将它们用作独立的库,构建在自己的构建树中。

此外,您提到:"My libraries include directories of external libraries"。让我们涵盖 engine 可以依赖的所有此类可能的库:

  • 说,LIB1LIB2 是私有的,public 外部依赖 engine 及其配置模块导出老式 LIB1_*LIB2_*变量
  • LIB3LIB4 是私有的,public 外部依赖 engine 及其配置模块导出 LIB3LIB4 导入 个库

通过public和私有依赖项我的意思是在engine.

的接口上是否使用了特定的库。

现在,如果 engine 仅用作子目录,则 engine/CMakeLists.txt 的相关部分是:

add_library(engine ...)
target_include_directories(engine
    PRIVATE
        ${LIB1_INCLUDE_DIRS}
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        ${LIB2_INCLUDE_DIRS})
target_compiled_definitions(engine
    PRIVATE ${LIB1_DEFINITIONS}
    PUBLIC ${LIB2_DEFINITIONS})
target_link_libraries(engine
    PRIVATE LIB3
    PUBLIC LIB4)

repository/CMakeLists.txt中:

add_subdirectory(engine)
add_subdirectory(game)

game/CMakeLists.txt中:

add_executable(game ...)
target_link_libraries(game engine)

引擎及其 public 依赖项的包含目录将正确转发到 game

如果 engine 也将构建在它自己的构建树中(在另一个项目中),您需要将导出代码添加到 engine/CMakeLists.txt 并且可能需要一个调用 [=40 的自定义配置模块=](或find_dependency)的依赖项。有关详细信息,请参阅 How to use CMake to find and link to a library using install-export and find_package?。该答案中未讨论的一个问题是在库的配置模块中查找库的依赖项:

引用的 SO 答案只是安装由 install(EXPORT ...) 命令生成的 <lib>-targets.cmake 脚本作为配置模块:

install(EXPORT engine-targets
    FILE engine-config.cmake
    DESTINATION lib/cmake/engine)

engine 没有更多依赖项时,此解决方案很好。如果有,则需要在config模块的开头找到它们,这应该是手动编写的。

engine/engine-config.cmake:

include(CMakeFindDependencyMacro)
find_dependency(some-dep-of-engine)
include(${CMAKE_CURRENT_LIST_DIR}/engine-targets.cmake)

engine/CMakeLists.txt中:

install(EXPORT engine-targets
    FILE engine-targets.cmake
    DESTINATION lib/cmake/engine)
install(FILES engine-config.cmake
    DESTINATION lib/cmake/engine)

注:CMake 3.0 引入了CMakeFindDependencyMacro。对于较旧的 CMake,您可以使用 find_package 而不是 find_dependency(QUIET 和 REQUIRED 选项的处理将不会转发到依赖项)。

谢谢@Tsyvarev and @tamas.kenez you for the two good answers. I ended up using the super-build pattern。顶级项目在配置时不会做太多事情。在构建时,它运行外部 CMake 进程来配置、构建和安装项目。

通常,这是使用 ExternalProject_Add() 而不是 add_subdirectory() 来添加项目。我发现 add_custom_command() 工作得更好,因为它不会在后台执行其他任务,例如创建图章文件等。

# add_project(<project> [DEPENDS project...])
function(add_project PROJECT)
    cmake_parse_arguments(PARAM "" "" "DEPENDS" ${ARGN})
    add_custom_target(${PROJECT} ALL DEPENDS ${PARAM_DEPENDS})
    # Paths for this project
    set(SOURCE_DIR  ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT})
    set(BUILD_DIR   ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT})
    set(INSTALL_DIR ${CMAKE_INSTALL_PREFIX}/${PROJECT})
    # Configure
    escape_list(CMAKE_MODULE_PATH)
    escape_list(CMAKE_PREFIX_PATH)
    add_custom_command(TARGET ${TARGET}
        COMMAND ${CMAKE_COMMAND}
            --no-warn-unused-cli
            "-DCMAKE_MODULE_PATH=${CMAKE_MODULE_PATH_ESCAPED}"
            "-DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH_ESCAPED}"
            -DCMAKE_BINARY_DIR=${BUILD_DIR}
            -DCMAKE_INSTALL_PREFIX=${INSTALL_DIR}
            -DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS}
            ${SOURCE_DIR}
        WORKING_DIRECTORY ${BUILD_DIR})
    # Build
    add_custom_command(TARGET ${TARGET}
        COMMAND ${CMAKE_COMMAND}
            --build .
            --target install
        WORKING_DIRECTORY ${BUILD_DIR})
    # Help later find_package() calls
    append_global(CMAKE_PREFIX_PATH ${INSTALL_DIR})
endfunction()

这是两个辅助函数。我花了很长时间才弄清楚将列表参数传递给其他 CMake 进程的正确方法,而不会将它们解释为多个参数。

# escape_list(<list-name>)
function(escape_list LIST_NAME)
    string(REPLACE ";" "\;" ${LIST_NAME}_ESCAPED "${${LIST_NAME}}")
    set(${LIST_NAME}_ESCAPED "${${LIST_NAME}_ESCAPED}" PARENT_SCOPE)
endfunction()

# append_global(<name> value...)
function(append_global NAME)
    set(COMBINED "${${NAME}}" "${ARGN}")
    list(REMOVE_DUPLICATES COMBINED)
    set(${NAME} "${COMBINED}" CACHE INTERNAL "" FORCE)
endfunction()

唯一的缺点是每个项目都需要有一个安装目标。因此,您需要向没有安装命令的项目添加像 install(CODE "") 这样的虚拟安装命令,例如那些刚刚打电话的人ExternalProject_Add