chapter07 - managing dependencies

Best option: find_package

https://cmake.org/cmake/help/latest/command/find_package.html

有三种工作模式。

Module mode

Find<PackageName>.cmake

主要是包外提供,比如 CMake、操作系统、或者写当前工程的人提供的试探性地去搜索库的方式。先找 CMAKE_MODULE_PATH,然后就是 cmake 的安装目录。

我们实验室项目不少路径都是硬编码的,感觉写 Find Module 会更合适。

Config mode

In this mode, CMake searches for a file called <lowercasePackageName>-config.cmake or <PackageName>Config.cmake. It will also look for <lowercasePackageName>-config-version.cmake or <PackageName>ConfigVersion.cmake if version details were specified.

这些文件是由作者提供。由于作者知道包的结构,所以一般搜索起来更加直接。

Config 模式下 cmake find_package比较复杂。

FetchContent redirection mode

cmake version 3.24 出现。find_package 在 config 模式下可以内在转换成对 FetchContent 的调用。

See link and link for further details.

如何工作?

When the link is used, the command searches in Module mode first. If the package is not found, the search falls back to Config mode. A user may set the link variable to true to reverse the priority and direct CMake to search using Config mode first before falling back to Module mode. The basic signature can also be forced to use only Module mode with a MODULE keyword. If the link is used, the command only searches in Config mode.

可以先搜两者之一再 fallback,也可以仅搜索其一。

用变量可能是为了向后兼容。如果有导入的 targets 可以使用,应该优先使用 targets。比如 protobuf 有一些 targets:protobuf::libprotobufprotobuf::libprotobuf-lite 等。

find_package 不一定能够找到包,所以调用后一定要检查结果变量。也可以加上 REQUIRED 要求。

Discovering legacy packages with FindPkgConfig

FindPkgConfig 是老方法,也是受到 cmake 支持的。

介绍 pkg-config

To properly use PkgConfig in a build scenario, your build system has to find the pkg-config executable in the OS, run it a few times and provide appropriate arguments, and store the responses in variables so they can be passed later to the compiler.

如果我们要自己使用,则要先找包,再试探性编译几个程序确定可用,这有点麻烦。不过 cmake 为我们提供了 FindPkgConfig 模块(用 find_package(PkgConfig) 使用)。例子:

cmake_minimum_required(VERSION 3.20.0)
project(FindPkgConfig CXX)

find_package(PkgConfig REQUIRED)
pkg_check_modules(PQXX REQUIRED IMPORTED_TARGET libpqxx)
message("PQXX_FOUND: ${PQXX_FOUND}")

add_executable(main main.cpp)
target_link_libraries(main PRIVATE PkgConfig::PQXX)

pqxx 是 PostgreSQL 的一个客户端库。

Write your own find-modules

如果作者没有提供 cmake 的 config,也没有提供 pkg-config,那么还可以自己写 FindModule 放在 cmake/module (只是建议这样命名目录)中。

自己写的 FindModule 的时候要尊重 convention。

CMake will provide a <PKG_NAME>_FIND_REQUIRED variable set to 1 when find_package(<PKG_NAME> REQUIRED) is called. A find-module should call message(FATAL_ERROR) when a library is not found.

CMake will provide a <PKG_NAME>_FIND_QUIETLY variable set to 1 when find_package(<PKG_NAME> QUIET) is called. A find-module should avoid printing diagnostic messages (other than the one mentioned previously).

CMake will provide a <PKG_NAME>_FIND_VERSION variable set to the version required by calling the list file. A find-module should find the appropriate version or issue a FATAL ERROR.

书中的主要代码是这样写的(要先找到 library 和 headers,然后由于这里要找的就是 PQXX,所以这个部分没有被参数化):

function(add_imported_library library headers)
  add_library(PQXX::PQXX UNKNOWN IMPORTED)   # Unknown: 不知道是 STATIC 还是 SHARED
  set_target_properties(PQXX::PQXX PROPERTIES
    IMPORTED_LOCATION ${library}
    INTERFACE_INCLUDE_DIRECTORIES ${headers} # 用 INTERFACE_ 开头的特殊属性
  )
  set(PQXX_FOUND 1 CACHE INTERNAL "PQXX found" FORCE) # CACHE 是全局的,这样才能被 find_package 的调用者看到
  set(PQXX_LIBRARIES ${library}
      CACHE STRING "Path to pqxx library" FORCE)
  set(PQXX_INCLUDES ${headers}
      CACHE STRING "Path to pqxx headers" FORCE)
  mark_as_advanced(FORCE PQXX_LIBRARIES) # 从 GUI 隐藏
  mark_as_advanced(FORCE PQXX_INCLUDES)  # 从 GUI 隐藏
endfunction()

Finally, we mark cache variables as advanced, which means they won’t be visible in the CMake GUI unless the “advanced” option is enabled.

搜索库的流程是这样的:

  1. 先检查变量是否被设置,如果被设置,说明可能是用户在命令行提供的,应该尊重。
  2. 然后去检查 PostgreSQL,通过它去找 PQXX。

find_dependency 宏是去找依赖的,如果找不到将会直接调用 return(),所以使用时要小心。

找完之后可以用 find_package_handle_standard_args 去处理。这个包尊重 REQUIREDQUIET 参数,然后在提供的参数都被找到时设置 <pkgname>_FOUND 为 1。

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
  PQXX DEFAULT_MSG PQXX_LIBRARY_PATH PQXX_HEADER_PATH
)

附:FindPQXX.cmake 完整内容

function(add_imported_library library headers)
  add_library(PQXX::PQXX UNKNOWN IMPORTED)
  set_target_properties(PQXX::PQXX PROPERTIES
    IMPORTED_LOCATION ${library}
    INTERFACE_INCLUDE_DIRECTORIES ${headers}
  )
  set(PQXX_FOUND 1 CACHE INTERNAL "PQXX found" FORCE)
  set(PQXX_LIBRARIES ${library}
      CACHE STRING "Path to pqxx library" FORCE)
  set(PQXX_INCLUDES ${headers}
      CACHE STRING "Path to pqxx headers" FORCE)
  mark_as_advanced(FORCE PQXX_LIBRARIES)
  mark_as_advanced(FORCE PQXX_INCLUDES)
endfunction()

if (PQXX_LIBRARIES AND PQXX_INCLUDES)
  add_imported_library(${PQXX_LIBRARIES} ${PQXX_INCLUDES})
  return()
endif()

# deliberately used in FindModule against the documentation
include(CMakeFindDependencyMacro)
find_dependency(PostgreSQL)

file(TO_CMAKE_PATH "$ENV{PQXX_DIR}" _PQXX_DIR)
find_library(PQXX_LIBRARY_PATH NAMES libpqxx pqxx
  PATHS
    ${_PQXX_DIR}/lib/${CMAKE_LIBRARY_ARCHITECTURE}
    ${CMAKE_INSTALL_PREFIX}/lib/${CMAKE_LIBRARY_ARCHITECTURE}
    /usr/local/pgsql/lib/${CMAKE_LIBRARY_ARCHITECTURE}
    /usr/local/lib/${CMAKE_LIBRARY_ARCHITECTURE}
    /usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}
    ${_PQXX_DIR}/lib
    ${_PQXX_DIR}
    ${CMAKE_INSTALL_PREFIX}/lib
    ${CMAKE_INSTALL_PREFIX}/bin
    /usr/local/pgsql/lib
    /usr/local/lib
    /usr/lib
  NO_DEFAULT_PATH
)

find_path(PQXX_HEADER_PATH NAMES pqxx/pqxx
  PATHS
    ${_PQXX_DIR}/include
    ${_PQXX_DIR}
    ${CMAKE_INSTALL_PREFIX}/include
    /usr/local/pgsql/include
    /usr/local/include
    /usr/include
  NO_DEFAULT_PATH
)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(
  PQXX DEFAULT_MSG PQXX_LIBRARY_PATH PQXX_HEADER_PATH
)
if (PQXX_FOUND)
  add_imported_library(
    "${PQXX_LIBRARY_PATH};${POSTGRES_LIBRARIES}"
    "${PQXX_HEADER_PATH};${POSTGRES_INCLUDE_DIRECTORIES}"
  )
endif()

Working with Git repositories

直接使用 git submodule 是很好,但是这样需要手动下载,不能自动化。书中加了一些判断,先检查是否能够在本机找到 yaml-cpp,如果找不到就使用已经创建的 submodule 去更新下载,然后将子目录加入到构建过程。

find_package(yaml-cpp QUIET)
if (NOT TARGET yaml-cpp)
  message("yaml-cpp not found, initializing submodule")
  execute_process(
    COMMAND git submodule update --init -- external/yaml-cpp
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  )
  add_subdirectory(extern/yaml-cpp)
endif()

如果不能使用 git submodule 但是能够使用 git,可以先用 find_package(Git) 去找 git,然后用 ${GIT_EXECUTABLE} 去克隆仓库(当作下载)。这样做比直接使用 git (需要假设 git 在 PATH 中)要好一点(但是几乎也没有好多少?)。

Using ExternalProject and FetchContent modules

ExternalProject_Add

ExternalProject_Add 的参数实在是太多了,功能也很强大。但是他是在 build 阶段才会去下载文件!

https://cmake.org/cmake/help/latest/module/ExternalProject.html

cmake_minimum_required(VERSION 3.20.0)
project(ExternalProjectGit CXX)

add_executable(welcome main.cpp)
configure_file(config.yaml config.yaml COPYONLY)

include(ExternalProject)
ExternalProject_Add(external-yaml-cpp
  GIT_REPOSITORY    https://github.com/jbeder/yaml-cpp.git
  GIT_TAG           yaml-cpp-0.6.3
)
target_link_libraries(welcome PRIVATE yaml-cpp)

include(CMakePrintHelpers)
cmake_print_properties(TARGETS yaml-cpp
                       PROPERTIES TYPE SOURCE_DIR)

cmake_print_properties 去打印信息输出的是: No such TARGET "yaml-cpp" !

ExternalProject 不仅是在 build 时候才会开始下载,而且其中定义的变量不能被调用者访问。

To sum it up, ExternalProject can get us out of a bind when there are namespacing collisions across projects, but in all other cases, FetchContent is far superior. Let’s figure out why.

FetchContent_{Declare,MakeAvailable}

The key difference is in the stage of execution - unlike ExternalProject, FetchContent populates dependencies during the configuration stage, bringing all the targets declared by an external project to the scope of the main project. This way, we can use them exactly like the ones we defined ourselves.

FetchContent 在配置阶段运行,但是也有缺点,比如他会默认将第三方依赖下载到 build/_deps 目录,这样删掉 build 之后就要重新下载!FetchContent_Declare 的参数和 ExternalProject_Add 一样。有疑问可以参考 ExternalProject 的参数。

https://stackoverflow.com/questions/60981812/how-can-i-get-fetchcontent-to-download-to-a-specific-location

cmake_minimum_required(VERSION 3.20.0)
project(ExternalProjectGit CXX)

add_executable(welcome main.cpp)
configure_file(config.yaml config.yaml COPYONLY)

include(FetchContent)
FetchContent_Declare(external-yaml-cpp
  GIT_REPOSITORY    https://github.com/jbeder/yaml-cpp.git
  GIT_TAG           yaml-cpp-0.6.3
)
FetchContent_MakeAvailable(external-yaml-cpp)
target_link_libraries(welcome PRIVATE yaml-cpp)

include(CMakePrintHelpers)
cmake_print_properties(TARGETS yaml-cpp
                       PROPERTIES TYPE SOURCE_DIR)

FetchContent_{Declare,MakeAvailable} 将配置和依赖传播过程分开了。这样在依赖传播之前,就能对复杂的依赖关系中被多次使用的库指定同一个版本。

Obviously, we may have a situation where two unrelated projects declare a target with the same name. This is a problem that can only be solved by falling back to ExternalProject or other methods. Luckily, it doesn’t happen too often.

如果文件已经被下载,可以用 https://stackoverflow.com/a/74127927/ 提到的 SOURCE_DIR 参数但不提供下载方法,提供下载方法将会清空之前的文件。