目录
- [1. 从最小 CMake 项目开始](#1. 从最小 CMake 项目开始)
- [1.1 最小项目](#1.1 最小项目)
- [1.2 多源文件项目](#1.2 多源文件项目)
- [1.3 把代码拆成库](#1.3 把代码拆成库)
- [2. 构建目标 target](#2. 构建目标 target)
- [3. 工程化配置](#3. 工程化配置)
- [4. 标准多目录项目](#4. 标准多目录项目)
- [4.1 顶层 CMakeLists.txt](#4.1 顶层 CMakeLists.txt)
- [4.2 src/CMakeLists.txt](#4.2 src/CMakeLists.txt)
- [4.3 app/CMakeLists.txt](#4.3 app/CMakeLists.txt)
- [4.4 tests/CMakeLists.txt](#4.4 tests/CMakeLists.txt)
- [4.5 构建并运行整个项目](#4.5 构建并运行整个项目)
- [5. CMake 交叉编译](#5. CMake 交叉编译)
- [更改 toolchain](#更改 toolchain)
- 安装路径
你可能需要的 Make/Makefille/CMake 知识
在 C 语言中,gcc 是最底层的编译器,它真正负责把源代码编译、链接成可执行文件;make 是底层的执行工具,它并不理解代码,只是按照规则调用 gcc;Makefile 则是这些规则的具体形式,明确写出了哪些文件依赖哪些文件、在什么情况下调用 gcc 执行哪些命令;而 CMake 站在更高一层,用来描述工程的整体结构,并根据这些结构描述自动生成 Makefile 等构建文件。
开发者用CMake 描述工程结构,CMake根据环境生成构建规则,具体的构建工具负责执行,而编译器只专注于把代码变成程序。每一层只做一件事,大家各司其职。

在 Linux 下,常见流程是:
bash
cmake -S . -B build
cmake --build build
其中:
text
cmake -S . -B build
是配置阶段,意思是:
-S .:源码目录是当前目录;
-B build:把生成的构建文件放到 build 文件夹里。
text
cmake --build build
用于构建,意思是:进入 build 对应的构建目录,调用实际构建工具生成可执行文件或库文件。
1. 从最小 CMake 项目开始
1.1 最小项目
创建如下结构:
hello_cmake/
├── CMakeLists.txt
└── main.cpp
main.cpp:
cpp
#include <iostream>
int main() {
std::cout << "Hello CMake!" << std::endl;
return 0;
}
CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.16)
project(HelloCMake LANGUAGES CXX)
add_executable(hello main.cpp)
其中:
cmake
cmake_minimum_required(VERSION 3.16)
表示这个项目要求 CMake 版本至少为 3.16。
cmake
project(HelloCMake LANGUAGES CXX)
定义项目名称为 HelloCMake,使用的语言是 C++。
如果是 C 项目,可以写:
cmake
project(MyCProject LANGUAGES C)
如果同时使用 C 和 C++:
cmake
project(MyProject LANGUAGES C CXX)
cmake
add_executable(hello main.cpp)
表示生成一个可执行文件,名字叫 hello,由 main.cpp 编译而来。
在 Linux 中最终生成:hello
在 Windows 中可能生成:hello.exe
在 hello_cmake 目录下执行:
bash
cmake -S . -B build
cmake --build build
运行程序:
bash
./build/hello

1.2 多源文件项目
真实项目不会只有一个 main.cpp。
multi_source/
├── CMakeLists.txt
├── main.cpp
├── print.cpp
└── print.h
print.h:
cpp
#pragma once
void print_message();
print.cpp:
cpp
#include "print.h"
#include <iostream>
void print_message() {
std::cout << "Message from print.cpp" << std::endl;
}
main.cpp:
cpp
#include "print.h"
int main() {
print_message();
return 0;
}
CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.16)
project(MultiSource LANGUAGES CXX)
add_executable(app
main.cpp
print.cpp
)
app 这个可执行文件由 main.cpp 和 print.cpp 共同编译链接得到。
头文件 print.h 不一定写进 add_executable,因为它不会单独编译。当然也可以写:
cmake
add_executable(app
main.cpp
print.cpp
print.h
)
这不会改变最终链接逻辑,只是让头文件也出现在工程管理中。
头文件通常会放到 include 目录。
text
include_project/
├── CMakeLists.txt
├── include/
│ └── print.h
└── src/
├── main.cpp
└── print.cpp
此时CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.16)
project(IncludeProject LANGUAGES CXX)
add_executable(app
src/main.cpp
src/print.cpp
)
target_include_directories(app PRIVATE
${PROJECT_SOURCE_DIR}/include
)
表示:编译 app 这个目标时,额外去 include 目录里寻找头文件。
其中:${PROJECT_SOURCE_DIR}表示当前项目源码根目录,也就是包含顶层 CMakeLists.txt 的目录。
1.3 把代码拆成库
真实项目中,通常不会把所有 .cpp 都塞进一个 add_executable 里。更常见的是一部分代码形成库,main.cpp 只负责调用库
library_project/
├── CMakeLists.txt
├── include/
│ └── math_utils.h
└── src/
├── main.cpp
└── math_utils.cpp
include/math_utils.h:
cpp
#pragma once
int add(int a, int b);
src/math_utils.cpp:
cpp
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
src/main.cpp:
cpp
#include "math_utils.h"
#include <iostream>
int main() {
std::cout << add(3, 4) << std::endl;
return 0;
}
此时的 CMakeLists.txt 为:
cmake
cmake_minimum_required(VERSION 3.16)
project(LibraryProject LANGUAGES CXX)
add_library(math_utils
src/math_utils.cpp
)
target_include_directories(math_utils PUBLIC
${PROJECT_SOURCE_DIR}/include
)
add_executable(app
src/main.cpp
)
target_link_libraries(app PRIVATE
math_utils
)
这里有两个 target:
cmake
add_library(math_utils src/math_utils.cpp)
创建一个库 target,名字是 math_utils。
cmake
add_executable(app src/main.cpp)
创建一个可执行文件 target,名字是 app。
之后:
cmake
target_link_libraries(app PRIVATE math_utils)
表示 app 依赖 math_utils,所以链接 app 时要把 math_utils 链接进去。
由于 math_utils 的 include 目录是 PUBLIC:
cmake
target_include_directories(math_utils PUBLIC
${PROJECT_SOURCE_DIR}/include
)
所以当 app 链接 math_utils 时,app 也自动获得了 math_utils 的头文件搜索路径。
静态库和动态库
静态库 就是在链接阶段被合并进最终可执行程序的库。如:
cmake
add_library(math_utils STATIC
src/math_utils.cpp
)
程序 app 运行时,不需要再去找 math_utils 这个库文件,因为库里的代码已经被合进 app 了。
动态库 不是在链接阶段完全合进程序,而是在程序运行时被加载。写法是
cmake
add_library(math_utils SHARED
src/math_utils.cpp
)
最终程序 app 运行时,系统需要能找到这个动态库文件。如果找不到,就可能出现"缺少 dll""cannot open shared object file"之类的错误。
如果不写 STATIC 或 SHARED,CMake 会根据变量 BUILD_SHARED_LIBS 决定生成静态库还是动态库。
2. 构建目标 target
在 CMake 中,target 可以理解为一个构建目标。所有东西都围绕 target。app 需要什么头文件、需要什么 C++ 标准、需要什么宏、需要链接什么库,都写成:
cmake
target_xxx(app ...)
下面这个 calculator 就是一个 target:
cmake
add_executable(calculator
src/main.cpp
src/add.cpp
)
可以给这个 target 添加头文件目录:
cmake
target_include_directories(calculator
PRIVATE
${PROJECT_SOURCE_DIR}/include
)
还可以给它添加 C++ 标准:
cmake
target_compile_features(calculator
PRIVATE
cxx_std_17
)
也可以添加编译警告:
cmake
target_compile_options(calculator
PRIVATE
-Wall
-Wextra
)
链接库:
cmake
target_link_libraries(calculator
PRIVATE
some_library
)
这是 cmake 的一般写法,先创建 target,再给 target 添加属性
2.1 PRIVATE、PUBLIC 和 INTERFACE
在 CMake 中:
PRIVATE 表示只有我自己用,别人不需要。
PUBLIC 表示自己要用,链接我的人也要用。
INTERFACE 表示自己不用,链接我的人要用。
| 自己需要 | 使用者需要 | 写法 |
|---|---|---|
| 是 | 否 | PRIVATE |
| 是 | 是 | PUBLIC |
| 否 | 是 | INTERFACE |
PUBLIC
对于库:
text
math_utils
它的头文件在:
text
include/math_utils.h
它的源文件:
cpp
#include "math_utils.h"
别的程序也会:
cpp
#include "math_utils.h"
那么 include 目录应该写成:
cmake
target_include_directories(math_utils PUBLIC include)
因为math_utils 自己编译时需要 include;使用 math_utils 的 app 也需要 include。所以是 PUBLIC。
PRIVATE
如果某个 include 目录只给库内部 .cpp 使用,外部用户完全不需要知道:
library_project/
├── include/
│ └── public_api.h
├── src/
│ ├── internal_impl.h
│ └── internal_impl.cpp
则可以写:
cmake
target_include_directories(my_lib
PUBLIC
${PROJECT_SOURCE_DIR}/include
PRIVATE
${PROJECT_SOURCE_DIR}/src
)
意思是:include 是公共头文件目录,别人也要用;src 是内部实现目录,只有库自己用。
INTERFACE
有些库只有头文件,没有 .cpp,比如模板库、纯头文件工具库。
项目结构:
header_only_project/
├── CMakeLists.txt
├── include/
│ └── logger.h
└── src/
└── main.cpp
logger.h:
cpp
#pragma once
#include <iostream>
inline void log_message(const char* msg) {
std::cout << msg << std::endl;
}
CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.16)
project(HeaderOnlyProject LANGUAGES CXX)
add_library(logger INTERFACE)
target_include_directories(logger INTERFACE
${PROJECT_SOURCE_DIR}/include
)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE logger)
logger 没有自己的 .cpp 要编译,它自己不需要 include 目录参与编译。但是使用它的 app 需要这个 include 目录。
2.2 编译选项和宏定义
编译选项就是传给编译器的参数。
例如:
cmake
target_compile_options(app PRIVATE
-Wall
-Wextra
-Wpedantic
)
意思是:
cmake
-Wall 开启常见警告
-Wextra 开启更多额外警告
-Wpedantic 更严格地按照标准检查
CMake 最终生成的编译命令中,会自动带上这些参数
编译选项不是 CMake 自己定义的,而是具体编译器定义的。这些选项是 GCC/Clang 风格,在 MSVC 下不一定适用。
更严谨的写法是:
cmake
if(MSVC)
target_compile_options(app PRIVATE /W4)
else()
target_compile_options(app PRIVATE -Wall -Wextra -Wpedantic)
endif()
如果当前使用的是 MSVC 编译器,就使用 /W4;否则使用 GCC/Clang 风格的 -Wall -Wextra -Wpedantic。
宏定义是给 C/C++ 预处理器看的开关
C++ 代码:
cpp
#include <iostream>
int main() {
#ifdef USE_FAST_MODE
std::cout << "Fast mode enabled" << std::endl;
#else
std::cout << "Normal mode" << std::endl;
#endif
return 0;
}
如果 CMake 中定义了:
cmake
target_compile_definitions(app PRIVATE USE_FAST_MODE)
程序会编译成:Fast mode enabled;如果没有定义这个宏,则会编译成:Normal mode
如果要定义带值的宏:
cpp
#include <iostream>
int main() {
std::cout << "Version: " << VERSION << std::endl;
return 0;
}
CMake 中可以写:
cmake
target_compile_definitions(app PRIVATE VERSION="1.0.0")
3. 工程化配置
这一章解决真实项目中常见的配置问题:
3.1 Debug 和 Release
同一份 C++ 代码,可以有很多种编译方式。最常见的就是:Debug、Release
我们写了一个程序,程序运行后崩溃了。这时候最关心的是:程序崩在哪里?变量是多少?调用栈是什么?为了方便调试,编译器需要:保留变量信息、行号,让调试器容易追踪代码。构建方式就是Debug
但是Debug 程序通常更大、更慢,因为它保留了大量调试信息。真正发布程序时,用户不关心源码行号,编译器会开启优化、删除无用代码、减少调试信息,构建方式就是Release
CMake 里,构建类型通通过 CMAKE_BUILD_TYPE 控制。例如:
cmake
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
意思是生成 Debug 配置的构建系统。然后
cmake
cmake --build build
真正执行编译。
单配置生成器
CMake 本身不负责编译;它只是生成构建系统。在 Linux 中,通常是单配置生成器。一个 build 目录,只对应一种配置。
配置 Debug:
bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
配置 Release:
bash
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
常见构建类型:
- Debug 带调试信息,优化较少
- Release 优化较多,适合发布
- RelWithDebInfo 优化 + 调试信息
- MinSizeRel 尽量减小体积
多配置生成器
Visual Studio 是多配置生成器。配置时不一定指定 CMAKE_BUILD_TYPE,而是在 build 时指定:
bash
cmake -S . -B build
cmake --build build --config Release
3.2 变量、option 和缓存
项目变复杂以后,我们经常会遇到一些问题,源文件很多,每次都写一长串不方便;有些配置希望第一次设置后,下一次还能记住;Debug / Release、是否构建测试、是否启用示例程序,都需要配置,CMake 提供了几类常见配置方式:
- 普通变量:set(...)
- 用户开关:option(...)
- 缓存变量:CMakeCache.txt
普通变量
cmake
set(SRC_FILES
src/main.cpp
src/print.cpp
)
add_executable(app ${SRC_FILES})
这里定义了一个变量:变量名:SRC_FILES,变量值:src/main.cpp 和 src/print.cpp
后面可以这样使用:
cmake
add_executable(app
${SRC_FILES}
)
其中:${SRC_FILES}表示取出变量 SRC_FILES 的值。对于小项目,直接写源文件列表往往更清晰:
option
option 用来给用户提供开关。
cmake
option(ENABLE_TESTS "Build tests" ON)
定义一个名为 ENABLE_TESTS 的选项;它的说明文字是 Build tests;默认值是 ON。
使用这个开关:
cmake
if(ENABLE_TESTS)
add_subdirectory(tests)
endif()
如果 ENABLE_TESTS 是 ON,就进入 tests 目录,构建测试代码。如果 ENABLE_TESTS 是 OFF,就跳过 tests 目录。
完整例子:
cmake
cmake_minimum_required(VERSION 3.16)
project(option_demo LANGUAGES CXX)
add_executable(app
src/main.cpp
)
option(ENABLE_TESTS "Build tests" ON)
if(ENABLE_TESTS)
add_subdirectory(tests)
endif()
这样用户就可以决定要不要构建测试。不想构建测试,可以写:
cmake
cmake -S . -B build -DENABLE_TESTS=OFF
这里的:-DENABLE_TESTS=OFF意思是:在配置项目时,把 ENABLE_TESTS 设置为 OFF。
CMakeCache.txt
当执行:
cmake
cmake -S . -B build -DENABLE_TESTS=OFF
CMake 会在 build 目录中生成一个文件:build/CMakeCache.txt,这个文件就是 CMake 的缓存文件。它会记录很多配置结果,例如:
ENABLE_TESTS:BOOL=OFF
CMAKE_BUILD_TYPE:STRING=Debug
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++
也就是说,CMakeCache.txt 会记住:
- 用户通过 -D 传入的配置
- CMake 自动检测到的编译器路径
- 一些项目配置选项
- 某些第三方库查找结果
避免每次重新配置时都从零开始;让上一次配置结果可以继续使用。
下次再运行:
bash
cmake -S . -B build
它可能还记得之前的值。如果配置很乱,最简单的重置方式是删除 build:
bash
rm -rf build
然后重新配置。
3.3 接入第三方库:find_package
真实项目经常需要使用第三方库,例如 OpenCV、Eigen、fmt、Boost、Qt 等。基本形式为
cmake
find_package(SomeLib REQUIRED)
在系统中找到 SomeLib,并加载它的 CMake 配置。REQUIRED表示这个库必须存在;找不到就直接报错停止
我们自己的库 add_library(math_utils ...) 会产生一个 target:math_utils,第三方库也一样。例如:
cmake
find_package(fmt REQUIRED)
之后,fmt 会提供 fmt::fmt 这个 target。可以直接:
cmake
target_link_libraries(app PRIVATE
fmt::fmt
)
库名::目标名 是现代 CMake 常见的风格,它的意义类似 命名空间::对象。表面上只是"链接 fmt",实际上 CMake 自动知道 fmt 的 include 路径、库文件路径、宏定义、依赖库、Debug / Release 区别、平台差异等信息
早期很多库还没有现代 target 风格,比如OpenCV,它通过变量暴露信息
cmake
find_package(OpenCV REQUIRED)
target_link_libraries(app PRIVATE
${OpenCV_LIBS}
)
3.4 安装规则 install
当项目不仅仅是自己运行,还要被别人使用时,或要发布 SDK 给其他项目使用,CMake 提供了安装规则 install(...),用于描述最终应该把哪些文件安装到哪里
假设我们写了一个库:my_project_core,别人也想使用,如果没有 install,别人必须手动去 build 目录里找头文件在哪、库在哪。而 install 的目标就是:把最终需要的文件整理成标准结构。
例如:
install/
├── bin/
├── lib/
└── include/
程序放 bin、库放 lib、头文件放 include,别人就知道怎么使用了。
安装可执行文件
假设项目:
my_project/
├── CMakeLists.txt
└── src/
└── main.cpp
CMakeLists.txt:
cmake
add_executable(my_app
src/main.cpp
)
想要安装这个程序,可以写:
cmake
install(TARGETS my_app
RUNTIME DESTINATION bin
)
这里 TARGETS my_app 表示安装 my_app 这个 target。RUNTIME DESTINATION bin 表示把可执行文件安装到 bin 目录。
真正执行 install 需要先 build:
cmake
cmake -S . -B build
cmake --build build
然后执行:
cmake
cmake --install build --prefix install
这里 --prefix install 意思是:把安装目录设置为 ./install。最终得到:
install/
└── bin/
└── my_app
安装库
假设:
cmake
add_library(my_project_core STATIC
src/core.cpp
)
可以写:
cmake
install(TARGETS my_project_core
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
含义:
ARCHIVE:静态库,例如 .a、.lib,安装到 lib 目录
LIBRARY:动态库主体,例如 .so、.dylib,放到 lib 目录
RUNTIME:可执行文件或 Windows 下的 .dll,放 bin
安装头文件
cmake
install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/
DESTINATION include
)
这里DIRECTORY表示安装整个目录。DESTINATION include表示复制到 install/include
例如:
include/
└── my_project/
└── core.h
最终安装后:
install/
└── include/
└── my_project/
└── core.h
3.5 CMake Presets
项目越来越大,这些命令越来越长,越来越难记,团队里每个人的配置也可能不一样。如何配置项目本身也变成了一个需要管理的问题。CMake Presets是 CMake 提供的一种标准化配置方案。
它的意思很简单,把配置方案保存下来。
保存在 CMakePresets.json,它把一长串 cmake 命令参数保存成一个有名字的配置。
例如:
debug
release
clang
gcc
windows
linux
arm
以后不再需要记:
cmake
-DCMAKE_BUILD_TYPE=Release
-DENABLE_TESTS=OFF
...
而是:
cmake
cmake --preset release
这会简单很多。假设项目根目录:
my_project/
├── CMakeLists.txt
└── CMakePresets.json
创建:CMakePresets.json
json
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
}
]
}
这里configurePresets 就是这一阶段的配置,generator表示CMake 最终生成哪种构建系统文件
| Generator | 生成什么 |
|---|---|
| Ninja | build.ninja |
| Unix Makefiles | Makefile |
| Visual Studio | VS 工程 |
| Xcode | Xcode 工程 |
binaryDir 表示build 目录放在哪里。这里${sourceDir}是项目源码根目录,最终build/debug会成为这个 preset 对应的构建目录,等价于:
cmake
-B build/debug
cacheVariables 就是平时命令行里的 -D 参数
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"ENABLE_TESTS": "ON"
}
等价于:
-DCMAKE_BUILD_TYPE=Debug
-DENABLE_TESTS=ON
因为这些变量最终都会进入CMakeCache.txt,所以叫cacheVariables
使用 Preset:
cmake
cmake --preset debug
CMake 也允许定义 build 阶段的 preset。例如:
cmake
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
}
]
表示这个 build preset对应 configure preset: debug
然后可以:
cmake
cmake --build --preset debug
而不再需要:
cmake
cmake --build build/debug
一个完整例子
cmake
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"displayName": "Debug Build",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"ENABLE_TESTS": "ON"
}
},
{
"name": "release",
"displayName": "Release Build",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"ENABLE_TESTS": "OFF"
}
}
],
"buildPresets": [
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "release",
"configurePreset": "release"
}
]
}
这里定义了两套完整配置方案。debug preset等价于:
cmake
cmake -S . -B build/debug \
-G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DENABLE_TESTS=ON
release preset等价于:
cmake
cmake -S . -B build/release \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DENABLE_TESTS=OFF
以后只需要:
cmake
cmake --preset debug
cmake --build --preset debug
或者:
cmake
cmake --preset release
cmake --build --preset release
4. 标准多目录项目
当项目变大后,所有内容放在一个 CMakeLists.txt 里会很乱。这时可以使用多目录分层管理,每个子目录可以有自己的 target。
my_project/
├── CMakeLists.txt
├── include/
│ └── my_project/
│ ├── math_utils.h
│ └── signal_filter.h
├── src/
│ ├── CMakeLists.txt
│ ├── math_utils.cpp
│ └── signal_filter.cpp
├── app/
│ ├── CMakeLists.txt
│ └── main.cpp
├── tests/
│ ├── CMakeLists.txt
│ └── test_math_utils.cpp
└── README.md
每个目录的职责比较清楚
include/ 放公共头文件,供库本身和外部程序使用。
src/ 放核心库源码,生成 library target
app/ 放主程序入口,例如 main.cpp
tests/ 放测试代码,生成测试用的 executable target。
CMakeLists.txt 顶层 CMake 文件,只负责组织子目录和全局选项。
公共头文件放在:include/my_project/xxx.h,这样在代码中包含头文件时可以写成:
c
#include <my_project/math_utils.h>
而不是:
c
#include "math_utils.h"
减少头文件重名的风险。比如别的库里也可能有一个 math_utils.h,但是不太可能也叫 my_project/math_utils.h。
4.1 顶层 CMakeLists.txt
顶层 CMakeLists.txt 负责声明项目、定义选项、加入子目录。
cmake
cmake_minimum_required(VERSION 3.16)
project(MyProject
VERSION 1.0.0
DESCRIPTION "A modern CMake example project"
LANGUAGES CXX
)
option(ENABLE_TESTS "Build tests" ON)
add_subdirectory(src)
add_subdirectory(app)
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
cmake
add_subdirectory(src)
表示进入 src 目录,读取 src/CMakeLists.txt。
cmake
add_subdirectory(app)
表示进入 app 目录,读取 app/CMakeLists.txt。
测试部分用条件控制:
cmake
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
意思是:如果 ENABLE_TESTS 为 ON,就启用 CMake 的测试功能,并进入 tests 目录构建测试程序。
4.2 src/CMakeLists.txt
src/ 目录一般用来放项目的核心源码。这里我们把 math_utils.cpp 和 signal_filter.cpp 编译成一个库,名字叫 my_project_core。
cmake
add_library(my_project_core
math_utils.cpp
signal_filter.cpp
)
target_include_directories(my_project_core
PUBLIC
${PROJECT_SOURCE_DIR}/include
)
target_compile_features(my_project_core PUBLIC cxx_std_17)
if(MSVC)
target_compile_options(my_project_core PRIVATE /W4)
else()
target_compile_options(my_project_core PRIVATE -Wall -Wextra -Wpedantic)
endif()
4.3 app/CMakeLists.txt
cmake
add_executable(my_app
main.cpp
)
target_link_libraries(my_app PRIVATE
my_project_core
)
my_app 需要链接 my_project_core
4.4 tests/CMakeLists.txt
测试代码单独放在 tests/ 目录中。
cmake
add_executable(test_math_utils
test_math_utils.cpp
)
target_link_libraries(test_math_utils PRIVATE
my_project_core
)
add_test(NAME test_math_utils COMMAND test_math_utils)
第三句:
cmake
add_test(NAME test_math_utils COMMAND test_math_utils)
表示把 test_math_utils 注册成一个 CTest 测试项。测试名字:test_math_utils;执行命令:test_math_utils
以后运行:
cmake
ctest --test-dir build
CTest 就会执行这个测试程序。如果程序返回 0,CTest 认为测试通过。如果程序返回非 0,CTest 认为测试失败。
4.5 构建并运行整个项目
配置工程:
cmake
cmake -S . -B build -DENABLE_TESTS=ON # 配置工程
cmake --build build # 编译工程
ctest --test-dir build # 运行测试
如果想查看详细测试输出,可以使用:
cmake
ctest --test-dir build --verbose
如果想查看详细编译命令:
cmake
cmake --build build --verbose
如果不想构建测试,可以关闭 ENABLE_TESTS:
cmake
cmake -S . -B build -DENABLE_TESTS=OFF
cmake --build build
这时顶层的这段代码:
cmake
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
就不会执行,tests/ 目录也不会被加入构建系统。
5. CMake 交叉编译
交叉编译是指:编译发生在一个平台上,但最终程序要运行在另一个平台上 。例如做嵌入式 Linux、树莓派、ARM 平台,此时CMake 无法自动判断目标平台,不能简单地从当前系统目录中查找目标平台的头文件和库,而且交叉编译生成的可执行文件通常不能直接在构建主机上运行。因此,需要通过 toolchain 文件告诉 CMake:目标系统是什么、使用哪个编译器、去哪里找目标平台的头文件和库。
toolchain 文件是一个 CMake 脚本,通常命名为:
arm-linux-toolchain.cmake
toolchain-arm.cmake
aarch64-linux-gnu.cmake
它提前告诉 CMake要编译给哪个系统、目标 CPU 架构是什么?、C 编译器、C++ 编译器在哪里、目标平台的 sysroot 在哪里、find_library、find_path、find_package 应该去哪里找等
建立一个最小工程:
cross-demo/
├── CMakeLists.txt
├── main.cpp
└── toolchains/
└── arm-linux-gnueabihf.cmake
main.cpp 内容如下:
CPP
#include <iostream>
int main()
{
std::cout << "Hello cross compile!" << std::endl;
return 0;
}
CMakeLists.txt 内容如下:
CMAKE
cmake_minimum_required(VERSION 3.16)
project(CrossDemo LANGUAGES C CXX)
add_executable(cross_demo main.cpp)
target_compile_features(cross_demo PRIVATE cxx_std_11)
这里没有手动指定 g++,也没有写死编译器路径。CMakeLists.txt 描述的是"这个项目怎么构建",而 toolchain 文件描述的是"用哪套工具链为哪个目标平台构建"。两者职责不同。
在 Linux 环境里,执行:
bash
sudo apt update
sudo apt install gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
安装 Ubuntu 自带的 ARM 交叉编译器,安装完成后检查:
bash
which arm-linux-gnueabihf-gcc
which arm-linux-gnueabihf-g++
输出文件路径:
/usr/bin/arm-linux-gnueabihf-gcc
/usr/bin/arm-linux-gnueabihf-g++

那么可以写一个 toolchains/arm-linux-gnueabihf.cmake:
cmake
# 1. 指定目标系统
set(CMAKE_SYSTEM_NAME Linux)
# 2. 指定目标处理器架构
set(CMAKE_SYSTEM_PROCESSOR arm)
# 3. 指定交叉编译器
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)
# 不要用当前电脑默认的 /usr/bin/gcc 和 /usr/bin/g++,而是用 ARM 交叉编译器。
# 4. 指定目标系统根目录
set(CMAKE_SYSROOT /opt/arm-sysroot)
# 5. 控制 CMake 查找程序、库和头文件的位置
set(CMAKE_FIND_ROOT_PATH /opt/arm-sysroot)
# 构建过程中需要运行的程序,通常应该在宿主机上找
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# 头文件、库、CMake 包,通常在目标系统环境中找
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
这一行:
cmake
set(CMAKE_SYSROOT /opt/arm-sysroot)
sysroot 可以理解成"目标系统根目录的复制品"。里面一般会有目标板上的头文件和库
最后这几行:
cmake
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)
含义是:
find_program找构建过程中要运行的工具,例如 Python、protoc、代码生成器。这些程序要在当前电脑上运行,所以应该从宿主机环境里找。
find_library找目标程序链接用的库。这些库必须属于 ARM 目标平台,所以应该从 sysroot 里找。
find_path / find_file找头文件。头文件也应该来自目标平台 sysroot。
find_package找 CMake 包配置。交叉编译应该优先找目标平台的包。
进入工程目录后,建立单独的构建目录:
cmake
cd cross-demo
cmake -S . -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=toolchains/arm-linux-gnueabihf.cmake \
-DCMAKE_BUILD_TYPE=Release
然后编译:
cmake
cmake --build build-arm
用 file 命令检查:
bash
file build-arm/cross_demo
看到信息:
ELF 32-bit LSB executable, ARM, EABI5, dynamically linked ...

更改 toolchain
修改 toolchain 文件之后,重新执行 cmake --build 不一定会重新识别所有工具链设置。因为 CMake 第一次 configure 时会把很多结果写入 CMakeCache.txt。如果换了工具链、换了编译器、换了 sysroot,稳妥做法是删除构建目录:
bash
rm -rf build-arm
然后重新配置:
cmake
cmake -S . -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=toolchains/arm-linux-gnueabihf.cmake \
-DCMAKE_BUILD_TYPE=Release
不要在同一个 build 目录里反复切换不同 toolchain 文件。这样很容易出现缓存污染。
安装路径
cmake
install(TARGETS cross_demo
RUNTIME DESTINATION bin
)
执行安装
cmake
cmake --install build
会安装 cross_demo 这个可执行文件,并放到安装前缀下面的 bin 目录中。
如果安装前缀是:/usr/local,那么目标运行环境中的安装位置就是/usr/local/bin/cross_demo
交叉编译时,问题会变复杂一点。因为这里至少有两个系统:宿主机与目标机,所以 /usr/local 这个路径必须说清楚到底是谁的 /usr/local。
在交叉编译语境下,CMAKE_INSTALL_PREFIX 表示的是:程序最终在目标设备上的运行安装前缀 。也就是说,即使是在当前电脑上执行 cmake --install,CMAKE_INSTALL_PREFIX=/usr/local 仍然应该理解为目标设备上的 /usr/local,
例如:
cmake
-DCMAKE_INSTALL_PREFIX=/usr/local
表示程序将来放到开发板上时,应该位于:/usr/local/bin/cross_demo
但是,现在执行 cmake --install 的机器是宿主机。如果直接把 ARM 程序安装到宿主机的 /usr/local/bin,通常是不合适的。因为这个程序是给 ARM 开发板用的。这时就可以使用:
cmake
CMAKE_STAGING_PREFIX
它的作用是指定一个宿主机上的临时安装目录。
如果我们希望:宿主机上的临时安装目录是当前工程目录下的 stage-arm/,目标设备上的最终安装前缀:/usr/local,那么可以在配置时这样写:
cmake
cmake -S . -B build-arm \
-DCMAKE_TOOLCHAIN_FILE=toolchains/arm-linux-gnueabihf.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr/local \
-DCMAKE_STAGING_PREFIX="$PWD/stage-arm"
$PWD/stage-arm 的意思是:在当前工程目录下创建一个 stage-arm 目录,用它作为临时安装目录。然后编译:
cmake
cmake --build build-arm
再执行安装:
cmake
cmake --install build-arm
安装完成后,在当前工程目录下看到:
stage-arm/
└── bin/
└── cross_demo
文件实际先被安装到了宿主机的:当前工程目录/stage-arm/bin/cross_demo,但是这个程序在目标设备上的设计安装位置仍然是:/usr/local/bin/cross_demo。这两个路径不是一回事。