欢迎拜访:雾里看山-CSDN博客
本篇主题:第一个 CMake 项目:最小CMakeLists.txt到底写了什么
发布时间:2026.9.3
隶属专栏:CMake
目录
- 这一篇的目标
- 最小的工程长什么样
- 文件结构
- main.cpp
- CMakeLists.txt
- 逐行拆解
- 第一行:cmake_minimum_required
- 第二行:project
- 关于语言的默认值
- 关于 C 和 CXX
- VERSION 字段
- 第三行:add_executable
- 把它跑起来
- 容易踩的几个坑
- 坑 1:文件名大小写
- 坑 2:忘记写 `cmake_minimum_required`
- 坑 3:target 名和源文件同名
- 坑 4:相对路径的理解
- 坑 5:源文件里有 `.c` 又想当 C++ 编
- 加点东西看看效果
- 1. 加多个源文件
- 2. 加头文件目录
- 3. 加 C++ 标准
- 4. 加宏定义
- target 是 CMake 的核心
- 把最小工程再压缩到极致
- 这一篇抓什么
- 总结
这一篇的目标
上一篇跑通了命令行工作流。这一篇就把CMakeLists.txt真正打开看看:一个最小可运行的 CMake 工程到底由哪些行组成,每一行的作用是什么。
虽然只有几行,但里面每一行都有自己的角色。把这几行吃透,后面再加 target、加依赖、装包都不会陌生。
最小的工程长什么样
文件结构
一个最小 CMake 工程通常长这样:
hello/ ├── CMakeLists.txt └── main.cppmain.cpp
先写一个简单的 C++ 程序:
#include<iostream>intmain(){std::cout<<"Hello, CMake!"<<std::endl;return0;}CMakeLists.txt
对应写一个最小版本:
cmake_minimum_required(VERSION 3.15) project(HelloCMake LANGUAGES CXX) add_executable(hello main.cpp)只有三行命令,但它已经是一个完整的、可构建的工程。
逐行拆解
第一行:cmake_minimum_required
cmake_minimum_required(VERSION 3.15)这一行的作用是:声明这个项目至少需要哪个版本的 CMake 才能正常构建。
为什么要写这一行?
CMake 自己也在演进,每个版本会引入新的命令、新的策略(policy)、新的行为。如果项目用到了一些新特性,而对方的 CMake 版本太旧,就可能出问题。
所以:
- 这是兼容性保护,不是“版本号越高越好”
- 选择版本的原则是:用到你需要的特性的最低版本
比如:
- 要用
target_link_libraries(... PRIVATE/PUBLIC/INTERFACE)的现代语义,至少 3.x 任意版本都行 - 要用
FetchContent,需要 3.14+ - 要用
imported targets的标准改进,需要 3.15+ - 要用
CMakePresets.json,需要 3.19+
如果团队用的 CMake 都在 3.20 以上,那直接写 3.20 也没问题。
另外,cmake_minimum_required必须是CMakeLists.txt中的第一行有效命令(注释和空行不算),否则 CMake 会报错。
第二行:project
project(HelloCMake LANGUAGES CXX)这一行做几件事:
- 给项目取一个名字
HelloCMake - 声明这个项目使用的语言:
LANGUAGES CXX表示只用 C++ - 同时,CMake 会自动给一些变量赋值:
PROJECT_NAME→HelloCMakeCMAKE_PROJECT_NAME→ 顶层项目名PROJECT_SOURCE_DIR、PROJECT_BINARY_DIR等
关于语言的默认值
如果不写LANGUAGES,CMake 默认是空,也就是说不会自动启用任何语言的检查。
但很多人会看到这种写法:
project(HelloCMake CXX)这种是不带关键字的写法,靠位置参数识别。两种写法等价:
project(HelloCMake CXX) project(HelloCMake LANGUAGES CXX)推荐带LANGUAGES关键字,更清楚。
关于 C 和 CXX
如果工程同时有 C 和 C++:
project(MyApp LANGUAGES C CXX)如果只有 C:
project(MyApp LANGUAGES C)VERSION 字段
project还可以接受一个版本号:
project(HelloCMake VERSION 1.0.0 LANGUAGES CXX)它会顺带设置:
PROJECT_VERSION→1.0.0PROJECT_VERSION_MAJOR→1PROJECT_VERSION_MINOR→0PROJECT_VERSION_PATCH→0
后面做打包、安装、版本头文件生成时经常会用到这些变量。
第三行:add_executable
add_executable(hello main.cpp)这一行是 CMake 中最关键的一行之一,作用是:声明一个可执行文件目标。
把它拆开看:
hello:target 名(目标名),也是最终生成的产物名(在 Linux 上是hello,Windows 上是hello.exe)main.cpp:构成这个可执行文件的源文件列表
执行完这一行之后,CMake 内部就建立了一个名为hello的 target,后续可以围绕它做很多事情:
- 链接库
- 加头文件目录
- 加编译选项
- 加宏定义
不过这些是后面的内容,这一篇先建立“target 是 CMake 里的第一公民”这个概念。
把它跑起来
假设工程根目录是hello/,执行:
cdhello cmake-S.-Bbuild-GNinja cmake--buildbuild执行完后:
hello/ ├── CMakeLists.txt ├── main.cpp └── build/ ├── build.ninja └── hello (可执行文件)运行:
./build/hello输出:
Hello, CMake!至此,一个最小可运行 CMake 工程就跑通了。
容易踩的几个坑
坑 1:文件名大小写
在 Linux/macOS 上:
CMakeLists.txt必须大小写完全一致CMakeLists.txt不是cmakeLists.txt,也不是CMakelists.txt
写错大小写,CMake 直接报错:Could not find file: ... CMakeLists.txt。
坑 2:忘记写cmake_minimum_required
没写时,CMake 会用一个非常老的默认行为,很多现代写法会出问题。
所以:任何CMakeLists.txt都应该先写这一行。
坑 3:target 名和源文件同名
下面这种写法是错的:
add_executable(main.cpp main.cpp)target 名和文件名重复会导致 CMake 把第一个参数当成 target,源文件列表变成(main.cpp)(一个空名字 + 一个 main.cpp),逻辑混乱。
正确写法:
add_executable(my_app main.cpp)或者:
add_executable(${PROJECT_NAME} main.cpp)坑 4:相对路径的理解
add_executable(hello main.cpp)中的main.cpp是相对于当前CMakeLists.txt所在目录的,而不是当前执行cmake命令时的目录。
所以无论你在哪里调用cmake -S,源文件路径都是从CMakeLists.txt自身算起的。这一点在后面多目录工程里尤其重要。
坑 5:源文件里有.c又想当 C++ 编
如果main.cpp实际写的是 C 代码,CMake 默认会按CXX(C++)处理,可能出现奇怪错误。这种情况要么:
- 把扩展名改成
.c,并在project里加LANGUAGES C - 或者明确加
set_source_files_properties(main.cpp PROPERTIES LANGUAGE CXX)
加点东西看看效果
下面在最小工程基础上,做一些最常见的扩展,方便感受 CMake 描述能力。
1. 加多个源文件
cmake_minimum_required(VERSION 3.15) project(HelloCMake LANGUAGES CXX) add_executable(hello main.cpp greet.cpp )CMake 会把它们一起编译进同一个可执行文件。
2. 加头文件目录
cmake_minimum_required(VERSION 3.15) project(HelloCMake LANGUAGES CXX) add_executable(hello main.cpp) target_include_directories(hello PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)这时:
hello/ ├── CMakeLists.txt ├── main.cpp ├── greet.cpp └── include/ └── greet.hgreet.cpp就可以这样写:
#include"greet.h"#include<iostream>voidgreet(conststd::string&name){std::cout<<"Hello, "<<name<<"!"<<std::endl;}3. 加 C++ 标准
cmake_minimum_required(VERSION 3.15) project(HelloCMake LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_executable(hello main.cpp)或者更推荐的做法是写到 target 上:
cmake_minimum_required(VERSION 3.15) project(HelloCMake LANGUAGES CXX) add_executable(hello main.cpp) target_compile_features(hello PRIVATE cxx_std_17)这两个写法效果类似,后者更符合 target-first 的思想。
4. 加宏定义
target_compile_definitions(hello PRIVATE USE_FANCY_LOG=1)源码里就可以:
#ifdefUSE_FANCY_LOGstd::cout<<"[FANCY] Hello, CMake!"<<std::endl;#elsestd::cout<<"Hello, CMake!"<<std::endl;#endiftarget 是 CMake 的核心
这一节很重要,因为它决定了你后面写 CMake 的风格。
add_executable创建了一个 targethello。从此以后:
- 头文件目录挂到
hello上 - 宏定义挂到
hello上 - 编译选项挂到
hello上 - 依赖的库挂到
hello上
所有内容都围绕 target 转,这是现代 CMake 的核心思维。
与之相对的是“旧式 CMake”:
# 旧式:到处用全局变量 include_directories(include) add_compile_options(-Wall) add_definitions(USE_FANCY_LOG=1) add_executable(hello main.cpp) target_link_libraries(hello some_lib)这种写法会让所有 target 都共享这些设置,互相污染,所以现代 CMake 强烈不推荐。
记住一句话:target 是第一公民,全局命令少用。
把最小工程再压缩到极致
如果只允许保留三行,下面这三行就是一个最小 CMake 工程:
cmake_minimum_required(VERSION 3.15) project(Min LANGUAGES CXX) add_executable(min main.cpp)跑一下:
cmake-S.-Bbuild-GNinja cmake--buildbuild ./build/min你会发现,CMake 工程其实没有想象中复杂。
这一篇抓什么
这一篇真正要带走的,是这几点:
CMakeLists.txt最小有三行:cmake_minimum_required、project、add_executablecmake_minimum_required是兼容性护栏,必须写在最前面project命名项目,并设置语言和(可选)版本号add_executable创建 target,是现代 CMake 的起点- target 是 CMake 的第一公民,所有构建属性最终挂到 target 上
只要这三行命令的作用清楚了,后面学target_include_directories、target_link_libraries就非常自然。
总结
第一个 CMake 项目这一篇真正要抓住的,是这一组主线:
cmake_minimum_required设定最低版本project命名项目、声明语言add_executable创建可执行文件 target- target 是 CMake 一切行为的起点
- 最小工程只需要这三行
下一篇会讲源码目录、构建目录与 out-of-source build,把“源码”和“构建产物”为什么要分开这件事说清楚。
⚠️ 写在最后:以上内容是我在学习以后得一些总结和概括,如有错误或者需要补充的地方欢迎各位大佬评论或者私信我交流!!!