7、CMake 入门:CMake 与 Makefile 的关系、CMakeLists.txt 的编写、add_executable 与 add_library、实战:用 CMake 管理模块化项目
说实话,很多嵌入式工程师对 Makefile 又爱又恨。爱的是它灵活,恨的是它写起来太痛苦。尤其是项目一大了,依赖关系、编译选项、跨平台支持……光维护 Makefile 就能让人崩溃。
我个人习惯用 CMake 来管理项目。它不直接编译代码,而是生成 Makefile 或其他构建系统的文件。说白了,CMake 是「构建系统的生成器」,而 Makefile 是「构建系统的执行者」。你写 CMakeLists.txt,CMake 帮你生成 Makefile,然后 make 去干活。
核心关系:CMake 是元构建系统,Makefile 是具体构建脚本。CMake 解决跨平台和模块化问题,Makefile 解决执行效率问题。
7.1 CMake 与 Makefile 的关系
我在项目中遇到过不少同事,直接手写 Makefile,结果换了个编译器就全废了。CMake 的好处是:你只需要描述「项目有什么源文件」「要生成什么目标」,剩下的交给 CMake 去适配。
举个例子:
- Makefile:你写死了 gcc 路径、编译选项、链接库路径。换到 Windows 上,全得改。
- CMake:你写
target_link_libraries,CMake 自动帮你找库、生成对应平台的 Makefile。
嗯,这里要注意:CMake 不是替代 Makefile,而是让 Makefile 的生成自动化。你想想看,一个项目有 50 个源文件,手写 Makefile 的依赖关系得累死。CMake 一句 file(GLOB) 就搞定了。
我的建议:新项目直接用 CMake。老项目如果 Makefile 已经稳定,没必要强行迁移。但如果你要跨平台,CMake 是唯一省心的选择。
7.2 CMakeLists.txt 的编写
CMakeLists.txt 是 CMake 的配置文件。每个模块一个文件,顶层一个主文件。结构非常清晰。
最基本的 CMakeLists.txt 长这样:
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0 LANGUAGES C)
add_executable(my_app main.c utils.c)
这里我解释一下:
cmake_minimum_required:指定 CMake 最低版本。我习惯写 3.10,太老的版本有些特性不支持。project:定义项目名称、版本、语言。C 项目就写LANGUAGES C,C++ 写CXX。add_executable:生成可执行文件。第一个参数是目标名,后面是源文件列表。
如果你要生成库文件,用 add_library:
add_library(my_lib STATIC utils.c math.c)
STATIC 是静态库,SHARED 是动态库。我一般嵌入式项目用静态库,省去动态加载的麻烦。
我曾经踩过的坑:源文件列表不要手写!用 file(GLOB SOURCES "src/*.c") 自动收集。否则你加个文件忘了改 CMakeLists.txt,编译半天找不到符号。
7.3 add_executable 与 add_library 详解
这两个命令是 CMake 的核心。我画个图帮你理解它们的关系:
看明白了吗?add_executable 生成的是能直接跑的程序,add_library 生成的是供别人调用的模块。在模块化项目中,我们通常把功能模块做成库,然后主程序链接这些库。
7.4 实战:用 CMake 管理模块化项目
好了,理论说完了,咱们直接上手。假设我有一个项目,结构如下:
project/
├── CMakeLists.txt # 顶层
├── main.c
├── module_a/
│ ├── CMakeLists.txt # 模块 A
│ ├── a.c
│ └── a.h
└── module_b/
├── CMakeLists.txt # 模块 B
├── b.c
└── b.h
顶层 CMakeLists.txt 这样写:
cmake_minimum_required(VERSION 3.10)
project(ModularDemo VERSION 1.0 LANGUAGES C)
# 添加子目录,每个子目录有自己的 CMakeLists.txt
add_subdirectory(module_a)
add_subdirectory(module_b)
# 生成主程序
add_executable(main main.c)
# 链接模块库
target_link_libraries(main PRIVATE module_a module_b)
模块 A 的 CMakeLists.txt:
add_library(module_a STATIC a.c)
# 指定头文件路径,这样外部也能引用
target_include_directories(module_a PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
模块 B 的 CMakeLists.txt 类似:
add_library(module_b STATIC b.c)
target_include_directories(module_b PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
这里有个关键点:target_include_directories 的 PUBLIC 表示头文件路径会传递给链接这个库的目标。如果你用 PRIVATE,外部就看不到头文件了。
我的习惯:模块内部头文件用 PRIVATE,对外接口用 PUBLIC。这样封装性好,别人不会乱包含你的内部实现。
编译方法很简单:
mkdir build
cd build
cmake ..
make
CMake 会自动生成 Makefile,然后 make 编译。你想想看,如果手写 Makefile,你得处理多少依赖关系?现在 CMake 全帮你搞定了。
7.5 避坑指南
我这些年用 CMake 踩过不少坑,分享几个给你:
- 路径问题:
${CMAKE_CURRENT_SOURCE_DIR}是当前 CMakeLists.txt 所在目录,${CMAKE_SOURCE_DIR}是顶层目录。别搞混了。 - 静态库链接顺序:GCC 链接器对静态库顺序敏感。CMake 会自动处理,但如果你手写
target_link_libraries顺序错了,可能报 undefined reference。 - 缓存问题:修改 CMakeLists.txt 后,最好删掉 build 目录重新 cmake。有时候缓存会坑你。
我曾经遇到一个奇葩问题:同事在 Windows 上用 CMake 生成 Visual Studio 工程,结果路径里有中文,编译报错。后来统一用英文路径才解决。跨平台时,路径里别带特殊字符。
7.6 小结
CMake 是现代 C 项目构建的标准工具。它和 Makefile 不是替代关系,而是上下层关系。你写 CMakeLists.txt,CMake 帮你生成 Makefile,make 去执行编译。
核心命令就三个:add_executable、add_library、target_link_libraries。配合 add_subdirectory 实现模块化管理。记住:每个模块一个 CMakeLists.txt,顶层负责组装。
嗯,这一章就到这里。CMake 的内容其实很深,但入门这些足够你管理一个模块化项目了。后面用到更高级的特性时,我们再细聊。
公众号:蓝海资料掘金营,微信 deep3321