CMake 入门实战手册:从理解原理开始,打造高效 C/C++ 开发流程

文章目录

一.什么是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:

  1. 先更新软件包列表。打开终端,执行以下命令更新系统的软件源信息,确保能获取到最新的 CMake 版本:
bash 复制代码
sudo apt update
  1. 直接通过 apt 包管理器安装 CMake:
bash 复制代码
sudo apt install cmake

如果需要安装 CMake 的额外组件(如开发文档、测试工具等),可以安装扩展包:

bash 复制代码
sudo apt install cmake cmake-doc cmake-extras
  1. 安装完成后,通过以下命令查看 CMake 版本,确认安装成功:
bash 复制代码
cmake --version

如果输出类似以下内容,说明安装成功:

bash 复制代码
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插件:

  1. 打开 VS Code,点击左侧活动栏中的扩展图标::
  2. 在搜索框中输入 CMake ,选择安装以下4个插件: CMake、CMake Tools、CMake Language Support、CMake IntelliSence

2.3HelloWorld快速搭建

新建一个文件夹,在Ubunto主机中使用CMake编译打印hellowrold的程序:

c 复制代码
//目录结构
testCMake/
├── CMakeLists.txt
└── main.cc
  1. 创建main.cc文件,写好程序:
  2. 创建CMakeLists.txt文件,注意不要写错了名字 ! 在此文件中设置三行内容:

    如果项目中使用了高版本 CMake 才支持的特性(例如特定的函数、生成器表达式、目标属性等),而用户本地安装的cmake版本低于项目要求的版本,就会出现无法解释或者产生不可预知的行为,其次要设置要生成的项目名称以及可执行程序名称(main main.cc代表由main.cc生成main可执行程序)
  3. 运行CMake:
  • 选择在当前路径下cmake:
bash 复制代码
cmake .

如果生成了cmake_install.cmake文件夹,以及CMakeCache.txt说明生成成功

  • 现在惊喜的发现目录中多出了makefile文件,这说明cmake自动帮我们生成了需要我们手写的makefile文件,直接make即可
bash 复制代码
make
  • 运行main程序
bash 复制代码
./main

三.CMake 命令行工具介绍

3.1 CMake 工程构建流程图

一个项目通常经过以下流程:

  1. 代码编写(IDE 阶段)
    使用集成开发环境(IDE)编写工程代码,并创建 CMakeLists.txt(CMake 的配置文件,描述项目的编译规则、依赖等)。
  2. CMake 配置阶段
    第一个 CMake 环节为配置阶段:根据 CMakeLists.txt 和工程代码,分析项目的目标(可执行程序、库)与依赖关系(如依赖的第三方库、编译选项等)。
  3. CMake 生成阶段
    第二个 CMake 环节为生成阶段:基于配置结果,生成 Makefile(make 工具的编译规则文件,包含编译、链接的具体指令)。
  4. make 编译链接
    make 工具读取 Makefile,执行编译(将源码转为目标文件)和链接(将目标文件组合为可执行文件 / 库),最终生成 exe(可执行文件)或 so(动态库)等产物。
  5. CTest 测试
    使用 CTest 工具,对编译生成的 exe/so 进行自动化测试,验证程序功能是否符合预期。
  6. install 本机安装
    通过 install 操作,将测试通过的 exe/so 安装到本地系统的指定目录(如系统库路径、程序安装路径)。
  7. CPack 打包分发
    使用 CPack 工具,将已安装的文件打包为 tar/zip 等格式的压缩包,以便进行网络分发(如发布到软件仓库、供其他用户下载部署)。

现在根据这个流程,进一步修改我们的main.cctest.cc

cpp 复制代码
//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:(非常重要,后续命令行都基于这个文件!)

c 复制代码
#先来设置最低版本
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 生成构建系统

这里可以采用三种方式:

  1. cmake [options] <path-to-source>
    将当前工作目录作为「构建树」, 作为「源码树」(源码树必须包含 CMakeLists.txt,且不能存在 CMakeCache.txt------ 否则会被识别为 "已有构建树")。
bash 复制代码
mkdir build && cd build
cmake ../src  # 构建树是当前`build`目录,源码树是`../src`
  1. cmake [options] <path-to-existing-build>
    <path-to-existing-build> 作为「构建树」,并从该目录的 CMakeCache.txt 中加载源码树的路径(因此该构建目录必须是 "之前 CMake 运行过、已生成 CMakeCache.txt" 的目录)。
c 复制代码
cd build
cmake .  # 构建树是当前`build`目录,源码树从`build/CMakeCache.txt`加载
  1. cmake [options] -S <path-to-source> -B <path-to-build>
    通过 -S 明确指定「源码树」(需包含 CMakeLists.txt ),通过 -B 明确指定「构建树」(通常包含一个CMakeCache.txt)。
bash 复制代码
cmake -S src -B build  # 源码树是`src`,构建树是`build`(不存在则创建)

源内构建 :在源代码树包含的顶级CMakeLists.txt的目录下进行直接构建;
源外构建 :使用 -B 参数单独指定一个build 目录,然后在子目录里制定源文件目录也就是包含CMakeLists.txt的目录。源码与构建目录分离的规范用法,能让源码目录保持 "干净"(无构建生成的临时文件),构建产物集中在指定目录,便于管理,是现代推荐的用法

3.3 编译链接

构建完毕系统之后,就可以进行编译链接了,使用以下命令:

bash 复制代码
cmake --build ./
#或者
make


  • 对于make:由于cmake之后会生成makefile文件,所以make就可以直接完成编译链接;
  • 对于cmake --build:输入命令后其实会执行一个软连接:
c 复制代码
 //Path to a program.
CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/gmake #这是CMakeCache.txt中命令对应的目录

将其打印出来,就是make:


而build命令代表在当前目录生成项目:

3.4 测试

对程序进行测试,加上之前写好的testing函数,可以使用以下命令:

bash 复制代码
ctest
#或者
make test

如果测试通过:


如果测试未通过:


可以去目录testCMake/build/Testing/Temporary/LastTest.log中查看错误信息:

看看makefile文件中对应的make test命令:

c 复制代码
# 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文件将包含的文件全部执行一遍:

c 复制代码
# 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来执行。

bash 复制代码
cmake --install .
#或者
make install



说说安装的目的:main函数原先是在自己的用户目录下,使用这台主机的其他人访问不到,如果安装到bin目录下便是公共目录,他人可以访问到。

看看makefile文件中对应的make install命令:

c 复制代码
# 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来执行:

bash 复制代码
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脚本模式不会生成构建产物,也不会生成中间过程。适合处理各种与构建系统无关的自

动化任务,通过编写简洁的脚本文件,你可以实现环境检查、文件处理、部署打包等功能。

bash 复制代码
通过 cmake -P <脚本文件> 执行脚本
#cmake -P my_script.cmake
c 复制代码
# 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 命令):

bash 复制代码
#通过 cmake -E help 可查看所有内置命令
# 打印文本(支持变量和转义字符)
cmake -E echo "Hello, CMake!"  # 输出:Hello, CMake!
cmake -E echo "Current dir: $PWD"  # 结合系统变量(Linux/macOS)

四.CMake工程实践场景

4.1 可执行文件(编译-链接-安装)

4.1.1单步操作:

这里模拟的是工程实践场景,顺带将命令行复习一下:

目录结构:
c 复制代码
├── CMakeLists.txt
├── build
└── main.cc
新建文件-CMakeLists.txt:
c 复制代码
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
cpp 复制代码
#include <iostream>

int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}
运行CMake:

4.1.2 重点命令解释:

下面将对CMakeLists.txt文件中的各个命令进行详细介绍:

cmake_minimum_required

函数作用:

指定项目所需的最低 CMake 版本,应放在顶级 CMakeLists.txt 的第一行。

基本形式

c 复制代码
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文件的第二行,子目录中一般无需调用。

基本形式

c 复制代码
project(<PROJECT-NAME>)

完整形式

c 复制代码
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文件中添加这两行:

c 复制代码
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++编程语言。

c 复制代码
project(main 
        VERSION 1.0.0
        LANGUAGES C CXX)#设置多种语言

除此之外,还有如下使用场景:
PROJECT_NAME 变量的使用场景:

  1. 动态库的输出名称
  2. cmake配置文件的名称
  3. 命名空间的名称

PROJECT_VERSION 变量的使用场景:

  1. 打印变量
  2. 生成pkg-config或者.cmake对应的版本配置文件
  3. 动态库/静态库的的版本号
include

函数作用:

加载指定的脚本文件或者模块到当前CMakeLists执行上下文中并运行。

基本形式

c 复制代码
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脚本验证一下:

c 复制代码
#CMakeLists.txt
cmake_minimum_required(VERSION 3.18)

project(main
        VERSION 1.0.0
        LANGUAGES 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_FILECMAKE_CURRENT_LIST_DIR 能真实定位正在执行的cmake文件,include的本质是在当前构建环境中,插入执行另一个文件的代码"------ 不会创建新的构建模块,只是代码片段的 "拼接执行",所以不会改变CMAKE_CURRENT_SOURCE_DIRCMAKE_CURRENT_BINARY_DIR

install

函数作用

安装(简单理解为cp) 将 二进制,静态库,动态库,头文件,配置文件 部署到指定目录。

基本形式

c 复制代码
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 从源代码生成一个可执行文件。

基本形式

c 复制代码
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
c 复制代码
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
c 复制代码
# 收集库的源文件(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 PROPERTIES
        ARCHIVE_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
c 复制代码
# 收集可执行程序的源文件(当前目录下所有 .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 PROPERTIES
        RUNTIME_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
c 复制代码
pragma once
int add(int a, int b);
int sub(int a, int b);
新建文件add.cpp/sub.cpp/main.cpp
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 从指定的文件列表生成。

基本形式

c 复制代码
add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] [<source>...])

参数解释:

参数 含义
<name> 库的名称(不包含前缀和后缀,如 Foo 会生成 libFoo.a)。项目内部唯一
STATIC 创建静态库(默认值,若不指定类型)。
SHARED 创建动态库(共享库)。
[source] 构建库的源文件列表。

add_library还支持多种库文件的生成与添加:

target_include_directories

函数作用:

设置目标在开发和发布阶段的头文件搜索目录,可以传递性传播给下游的使用方。

基本形式

c 复制代码
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 参数传递给编译器的。

函数作用:

设置二进制目标的依赖库列表,相当于使用通用的set属性设置函数设置了LINK_LIBRARIES或者

INTERFACE_LINK_LIBRARIES这个属性,最终以-l的形式出现在gcc参数里。

基本形式

c 复制代码
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:

c 复制代码
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查看编译的详细信息:

c 复制代码
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_propertiesget_target_properties

函数作用:

设置/查询 目标(如可执行文件、库)的各种属性,控制编译、链接、安装等行为

基本形式:

c 复制代码
set_target_properties(<target1> <target2> ...
PROPERTIES <prop1> <value1>
<prop2> <value2> ...)
get_target_property(<variable> <target> <property>)

参数解释:

参数 含义
< target1 > 库的名称
< prop 1> < value1 > 属性名字和值 (常见的的属性名字和含义

项目演示:

c 复制代码
#./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文
件。

基本形式

c 复制代码
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])

参数解释

参数 含义
source_dir 通常为当前文件夹下的子目录的名字。
binary_dir cmake会在构建树里创建同名的子目录,用于保存子目录的的cmake文件里生成的目标和二进制

关键行为

  1. 处理顺序
    ◦ CMake 会立即处理 source_dir 中的 CMakeLists.txt,当前文件的处理会暂停,直到子目录处理完毕,再继续处理当前文件add_subdirectory之后的命令。
  2. 路径解析
    source_dir 相对路径:相对于当前 CMakeLists.txt 文件所在目录。
    binary_dir 相对路径:相对于当前构建目录(不指定,则使用 source_dir )。
  3. 变量作用域
    ◦ 子目录中定义的变量默认是局部的,不会影响父目录。
    可通过 set(xxx PARENT_SCOPE) 将变量传递到父目录
    ◦ 缓存变量是全局的,子目录里设置的缓存变量,父目录也可以获取到
  4. 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

函数作用:

查看目录下的所有文件,如果匹配规则,则添加文件名字到文件列表中,是否需要递归,需要显
示指定。

基本形式

c 复制代码
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)。
CMake 内部静态库的生成与定位流程

在cmake里当添加一个目标比如静态库后,cmake会在配置阶段自动跟踪静态库的输出位置,

后在生成阶段,会把这个输出地址通过-L参数传递给链接器,链接器就可以找到项目内部的静态

库,整个过程的产物就是可视化的makefile。一切尽在makefile里。

下面详细展开这个步骤:

  • 第一步:目标生成
    当你写下add_library(MyMath STATIC add.cpp sub.cpp)这一行命令时,cmake内部会在全局的
    Targets 容器中注册一个名为 MyMath 的cmTarget目标。
  • 第二步:目标信息存储
    每个cmTargetInternals会存储目标的名称,类型,源文件列表,输出地址等属性。当你使用
    LIBRARY_OUTPUT_DIRECTORY等属性修改目标输出地址的时候,cmake也会更新最终的输出地址。
  • 第三步:生成器阶段-定位静态库路径
    CMake 配置阶段结束后,进入生成阶段(cmLocalGenerator、cmGlobalGenerator),生成器会
    遍历所有目标(cmTarget),根据目标的属性(如 ARCHIVE_OUTPUT_DIRECTORY)和平台规则,推导出静态库的实际输出路径
    。比如:build/libmylib.a。
  • 第四步:链接命令的生成
    生成器会为每一个目标生成链接规则,其中包括目标文件的输出路径,最终会生成一下 makefile
    指令 g++ main.o -o myexe build/libmylib.a 这样,链接器(g++/ld)就能找到并链接libmylib.a。

下面演示如何查看输出路径:

相关推荐
你赖东东很不错嘛_2 小时前
【C语言】char * 、char [ ]、const char * 和 void *的使用以及区别
c语言
青草地溪水旁2 小时前
设计模式(C++)详解——策略模式(1)
c++·设计模式·策略模式
secondyoung2 小时前
Markdown转换为Word:Pandoc模板使用指南
开发语言·经验分享·笔记·c#·编辑器·word·markdown
lly2024062 小时前
Django ORM - 聚合查询
开发语言
余衫马3 小时前
llama.cpp:本地大模型推理的高性能 C++ 框架
c++·人工智能·llm·llama·大模型部署
胖咕噜的稞达鸭3 小时前
算法入门:专题攻克主题一---双指针(2)快乐数 呈最多水的容器
开发语言·数据结构·c++·算法
沐知全栈开发3 小时前
Perl 简介
开发语言
孞㐑¥3 小时前
Linux网络部分—网络层
linux·c++·经验分享·笔记
班公湖里洗过脚4 小时前
第1章 线程安全的对象生命期管理
c++