7、CMake进阶:静态库与动态库构建、find_package()、自定义模块、安装规则

好,咱们继续往下聊。上一章我们把CMake的基础语法和项目结构捋了一遍,算是入了门。但说实话,真正在工程里用CMake,光会写add_executable是远远不够的。你想想看,一个稍微像样点的项目,少说也得拆成几个模块吧?模块之间怎么依赖?怎么让别人方便地用你的库?怎么把编译好的东西规规矩矩地装到系统里?

嗯,这一章我们就来啃这些硬骨头。我个人习惯把CMake的进阶用法分成四块:库的构建包查找自定义模块安装规则。咱们一个一个来。

7.1 静态库与动态库:从源码到二进制

先说说最基础的东西——怎么用CMake生成一个库。其实语法很简单,就一个命令:

add_library(my_lib STATIC src/lib.c)

STATIC换成SHARED,就是动态库。但这里有个坑,我当年第一次用CMake的时候踩过——动态库在Windows和Linux下的行为完全不一样。Windows下动态库需要导出符号,你得在代码里加__declspec(dllexport),而Linux下默认所有非static符号都是导出的。

所以我的建议是:跨平台项目,统一用一套宏来控制导出。比如这样:

#ifdef _WIN32
  #ifdef MYLIB_EXPORTS
    #define MYLIB_API __declspec(dllexport)
  #else
    #define MYLIB_API __declspec(dllimport)
  #endif
#else
  #define MYLIB_API
#endif

然后在CMake里加上一句:

target_compile_definitions(my_lib PRIVATE MYLIB_EXPORTS)

这样,编译库的时候自动定义导出宏,别人用的时候自动变成导入宏。省心。

小技巧: 如果你不确定该用静态库还是动态库,我的经验是——内部模块用静态库,对外发布的用动态库。静态库省去了部署的麻烦,动态库方便版本更新。

7.2 find_package():让CMake帮你找依赖

接下来是find_package()。这个命令说白了就是让CMake去系统里找某个库,然后帮你设置好头文件路径、库路径、编译选项等等。你只需要写:

find_package(OpenCV REQUIRED)
target_link_libraries(my_app PRIVATE ${OpenCV_LIBS})

CMake就会自动把OpenCV的include目录和库文件链接进来。方便是方便,但有个前提——这个库必须提供了CMake的配置文件。比如OpenCV、Boost、PCL这些主流库都支持。

那如果库没有提供配置文件呢?比如一些老旧的库,或者你自己写的库。这时候就需要用到Find模块了。CMake自带了很多Find模块,比如FindThreads.cmakeFindZLIB.cmake。你也可以自己写。

我曾经在一个项目里需要找一个第三方闭源库,对方只给了头文件和.a文件。没办法,我只能自己写了个FindFoo.cmake

find_path(FOO_INCLUDE_DIR foo.h)
find_library(FOO_LIBRARY foo)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Foo DEFAULT_MSG
  FOO_INCLUDE_DIR FOO_LIBRARY)

mark_as_advanced(FOO_INCLUDE_DIR FOO_LIBRARY)

然后设置两个变量:FOO_INCLUDE_DIRFOO_LIBRARY。调用方只要find_package(Foo)就能用了。

注意: 自己写的Find模块要放在项目里的cmake/Modules/目录下,然后在CMakeLists.txt里加上list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules")。不然CMake找不到。

7.3 自定义模块:把CMake代码封装起来

说到自定义模块,其实不只是Find模块。你还可以写一些工具函数模块,把重复的CMake代码抽出来。比如我习惯写一个Utils.cmake,里面放一些常用的宏:

macro(add_subdirectory_with_warning dir)
  if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${dir}/CMakeLists.txt)
    message(WARNING "Subdirectory ${dir} does not contain CMakeLists.txt")
  endif()
  add_subdirectory(${dir})
endmacro()

function(print_target_properties target)
  get_target_property(INCLUDE_DIRS ${target} INCLUDE_DIRECTORIES)
  message(STATUS "Target ${target} include dirs: ${INCLUDE_DIRS}")
endfunction()

然后在主CMakeLists.txt里include进来:

include(cmake/Utils.cmake)

这样,整个项目的CMake代码就干净多了。你想想看,如果每个子目录都要写一遍if(NOT EXISTS)的检查,那得多烦人。

7.4 安装规则:让用户能用make install

最后说说安装。说白了,make install就是把编译好的东西复制到系统目录下。但CMake的安装规则远不止复制文件那么简单——它还能生成CMake配置文件,让别人可以用find_package()找到你的库。

最基本的安装写法:

install(TARGETS my_lib
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  PUBLIC_HEADER DESTINATION include
)

这里要注意:LIBRARY对应动态库,ARCHIVE对应静态库和Windows下的导入库,PUBLIC_HEADER是你要公开的头文件。

但光这样还不够。为了让别人能用find_package()找到你的库,你还需要生成Config文件。CMake提供了install(EXPORT)configure_package_config_file()来帮你做这件事。大致流程是:

  1. install(TARGETS ... EXPORT MyLibTargets)导出目标
  2. install(EXPORT MyLibTargets DESTINATION lib/cmake/MyLib)安装导出文件
  3. 写一个MyLibConfig.cmake.in模板文件,用configure_package_config_file()生成
  4. 把生成的Config文件也安装到lib/cmake/MyLib目录下

嗯,这一步确实有点绕。我当年第一次配置的时候也折腾了半天。但一旦配好了,别人用你的库就只需要一行:

find_package(MyLib REQUIRED)
target_link_libraries(user_app PRIVATE MyLib::MyLib)

是不是很爽?

知识体系总览

说了这么多,咱们用一张图来总结一下这一章的核心逻辑:

CMake进阶知识体系 CMake进阶 库的构建 add_library() STATIC / SHARED 符号导出宏 find_package() Config模式 vs Find模式 FindXXX.cmake编写 CMAKE_MODULE_PATH 自定义模块 macro / function include() 复用 工具函数封装 安装规则 install(TARGETS ...) install(EXPORT ...) Config文件生成 make install 四大模块:库构建 → 依赖查找 → 代码复用 → 发布安装

避坑指南

最后,分享几个我踩过的坑:

  • 动态库的链接顺序:在Linux下,如果你用静态库链接动态库,记得加上-Wl,--whole-archive,否则符号可能被丢弃。我当年调试一个插件系统,折腾了两天才发现是这个原因。
  • find_package的版本匹配:如果你指定了find_package(SomeLib 2.0 EXACT),CMake会严格匹配版本号。但有些库的版本号格式不统一,比如"2.0.1"和"2.0"会被认为不匹配。这时候用... 2.0.0 ...更稳妥。
  • 安装路径的权限:默认安装到/usr/local需要root权限。我建议开发阶段用cmake -DCMAKE_INSTALL_PREFIX=~/local指定用户目录,避免频繁sudo。
  • Config文件的命名:CMake查找Config文件时,要求文件名必须是<PackageName>Config.cmake<lower-case-package-name>-config.cmake。大小写搞错了,find_package就找不到。

嗯,这一章的内容就到这里。CMake的进阶用法其实还有很多,比如生成器表达式、自定义命令、交叉编译等等。但掌握了今天讲的这四个核心模块,你已经能应对绝大多数项目的构建需求了。剩下的,遇到具体问题再查文档也不迟。


公众号:蓝海资料掘金营,微信deep3321