CMake 入门实战手册:从理解原理开始,打造高效 C/C++ 开发流程
文章目录
- 一.什么是CMake?
- 二.CMake快速开始
- 2.1CMake安装
- 2.2Visual Studio Code CMake插件安装
- 2.3HelloWorld快速搭建
- 三.CMake 命令行工具介绍
- 3.1 CMake 工程构建流程图
- 3.2 生成构建系统
- 3.3 编译链接
- 3.4 测试
- 3.5 本地安装
- 3.6 打包
- 3.7 脚本构建
- 3.8 调用外部命令
- 四.CMake工程实践场景
- 4.1 可执行文件(编译-链接-安装)
- 4.1.1单步操作:
- 目录结构:
- 新建文件-CMakeLists.txt:
- 新建文件main.cc:
- 运行CMake:
- 4.1.2 重点命令解释:
- cmake_minimum_required
- project
- include
- install
- add_executable
- 4.2 静态库(编译-链接-引用)
- 4.2.1单步操作:
- 目录结构:
- 新建文件CMakeLists.txt
- 新建文件mylib/CMakeLists.txt
- 新建文件app/CMakeLists.txt
- 新建文件math.h
- 新建文件add.cpp/sub.cpp/main.cpp
- 运行CMake
- 4.2.2 重点命令解释:
- Target
- Property
- add_library
- target_include_directories
- target_link_libraries
- set_target_properties和 [get_target_properties](https://cmake.org/cmake/help/latest/command/get_target_property.html)
- add_subdirectory
- file
一.什么是CMake?
让我们先来回顾一下传统开发平台(windows/Linux)是如何编译代码的:
- Linux 平台:源代码需先 “手写 Makefile” 来生成构建配置文件 Makefile,再通过 make 命令执行编译构建。
- Windows 平台:源代码依赖 “工程构建属性” 完成配置,随后通过 Visual Studio 等工具(涉及 “生成解决方案” 等操作)实现编译构建。
传统跨平台构建的缺点显而易见:跨平台场景下,要手动为每个平台适配对应的构建配置文件(如 Linux 的 Makefile、Windows 的工程属性),适配成本高。且Makefile 语法复杂,对于中大型项目,纯手写 Makefile 几乎难以实现。
回到问题,CMake由此诞生:
一款开源、跨平台的自动化构建系统,生成 “适配不同平台 / 工具的原生构建文件”,让开发者用一套配置(CMakeLists.txt),就能在多系统(Linux、Windows、macOS 等)、多工具链(Make、Visual Studio、Ninja、Xcode 等)下完成项目编译,在跨平台开发无需手动适配各平台构建规则”。
CMake 不直接编译代码:开发者编写 CMakeLists.txt(存储项目结构、编译规则、依赖等配置);CMake 根据 CMakeLists.txt,为目标平台 / 工具生成原生构建文件(如 Linux 下的 Makefile、Windows 下的 Visual Studio 工程文件、Ninja 构建配置文件等);再由这些原生构建文件调用编译器(如 gcc、MSVC 等),完成实际编译。
CMake具有以下优点:
- 跨平台兼容:支持 Linux、Windows、macOS,一套配置可在多系统复用。
- 生成器机制:能生成多种工具的构建文件(Makefile、Visual Studio .sln/.vcxproj、Ninja 配置等),开发者可沿用熟悉的工具链。
- 工程化能力丰富:支持 “out-of-source 构建”(编译产物与源码分离,方便多版本并行构建);
- 内置 CTest(跨平台测试系统,可自动运行、并行测试)、CPack(跨平台打包工具,生成 Linux/Windows/macOS 安装包);
- 采用 “目标(Target)中心” 的构建方式(将可执行文件、库文件等定义为 “目标”,清晰管理依赖与链接)。
主要优势作者已经放进表格里啦:
优势 | 传统方式 | CMake 方式 | 改进效果 |
---|---|---|---|
解决跨平台构建难题 | 人工编辑 Makefile 等配置文件 | CMake 自动生成构建配置文件 | 一处配置,到处构建 |
语法简单易上手 | Makefile 等语法复杂 | 语法简单,表达能力强大 | 大幅减少学习成本,提升研发效率 |
解决包管理难题 | 手动查找包 | 自动查找包 | 包管理规范化 |
IDE 对 CMake 支持度高 | 每个 IDE 都有自己的构建方式 | 各个 IDE 都支持使用 CMake 来构建程序 | 一处配置,多 IDE 支持 |
二.CMake快速开始
2.1CMake安装
请注意下面测试环境为Visual Studio Code+Ubunto 24.04
关于CMake的源代码和使用文档在下面啦:
CMake 官方源代码下载
CMake 官方英文文档
在Ubunto的主机中安装CMake:
- 先更新软件包列表。打开终端,执行以下命令更新系统的软件源信息,确保能获取到最新的 CMake 版本:
sudo apt update
- 直接通过 apt 包管理器安装 CMake:
sudo apt install cmake
如果需要安装 CMake 的额外组件(如开发文档、测试工具等),可以安装扩展包:
sudo apt install cmake cmake-doc cmake-extras
- 安装完成后,通过以下命令查看 CMake 版本,确认安装成功:
cmake --version
如果输出类似以下内容,说明安装成功:
cmake version 3.22.1 # 版本号可能因系统更新而不同
CMake suite maintained and supported by Kitware (kitware.com/cmake).
2.2Visual Studio Code CMake插件安装
作者将插件官方文档放在下面啦,有兴趣可以了解一下:
VS Code CMake 插件官方文档
在VS code中CMake插件功能包括:
- 语法高亮和代码补全:对 CMakeLists.txt 文件提供语法高亮显示,使代码结构更加清晰易读。同时,支持代码补全功能,当你输入 CMake 命令或变量时,插件会自动提示可能的选项,减少手动输入的错误和时间。
- 智能分析和错误检查:能够对 CMakeLists.txt 文件进行智能分析,检查其中的语法错误和潜在问题,并在编辑器中实时显示错误提示和警告信息,帮助你及时发现和解决问题。
在Windows的vs code中安装CMake插件:
- 打开 VS Code,点击左侧活动栏中的扩展图标::
- 在搜索框中输入 CMake ,选择安装以下4个插件: CMake、CMake Tools、CMake Language Support、CMake IntelliSence
2.3HelloWorld快速搭建
新建一个文件夹,在Ubunto主机中使用CMake编译打印hellowrold的程序:
//目录结构
testCMake/
├── CMakeLists.txt
└── main.cc
- 创建main.cc文件,写好程序:
- 创建CMakeLists.txt文件,注意不要写错了名字! 在此文件中设置三行内容:
如果项目中使用了高版本 CMake 才支持的特性(例如特定的函数、生成器表达式、目标属性等),而用户本地安装的cmake版本低于项目要求的版本,就会出现无法解释或者产生不可预知的行为,其次要设置要生成的项目名称以及可执行程序名称(main main.cc代表由main.cc生成main可执行程序) - 运行CMake:
- 选择在当前路径下cmake:
cmake .
如果生成了cmake_install.cmake文件夹,以及CMakeCache.txt说明生成成功
- 现在惊喜的发现目录中多出了makefile文件,这说明cmake自动帮我们生成了需要我们手写的makefile文件,直接make即可
make
- 运行main程序
./main
三.CMake 命令行工具介绍
3.1 CMake 工程构建流程图
一个项目通常经过以下流程:
- 代码编写(IDE 阶段)
使用集成开发环境(IDE)编写工程代码,并创建 CMakeLists.txt(CMake 的配置文件,描述项目的编译规则、依赖等)。 - CMake 配置阶段
第一个 CMake 环节为配置阶段:根据 CMakeLists.txt 和工程代码,分析项目的目标(可执行程序、库)与依赖关系(如依赖的第三方库、编译选项等)。 - CMake 生成阶段
第二个 CMake 环节为生成阶段:基于配置结果,生成 Makefile(make 工具的编译规则文件,包含编译、链接的具体指令)。 - make 编译链接
make 工具读取 Makefile,执行编译(将源码转为目标文件)和链接(将目标文件组合为可执行文件 / 库),最终生成 exe(可执行文件)或 so(动态库)等产物。 - CTest 测试
使用 CTest 工具,对编译生成的 exe/so 进行自动化测试,验证程序功能是否符合预期。 - install 本机安装
通过 install 操作,将测试通过的 exe/so 安装到本地系统的指定目录(如系统库路径、程序安装路径)。 - CPack 打包分发
使用 CPack 工具,将已安装的文件打包为 tar/zip 等格式的压缩包,以便进行网络分发(如发布到软件仓库、供其他用户下载部署)。
现在根据这个流程,进一步修改我们的main.cc,test.cc:
//main.cc
#include<iostream>
int main()
{std::cout << "Hello, World!" << std::endl;return 0;
}
//test.cc
#include<assert.h>
#include<iostream>int main()
{assert(1 == 1);std::cout << "All tests passed!" << std::endl;return 0;
}
进一步修改CMakeLists.txt:(非常重要,后续命令行都基于这个文件!)
#先来设置最低版本
cmake_minimum_required(VERSION 3.18)
#设置项目名称
project(testCMake)
#生成可执行文件
add_executable(main main.cc)
add_executable(testing test.cc)
#开启测试功能
include(CTest)
add_test(NAME mytest #指定测试名称COMMAND testing #指定测试可执行文件
)
#本地安装
include(GNUInstallDirs) #GNU推荐的安装目录变量
install(TARGETS main) #安装可执行文件
#打包
include(CPack) #包含CPack模块以启用打包功能
3.2 生成构建系统
这里可以采用三种方式:
cmake [options] <path-to-source>
将当前工作目录作为「构建树」, 作为「源码树」(源码树必须包含 CMakeLists.txt,且不能存在 CMakeCache.txt—— 否则会被识别为 “已有构建树”)。
mkdir build && cd build
cmake ../src # 构建树是当前`build`目录,源码树是`../src`
cmake [options] <path-to-existing-build>
将<path-to-existing-build>
作为「构建树」,并从该目录的 CMakeCache.txt 中加载源码树的路径(因此该构建目录必须是 “之前 CMake 运行过、已生成 CMakeCache.txt” 的目录)。
cd build
cmake . # 构建树是当前`build`目录,源码树从`build/CMakeCache.txt`加载
cmake [options] -S <path-to-source> -B <path-to-build>
通过 -S 明确指定「源码树」(需包含 CMakeLists.txt),通过 -B 明确指定「构建树」(通常包含一个CMakeCache.txt)。
cmake -S src -B build # 源码树是`src`,构建树是`build`(不存在则创建)
源内构建:在源代码树包含的顶级CMakeLists.txt的目录下进行直接构建;
源外构建:使用 -B 参数单独指定一个build 目录,然后在子目录里制定源文件目录也就是包含CMakeLists.txt的目录。源码与构建目录分离的规范用法,能让源码目录保持 “干净”(无构建生成的临时文件),构建产物集中在指定目录,便于管理,是现代推荐的用法
3.3 编译链接
构建完毕系统之后,就可以进行编译链接了,使用以下命令:
cmake --build ./
#或者
make
对于make
:由于cmake之后会生成makefile文件,所以make就可以直接完成编译链接;对于cmake --build
:输入命令后其实会执行一个软连接:
//Path to a program.
CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake #这是CMakeCache.txt中命令对应的目录
将其打印出来,就是make:
而build命令代表在当前目录生成项目:
3.4 测试
对程序进行测试,加上之前写好的testing函数,可以使用以下命令:
ctest
#或者
make test
如果测试通过:
如果测试未通过:
可以去目录testCMake/build/Testing/Temporary/LastTest.log中查看错误信息:
看看makefile文件中对应的make test命令:
# Special rule for the target test
test:@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running tests..."/snap/cmake/1481/bin/ctest $(ARGS)
.PHONY : test
其实make test本质就是ctest命令,ctest会去CTestTestfile.txt文件将包含的文件全部执行一遍:
# This file includes the relevant testing commands required for
# testing this directory and lists subdirectories to be tested as well.
add_test(mytest "/home/drw1/linux/testCMake/build/testing")
set_tests_properties(mytest PROPERTIES _BACKTRACE_TRIPLES "/home/drw1/linux/testCMake/CMakeLists.txt;10;add_test;/home/drw1/linux/testCMake/CMakeLists.txt;0;")
3.5 本地安装
在单元测试通过之后,我们可以使用cmake的安装命令把库和二进制发布到本机的标准路径,供开发使用。如果cmake的配置文件CMakeLists.txt里包含install函数,则生成的makefile里也会包含install伪目标,可以使用make来执行。
cmake --install .
#或者
make install
说说安装的目的:main函数原先是在自己的用户目录下,使用这台主机的其他人访问不到,如果安装到bin目录下便是公共目录,他人可以访问到。
看看makefile文件中对应的make install命令:
# Special rule for the target install
install: preinstall@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Install the project..."/snap/cmake/1481/bin/cmake -P cmake_install.cmake
.PHONY : install
其实make install本质就是cmake --install命令,会去cmake_install.cmake文件由file命令将包含的文件全部安装一遍:
其中INSTALL DESTINATION是文件的安装目的地,是由include(GNUInstallDirs)设置的,这在CMakeLists.txt文件撰写时提过。
安装完成之后还会生成一个install_manifest.txt文件,其中包含了已经安装的文件目录清单,可以查看哪些文件已经被安装,是否有遗漏。
3.6 打包
将程序进行打包可以使用cpack 把二进制或者动态库打包成压缩包的方式进行分发和共享。如果cmake的配置文件CMakeLists.txt里包含CPack功能,则生成的makefile里也会包含package 伪目标,可以使用make来执行:
cpack
#或者
make package
欸?这里为什么都有一个报错? 仔细查看报错信息,/home/drw1/linux/testCMake/build/cmake_install.cmake:80 (file): file failed to open for writing (Permission denied):是这个文件权限不够,打开失败,查看cmake_install.cmake:80行的内容:
原来是要对install_manifest.txt这个文件进行写入,那么带上sudo试试:
成功了,可以注意到build目录下生成了新的目录:_CPack_Packages,并且install_manifest.txt中有了内容:
与make install一样是已经安装的列表清单,证明这个这个路径下已经生成了打包文件!
这些都与CPack的执行步骤有关:
- 设置临时安装目录;
- 执行cmake_install.cmake;
- 收集临时目录的文件列表;
- 执行打包并拷贝压缩包到构建目录
其实cpack也是一种install,只不过install默认路径是/usr/local,而这里的cpack安装在临时目录将压缩包预览看看是不是目标程序:
同时build目录下有三个压缩文件生成:(因为先执行打包再拷贝到构建目录)
为什么是这三种类型的包以及为什么是这个名称?在CPackConfig.cmake配置文件可以查看:
3.7 脚本构建
CMake脚本模式不会生成构建产物,也不会生成中间过程。适合处理各种与构建系统无关的自
动化任务,通过编写简洁的脚本文件,你可以实现环境检查、文件处理、部署打包等功能。
通过 cmake -P <脚本文件> 执行脚本
#cmake -P my_script.cmake
# my_script.cmake
message("Hello, CMake Script Mode!") # 打印信息# 定义变量
set(MY_VAR "Hello")
message("MY_VAR: ${MY_VAR}") # 引用变量# 条件判断
if(MY_VAR STREQUAL "Hello")message("变量匹配成功")
endif()
(在makefile中也可以执行cmake脚本)
3.8 调用外部命令
cmake -E 是 CMake 提供的一个执行内置命令的工具,用于直接调用 CMake 自带的一系列跨平台基础命令(如文件操作、打印信息、创建目录等),无需通过生成构建系统(如 Makefile),也不依赖系统特定的工具(如 Linux 的 ls、Windows 的 cmd 命令):
#通过 cmake -E help 可查看所有内置命令
# 打印文本(支持变量和转义字符)
cmake -E echo "Hello, CMake!" # 输出:Hello, CMake!
cmake -E echo "Current dir: $PWD" # 结合系统变量(Linux/macOS)
四.CMake工程实践场景
4.1 可执行文件(编译-链接-安装)
4.1.1单步操作:
这里模拟的是工程实践场景,顺带将命令行复习一下:
目录结构:
├── CMakeLists.txt
├── build
└── main.cc
新建文件-CMakeLists.txt:
cmake_minimum_required(VERSION 3.18)project(main)add_executable(main main.cc)
add_executable(tests test.cc)include(CTest)
add_test(NAME test COMMAND tests)include(GNUInstallDirs)
install(TARGETS main)include(CPack)
新建文件main.cc:
#include <iostream>int main() {std::cout << "Hello, world!" << std::endl;return 0;
}
运行CMake:
4.1.2 重点命令解释:
下面将对CMakeLists.txt文件中的各个命令进行详细介绍:
cmake_minimum_required
函数作用:
指定项目所需的最低 CMake 版本,应放在顶级 CMakeLists.txt 的第一行。
基本形式
cmake_minimum_required(VERSION <min>[...<policy_max>] [FATAL_ERROR])
参数解释:
参数 | 含义 |
---|---|
VERSION | 关键字,表示后面跟的是版本号 |
< min > | 版本号,指定所需的最低 CMake 版本(如 3.18)。 |
\FATAL_ERROR(可选) | 若指定,当 CMake 版本不满足时会终止配置并报错(CMake 2.6+ 默认为此行为)。 |
版本号说明:
不同 Linux 发行版(如 Ubuntu、CentOS、Fedora 等)的软件仓库中,预装的 CMake 版本可能
随系统版本更新而变化。系统版本越新,预装的 CMake 版本通常也越新,在安装了cmake的前提下,可以使用cmake --version来查看cmake版本:
project
函数作用:
指定项目名字,放在顶级CMakeLists文件的第二行,子目录中一般无需调用。
基本形式
project(<PROJECT-NAME>)
完整形式
project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
关键参数
参数 | 含义 |
---|---|
< PROJECT-NAME > | 项目名称(如 MyProject),用于生成默认变量(如PROJECT_NAME)。 |
VERSION(可选) | 项目版本号(如 1.0.0),会自动定义 PROJECT_VERSION 等变量。 |
DESCRIPTION(可选) | 项目描述信息(用于生成文档或包配置)。 |
HOMEPAGE_URL(可选) | 项目主页 URL |
LANGUAGES(可选) | 指定项目支持的语言(如 C、CXX、Fortran、ASM 等)。 |
project() 执行之后,CMake会自动创建以下变量,可在后续命令中使用:
变量 | 描述 |
---|---|
PROJECT_NAME | 项目名称(如 MyProject)。 |
CMAKE_PROJECT_NAME | 顶级项目名称(与 PROJECT_NAME 相同)。 |
PROJECT_VERSION | 完整版本号(如 1.2.3)。 |
PROJECT_VERSION_MAJOR | 主版本号(如 1)。 |
PROJECT_VERSION_MINOR | 次版本号(如 2)。 |
PROJECT_VERSION_PATCH | 修订号(如 3)。 |
PROJECT_SOURCE_DIR | 顶级 CMakeLists.txt 所在目录(即源文件树根目录)。 |
PROJECT_BINARY_DIR | 构建目录(如 build/)。 |
就比如我想要打印项目的名称和版本号:就可以在CMakeLists.txt文件中添加这两行:
message(STATUS "PROJECT_NAME: ${PROJECT_NAME}")
message(STATUS "VERSION: ${PROJECT_VERSION}")
重新编译之后就可以看到(这里的message函数与printf相当)
关于[LANGUAGES < language-name >…]的设置:如果不设置编程语言,默认启动C/C++语言。在最顶层的CMakeLists.txt中如果没有对project进行字面的调用,那么cmake会发出警告,默认项目名字"Project"并默认开启C/C++编程语言。
project(main VERSION 1.0.0LANGUAGES C CXX)#设置多种语言
除此之外,还有如下使用场景:
PROJECT_NAME 变量的使用场景:
- 动态库的输出名称
- cmake配置文件的名称
- 命名空间的名称
PROJECT_VERSION 变量的使用场景:
- 打印变量
- 生成pkg-config或者.cmake对应的版本配置文件
- 动态库/静态库的的版本号
include
函数作用:
加载指定的脚本文件或者模块到当前CMakeLists执行上下文中并运行。
基本形式
include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>] [NO_POLICY_SCOPE])
#<file|module> 要运行的的文件或模块名称
文件的搜索路径:
◦ 如果指定的是相对路径,则相对当前的正在执行的CMakeLists.txt 所在的目录。
◦ 如果是绝对路径,那直接从对应的磁盘文件读取文件并加载执行。
模块搜索路径顺序:
◦ 首先在当前目录查找指定文件。
◦ 然后在 CMAKE_MODULE_PATH
变量指定的目录中查找。
执行逻辑:
◦ 在当前执行上下文执行被包含的camke代码。
CMake 进入子目录之后,内置变量的变化情况
变量名 | 进入子目录后是否变化? | 说明 |
---|---|---|
CMAKE_CURRENT_SOURCE_DIR | ❎不变化 | 还是父目录的源代码树的目录 |
CMAKE_CURRENT_BINARY_DIR | ❎不变化 | 还是父目录的构建树的目录 |
CMAKE_CURRENT_LIST_FILE | ✅ 变化 | 变为子目录的 CMakeLists.txt 文件全路径 |
CMAKE_CURRENT_LIST_DIR | ✅ 变化 | 变为子目录的 CMakeLists.txt 文件目录 |
我们修改一下cmakelists.txt文件以及创建一个test.cmake脚本验证一下:
#CMakeLists.txt
cmake_minimum_required(VERSION 3.18)project(mainVERSION 1.0.0LANGUAGES C CXX)add_executable(main main.cc)include(GNUInstallDirs)
install(TARGETS main)message(STATUS "from top-level CMakeLists.txt")
# 打印 当前正在执行的源代码目录--- 也就是CMakeLists.txt所在的目录
message(STATUS "CMAKE_CURRENT_SOURCE_DIR:" ${CMAKE_CURRENT_SOURCE_DIR})
# 打印 当前正在执行的cmake 脚本的 完整名称
message(STATUS "CMAKE_CURRENT_LIST_FILE:" ${CMAKE_CURRENT_LIST_FILE})
# 打印当前正在执行的cmake 脚本的 全目录
message(STATUS "CMAKE_CURRENT_LIST_DIR:" ${CMAKE_CURRENT_LIST_DIR})include(test.cmake)include(CPack)#test.cmake
message(STATUS "from testcmake/test.cmake")
# 打印 当前正在执行的源代码目录--- 也就是CMakeLists.txt所在的目录
message(STATUS "CMAKE_CURRENT_SOURCE_DIR:" ${CMAKE_CURRENT_SOURCE_DIR})
# 打印 当前正在执行的cmake 脚本的 完整名称
message(STATUS "CMAKE_CURRENT_LIST_FILE:" ${CMAKE_CURRENT_LIST_FILE})
# 打印当前正在执行的cmake 脚本的 全目录
message(STATUS "CMAKE_CURRENT_LIST_DIR:" ${CMAKE_CURRENT_LIST_DIR})
对比发现,只有CMAKE_CURRENT_LIST_FILE
和CMAKE_CURRENT_LIST_DIR
能真实定位正在执行的cmake文件,include的本质是在当前构建环境中,插入执行另一个文件的代码”—— 不会创建新的构建模块,只是代码片段的 “拼接执行”,所以不会改变CMAKE_CURRENT_SOURCE_DIR
和CMAKE_CURRENT_BINARY_DIR
!
install
函数作用:
安装(简单理解为cp) 将 二进制,静态库,动态库,头文件,配置文件 部署到指定目录。
基本形式
install(TARGETS <targets>... [EXPORT <export-name>]
[RUNTIME DESTINATION <dir>]
[LIBRARY DESTINATION <dir>]
[ARCHIVE DESTINATION <dir>]
[INCLUDES DESTINATION <dir>]
[...])
install(FILES <files>... DESTINATION <dir>
[PERMISSIONS <permissions>...]
[CONFIGURATIONS <configs>...]
[COMPONENT <component>]
[...])
install(DIRECTORY <dirs>... DESTINATION <dir>
[FILE_PERMISSIONS <permissions>...]
[DIRECTORY_PERMISSIONS <permissions>...]
[...])
install(EXPORT <export-name> DESTINATION <dir>
[NAMESPACE <namespace>::]
[FILE <filename>]
[...])
关键参数
参数 | 含义 |
---|---|
TARGETS | 安装 使用add_executable和add_library 构建的目标文件。 |
FILES | 安装 文件 |
DIRECTORY | 安装整个目录 |
EXPORT | 安装导出目录,用于发布自己的程序,供别人使用。 |
DESTINATION | 指定安装路径,路径可以是绝对路径,也可以是相对路径(相对于CMAKE_INSTALL_PREFIX )。 |
tips:install的具体过程前面已经讲解过:可以大致分为收集安装文件,生成cmake_install.cmake以及执行file内置API执行安装操作
add_executable
函数作用:
指示 cmake 从源代码生成一个可执行文件。
基本形式
add_executable(<target_name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 ...])
关键参数
参数 | 含义 |
---|---|
target_name | 可执行文件的名称(不包含扩展名,如 myapp),项目内部唯一 |
< sources > | 源文件列表(如 src/main.cpp) |
tips:1.可执行程序的名称在项目中必须是全局唯一的;
2.二进制程序在构建目录里的位置和定义二进制的CMakeLists.txt的位置相对于各自的根目录是相对应的。
3.默认情况下,将在与调用命令的CMakeLists.txt的目录相对应的 build tree directory 中创建可执行文件。
4.2 静态库(编译-链接-引用)
在最佳工程实践里,工程规模大一点的工程中,往往会把一些比较独立的功能(比如如网络、数据库、算法,或者一些基础组件,redis-client, mysql-client,jsoncpp, libcurl)封装为独立的库,由不同的团队来维护,在公司或者开源社区共享,达到复用目的。
Linux中如何制作并使用动静态库:剖析文件系统+软硬链接+动静态库:搞懂Linux基础三件套
这里将把加法和减法函数2个函数封装成一个MyMath的数学静态库,并在main二进制里引用静态库里的加法和减法函数,来演示下如何使用CMake来管理这一场景:
4.2.1单步操作:
目录结构:
(说明:app中存放测试main函数,mylib/include存放头文件,mylib/src存放加/减函数方法,此外还有创建于顶级目录的cmakelists,以及app/mylib中的cmakelists)
新建文件CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(testlib)
#以上的是cmake的最小版本和项目名称,常规操作#添加目录层级
add_subdirectory(app)
add_subdirectory(mylib)
- add_subdirectory:将 app 和 mylib 两个子目录纳入构建流程。CMake 会依次进入这两个目录,执行各自的 CMakeLists.txt !
新建文件mylib/CMakeLists.txt
# 收集库的源文件(src 目录下所有 .cpp)
file(GLOB SRC_FILES "src/*.cpp")# 创建静态库目标 mymath
add_library(mymath STATIC ${SRC_FILES})# 设置库的头文件搜索路径(对外公开)
target_include_directories(mymath PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include
)#修改默认的库输出目录
set_target_properties(mymath PROPERTIESARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib
)
- file(GLOB) 收集源文件:通过通配符 src/*.cpp 自动收集 mylib/src 下的所有 .cpp 文件,避免手动逐个添加(新增源码时无需修改 CMake 脚本)
- add_library(mymath STATIC …):创建静态库目标 mymath(STATIC 表示静态库,编译时会将库代码直接嵌入可执行文件)
- target_include_directories(PUBLIC …):把 mylib/include 目录设为公开的头文件路径
CMAKE_CURRENT_LIST_DIR
表示当前正在处理的文件所在的绝对目录- set_target_properties 用于为指定的目标(通常是库、可执行文件等)设置各种属性,
mymath
是目标名称,ARCHIVE_OUTPUT_DIRECTORY
是要设置的属性,专门用于指定静态库文件(.a/.lib) 的输出目录(对于动态库,对应的属性是LIBRARY_OUTPUT_DIRECTORY
;对于可执行文件,是RUNTIME_OUTPUT_DIRECTORY
)。 - ${CMAKE_BINARY_DIR}/lib 是属性值,指定了静态库的输出路径:为构建目录下的bin目录。
新建文件app/CMakeLists.txt
# 收集可执行程序的源文件(当前目录下所有 .cpp,即 main.cpp)
file(GLOB SRC_LISTS "*.cpp")# 创建可执行目标 main
add_executable(main ${SRC_LISTS})# 链接静态库 mymath 到可执行程序 main
target_link_libraries(main PRIVATE mymath
)#修改默认的可执行文件输出目录
set_target_properties(main PROPERTIESRUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
- file(GLOB) 收集可执行源码:自动收集 app 目录下的 .cpp 文件(如 main.cpp)。
- add_executable(main …):创建可执行程序目标 main。
- target_link_libraries(main PRIVATE mymath):将静态库 mymath 链接到 main 可执行程序。(private以及上面所提到的public后续详解)
- set_target_properties 用于为指定的目标设置各种属性,对于可执行文件,
RUNTIME_OUTPUT_DIRECTORY
用于指定输出目录,CMAKE_BINARY_DIR
代表构建目录(通常是项目根目录下的 build 文件夹,顶级的CMakeLists.txt放在源代码目录下);
新建文件math.h
pragma once
int add(int a, int b);
int sub(int a, int b);
新建文件add.cpp/sub.cpp/main.cpp
#add.cpp
int add(int a, int b)
{return a + b;
}
#sub.cpp
int sub(int a, int b)
{return a - b;
}
#main.cpp
#include <iostream>
#include "mymath.h"
using namespace std;
int main()
{cout << add(1, 2) << endl;cout << sub(1, 2) << endl;return 0;
}
运行CMake
之后,就可以去构建目录下的bin找到我们的可执行程序,以及构建目录下的lib去找到静态库!
4.2.2 重点命令解释:
CMake的 目标-属性-API:
Target
目标的种类类型 | 创建命令 | 产物例子 / 说明 |
---|---|---|
EXECUTABLE | add_executable | main ,curl |
STATIC | add_library(… STATIC) | libfoo.a, foo.lib |
SHARED | add_library(… SHARED) | libfoo.so, foo.dll |
MODULE | add_library(… MODULE) | 插件:libplugin.so, 使用dlopen 运行时加载 |
OBJECT | add_library(… OBJECT) | 仅 .o/.obj,存在于内存,不生成库文件 |
INTERFACE | add_library(… INTERFACE) | 无库文件,携带使用要求 |
IMPORTED | add_library(… IMPORTED) | 使用cmake 内存目标对象引用磁盘上的外部构建产物 |
ALIAS | add_library(… ALIAS) | 为同项目内的现有目标取别名 |
Property
类别 | 作用域 | 典型读/写命令 | 常用属性示例 |
---|---|---|---|
全局属性 (Global) | 整个 CMake 运行生命周期 | get/set_property(GLOBAL PROPERTY …) | CMAKE_ROLE |
目录属性 (Directory) | 当前源码目录及其子目录 | get/set_property(DIRECTORY PROPERTY …) | INCLUDE_DIRECTORIES |
目标属性 (Target) | 单个构建目标(库、可执行、接口⋯) | get/set_property(TARGET PROPERTY …) | LINK_LIBRARIES INCLUDE_DIRECTORIES |
源文件属性 (Source File) | 单个源码/资源文件 | get/set_source_files_properties | COMPILE_FLAGS |
测试属性 (Test) | 由 add_test() 定义的单个测试 | get/set_tests_properties() | WORKING_DIRECTORY |
安装文件属性 (Installed File) | install() 生成的安装清单条目 | set_property(INSTALL … PROPERTY …) | RPATH |
属性的作用域与传播范围(main ----> curl)
关键字 | 对当前目标的构建影响 | 是否传播 | 对当前目标使用者的影响 | 解释 | 例子(面包和面粉的例子) |
---|---|---|---|---|---|
PRIVATE | ✅生效 | ❎否 | ✅生效 | 只自己用 | 制作面包的面粉品牌不公开 |
PUBLIC | ✅生效 | ✅是 | ✅生效 | 自己+下游用 | 公开制作面包的面粉的品牌 |
INTERFACE | ❎不生效 | ✅是 | ✅生效 | 说明书,下游用 | 说明书,说明用什么面粉制作,不卖面粉 |
API
类 别 | 典型命令(可选关键词) | 主要作用 | 涉及的核心属性(部分示例) |
---|---|---|---|
1. 通用读/写接口 | set_target_properties() get_target_property() | 任意目标属性的设置、追加、查询(最底层API) | 任何 prop_tgt |
2. 编译阶段相关 | target_compile_definitions target_compile_options target_precompile_headers target_include_directories target_sources | 控制源文件编译:宏定义、编译选项、语言特性、预编译头、包含目录、源文件列表等 | COMPILE_DEFINITIONS、COMPILE_OPTIONS、 COMPILE_FEATURES、PRECOMPILE_HEADERS、INCLUDE_DIRECTORIES、SOURCES 等 |
3. 链接&输出阶段相关 | target_link_libraries target_link_options target_link_directories | 配置目标被链接的库、选项及搜索路径 | LINK_LIBRARIES INTERFACE_LINK_LIBRARIES LINK_OPTIONS INTERFACE_LINK_OPTIONS LINK_DIRECTORIES INTERFACE_LINK_DIRECTORIES |
4. 安装&打包阶段相关 | install(TARGETS …) install(EXPORT …) | 生成安装规则与包,控制目标在安装树中的布局及其运行时行为 | RUNTIME_OUTPUT_DIRECTORY LIBRARY_OUTPUT_DIRECTORY ARCHIVE_OUTPUT_DIRECTORY EXPORT_NAME、INSTALL_RPATH |
关键总结
阶段 | 核心输入 | 核心输出 | 核心作用 |
---|---|---|---|
配置期 | CMakeLists.txt | CMakeCache.txt | 解析规则,注册目标,记录属性 |
生成期 | 配置缓存、目标属性 | Makefile/build.ninja | 翻译属性为平台构建脚本 |
构建期 | 构建脚本、源代码 | 二进制产物(可执行/库) | 编译链接生成最终产物 |
安装期 | 二进制产物、cmake_install.cmake | 部署到安装目录的产物 | 标准化安装,方便复用 |
add_library
函数作用:
添加一个静态库或者动态库目标,让cmake 从指定的文件列表生成。
基本形式
add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [<source>...])
参数解释:
参数 | 含义 |
---|---|
<name> | 库的名称(不包含前缀和后缀,如 Foo 会生成 libFoo.a)。项目内部唯一 |
STATIC | 创建静态库(默认值,若不指定类型)。 |
SHARED | 创建动态库(共享库)。 |
[source] | 构建库的源文件列表。 |
add_library还支持多种库文件的生成与添加:
target_include_directories
函数作用:
设置目标在开发和发布阶段的头文件搜索目录,可以传递性传播给下游的使用方。
基本形式
target_include_directories(<target>
[SYSTEM] # path 告诉编译器“这些是系统头文件”,GCC/Clang 会用 -isystem 而非 -I,从而抑制第三方头引出的警告。
[BEFORE] # 把路径插到已有列表最前面
<INTERFACE|PUBLIC|PRIVATE> path1 [path2 ...]
[<INTERFACE|PUBLIC|PRIVATE> pathN ...] …
)
- < target >
必须是由add_executable()或add_library()创建的目标,也支持接口库(INTERFACE_LIBRARY)或导入目标(IMPORTED,CMake 3.11 + 允许为导入目标设置接口路径)。 - SYSTEM(可选)
标记头文件目录为系统级目录。部分编译器会因此:
抑制该目录下头文件的编译警告;跳过依赖计算(具体行为见编译器文档)。
若与PUBLIC/INTERFACE结合,会将路径写入INTERFACE_SYSTEM_INCLUDE_DIRECTORIES属性(而非普通的INTERFACE_INCLUDE_DIRECTORIES)。 - BEFORE / AFTER(可选)
控制新添加的头文件路径是前置(BEFORE,优先搜索)还是后置(AFTER,默认,后搜索)到目标已有的包含路径中。
关键字 | 自身目标编译时是否使用 | 是否传播给“依赖该目标”的其他目标 | 填充的目标属性 |
---|---|---|---|
PRIVATE | 是 | 否 | INCLUDE_DIRECTORIES |
PUBLIC | 是 | 是 | INCLUDE_DIRECTORIES + INTERFACE_INCLUDE_DIRECTORIES |
INTERFACE | 否(仅用于传播) | 是 | INTERFACE_INCLUDE_DIRECTORIES |
通过target_include_directories添加的 路径 最终是通过gcc 的 -I 参数传递给编译器的。
target_link_libraries
函数作用:
设置二进制目标的依赖库列表,相当于使用通用的set属性设置函数设置了LINK_LIBRARIES或者
INTERFACE_LINK_LIBRARIES这个属性,最终以-l的形式出现在gcc参数里。
基本形式
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
参数解释:
- PRIVATE 关键字:相当于使用set_target_properties 设置了LINK_LIBRARIES属性,设置的库列表只会写进目标的LINK_LIBRARIES列表里。
- INTERFACE 关键字:相当于使用set_target_properties 设置了INTERFACE_LINK_LIBRARIES,只会写进目标的INTERFACE_LINK_LIBARIERS列表里。
- PUBLIC 关键字设置的列表会同时写进LINK_LIBRARIES和INTERFACE_LINK_LIBRARIES里。
INTERFACE_LINK_LIBRARIES 列表出现的库会被传播给这个目标的使用方。通过 target_link_libraries最终是通过gcc 的-l 选项传递给链接器的。
target_link_libraries的两大作用:
1.设置目标的依赖库列表,列出的依赖者会以-l的形式出现在gcc的参数里;
2.建立依赖关系,被依赖者需要传播的属性可以沿着关系链传播给依赖者。
项目演示:
重点在于这里的sub/CMakeLists.txt以及顶级目录下的cmakelists.txt:
cmake_minimum_required(VERSION 3.18)project(MyProject VERSION 1.0 LANGUAGES CXX)add_subdirectory(sub)add_executable(MyExecutable main.cc)target_link_libraries(MyExecutable PRIVATE sub)###########################################################################
add_executable(mysub STATIC sub.cpp)
#设置头文件搜索路径
target_include_directories(mysub PUBLIC "/usr/local/include/private")
target_include_directories(mysub PRIVATE "/usr/local/include/public")
target_include_directories(mysub INTERFACE "/usr/local/include/interface")
#库文件搜索路径,(由于是静态库不需要动态链接,没用)
target_link_directories(mysub PRIVATE "/usr/local/lib/private")
target_link_directories(mysub PUBLIC "/usr/local/lib/public")
target_link_directories(mysub INTERFACE "/usr/local/lib/interface")target_link_libraries(mysub INTERFACE "pthread")
在build构建目录下cmake, cmake --build . -v
查看编译的详细信息:
usr/bin/c++ -Dadd_EXPORTS -I/usr/local/include/public -std=gnu++11 -isysroot
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/
SDKs/MacOSX15.4.sdk -mmacosx-version-min=15.3 -fPIC -MD -M/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/
usr/bin/c++ -I/usr/local/include/public -I/usr/local/include/interface -
std=gnu++11 -isysroot
1 target_link_directories 和 target_include_directories 设置的属性保存在 target上,并传递给gcc 编译和链接器。
2 PUBLIC的属性 自己在编译和链接(开发阶段)会使用,也会传递给使用者的开发阶段。
3 INTERFACE 属性 会传递给使用者的编译和链接阶段(开发阶段),库自己开发阶段不会使用。
set_target_properties和 get_target_properties
函数作用:
设置/查询 目标(如可执行文件、库)的各种属性,控制编译、链接、安装等行为
基本形式:
set_target_properties(<target1> <target2> ...
PROPERTIES <prop1> <value1>
<prop2> <value2> ...)
get_target_property(<variable> <target> <property>)
参数解释:
参数 | 含义 |
---|---|
< target1 > | 库的名称 |
< prop 1> < value1 > | 属性名字和值 (常见的的属性名字和含义) |
项目演示:
#./CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(test VERSION 1.0 LANGUAGES CXX)add_subdirectory(src)add_executable(test main.cc)target_link_libraries(test PRIVATE mylib)#src/CMakeLists.txt
add_library(mylib SHARED add.cpp)set_target_properties(mylib PROPERTIES# CMake 参数验证# 1. 编译类参数COMPILE_OPTIONS "-g" # 开启调试信息COMPILE_OPTIONS "-O3" # 最高级别优化COMPILE_OPTIONS "-fPIC" # 生成与位置无关的代码INCLUDE_DIRECTORIES "/public" # 添加头文件搜索路径INTERFACE_INCLUDE_DIRECTORIES "/interface" # 供依赖此目标的其他目标使用的头文件路径# 2. 链接类参数LINK_DIRECTORIES "/public" # 添加库文件搜索路径INTERFACE_LINK_DIRECTORIES "/interface" # 供依赖此目标的其他目标使用的库文件路径LINK_LIBRARIES "curl" # 链接 curl 库INTERFACE_LINK_LIBRARIES "jsoncpp" # 供依赖此目标的其他目标链接 jsoncpp 库# 3. 输出类参数RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" # 可执行文件输出目录ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" # 静态库输出目录LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" # 动态库输出目录# 4. 安装类参数BUILD_RPATH "${CMAKE_BINARY_DIR}/lib" # 构建时运行库搜索路径INSTALL_RPATH "lib" # 安装后运行库搜索路径(如 /usr/local/lib)OUTPUT_NAME "add" # 生成目标的输出名称VERSION "1.2.3" # 目标版本号SOVERSION "20" # 兼容性版本号
)
在build构建目录下cmake, cmake --build . -v
查看编译的详细信息:
下面是各属性在编译过程中的出现位置说明:
- COMPILE_OPTIONS
这些参数(如 -g、-O3、-fPIC)会在编译源文件时出现在编译器命令行,例如 g++ … -g -O3 -fPIC …。但本日志未显示详细编译命令,实际会在编译 add.cpp 时体现。 - INCLUDE_DIRECTORIES
头文件路径会以 -I/public 形式出现在编译命令行。 - LINK_DIRECTORIES
库文件搜索路径以 -L/public 形式出现在链接命令行,但本次链接命令只显示了 -L/interface,说明 /public 未被用到或未生效。 - LINK_LIBRARIES
链接库以 -lcurl 形式出现在链接命令行,但本次链接命令未显示 -lcurl,可能未被 test 目标使用。 - RUNTIME_OUTPUT_DIRECTORY
可执行文件输出目录,最终生成的可执行文件会放在 ${CMAKE_BINARY_DIR}/bin,但本次 test 目标输出在 build 目录下。 - ARCHIVE_OUTPUT_DIRECTORY、LIBRARY_OUTPUT_DIRECTORY
静态库和动态库输出目录,生成的 libadd.so.1.2.3 在 lib/ 目录下。 - BUILD_RPATH、INSTALL_RPATH
运行时库搜索路径以 -Wl,-rpath,… 形式出现在链接命令行,如 -Wl,-rpath,/interface:/home/drw1/linux/cmaketarget_properities/build/lib。 - OUTPUT_NAME、VERSION、SOVERSION
控制生成库的文件名和版本号,如 libadd.so.1.2.3。 - INTERFACE_INCLUDE_DIRECTORIES、INTERFACE_LINK_DIRECTORIES、INTERFACE_LINK_LIBRARIES
这些参数只对依赖此库的其他目标有效,在 test 目标链接时体现,如 -L/interface、-ljsoncpp。
add_subdirectory
函数作用:
添加子目录到构建树,cmake会自动进入到源码树子目录,执行位于子目录里的CMakeLists.txt文
件。
基本形式
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
参数解释:
参数 | 含义 |
---|---|
source_dir | 通常为当前文件夹下的子目录的名字。 |
binary_dir | cmake会在构建树里创建同名的子目录,用于保存子目录的的cmake文件里生成的目标和二进制 |
关键行为
- 处理顺序
◦ CMake 会立即处理source_dir
中的CMakeLists.txt
,当前文件的处理会暂停,直到子目录处理完毕,再继续处理当前文件add_subdirectory
之后的命令。 - 路径解析
◦source_dir
相对路径:相对于当前CMakeLists.txt
文件所在目录。
◦binary_dir
相对路径:相对于当前构建目录(不指定,则使用 source_dir )。 - 变量作用域
◦ 子目录中定义的变量默认是局部的,不会影响父目录。
◦ 可通过 set(xxx PARENT_SCOPE) 将变量传递到父目录。
◦ 缓存变量是全局的,子目录里设置的缓存变量,父目录也可以获取到 - CMake 在当前上下文执行完add_subdirectory命令,进入到子目录之后,在开始执行
CMakeLists.txt时会修改的内置变量:
变量名 | 进入子目录后是否变化? | 说明 |
---|---|---|
CMAKE_CURRENT_SOURCE_DIR | ✅变化 | 变为子目录的源代码树的目录 |
CMAKE_CURRENT_BINARY_DIR | ✅变化 | 变为子目录的构建树的目录 |
CMAKE_CURRENT_LIST_FILE | ✅变化 | 变为子目录的 CMakeLists.txt 文件全路径 |
CMAKE_CURRENT_LIST_DIR | ✅变化 | 变为子目录的 CMakeLists.txt 文件目录 |
Warning:注意同include变量的对CMAKE_CURRENT_SOURCE_DIR 的影响的区别,不管是include模式还是add_subdirectory,要得到相对于正在执行的cmake 文件,建议使用CMAKE_CURRENT_LIST_FILE 作为相对路径的参考点。
file
函数作用:
查看目录下的所有文件,如果匹配规则,则添加文件名字到文件列表中,是否需要递归,需要显
示指定。
基本形式
file(GLOB|GLOB_RECURSE <variable> [LIST_DIRECTORIES true|false]
[RELATIVE <path>] [CONFIGURE_DEPENDS] <globbing-expressions>...)
参数解释:
参数 | 含义 |
---|---|
GLOB | 匹配当前目录下的文件(不递归子目录) |
GLOB_RECURSE | 递归匹配当前目录及其所有子目录下的文件。 |
< out-var > | 收集到的文件列表变量 |
< globbing-expr > | 通配符表达式(如 .cpp、src/**/.h)。 |