当前位置: 首页 > news >正文

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:

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

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

sudo apt install cmake cmake-doc cmake-extras
  1. 安装完成后,通过以下命令查看 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插件:

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

2.3HelloWorld快速搭建

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

//目录结构
testCMake/
├── CMakeLists.txt
└── main.cc
  1. 创建main.cc文件,写好程序:
    在这里插入图片描述
  2. 创建CMakeLists.txt文件,注意不要写错了名字! 在此文件中设置三行内容:
    在这里插入图片描述
    如果项目中使用了高版本 CMake 才支持的特性(例如特定的函数、生成器表达式、目标属性等),而用户本地安装的cmake版本低于项目要求的版本,就会出现无法解释或者产生不可预知的行为,其次要设置要生成的项目名称以及可执行程序名称(main main.cc代表由main.cc生成main可执行程序)
  3. 运行CMake:
  • 选择在当前路径下cmake:
cmake .

如果生成了cmake_install.cmake文件夹,以及CMakeCache.txt说明生成成功
在这里插入图片描述

  • 现在惊喜的发现目录中多出了makefile文件,这说明cmake自动帮我们生成了需要我们手写的makefile文件,直接make即可
make

在这里插入图片描述

  • 运行main程序
./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.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 生成构建系统

这里可以采用三种方式:

  1. cmake [options] <path-to-source>
    将当前工作目录作为「构建树」, 作为「源码树」(源码树必须包含 CMakeLists.txt,且不能存在 CMakeCache.txt—— 否则会被识别为 “已有构建树”)。
mkdir build && cd build
cmake ../src  # 构建树是当前`build`目录,源码树是`../src`
  1. cmake [options] <path-to-existing-build>
    <path-to-existing-build> 作为「构建树」,并从该目录的 CMakeCache.txt 中加载源码树的路径(因此该构建目录必须是 “之前 CMake 运行过、已生成 CMakeCache.txt” 的目录)。
cd build
cmake .  # 构建树是当前`build`目录,源码树从`build/CMakeCache.txt`加载
  1. 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 变量的使用场景:

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

PROJECT_VERSION 变量的使用场景:

  1. 打印变量
  2. 生成pkg-config或者.cmake对应的版本配置文件
  3. 动态库/静态库的的版本号
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_FILECMAKE_CURRENT_LIST_DIR 能真实定位正在执行的cmake文件,include的本质是在当前构建环境中,插入执行另一个文件的代码”—— 不会创建新的构建模块,只是代码片段的 “拼接执行”,所以不会改变CMAKE_CURRENT_SOURCE_DIRCMAKE_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
目标的种类类型创建命令产物例子 / 说明
EXECUTABLEadd_executablemain ,curl
STATICadd_library(… STATIC)libfoo.a, foo.lib
SHAREDadd_library(… SHARED)libfoo.so, foo.dll
MODULEadd_library(… MODULE)插件:libplugin.so, 使用dlopen 运行时加载
OBJECTadd_library(… OBJECT)仅 .o/.obj,存在于内存,不生成库文件
INTERFACEadd_library(… INTERFACE)无库文件,携带使用要求
IMPORTEDadd_library(… IMPORTED)使用cmake 内存目标对象引用磁盘上的外部构建产物
ALIASadd_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_propertiesCOMPILE_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.txtCMakeCache.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,默认,后搜索)到目标已有的包含路径中。
关键字自身目标编译时是否使用是否传播给“依赖该目标”的其他目标填充的目标属性
PRIVATEINCLUDE_DIRECTORIES
PUBLICINCLUDE_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_dircmake会在构建树里创建同名的子目录,用于保存子目录的的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

函数作用:

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

基本形式

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)。
http://www.dtcms.com/a/434419.html

相关文章:

  • MySQL 5.7 主主复制 + Keepalived 高可用配置实例
  • 2014 年真题配套词汇单词笔记(考研真相)
  • 构建AI智能体:五十一、深思熟虑智能体:从BDI架构到认知推理的完整流程体系
  • 自由学习记录(104)
  • 【开题答辩全过程】以 ssm蛋糕销售网站的设计与实现为例,包含答辩的问题和答案
  • Photoshop - Photoshop 工具从工具栏消失
  • 专题网站建设策划dw一个完整网页的代码
  • 刷赞网站推广免费链接网站后台怎么添加栏目
  • LLM 笔记 —— 01 大型语言模型修炼史(Self-supervised Learning、Supervised Learning、RLHF)
  • 框架系统在自然语言处理深度语义分析中的作用、挑战与未来展望
  • LLM 笔记 —— 03 大语言模型安全性评定
  • d-分离:图模型中的条件独立性判定准则
  • 【自然语言处理】文本规范化知识点梳理与习题总结
  • 上海商城网站建设公司算命手机网站开发
  • 重塑Excel的智慧边界:ExcelAgentTemplate架构深度解析与LLM集成最佳实践
  • QoS之拥塞避免配置方法
  • vscode搭建C/C++配置开发环境
  • 在鸿蒙NEXT中发起HTTP网络请求:从入门到精通
  • 做网站商家网站公告栏代码
  • 做企业网站联系群晖网站建设
  • Java坐标转换的多元实现路径:在线调用、百度与高德地图API集成及纯Java代码实现——纯Java代码实现与数学模型深度剖析
  • 【socket编程中的常规操作,阻塞/非阻塞模式的差别】
  • 5G NR PDCCH DCI
  • 网站建设海淀区360浏览器打开是2345网址导航
  • 《代码随想录》二叉树专题算法笔记
  • CSS3 用户界面
  • 虚幻引擎UE5专用服务器游戏开发-32 使用Gameplay Tags阻止连招触发
  • 鼠标垫东莞网站建设网站建设公司的公司
  • SOAR技术与高效网络安全运营
  • Node.js 本地服务部署、常驻及调用完整笔记