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 的核心。我画个图帮你理解它们的关系:

源文件 (.c) 头文件 (.h) add_executable 生成可执行文件 add_library 生成库文件 可执行文件 (.elf/.exe) 库文件 (.a/.so/.lib) 可链接 add_executable 和 add_library 都从源文件生成目标 库文件可以被其他可执行文件或库链接

看明白了吗?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_directoriesPUBLIC 表示头文件路径会传递给链接这个库的目标。如果你用 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_executableadd_librarytarget_link_libraries。配合 add_subdirectory 实现模块化管理。记住:每个模块一个 CMakeLists.txt,顶层负责组装。

嗯,这一章就到这里。CMake 的内容其实很深,但入门这些足够你管理一个模块化项目了。后面用到更高级的特性时,我们再细聊。


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