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
)

这里有个关键点:PRIVATEPUBLICINTERFACE 这三个关键字怎么选?

关键字 作用范围 我常用的场景
PRIVATE 仅当前目标可见 内部实现细节的头文件
PUBLIC 当前目标和依赖者都可见 库的对外接口头文件
INTERFACE 仅依赖者可见,当前目标不需要 纯头文件库(header-only)
我的习惯:能选PRIVATE就别选PUBLIC。头文件暴露得越少,后续重构越轻松。这是我在一个维护了五年的嵌入式项目里悟出来的道理。

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} # 跨平台动态加载库
)
注意:链接顺序是有讲究的!在Linux下,被依赖的库要放在后面。比如A依赖B,你得写成 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进阶的核心就是围绕「怎么让项目在不同环境下都能正确编译和部署」:

CMake进阶核心 头文件路径管理 链接库 条件编译 install指令 PRIVATE / PUBLIC / INTERFACE STATIC / SHARED / 系统库 宏定义 / 平台判断 / 编译器判断 TARGETS / DIRECTORY / FILES 目标:一套CMakeLists.txt,适配多平台、多编译器、多部署场景 减少重复劳动,提高项目可维护性

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进阶功能时,别想着一步到位。先把手头项目的头文件路径管好,再慢慢加条件编译。每加一个功能,就编译测试一次。这样出了问题,你马上知道是哪里改坏了。

好了,这一章的内容就到这里。CMake进阶其实没那么玄乎,说白了就是让你能更灵活地控制编译过程。多写几个项目,这些指令自然就熟了。


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