目录
一、CMake 是什么,为什么要使用 CMake
二、CMakeLists.txt 文件结构与简单示例
三、进阶的CMake
四、静态库与动态库生成及其使用
五、注释的语法
六、 set、list、message 三个常用的 CMake 函数与命令
七、CMake 的控制语句以及自定义宏/函数
八、为STM32工程进行CMake(嵌套CMake)
一、CMake 是什么,为什么要使用 CMake
在学习 CMake 之前,我们先来思考一个问题:我们为什么需要构建工具?
在 C/C++ 项目中,通常我们写好源代码后,并不是直接能运行的,需要经过编译、链接等步骤才能变成可执行程序。对于简单的项目,手动使用 gcc
或 g++
进行编译也许问题不大,但项目一旦变复杂,手动维护这些编译命令就变得痛苦不堪。这时候,我们就需要一个构建系统来自动化这些操作。
那 Make 和 Makefile 不够用吗?
Make 和 Makefile 确实能解决这个问题,但它们存在一些痛点:
Makefile 语法晦涩,难以维护。
不同平台的差异大,移植性差。
难以和现代 IDE 或构建工具链(如 Ninja、MSVC)协作。
CMake 正是为了解决这些问题而生的
CMake 是一个跨平台的自动化构建系统生成工具。 它本身并不直接构建项目,而是生成本地平台的构建系统(比如 Makefile、Ninja 构建脚本,或者 Visual Studio 的工程文件),然后你再使用这些文件进行实际构建。
简单来说,CMake 的职责是生成工程的“构建说明”。
为什么要使用 CMake?
-
✅ 跨平台:支持 Linux、Windows、macOS 甚至嵌入式系统。
-
✅ 模块化管理项目结构:项目大了以后,管理源文件、库、头文件路径变得容易。
-
✅ 与现代 IDE 兼容良好:如 CLion、Visual Studio、VS Code 等。
-
✅ 支持多种构建工具链:如 Make、Ninja、MSBuild。
-
✅ 便于持续集成:CI/CD 环境中广泛使用。
-
✅ 支持条件编译、可选模块、外部依赖等高级特性。
二、CMakeLists.txt 文件结构与简单示例
顶层的 CMakeLists.txt
定义项目的全局配置,
现在有一个main.cpp
// main.cpp
#include <iostream>int main() {std::cout << "Hello, World!" << std::endl;return 0;
}
为了将其进行编译,先进行cmake的版本进行查询
cmake --version #查询cmake版本,低版本无法兼容高版本
可以看到cmake的版本是3.10.2
在main.cpp同目录下,编写一个简单的CMakeLists.txt,内容如下:
# 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.10) # 指定 CMake 最低版本要求
project(project_name LANGUAGES CXX) # 定义项目名称(project_name)和语言(CXX->C++)
add_executable(hello main.cpp) # 添加可执行文件目标(依据main.cpp将生成一个名为hello的可执行文件 )
再执行
cmake ./ # ”./ “是指CMakeLists.txt的地址,./就是指向本目录,也可以写出cmake .
执行命令“ls"可以看到cmake指令生成了很多东西:
CMakeCache.txt
作用:
这是 CMake 的缓存文件,用来保存你的配置选项,比如编译器路径、构建选项、库文件路径等。
如果你修改了CMakeLists.txt
中的配置或外部依赖,建议删除这个文件重新运行cmake
。
CMakeFiles/
作用:
这是一个目录,存储 CMake 在配置过程中生成的所有中间文件、依赖信息、目标描述等内容。
比如每个.cpp
文件会对应一些.o
文件,链接信息也会写在这里。建议:
这个目录一般不需要手动修改。你可以用make clean
或直接删除它来清理构建文件。
cmake_install.cmake
作用:
这个文件由 CMake 自动生成,用于描述如何安装该项目。它是make install
所依赖的脚本文件。内容包含:
文件如何被复制到安装路径
安装路径
权限设置等信息
如果你没有写安装规则,这个文件会比较简单
Makefile
作用:
这是最核心的构建文件,CMake 会把你在CMakeLists.txt
中定义的目标(如add_executable
)转成这个Makefile
,然后通过 GNU Make 编译你的项目。Make 会根据这个文件中的规则编译目标文件(
.o
)并链接生成最终可执行文件或库。
再执行:
make #依据
Makefile
规则编译目标文件(.o
)并链接生成最终可执行文件或库
可以看到生成了可执行文件”hello",执行它看看:
好了,这样就完成了一个简单的cmake过程。
三、进阶的CMake
接下来创建4个文件夹
mkdir include src build out
include/
—— 头文件目录 存放项目中需要对外暴露的头文件
src/
—— 源代码目录 存放所有源代码
build/
—— 构建输出目录(中间文件)存放 cmake
生成的中间构建文件
out/
—— 最终输出目录(可执行文件或库) 放置最终生成的产物
之后将在include与scr中创建如下.h文件与.cpp文件:
touch head1.h head2.h head3.h head4.h #在include目录下执行创建头文件
mv main.cpp ./src #将main.cpp移植到src下
touch app1.cpp app2.cpp app3.cpp #在src下创建源程序文件
include/head1.h
#pragma once
void func1();
include/head2.h
#pragma once
void func2();
include/head3.h
#pragma once
void func3();
include/head4.h
#pragma once
void func4();
src/app1.cpp
#include "head1.h"
#include <iostream>void func1() {std::cout << "This is func1()" << std::endl;
}
src/app2.cpp
#include "head2.h"
#include <iostream>void func2() {std::cout << "This is func2()" << std::endl;
}
src/app3.cpp
#include "head3.h"
#include "head4.h"
#include <iostream>void func3() {std::cout << "This is func3()" << std::endl;
}void func4() {std::cout << "This is func4()" << std::endl;
}
src/main.cpp
#include "head1.h"
#include "head2.h"
#include "head3.h"
#include "head4.h"int main() {func1();func2();func3();func4();return 0;
}
编写CMakeLists.txt
cmake_minimum_required(VERSION 3.10)# 项目名称
project(MyApp)# 包含头文件目录
include_directories(${CMAKE_SOURCE_DIR}/include)# 设置源文件路径
file(GLOB_RECURSE SOURCES "${CMAKE_SOURCE_DIR}/src/*.cpp")# 输出路径
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/out)# 生成可执行文件
add_executable(my_app ${SOURCES})
看到这个CMakeLists.txt是不是有点懵。
现在就来 详细讲解上面那个案例中的每一个 CMake 函数,
1. cmake_minimum_required
这是每个 CMake 项目的“起点指令”,告诉 CMake 最低支持的版本是多少。
有什么用:
确保 CMake 使用你指定的版本特性来处理脚本。如果用户的 CMake 太旧,就会报错而不是静默失败。
怎么用:
cmake_minimum_required(VERSION 3.10)
小贴士:
-
如果你用到
target_include_directories
或target_compile_features
等高级特性,一定要升级版本! -
常见版本推荐:
3.10
是 Ubuntu 18.04 默认的,3.16+
支持很多现代功能。
2. project
设置工程的名称、版本号、语言类型等。
有什么用:
告诉 CMake“我这个项目叫啥、用的语言是啥”,并设置一些默认变量。
怎么用:
project(MyApp)
或带语言:
project(MyApp LANGUAGES C CXX)
小贴士:
-
你可以用
PROJECT_NAME
、PROJECT_SOURCE_DIR
等变量,它们是project()
自动生成的。 -
可以加上版本号,比如:
project(MyApp VERSION 1.2.3)
3. include_directories
:
添加头文件的搜索路径,相当于 g++ -I
。
有什么用:
让编译器能找到 #include "xxx.h"
所引用的头文件。
怎么用:
include_directories(${CMAKE_SOURCE_DIR}/include)
小贴士:
-
不推荐全局用太多
include_directories
,用target_include_directories
会更优雅。 -
${CMAKE_SOURCE_DIR}
是项目根目录路径的变量(后面讲)。
4. file(GLOB_RECURSE ...)
读取目录下所有(符合匹配规则的)文件。
GLOB_RECURSE
代表“递归地查找”。
有什么用:
让你不用手动一行一行列出源文件名。
怎么用:
file(GLOB_RECURSE SOURCES "${CMAKE_SOURCE_DIR}/src/*.cpp")
表示把 src/
目录下所有 .cpp
文件放进变量 SOURCES
。
小贴士:
-
如果你新增了
.cpp
文件,CMake 不会自动重新扫描,必须手动cmake ..
一次。 -
你也可以用
file(GLOB ...)
只查当前目录。
5. set
设置变量的值。
有什么用:
定义变量路径、开关等,后面都可以复用。
怎么用:
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/out)
表示将编译生成的可执行文件放在 out/
目录。
小贴士:
-
你可以用
message(STATUS "路径:${EXECUTABLE_OUTPUT_PATH}")
打印变量。 -
set(VAR "value" CACHE TYPE "描述")
可以让变量出现在 GUI 配置器中。
6. add_executable
创建一个可执行程序。
有什么用:
把你写的 .cpp
文件编译链接成可执行文件。
怎么用:
add_executable(my_app ${SOURCES})
这里 my_app
是目标名称,也是生成的程序名;${SOURCES}
是所有源文件。
小贴士:
-
如果你只写一两个文件,也可以直接列出:
add_executable(my_app main.cpp app1.cpp)
-
my_app
后续可以用在其他函数里,比如加头文件、库文件。
7. CMake内置变量
CMAKE_SOURCE_DIR
是一个CMake内置变量
是 CMake 的顶层项目根目录。
有什么用:
配合 include_directories()
、file()
等引用项目路径。
怎么用:
include_directories(${CMAKE_SOURCE_DIR}/include)
小贴士:
-
如果用多级 CMake,可以注意还有个变量
CMAKE_CURRENT_SOURCE_DIR
表示“当前目录”。
CMake内置变量有很多,如下:
路径相关内置变量
变量名 | 说明 |
---|---|
CMAKE_SOURCE_DIR | 最顶层 CMakeLists.txt 所在目录(源代码根目录) |
CMAKE_CURRENT_SOURCE_DIR | 当前处理的 CMakeLists.txt 所在目录 |
CMAKE_BINARY_DIR | CMake 执行构建命令所在目录(一般是 build/ ) |
CMAKE_CURRENT_BINARY_DIR | 当前处理的 CMakeLists.txt 对应的构建输出目录 |
PROJECT_SOURCE_DIR | project() 函数所在 CMakeLists.txt 的目录,通常等同于 CMAKE_SOURCE_DIR |
PROJECT_BINARY_DIR | project() 执行后对应的构建输出目录,通常等同于 CMAKE_BINARY_DIR |
输出目录相关变量(现代推荐)
变量名 | 说明 |
---|---|
CMAKE_RUNTIME_OUTPUT_DIRECTORY | 可执行文件的输出目录(比 EXECUTABLE_OUTPUT_PATH 更推荐) |
CMAKE_LIBRARY_OUTPUT_DIRECTORY | 动态库(.so/.dll)的输出目录 |
CMAKE_ARCHIVE_OUTPUT_DIRECTORY | 静态库(.a/.lib)的输出目录 |
EXECUTABLE_OUTPUT_PATH | 老版本可执行文件输出目录 |
LIBRARY_OUTPUT_PATH | 老版本库文件输出目录 |
编译器相关变量
变量名 | 说明 |
---|---|
CMAKE_C_COMPILER | C 编译器路径 |
CMAKE_CXX_COMPILER | C++ 编译器路径 |
CMAKE_C_FLAGS | 传递给 C 编译器的选项 |
CMAKE_CXX_FLAGS | 传递给 C++ 编译器的选项 |
CMAKE_BUILD_TYPE | 编译类型(如:Debug / Release) |
CMAKE_CXX_STANDARD | 指定 C++ 标准,例如 11、14、17、20 |
CMAKE_SYSTEM_NAME | 操作系统名,如 Linux、Windows、Darwin |
CMAKE_SYSTEM_PROCESSOR | 架构,如 x86_64、ARM、aarch64 |
项目和目标相关
变量名 | 说明 |
---|---|
PROJECT_NAME | project() 指定的项目名称 |
PROJECT_VERSION | project(... VERSION 1.2.3) 设置的版本 |
CMAKE_PROJECT_NAME | 根项目名称(区别于子项目) |
CMAKE_VERBOSE_MAKEFILE | 如果为 ON ,生成详细的 Makefile,编译时能看到完整命令 |
CMAKE_INSTALL_PREFIX | 安装的根目录,默认为 /usr/local |
测试 & 环境变量(进阶)
变量名 | 说明 |
---|---|
CMAKE_TESTING_ENABLED | 是否启用了 enable_testing() |
CMAKE_INSTALL_RPATH | 动态链接库的运行路径设置 |
CMAKE_MODULE_PATH | 查找自定义 CMake 模块的路径 |
CMAKE_EXPORT_COMPILE_COMMANDS | 是否导出 compile_commands.json (给 clangd / LSP 用) |
常用内置变量推荐(你能马上用的)
类别 | 建议使用变量 |
---|---|
路径 | CMAKE_SOURCE_DIR , CMAKE_BINARY_DIR , CMAKE_CURRENT_SOURCE_DIR |
输出目录 | ✅ CMAKE_RUNTIME_OUTPUT_DIRECTORY (可执行)✅ CMAKE_LIBRARY_OUTPUT_DIRECTORY (动态库) |
编译控制 | CMAKE_CXX_STANDARD , CMAKE_BUILD_TYPE , CMAKE_CXX_FLAGS |
安装路径 | CMAKE_INSTALL_PREFIX |
编译器路径 | CMAKE_C_COMPILER , CMAKE_CXX_COMPILER |
好了,继续吧
进入到build下执行cmake与make
cd build && cmake .. && make
可以看到cmake生成的中间文件都放在了build里面,再到out下可以看到生成的my_app文件,执行它:
cd out
./my_app
可以看到main.cpp成功调用了app1~3的代码。
四、静态库与动态库生成及其使用
好了,接下来继续静态库与动态库的生成吧
什么是静态库(Static Library)
静态库(以 .a
或 .lib
结尾)是一种在编译阶段就被打包进最终可执行文件的库文件。
生成方式: 用 ar
工具打包 .o
文件(Linux 下 .a
,Windows 是 .lib
)
链接时机: 编译时链接,最终 .exe
或 .out
文件包含了所有库代码
部署方式: 最终可执行文件独立运行,不再依赖外部 .a
文件
举例:你编译一个 main.cpp
链接 libmath.a
,最后 main
程序会包含 math
库代码,运行时不需要带上 libmath.a
。
什么是动态库(Dynamic Library)
动态库(以 .so
或 .dll
结尾)是一种在程序运行时才加载的共享库文件。
生成方式: 编译为 .so
(Linux)或 .dll
(Windows)
链接时机:编译时生成依赖信息(例如 .so
名字)运行时由操作系统动态加载
部署方式: 可执行程序运行时必须能访问 .so
文件,否则会报错
举例:你编译 main.cpp
链接 libmath.so
,main
程序运行时会查找并加载它,若 .so
不在路径中就会失败。
区别总结对比
常见使用场景对比
使用静态库的场景:
嵌入式设备开发(如 STM32、树莓派):不方便部署
.so
编译时确保功能完整
对启动速度有极致要求
程序发布不希望依赖任何额外文件
使用动态库的场景:
多个程序共享同一个库(节省空间)
希望随时升级功能模块(比如:游戏引擎插件、浏览器)
需要插件系统或模块热更新
构建跨平台框架(Qt、OpenCV、Python 模块等)
总结一句话:
静态库 = 程序自带所有代码,运行独立;
动态库 = 程序轻巧灵活,运行依赖共享模块。
好了,项目开始把
首先,在out下创建lib与bin
mkdir lib bin #lib由于存放库文件,bin用于存放链接的可执行文件
然后再创建并编写include/math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_Hint add(int a, int b);
int square(int a);#endif
src/math_utils.cpp
#include "math_utils.h"int add(int a, int b) {return a + b;
}int square(int a) {return a * a;
}
src/main.cpp
#include <iostream>
#include "math_utils.h"int main() {std::cout << "3 + 4 = " << add(3, 4) << std::endl;std::cout << "5 squared = " << square(5) << std::endl;return 0;
}
math_utils.h/cpp
:封装一些数学函数(加法、平方等)
编译生成:
静态库:libmath_utils.a
动态库:libmath_utils.so
可执行文件:main
(链接静态库或动态库)
CMakeLists.txt
# 指定所需的最低 CMake 版本
cmake_minimum_required(VERSION 3.10)# 定义当前工程的名称为 MyMathProject
project(MyMathProject)# 设置使用的 C++ 标准版本为 C++11
# 若系统支持更高版本,可以改为 17 或 20
set(CMAKE_CXX_STANDARD 11)# 设置可执行文件输出目录为 out/bin(运行时的二进制文件)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/bin)# 设置库文件输出目录(包括静态库和动态库)为 out/lib
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/lib) # .so 文件输出目录
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/lib) # .a 文件输出目录# 添加头文件搜索路径(让编译器能找到 include/ 目录下的头文件)
include_directories(${CMAKE_SOURCE_DIR}/include)# 使用通配符收集 src/ 目录下所有 .cpp 源文件,保存到变量 SRC_FILES
# 虽然后面我们没有用到这个变量,它演示了如何批量获取源码文件
file(GLOB SRC_FILES src/*.cpp)# 静态库构建部分 --------------------------------------# 生成一个名为 math_utils_static 的静态库(libmath_utils.a)
# 只包含 src/math_utils.cpp,一个 cpp 文件也可以构成一个库
add_library(math_utils_static STATIC src/math_utils.cpp)# 设置该库的输出文件名为 math_utils
# 注意:这里设置的是逻辑输出名,最终输出是 libmath_utils.a(Linux)
set_target_properties(math_utils_static PROPERTIES OUTPUT_NAME "math_utils")# 动态库构建部分 --------------------------------------# 生成一个名为 math_utils_shared 的动态库(libmath_utils.so)
add_library(math_utils_shared SHARED src/math_utils.cpp)# 设置该库的输出文件名为 math_utils
# 同样最终输出为 libmath_utils.so
set_target_properties(math_utils_shared PROPERTIES OUTPUT_NAME "math_utils")# 可执行文件构建部分 -----------------------------------# 构建可执行文件 main_static,使用 main.cpp
add_executable(main_static src/main.cpp)# 链接静态库 math_utils_static 到 main_static 中
# 也就是说 math_utils.a 的代码会打包进 main_static 中
target_link_libraries(main_static math_utils_static)# 构建另一个可执行文件 main_shared,使用相同的 main.cpp
add_executable(main_shared src/main.cpp)# 链接动态库 math_utils_shared 到 main_shared
# 程序运行时需要找到 libmath_utils.so 才能启动成功
target_link_libraries(main_shared math_utils_shared)
在build下执行如下指令:
make clean #清除生成make结果
cmake ..
make
再到out下查看:
可以看到生成了这四个文件:
/out
├── bin/
│ ├── main_static # 静态库链接的可执行文件
│ └── main_shared # 动态库链接的可执行文件
└── lib/
├── libmath_utils.a # 静态库文件
└── libmath_utils.so # 动态库文件
再对可执行文件继续执行:
好了,到这里,基本的cmake就结束了,下面进行补充与升级:
五、注释的语法
# 单行注释 - 使用 # 符号
#[[多行注释 - 使用 包围
]]
六、 set
、list
、message
三个常用的 CMake 函数与命令
下面我将详细讲解 set
、list
、message
三个常用的 CMake 函数与命令
message("这是一条很重要的消息") # 普通消息(白色)
message(STATUS "这是状态消息") # 状态消息(前缀带 --,绿色)
message(WARNING "这是警告消息") # 警告消息(黄色)
#message(FATAL_ERROR "致命错误") # 错误消息(红色,会终止构建)
( 一)、set()
命令
作用:
set()
是 CMake 中最常用的命令之一,用于定义或修改变量的值。
基本语法:
set(<variable> <value>... [CACHE <type> <docstring> [FORCE]])
常见用法:
1. 定义普通变量
set(MY_NAME "Alice")
message(STATUS "Name is ${MY_NAME}") # 输出:Name is Alice
2. 定义包含多个元素的变量(类似列表)
set(SOURCES main.cpp util.cpp helper.cpp)
3. 定义缓存变量(适合用户配置的变量)
set(USE_MATH_LIB ON CACHE BOOL "Use math library")
4. 修改已有变量的值
set(MY_NAME "Bob") # 重新赋值
5. 设置环境变量
set(ENV{MY_PATH} "/usr/local/bin")
(二)、list()
命令
作用:
对 CMake 中的列表变量(空格分隔)进行各种操作,如添加、删除、搜索等。
基本语法:
list(<COMMAND> <list_variable> [ARGS...])
常用操作示例:
1. 添加元素到列表尾部
set(FRUITS apple banana)
list(APPEND FRUITS orange)
message(STATUS "Fruits: ${FRUITS}") # 输出:apple;banana;orange
2. 插入元素到指定位置
list(INSERT FRUITS 1 mango) # 插入 mango 到第2个位置
3. 移除指定元素
list(REMOVE_ITEM FRUITS banana)
4. 获取列表长度
list(LENGTH FRUITS FRUITS_COUNT)
message(STATUS "Fruits count: ${FRUITS_COUNT}")
5. 访问指定索引元素
list(GET FRUITS 0 FIRST_FRUIT)
message(STATUS "First fruit: ${FIRST_FRUIT}")
(三)、message()
命令
作用:
用于在配置阶段向终端输出信息,便于调试或传递信息给用户。
基本语法:
message([<mode>] "message to display")
常用模式:
模式 | 说明 |
---|---|
STATUS | 正常信息(推荐调试输出) |
WARNING | 警告信息 |
AUTHOR_WARNING | 针对开发者的警告 |
SEND_ERROR | 错误信息,但继续执行 |
FATAL_ERROR | 致命错误,立即停止 CMake |
DEPRECATION | 弃用警告 |
示例:
1. 普通信息输出
message(STATUS "Compiling project...")
2. 警告信息
message(WARNING "This feature is experimental.")
3. 错误信息并中断
if(NOT DEFINED USER_OPTION)message(FATAL_ERROR "USER_OPTION is not defined!")
endif()
示例:综合使用
cmake_minimum_required(VERSION 3.10)
project(DemoSetListMessage)# set 一个变量
set(MY_LIST "one" "two" "three")# list 增加一个元素
list(APPEND MY_LIST "four")# 获取长度
list(LENGTH MY_LIST LEN)
message(STATUS "List length: ${LEN}") # 输出长度# 获取第一个元素
list(GET MY_LIST 0 FIRST_ITEM)
message(STATUS "First item: ${FIRST_ITEM}")# 输出整个列表
message(STATUS "Current list: ${MY_LIST}")# 检查某个条件
if(LEN GREATER 3)message(WARNING "List has more than 3 items")
endif()
总结
命令 | 用途 |
---|---|
set() | 设置变量、缓存值、环境变量等 |
list() | 操作字符串列表 |
message() | 输出信息、调试、报错等 |
七、CMake 的控制语句以及自定义宏/函数
(一)、foreach
循环
用途:
用于遍历列表或范围,类似其他语言中的 for
循环。
语法:
foreach(var IN LISTS mylist)message(STATUS "Item: ${var}")
endforeach()
示例:
遍历列表:
set(FRUITS apple banana orange)
foreach(fruit IN LISTS FRUITS)message(STATUS "Fruit: ${fruit}")
endforeach()
遍历整数区间:
foreach(i RANGE 1 5)message(STATUS "Number: ${i}")
endforeach()
(二)、if
条件语句
用途:
条件判断,控制执行逻辑。
常见条件:
if(DEFINED VAR)
:是否定义
if(VAR STREQUAL "abc")
:字符串比较
if(VAR MATCHES ".*test.*")
:正则匹配
if(NOT ...)
:取反
if(EXISTS path)
:文件是否存在
示例:
set(MODE debug)if(MODE STREQUAL "debug")message(STATUS "Debug mode")
else()message(STATUS "Release mode")
endif()
(三)、option
配置选项
用途:
为用户提供编译选项,配合 cmake-gui
、ccmake
或命令行传参使用。
语法:
option(USE_MY_LIB "Enable my library" ON)
示例:
option(ENABLE_LOG "Enable logging" OFF)if(ENABLE_LOG)add_definitions(-DENABLE_LOGGING)message(STATUS "Logging is enabled")
endif()
用户可用:
cmake -DENABLE_LOG=ON ..
(四)、macro
宏定义
用途:
定义一段可复用的 CMake 脚本片段,参数替换更简单(不支持局部作用域)。
📌 语法:
macro(print_var var)message(STATUS "Value of ${var}: [${${var}}]")
endmacro()
🔍 示例:
set(NAME "CMake")
print_var(NAME)
输出:Value of NAME: [CMake]
(五)、function
函数定义
用途:
功能与 macro
类似,但有局部作用域(更安全),推荐用函数替代宏。
语法:
function(print_var var)message(STATUS "Value: ${${var}}")
endfunction()
支持 return:
function(double_input INPUT OUTPUT)math(EXPR result "${INPUT} * 2")set(${OUTPUT} ${result} PARENT_SCOPE)
endfunction()double_input(5 MY_RESULT)
message(STATUS "Result: ${MY_RESULT}")
总结表
指令 | 功能 | 特点/备注 |
---|---|---|
foreach | 遍历列表、范围 | 支持 LISTS 和 RANGE |
if | 条件判断 | 支持字符串、文件、正则等 |
option | 用户可设置的布尔开关 | 可用于条件编译 |
macro | 定义重复逻辑 | 变量为全局作用域 |
function | 定义函数逻辑(推荐) | 支持局部作用域,推荐使用 |
八、为STM32工程进行CMake(嵌套CMake)
准备好stm32完整工程文件,
去 Keil / STM32CubeMX / IAR 工程里找 .ld
文件
将其拷贝到STM32中
安装gcc-arm-none-eabi工具链版
sudo apt-get update
sudo apt-get install gcc-arm-none-eabi
创建并编写CMakelists.txt
project/CMakeLists.txt
cmake_minimum_required(VERSION 3.5)project(STM32_Project LANGUAGES C ASM)# 强制设置交叉编译器
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP arm-none-eabi-objdump)# 设置系统类型(必须放在project命令后)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)
# Set the toolchain file for ARM cross-compilation
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/STM32/gcc-arm-none-eabi.cmake)# Add the STM32 subdirectory
add_subdirectory(STM32)
project/STM32/CMakeLists.txt
# 设置MCU特定编译选项
set(CPU "-mcpu=cortex-m3")
set(FPU "")
set(FLOAT_ABI "")# 设置全局编译选项
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CPU} ${FPU} ${FLOAT_ABI} -mthumb -ffunction-sections -fdata-sections -Wall -DSTM32F10X_HD -DUSE_STDPERIPH_DRIVER -march=armv7-m -mtune=cortex-m3 -fno-exceptions")# 设置汇编文件的编译选项
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} ${CPU} ${FPU} ${FLOAT_ABI} -mthumb")# 设置链接选项
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${CPU} ${FPU} ${FLOAT_ABI} -mthumb -specs=nosys.specs -specs=nano.specs -T${CMAKE_CURRENT_SOURCE_DIR}/STM32F103ZETX_FLASH.ld -Wl,--gc-sections -static -Wl,-Map=${PROJECT_NAME}.map")# 包含路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/CORE${CMAKE_CURRENT_SOURCE_DIR}/STM32F10x_FWLib/inc${CMAKE_CURRENT_SOURCE_DIR}/SYSTEM/delay${CMAKE_CURRENT_SOURCE_DIR}/SYSTEM/sys${CMAKE_CURRENT_SOURCE_DIR}/SYSTEM/usart${CMAKE_CURRENT_SOURCE_DIR}/USER
)# 收集所有源文件
file(GLOB_RECURSE SOURCES"${CMAKE_CURRENT_SOURCE_DIR}/CORE/*.c""${CMAKE_CURRENT_SOURCE_DIR}/STM32F10x_FWLib/src/*.c""${CMAKE_CURRENT_SOURCE_DIR}/SYSTEM/*/*.c""${CMAKE_CURRENT_SOURCE_DIR}/USER/*.c"
)# 创建可执行文件
add_executable(${PROJECT_NAME}.elf ${SOURCES})
target_compile_options(${PROJECT_NAME}.elf PRIVATE -O2)# 为特定目标设置编译选项
target_compile_options(${PROJECT_NAME}.elf PRIVATE -O0 -fno-builtin)# 生成二进制文件
add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILDCOMMAND ${CMAKE_OBJCOPY} -Obinary ${PROJECT_NAME}.elf ${PROJECT_NAME}.binCOMMENT "Generating binary file ${PROJECT_NAME}.bin"
)
由于需要进行交叉编译(简单说就是需要同时进行X86与ARM指令),所以要进行创建并编写一个.cmake文件,
project/STM32/gcc-arm-none-eabi.cmake
# 设置系统类型
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)# 指定交叉编译器
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
# 禁止尝试编译运行测试程序
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
set(CMAKE_OBJDUMP arm-none-eabi-objdump)
set(CMAKE_SIZE arm-none-eabi-size)# 设置查找路径规则
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
创建build并进行cmake与make操作,当出现如下报错是因为CMSIS内联汇编与编译器不兼容,
所以需要对core_cm3.c进行更改如下:
/*** @brief STR Exclusive (8 bit)** @param value value to store* @param *addr address pointer* @return successful / failed** Exclusive STR command for 8 bit values*/
uint32_t __STREXB(uint8_t value, uint8_t *addr)
{uint32_t result=0;__ASM volatile ("strexb %0, %2, [%1]" : "=&r" (result) : "r" (addr), "r" (value) );return(result);
}/*** @brief STR Exclusive (16 bit)** @param value value to store* @param *addr address pointer* @return successful / failed** Exclusive STR command for 16 bit values*/
uint32_t __STREXH(uint16_t value, uint16_t *addr)
{uint32_t result=0;__ASM volatile ("strexh %0, %2, [%1]" : "=&r" (result) : "r" (addr), "r" (value) );return(result);
}
当使用的是正点原子的代码,会出现如下报错:
这段代码用的是 Keil 风格的 __asm void
,而现在使用的是 GCC 工具链(arm-none-eabi-gcc),所以会报错。
对sys.c文件更改成如下:
#include "sys.h"// 执行 WFI 指令
void WFI_SET(void)
{__asm volatile("wfi");
}// 关闭所有中断(设置 PRIMASK)
void INTX_DISABLE(void)
{__asm volatile("cpsid i");
}// 开启所有中断(清除 PRIMASK)
void INTX_ENABLE(void)
{__asm volatile("cpsie i");
}// 设置主堆栈指针(MSP)
void MSR_MSP(u32 addr)
{__asm volatile ("msr msp, %0\n":: "r" (addr):);
}
最后,在build再进行
cmak ..
make
可以在build/STM32目录下找到生成的ELF与BIN文件:
至此完成。
使用例程链接:【免费】学习CMake的例程-学习CMake的例程资源-CSDN文库
结语:用 CMake 掌控跨平台构建的力量
本文从 “CMake 是什么” 的基础出发,逐步深入讲解了 CMakeLists.txt
的结构与常用命令,展示了从构建可执行文件到静态库、动态库的完整流程。同时也涵盖了项目实战示例、嵌套使用、以及与 STM32 工程结合的案例,帮助你建立起对 CMake 的全面认知。
CMake 不仅是一个跨平台构建工具,更是现代 C++ 工程组织不可或缺的一部分。与嵌入式、Linux 系统深度集成。
如果你是一位正在开发嵌入式项目、跨平台应用、或者希望将工程进行自动化构建的开发者,那么深入掌握 CMake,将极大提升你的开发效率和项目可维护性。