cmake之旅(6)
cmake之旅6查找和使用第三方库1 最简单的第三方库使用2 find_package 的两种模式2.1 Module 模式查找模块2.2 Config 模式配置文件3 find_package 找到后提供了什么3.1 传统变量方式3.2 导入目标方式推荐4 实战使用一个真实的第三方库4.1 方式一系统安装 find_package4.2 方式二FetchContent推荐5 编写自己的 FindXXX.cmake6 指定查找路径7 find_package 的常用参数8 pkg-config 作为补充9 本篇命令速查表10 总结与下一篇预告同系列文章cmake之旅(1):构建的过程cmake之旅(2):CMakeLists.txt 核心语法cmake之旅(3):多目录项目管理cmake之旅(4):静态库与动态库cmake之旅5):函数、宏与 .cmake 模块cmake之旅6查找和使用第三方库cmake之旅7编译选项与条件编译cmake之旅8Modern CMake 与 target 思维cmake之旅9安装与导出cmake之旅10自动化测试与 CTest查找和使用第三方库上一篇我们学习了.cmake模块文件的编写和使用。这一篇我们将看到.cmake文件最重要的应用场景之一——查找第三方库。实际项目中我们几乎不可能从零写所有代码。JSON 解析用 nlohmann/json图像处理用 OpenCV网络通信用 Boost.Asio——这些第三方库已经帮我们解决了大量的基础问题。但问题是这些库安装在哪里CMake 怎么找到它们找到之后怎么用这就是find_package要解决的问题。1 最简单的第三方库使用先看一个最常见的场景使用系统中已安装的线程库Threads。cmake_minimum_required(VERSION 3.10) project(ThreadDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) # 查找线程库 find_package(Threads REQUIRED) add_executable(app main.cpp) # 链接线程库 target_link_libraries(app PRIVATE Threads::Threads)就这么简单。find_package(Threads REQUIRED)告诉 CMake“去帮我找线程库找不到就报错REQUIRED。” 找到之后我们通过Threads::Threads这个导入目标来链接它。但这背后到底发生了什么CMake 是去哪里找的Threads::Threads又是什么东西我们一步步来看。2 find_package 的两种模式find_package有两种完全不同的工作模式Module 模式和Config 模式。理解这两种模式的区别是掌握find_package的关键。2.1 Module 模式查找模块Module 模式下CMake 会去寻找一个名为FindXXX.cmake的文件XXX 是你要查找的包名然后执行这个文件中的查找逻辑。查找顺序先在CMAKE_MODULE_PATH指定的目录中查找再在 CMake 安装目录下的Modules/目录中查找前面提到的find_package(Threads)就是 Module 模式——CMake 自带了一个FindThreads.cmake文件里面定义了如何在各种系统上找到线程库。你可以查看 CMake 自带了哪些FindXXX.cmakecmake --help-module-list|grep^Find常见的 CMake 自带查找模块有FindThreads、FindOpenGL、FindZLIB、FindPython等等。2.2 Config 模式配置文件Config 模式下CMake 不再找FindXXX.cmake而是寻找由库自身提供的配置文件文件名为XXXConfig.cmake或xxx-config.cmake。这些配置文件通常在库安装时自动生成存放在库的安装目录中。CMake 会在以下位置搜索库安装前缀/lib/cmake/XXX/库安装前缀/share/cmake/XXX/以及系统的标准库目录Module 模式和 Config 模式的对比对比项Module 模式Config 模式查找的文件FindXXX.cmakeXXXConfig.cmake文件由谁提供CMake 自带或项目自己编写库本身安装时提供优先级先尝试Module 失败后再尝试适用场景库本身不支持 CMake 时库本身用 CMake 构建时实际工作流程当你调用find_package(XXX)时CMake 先尝试 Module 模式如果找不到FindXXX.cmake再自动切换到 Config 模式。你也可以强制指定模式# 强制 Module 模式 find_package(XXX MODULE REQUIRED) # 强制 Config 模式 find_package(XXX CONFIG REQUIRED)3 find_package 找到后提供了什么无论是哪种模式find_package成功后通常会设置以下内容3.1 传统变量方式find_package(ZLIB REQUIRED) # find_package 成功后以下变量可用 message(STATUS 找到了: ${ZLIB_FOUND}) message(STATUS 头文件: ${ZLIB_INCLUDE_DIRS}) message(STATUS 库文件: ${ZLIB_LIBRARIES}) message(STATUS 版本号: ${ZLIB_VERSION})命名规则通常是包名_FOUND、包名_INCLUDE_DIRS、包名_LIBRARIES等。使用方式传统方式find_package(ZLIB REQUIRED) add_executable(app main.cpp) target_include_directories(app PRIVATE ${ZLIB_INCLUDE_DIRS}) target_link_libraries(app PRIVATE ${ZLIB_LIBRARIES})3.2 导入目标方式推荐现代 CMake 更推荐使用导入目标Imported Target通常以包名::组件名的格式命名find_package(ZLIB REQUIRED) add_executable(app main.cpp) target_link_libraries(app PRIVATE ZLIB::ZLIB)为什么导入目标更好因为ZLIB::ZLIB这个目标自身已经携带了头文件路径、链接库、编译选项等所有信息。你只需要一行target_link_libraries不需要再手动写target_include_directories。这些信息会通过 CMake 的目标属性自动传播。建议优先使用导入目标方式。只有在库太老、不提供导入目标时才退回到传统变量方式。4 实战使用一个真实的第三方库我们来演示一个真实的例子——在项目中使用nlohmann/json一个非常流行的 C JSON 库。4.1 方式一系统安装 find_package如果你已经在系统中安装了 nlohmann/json例如通过apt install nlohmann-json3-dev可以直接用find_packagecmake_minimum_required(VERSION 3.10) project(JsonDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) # 查找 nlohmann_jsonConfig 模式因为这个库自带 CMake 配置文件 find_package(nlohmann_json 3.2.0 REQUIRED) add_executable(app main.cpp) target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)find_package的第二个参数3.2.0是最低版本要求。如果系统安装的版本低于 3.2.0CMake 会报错。4.2 方式二FetchContent推荐如果不想让用户预先安装第三方库可以使用 CMake 3.11 引入的FetchContent模块在配置阶段自动下载cmake_minimum_required(VERSION 3.14) project(JsonDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) # 引入 FetchContent 模块 include(FetchContent) # 声明要下载的内容 FetchContent_Declare( json # 名称自定义 GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3 # 版本标签 ) # 下载并引入如果已经下载过会跳过 FetchContent_MakeAvailable(json) add_executable(app main.cpp) target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)FetchContent 的优势使用者不需要预先安装任何依赖cmake ..时自动下载版本由GIT_TAG精确控制不会因为系统版本不同导致构建失败代码完全自包含克隆仓库后就能构建FetchContent 的注意事项首次配置时需要联网下载可能比较慢下载的内容默认存放在build/_deps/目录中建议使用GIT_TAG指定具体的版本号或 commit hash而不是分支名5 编写自己的 FindXXX.cmake有些第三方库既没有自带 CMake 配置文件CMake 也没有内置对应的 FindXXX.cmake。这时候就需要我们自己编写查找模块。假设我们要为一个名为mymath的库编写查找模块。这个库的头文件在/usr/local/include/mymath/库文件在/usr/local/lib/。cmake/FindMyMath.cmake# # FindMyMath.cmake # 描述查找 MyMath 库 # 提供MyMath_FOUND, MyMath_INCLUDE_DIRS, MyMath_LIBRARIES # 以及导入目标 MyMath::MyMath # include_guard(GLOBAL) # 查找头文件路径 find_path(MyMath_INCLUDE_DIR NAMES mymath.h # 要查找的头文件名 PATHS /usr/local/include # 搜索路径 PATH_SUFFIXES mymath # 子目录后缀 ) # 查找库文件 find_library(MyMath_LIBRARY NAMES mymath # 库名会自动尝试 libmymath.a / libmymath.so PATHS /usr/local/lib # 搜索路径 ) # 使用 CMake 内置的 FindPackageHandleStandardArgs 来处理结果 include(FindPackageHandleStandardArgs) find_package_handle_standard_args(MyMath REQUIRED_VARS MyMath_LIBRARY MyMath_INCLUDE_DIR ) # 设置输出变量 if(MyMath_FOUND) set(MyMath_LIBRARIES ${MyMath_LIBRARY}) set(MyMath_INCLUDE_DIRS ${MyMath_INCLUDE_DIR}) # 创建导入目标推荐 if(NOT TARGET MyMath::MyMath) add_library(MyMath::MyMath UNKNOWN IMPORTED) set_target_properties(MyMath::MyMath PROPERTIES IMPORTED_LOCATION ${MyMath_LIBRARY} INTERFACE_INCLUDE_DIRECTORIES ${MyMath_INCLUDE_DIR} ) endif() endif() # 将内部缓存变量标记为高级不在 cmake-gui 中默认显示 mark_as_advanced(MyMath_INCLUDE_DIR MyMath_LIBRARY)使用方式list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) find_package(MyMath REQUIRED) add_executable(app main.cpp) target_link_libraries(app PRIVATE MyMath::MyMath)关键命令说明find_path在指定的路径中搜索包含目标头文件的目录。find_library在指定的路径中搜索库文件。find_package_handle_standard_args是 CMake 提供的辅助宏它会检查所有 REQUIRED_VARS 是否都被找到自动设置XXX_FOUND变量并在找不到时输出统一格式的错误信息。6 指定查找路径有时候第三方库安装在非标准路径下CMake 默认找不到。有几种方式可以指定搜索路径。方式一CMAKE_PREFIX_PATH最常用的方式在命令行中指定cmake-DCMAKE_PREFIX_PATH/opt/custom_libs..CMake 会在/opt/custom_libs/lib/、/opt/custom_libs/include/等标准子目录中搜索。可以指定多个路径用分号分隔cmake-DCMAKE_PREFIX_PATH/opt/lib_a;/opt/lib_b..方式二XXX_DIR为特定的包指定配置文件所在目录cmake-Dnlohmann_json_DIR/opt/json/lib/cmake/nlohmann_json..方式三在 CMakeLists.txt 中设置# 在 find_package 之前设置 set(CMAKE_PREFIX_PATH /opt/custom_libs) find_package(XXX REQUIRED)建议优先使用CMAKE_PREFIX_PATH因为它不侵入 CMakeLists.txt 代码更灵活。7 find_package 的常用参数find_package(XXX 1.2.3 # 最低版本要求可选 EXACT # 要求精确匹配版本可选 REQUIRED # 找不到则报错可选 COMPONENTS # 指定需要的组件可选 component_a component_b QUIET # 静默模式不输出查找信息可选 )COMPONENTS 的用途有些大型库由多个组件组成。比如 Boost 包含文件系统、线程、正则表达式等众多组件你可以只选择需要的find_package(Boost 1.70 REQUIRED COMPONENTS filesystem thread) add_executable(app main.cpp) target_link_libraries(app PRIVATE Boost::filesystem Boost::thread)这样 CMake 只会查找 filesystem 和 thread 两个组件不需要整个 Boost 都可用。8 pkg-config 作为补充有些 C/C 库不支持 CMake但提供了pkg-config的.pc文件这在 Linux 系统上很常见。CMake 可以通过PkgConfig模块来调用pkg-config# 引入 PkgConfig 模块 find_package(PkgConfig REQUIRED) # 使用 pkg-config 查找库 pkg_check_modules(LIBFOO REQUIRED libfoo) add_executable(app main.cpp) target_include_directories(app PRIVATE ${LIBFOO_INCLUDE_DIRS}) target_link_libraries(app PRIVATE ${LIBFOO_LIBRARIES})CMake 3.6 以上还支持自动创建导入目标pkg_check_modules(LIBFOO REQUIRED IMPORTED_TARGET libfoo) add_executable(app main.cpp) target_link_libraries(app PRIVATE PkgConfig::LIBFOO)IMPORTED_TARGET让pkg_check_modules自动创建一个PkgConfig::LIBFOO导入目标使用起来和find_package的导入目标一样方便。9 本篇命令速查表命令作用示例find_package查找第三方库find_package(ZLIB REQUIRED)FetchContent_Declare声明要下载的外部内容见第 4.2 节FetchContent_MakeAvailable下载并引入外部内容FetchContent_MakeAvailable(json)find_path搜索头文件所在目录find_path(X_DIR NAMES x.h)find_library搜索库文件find_library(X_LIB NAMES x)pkg_check_modules通过 pkg-config 查找库pkg_check_modules(FOO REQUIRED foo)10 总结与下一篇预告这一篇我们深入学习了find_package的两种模式Module 和 Config、导入目标的使用、FetchContent 自动下载依赖、编写自定义 FindXXX.cmake、指定查找路径、以及 pkg-config 作为补充手段。到目前为止我们已经能够构建多目录项目、创建库、查找和链接第三方库了。但是还有一类需求我们没有涉及在构建时控制代码的行为。比如你想发布两个版本——一个带调试日志、一个不带或者根据用户的选择启用/禁用某个功能模块。这些需求涉及到编译选项和条件编译。下一篇——cmake之旅7编译选项与条件编译我们来学习如何用 CMake 控制代码的编译行为。