9、CMake进阶:头文件路径、链接库、条件编译、install指令
各位同学,欢迎来到CMake进阶篇。上一章我们学会了怎么用CMake组织一个多文件项目,但说实话,那只是入门。真正到了实际项目中,你会遇到各种头疼的问题:头文件找不到、库链接不上、不同平台编译选项不一样……今天这一章,我就把这些坑一个个填上。
9.1 头文件路径管理:别再写相对路径了
我记得刚带团队那会儿,有个同事把项目里所有头文件路径都写成../../include/xxx.h。结果项目一重构,整个工程编译全崩了。这种痛苦,经历过一次就够了。
在CMake里,管理头文件路径的正确姿势是用target_include_directories。看个例子:
# 给目标 my_app 添加头文件搜索路径
target_include_directories(my_app
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/third_party/include
)
这里有个关键点:PRIVATE、PUBLIC、INTERFACE 这三个关键字怎么选?
| 关键字 | 作用范围 | 我常用的场景 |
|---|---|---|
| PRIVATE | 仅当前目标可见 | 内部实现细节的头文件 |
| PUBLIC | 当前目标和依赖者都可见 | 库的对外接口头文件 |
| INTERFACE | 仅依赖者可见,当前目标不需要 | 纯头文件库(header-only) |
9.2 链接库:静态库和动态库的玩法
链接库这件事,说白了就是告诉编译器:「我这个目标要用到哪些现成的代码」。CMake里用target_link_libraries搞定。
先看怎么创建一个库:
# 创建静态库
add_library(my_lib STATIC
src/utils.c
src/comm.c
)
# 创建动态库
add_library(my_shared_lib SHARED
src/utils.c
src/comm.c
)
然后链接到可执行文件:
add_executable(my_app main.c)
target_link_libraries(my_app
PRIVATE
my_lib # 链接我们自己建的库
pthread # 链接系统库
${CMAKE_DL_LIBS} # 跨平台动态加载库
)
target_link_libraries(A B)。我曾经因为这个顺序问题,排查了整整一个下午。
9.3 条件编译:一套代码适配多平台
做嵌入式开发的都懂,同一个项目可能要跑在ARM、RISC-V、x86上。条件编译就是解决这个问题的。
CMake里最常用的方式是target_compile_definitions:
# 根据平台添加宏定义
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_compile_definitions(my_app PRIVATE PLATFORM_LINUX=1)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_compile_definitions(my_app PRIVATE PLATFORM_WINDOWS=1)
endif()
# 根据编译器添加
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
target_compile_definitions(my_app PRIVATE COMPILER_GCC=1)
elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
target_compile_definitions(my_app PRIVATE COMPILER_CLANG=1)
endif()
然后在C代码里这样用:
#ifdef PLATFORM_LINUX
#include <unistd.h>
#define SLEEP_MS(ms) usleep((ms) * 1000)
#elif PLATFORM_WINDOWS
#include <windows.h>
#define SLEEP_MS(ms) Sleep(ms)
#endif
嗯,这里要注意:宏定义的名字最好加个前缀,比如MY_PROJECT_,避免跟第三方库冲突。我见过有人用DEBUG做宏名,结果跟某个库的调试宏撞了,编译报错完全看不懂。
9.4 install指令:让项目安装更规范
你想想看,项目开发完了,总得把编译产物部署到目标机器上吧?install指令就是干这个的。
# 安装可执行文件
install(TARGETS my_app
RUNTIME DESTINATION bin
)
# 安装库文件
install(TARGETS my_lib my_shared_lib
ARCHIVE DESTINATION lib # 静态库
LIBRARY DESTINATION lib # 动态库
RUNTIME DESTINATION bin # Windows下的DLL
)
# 安装头文件
install(DIRECTORY include/
DESTINATION include
FILES_MATCHING
PATTERN "*.h"
)
配合CMakeLists.txt里的CMAKE_INSTALL_PREFIX变量,可以指定安装路径:
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..
make install
FILES_MATCHING PATTERN "*.h",结果把整个include目录下的临时文件、备份文件全装进去了。所以,安装头文件时一定要指定匹配模式。
9.5 知识体系总览
说了这么多,我画张图帮你理清思路。CMake进阶的核心就是围绕「怎么让项目在不同环境下都能正确编译和部署」:
9.6 实战小技巧:组合使用
实际项目中,这几个功能往往是组合使用的。比如我最近做的一个传感器驱动库:
# 传感器驱动库
add_library(sensor_driver STATIC
src/sensor_i2c.c
src/sensor_spi.c
)
# 根据硬件平台选择通信协议
if(HARDWARE_PLATFORM STREQUAL "stm32")
target_compile_definitions(sensor_driver PRIVATE USE_I2C=1)
target_include_directories(sensor_driver PRIVATE hal/stm32)
elseif(HARDWARE_PLATFORM STREQUAL "esp32")
target_compile_definitions(sensor_driver PRIVATE USE_SPI=1)
target_include_directories(sensor_driver PRIVATE hal/esp32)
endif()
# 安装
install(TARGETS sensor_driver
ARCHIVE DESTINATION lib
)
install(DIRECTORY include/ DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
你看,头文件路径、条件编译、链接库、安装指令全用上了。这就是实际项目中的常态。
好了,这一章的内容就到这里。CMake进阶其实没那么玄乎,说白了就是让你能更灵活地控制编译过程。多写几个项目,这些指令自然就熟了。
公众号:蓝海资料掘金营,微信deep3321