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.cmake、FindZLIB.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_DIR和FOO_LIBRARY。调用方只要find_package(Foo)就能用了。
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()来帮你做这件事。大致流程是:
- 用
install(TARGETS ... EXPORT MyLibTargets)导出目标 - 用
install(EXPORT MyLibTargets DESTINATION lib/cmake/MyLib)安装导出文件 - 写一个
MyLibConfig.cmake.in模板文件,用configure_package_config_file()生成 - 把生成的Config文件也安装到
lib/cmake/MyLib目录下
嗯,这一步确实有点绕。我当年第一次配置的时候也折腾了半天。但一旦配好了,别人用你的库就只需要一行:
find_package(MyLib REQUIRED)
target_link_libraries(user_app PRIVATE MyLib::MyLib)
是不是很爽?
知识体系总览
说了这么多,咱们用一张图来总结一下这一章的核心逻辑:
避坑指南
最后,分享几个我踩过的坑:
- 动态库的链接顺序:在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的进阶用法其实还有很多,比如生成器表达式、自定义命令、交叉编译等等。但掌握了今天讲的这四个核心模块,你已经能应对绝大多数项目的构建需求了。剩下的,遇到具体问题再查文档也不迟。