第七章 管理依赖关系

27 阅读4分钟

7.1

7.2 如何查找已安装的软件包

比如查找protobuf

// message.proto
syntax = "proto3"
message Message{
    int32 id = 1;
}
// main.cpp
#include "message.pb.h"
#include <fstream>

using namespace std;

int main(){
    Message m;
    m.set_id(123);
    m.PrintDebugString();
    fstream fo("./hello.data", ios::binary | ios::out);
    m.SerailizeToOstream($fo);
    fo.close();
    return 0;
}

// CMakeLists.txt

find_package(Protobuf REQUIRED)
protobuf_generate_cpp(GENERATED_SRC GENERATED_HEADER message.proto)
add_executable(main main.cpp ${GENERATED_SRC} ${GENERATED_HEADER})
target_link_libraries(main PRIVATE ${Protobuf_LIBRARIES})
target_include_directories(main PRIVATE ${Protobuf_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})

使用find_package时,会预期设置一些变量: ·<PKG_NAME>_ FOUND ·<PKG_NAME>_ INCLUDE_DIRS 或 <PKG_NAME>_ INCLUDES ·<PKG_NAME>_ LIBRARIES 或 <PKG_NAME>_ LIBRARIES 或 <PKG_NAME>_ LIBS ·<PKG_NAME>_ DEFINITIONS ·IMPORTED由查找模块或配置文件指定的目标

如果导入的包(Protobuf)支持所谓的“modern cmake”(围绕目标构建),该包将提供那些导入的目标来替代这些变量. 例如,protobuf:

// CMakeLists.txt
find_package(Protobuf REQUIRED)
protobuf_generate_cpp(GENERATED_SRC GENERATED_HEADER message.proto)

add_executable(main main.cpp ${GENERATED_SRC} ${GENERATED_HEADER})

target_link_libraries(main PRIVATE protobuf::libprotobuf)
target_include_directories(main PRIVATE #{CMAKE_CURRENT_BINARY_dIR})

此处target_link_libraries(main PRIVATE prtobuf::libprotobuf) 隐式指定了包含目录。

接下来介绍find_package命令的选项:

find_package(<Name> [version] [EXACT] [QUIET] [REQUIRED])

[verion]:请求包的特定版本,使用major.minor.pathc.tweak格式。或者提供一个范围1.22...1.40.1,使用三个圆点作为分隔符。 EXACT: 精确的版本 QUIET: 使所有关于已找到和未找到包的消息静默。 REQUIRED: 如果没有找到包,REQUIRED关键字将停止执行并打印诊断消息(即使启用了QUIET)

7.3使用FindPkgConfig

· 如果一个库非常流行,即cmake已经有查找该库的模块。使用cmake的find package · 如果没有该库的查找模块,并且只支持PkgConfig.pc,使用现成的就行。

CMake提供了FindPkgConfig模块,设置了一个直接指向pkg-config二进制文件的变量:PKG_CONFIG_EXECUTABLE

比如postgresql提供了pc文件,那么如何通过pkg-config来找到并配置该库呢?

// main.cpp

#include <pqxx/pqxx>
int main(){
    pqxx::nullconnection connection;
}

// CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
project(FindPkgConfig CXX)

find_package(PkgConfig REQUIRED)

pkg_check_modules(PQXX REQUIRED IMPORTED_TARGET libqxx)  // FindPkgConfig自定义宏
message("PQXX: ${PQXX_FOUND}")

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

7.4编写自己的查找模块

项目既没有config文件也没有PkgConfig文件,CMake中也没有线程的查找模块,可以为这个库编写一个自定义查找模块。

编写一个新的FindPQXX.cmake文件,将他存储在项目源代码树的cmake/module目录中,需要确保当使用find_package()时,CMake可以找到这个查找模块,所以会在CMakeLists.txt中将该目录追加到CMAKE_MODULE_PATH变量中。

// CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
project(FindPackageCustom CXX)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/module")  //追加模块查找路径
find_package(PQXX REQUIRED)  // 查找模块
add_executable(main main.cpp)
target_link_libraries(main PRIVATE PQXX:PQXX)

现在开始写查找模块,如果某些特定的变量没有设置,CMake不会报错。最好是遵守CMake文档中的约定: 使用find_package模块式。CMake提供: · <PKG_NAME>_ FIND_ REQURED <= REQUIRED · <PKG_NAME>_ FIND_ QUIETLY <= QUIET · <PKG_NAME>_ FIND_ VERSION

为PQXX创建一个优雅的查找模块所需的步骤:

  1. 如果库和头文件的路径已经知道(用户提供或缓存),则使用这些路径并创建导入的目标。结束。
  2. 否则找到嵌套依赖的库和头文件-PostgreSQL
  3. 已知的路径中搜索二进制版本的PostgreSQL客户端库
  4. 搜索PostgreSQL客户端包含头文件的已知路径
  5. 检查是否找到库和include头文件。是的话创建一个IMPORTED目标。

IMPORTED目标的创建会发生两次——用户提供的库的路径,或者自动找到。 首先编写一个函数来处理搜索过程的结果来保持代码的DRY。

要创建IMPORTED目标,只需要一个IMPORTED关键字的库。库必须提供一个类型——将其标记为UNKNOWN类型,表示我们不想检测所找到的库是静态的还是动态的,只是想为链接器提供一个参数。

接下来,IMPORTED_LOCATIONINTERFACE_INCLUDE_DIRECTORIES导入目标的必须属性设置为函数实参。

之后,把路径存储在缓存变量中,这样就不用再次搜索了。PQXX_FOUND是在缓存中显示设置的,因此在全局作用域中是可见的。

最后将缓存标记为高级,除非启用了高级选项,否则它们在CMAKE GUI中是不可见的。

// FindPQXX.cmake

function(add_imported_library library header)
    add_library(PQXX:PQXX UNKOWN 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 ${header} CACHE STRING "Path to pqxx headers" FORCE)
    mark_as_advanced(FORCE PQXX_LIBRARIES)
    mark_as_advanced(FORCE PQXX_INCLUDES)
endfunction()

介绍第一种情况1. 如果库和头文件的路径已经知道(用户提供或缓存),则使用这些路径并创建导入的目标。 将PQXX安装在非标准位置的用户可以使用-D参数通过命令行提供必要的路径。如果是这种情况,只需要调用刚刚定义的function,并通过return进行转移来放弃搜索。 同时如果配置阶段在过去执行过,这个条件也为true,因为PQXX_LIBRARIESPQXX_INCLUDES变量被缓存了。

代码如下:

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

对于一些嵌套依赖项,比如使用PQXX还需要PostgreSQL,查找模块中使用另一个查找模块没问题,但应该将REQUIREDQUIET标志进行转发(这样嵌套的搜索行为与外部的搜索行为一致)。

CMake有一个内置的帮助宏find_dependency。文档中指出该宏不适合查找模块是因为,在没有找到对应模块的情况下,宏的行为区别于函数,宏会直接调用return(),导致退出FindPQXX.cmake文件,停止外部查找模块的执行。某些情况这种行为不可取,但是在当前例子下,我们需要这个特质——防止找不到库时cmake掉进坑里。

include(CMakeFindDependencyMacro)
find_dependency(PostgreSQL)

要查找PQXX库,将设置一个_PQXX_DIR帮助变量,并使用find_library()扫描路径列表,在PATHS关键字后面提供路径。该指令将检查是否存在与另一个关键字NAMES后面提供的名称相匹配的库二进制文件,如果找到了,路径存储在PQXX_LIBRARY_PATH变量中,否则设置为<var>NOTFOUND或者

其中NO_DEFAULT_PAHT关键字禁用默认行为,该行为将扫描CMake为该主机环境提供的一长串默认路径。

file(TO_CMAKE_PATH "$ENV{PQXX_DIR}" _PQXX_DIR)
find_library(PQXX_LIBRARY_PATH NAMES libpqxx pqxx PATHS
    ${PQXX_DIR}/lib/${CMAKE_LIBRARY_ARCHITECTURE}
    # {...} many other paths
    /usr/lib
  NO_DEFAULT_PATH
)

接下来用find_path指令搜索已知的头文件,该指令的工作方式和find_library非常相似。主要是区别是find_library知道库特定于系统的扩展名,但是find_path不知道,我们需要提供确切的名称。

find_path(PQXX_HEADER_PATH NAMES pqxx/pqxx
    PATHS
    ${_PQXX_DIR}/include
    # (...) many other paths
    /usr/include
  NO_DEFAULT_PATH
)

如果找到了库,将调用函数来定义导入的目标,并将路径存储在缓存中:

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

7.5使用Git库

7.5.1 通过Git子库提供外部库

向存储库中添加一个子模块 git submodule add <repository-url>

如果提取的存储库?已经有子模块,需要初始化它们: git submodule update --init -- <local-path-to-submodule>

一、在代码中使用git submodule add <url>创建的子模块

github.com/jbeder/yaml… 使用来自该仓库的yaml-cpp模块

// main.cpp
#include <iostream>
#include <string>
#include "yaml-cpp/yaml.h"

int main(){
    std::string = "Guest";
    YAML::Node config = YAML::LoadFile("config.yaml");
    if(config["name"])
        name = config["name"].as<std::string>();
    std::cout << "Welcom " << name << std::endl;
    return 0;
}

// config.yaml
name: Rafal
// 在项目的根目录下
mkdir extern
cd extern
git submodule add https://github.com/jbeder/yaml-cpp
// 如果提示不是git仓库,就git init
//CMakeLists.txt

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

add_subdirectory(extern/yaml-cpp)
target_link_libraries(GitSubMain PRIVATE yaml-cpp)

值得一提的是,这里yaml-cpp的作者遵循了第三章的实践,将公共头文件存储在一个单独的目录中——<project-name>/include/<project-name>,这允许库的使用者包含"yaml-cpp/yaml.h",这样的命名方法对于查找头文件非常有用,能够从名称中看出是哪个库的提供的头文件。

综上,这种方式需要用户自己手动克隆子模块。更糟糕的是,没有考虑到用户可能已经在系统中安装了这个库。

二、自动初始化Git子模块

由于存在用户可能已经安装了yaml-cpp模块,要首先通过find_package寻找该模块。


add_executable(GitSubMain main.cpp)

find_package(yaml-cpp QUIET)
if(NOT yaml-cpp_FOUND)
    message("yaml-cpp is not found.")
    execute_process(
        COMMAND git submodule update --init -- extern/yaml-cpp
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    )
    add_subdirectory(extern/yaml-cpp)
endif()
target_link_directories(GitSubMain PRIVATE yaml-cpp)

7.5.2 不使用Git的项目克隆依赖项

增加了一个找Git的判断逻辑

7.6 使用ExternalProject和FetchContent模块

7.6.1 ExternalProject模块