Cmake学习笔记
- 1. Cmake是什么?
- 2. 为什么需要Cmake?
- 3. Cmake语法
-
- 3.1 注释
- 3.2 cmake_minimun_required
- 3.3 project
- 3.4 add_executable
- 3.6 set
- 3.7 aux_source_directory
- 3.8 file
- 3.9 include_directories
- 3.10 target_include_directories
- 3.11 CMAKE_CXX_STANDARD
- 3.12 EXECUTABLE_OUTPUT_PATH
- 3.13 add_library
- 3.14 LIBRARY_OUTPUT_PATH
- 3.15 target_link_libraries
- 3.16 message
- 3.17 list
- 3.18 常用宏
- 3.19 add_subdirectory
- 4. 流程控制
1. Cmake是什么?
CMake 是一个跨平台的构建系统生成工具,它的核心作用是帮助开发者在不同操作系统(如Windows、Linux、macOS)和不同编译环境(如 GCC、Clang、MSVC)下,自动生成适配当前环境的构建系统配置文件(比如 Makefile、Ninja 脚本、Visual Studio 项目文件、Xcode 项目等),从而简化跨平台项目的编译流程。
2. 为什么需要Cmake?
在没有 CMake 之前,如果你想让一个项目在 Windows(用 Visual Studio)、Linux(用 Make)、macOS(用 Xcode)上都能编译,需要手动为每个平台编写对应的构建脚本(比如 Windows 写 .sln,Linux 写 Makefile)。这不仅繁琐,还容易因为平台差异导致脚本不一致,维护成本极高。
CMake 解决了这个问题:你只需要编写一份平台无关的配置文件(名为 CMakeLists.txt),描述项目的源码结构、依赖库、编译选项等信息,然后 CMake 会根据当前系统环境,自动生成对应平台的构建文件(比如在 Linux 上生成 Makefile,在 Windows 上生成 Visual Studio 项目)。之后你直接用对应平台的构建工具(如 make、ninja、Visual Studio 编译器)编译即可。
CMake 的工作流程
- 编写 CMakeLists.txt:开发者在项目根目录下编写 CMakeLists.txt,定义项目名称、源码文件、依赖库、编译选项(如 -O2 优化)、输出目录等。
示例:(简单的 C++ 项目):
cmake_minimum_required(VERSION 3.10) # 最低 CMake 版本要求
project(MyProject) # 项目名称
add_executable(myapp main.cpp) # 定义可执行文件,依赖 main.cpp
- 运行 cmake 命令,指定源码目录(通常是 CMakeLists.txt 所在目录)和构建目录(存放生成的文件),并可选指定目标构建系统(如 -G “Ninja” 生成 Ninja 脚本)。
示例: cmake … -G “Ninja”(假设当前在构建目录,… 是源码目录)。
- 编译项目:使用生成的构建系统工具(如 ninja、make、或直接打开 Visual Studio 项目)执行编译,生成可执行文件或库。
示例:ninja(如果生成了 Ninja 脚本)或 make(如果生成了 Makefile)。
Cmake和其它工具的区别:
- Cmake不是编译器:他不直接编译代码,只是生成编译所需要的配置文件。实际的编译过程还是由GCC、G++、MSVN、Clang等编译器来完成的。
- Cmake不是构建工具:Make、Ninja、Visual Studio的msbuild才是构建工具,Cmake只是个“构建工具的生成器”。
总结:
CMake 的核心价值是跨平台一致性:通过一份 CMakeLists.txt,让项目在不同系统、不同工具链下都能便捷编译,避免手动维护多套平台相关的构建脚本。这也是它被广泛用于 C/C++ 大型项目(如 Qt、LLVM、OpenCV 等)的原因。
3. Cmake语法
Cmake命令是依赖CMakeLists.txt文件跑起来的,因此想要运行Cmake命令,我们必须先写一个CMakeLists.txt文件,这个文件名字不要写错了,大小写敏感!我们一般会将其放在于“源文件”同级的目录下;
3.1 注释
3.1.1 行注释
通过“
#
”开头的叫做行注释
3.1.2 块注释
如果我们想要达到多行注释的效果,可以使用:“
#[[这里写注释]]
”这样的方式来实现:
3.2 cmake_minimun_required
语法:
cmake_minimum_required(VERSION <min>[...<policy_max>] [FATAL_ERROR])
功能:
指定cmake运行CMakeLists.txt文件的最低版本要求;如果cmake版本低于了指定版本会报错;
常用参数:
VERSION: 指定cmake运行的最低版本要求
实战:
cmake_minimum_required(VERSION 3.16)
3.3 project
语法:
project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[COMPAT_VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
功能:
设置项目的名称,并将其存储在PROJECT_NAME变量里,当在顶级的CMakeLists.txt文件调用时,也将项目名称设置进CMAKE_PROJECT_NAME变量中。
常用参数:
PROJECT-NAME: 项目名称
language-name : 项目所使用的编程语言;
实战:
# 设置项目名称
project(CMakeTest)
# 设置项目名称和编程语言
project(CMakeTest CXX) # CXX就是C++
3.4 add_executable
语法:
add_executable(<name> <options>... <sources>...)
功能:
生成一个可执行程序,依赖命令调用中列出的源文件构建。这个name必须在项目范围内全局唯一;
实战:
cmake_minimum_required(VERSION 3.16)
project(CmakeTest CXX)
add_executable(main main.cpp)
下面是main.cpp的内容
#include <iostream>
int main()
{
std::cout << "Hello World!" << std::endl;
return 0;
}
cmake运行效果:
实际运行效果:
再来一个复杂一点的:
// add.h
#pragma once
int Add(int,int);
// add.cpp
#include"add.h"
int Add(int x,int y){
return x+y;
}
// sub.h
#pragma once
int Sub(int,int );
// sub.cpp
#include"sub.h"
int Sub(int x,int y){
return x-y;
}
// mul.h
#pragma once
int Mul(int,int);
// mul.cpp
#include"mul.h"
int Mul(int x,int y){
return x*y;
}
// div.h
#pragma once
int Div(int,int);
//div.cpp
#include"div.h"
int Div(int x,int y){
if(y==0) return 0;
return x/y;
}
//main.cpp
#include"head.h"
#include<stdio.h>
int main(){
int a=130;
int b=20;
printf("Add(%d,%d)=%d\n",a,b,Add(a,b));
printf("Sub(%d,%d)=%d\n",a,b,Sub(a,b));
printf("Mul(%d,%d)=%d\n",a,b,Mul(a,b));
printf("Div(%d,%d)=%d\n",a,b,Div(a,b));
return 0;
}
对应的CMakeLists.txt文档编写:
cmake_minimum_required(VERSION 3.16)
project(CmakeTest CXX)
add_executable(main main.cpp add.cpp sub.cpp mul.cpp div.cpp)
运行结果:
3.6 set
除了向上面那样将生成可执行程序“main”依赖的源文件全部写在add_executable函数的后面之外,我们其实也可以将依赖的源文件先存储在一个“变量”中,后续再取出这个变量的值,放入到add_executable函数的后面也可;
语法:
set(<variable> <value>... [PARENT_SCOPE])
功能:
在当前函数或目录作用域内设置一个变量;
实战:
运行cmake命令,并展示运行结果:
3.7 aux_source_directory
使用set的方法来保存依赖的源文件是个方法,但不是个好方法,如果我们的项目比较打,有很多个源文件,那么我们难道要一个一个手动的将这些源文件添加到set设置的变量中去吗?这是不现实的,因此cmake推出了aux_source_ditectory函数,我们只需要告诉他要去搜索的路径,他就会去特定的路径下进行搜索;
语法:
aux_source_directory(<dir> <variable>)
功能:
在dir目录下搜索源文件,并将其放在variable变量中;
aux_source_directory不会递归搜索dir目录的子目录下的源文件;
实战:
执行结果:
3.8 file
上面的aux_aource_directory固然好用,但是他却有个致命的缺点,就是无法去dir的子目录下进行搜索,为了弥补这一缺点,我们就需要使用file函数来弥补;
语法:
file({GLOB | GLOB_RECURSE} <out-var> [...] <globbing-expr>...)
功能:
去指定目录下匹配特定的文件,并将其当如out-var变量中;
常用选项:
GLOB: 不会去递归子目录;
GLOB_RECURSE: 会去递归子目录;
实战:
为了方便管理,我们对文件结构进行了如下调整,也推荐在实际的项目开发中,按照如下目录来管理项目:
CMakeLists.txt文件的编写:
在本次的CMakeLists.txt文件的编写中我们使用到了CMAKE_CURRENT_LIST_DIR变量,这是Cmake内置的变量,该变量里面存的是当前CMakeLists.txt文件的绝对目录;
同时我们也用到了include_directories函数,我们使用该函数来指明编译器后续应该去哪里寻找头文件;
运行结果:
3.9 include_directories
语法:
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])
功能:
将给定目录添加到编译器用于查找包含文件的目录中;
实战:
见上文;
3.10 target_include_directories
语法:
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...] [<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
功能:
指定编译target目标时,去那里寻找头文件,比include_directories控制的更加准确,include_directories设置的属性对全局目标都生效,但是target_include_directories 只对target目标生效;
常用选项:
PRIVATE:当目标的头文件仅限于被自身使用,而不希望被其他目标使用,那么建议用PRIVATE;
PUBLIC:当目标的头文件不仅自己需要用,也希望被依赖它的目标所使用时,那么建议PUBLIC;
INTERFACE:当目标自身不需要用,但是希望被依赖他的目标所使用时,那么建议INTERFACE;eg:
PRIVATE:路径 “自己用,不给别人”
PUBLIC:路径 “自己用,也给别人”
INTERFACE:路径 “自己不用,只给别人”
实战:
运行结果:
3.11 CMAKE_CXX_STANDARD
功能:这是一个设置C++编译标准的变量,有两种指定方式;
- 通过set设置;
- 通过cmake命令行指定
实战:
通过命令行指定C++编译标准为C++17,这里我们需要使用到-D选项;
3.12 EXECUTABLE_OUTPUT_PATH
当我们使用cmake命令生成Makefile文件之后,我们会使用make命令来编译我们的目标程序,而生成的目标程序默认放在当前的构建目录下,但是有些时候为了管理的方便,我们希望将生成的可执行程序都放到指定的目录下,那么这时候我们就可以利用EXECUTABLE_OUTPUT_PATH变量来设置可执行程序的输出路径;如果未指定的话,那么默认输出路径就是当前构建目录;
实战:
接着我们来看看目前的目录结构:
紧接着我们进入build目录进行cmake构建:
同样的我们也可以通过命令行指定,当时在cmake中命令行指定的优先级低于CMakeLists.txt中set设置的:
3.13 add_library
语法:
add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...)
功能:
创建一个动态库或静态库;
常用选项:
name:库名,“掐头去尾”,eg:libmyku.so 的库名就是myku;
type:STATIC:创建一个静态库; SHARED:创建一个共享库;
sources:依赖的源文件;
实战:
目前的目录结构:
现在我们的目标是将src目录下的代码打包成一个静态库,然后利用链接静态库的方式来生成我们的可执行目标;
运行cmake过后:
生成动态库:
3.14 LIBRARY_OUTPUT_PATH
功能:
设置静态库和动态库的输出路径;
3.15 target_link_libraries
语法:
target_link_libraries(<target> <PRIVATE|PUBLIC|INTERFACE> <item>... [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
功能: 链接一个动态库或静态库,控制范围比link_libraries更加精确;link_libraries会给所有的target目标进行链接,但是target_link_libraries只会给指定的目标进行链接;
常用选项:
与target_include_directories 的使用方法一致;
实战:
实际运行结果:
3.16 message
语法:
message([<mode>] "message text" ...)
功能:
在日志中记录指定的消息文本。如果给出了多个消息字符串,那么它们将连接成一条消息,字符串之间没有分隔符。
常用选项:
mode:
FATAL_ERROR:Cmake打印致命消息,并停止当前cmake的处理和运行;
WARNING:打印Warning类型信息,但不会停止cmake执行;
AUTHOR_WARNING:打印AUTHOR_WARNING类型信息,但不会停止cmake执行;
SEND_ERROR:CMake 错误,继续处理,但跳过生成
STATUS:非重要消息
CMake的命令行工具会在stdout上显示STATUS消息,在stderr上显示其他所有消息。
实战:
运行Cmake:
3.17 list
语法:
list(APPEND <list> [<element>...])
功能:
追加元素并输出一个list
实战:
执行cmake:
当然除了使用list进行追加字符串操作外,我们还可以利用set函数来实现字符串追加功能:
实际上使用set命令来追加一个字符串,实际上是也是创建了一个list来管理追加的元素,在list底层,它使用
;
来分割一个一个的元素; 但是我们在最终打印list的时候,它不会把;
打印出来;
我们重点看一下SET2的len2:
通过运行结果我们可以发现,len2的长度是5,但是我们只追加了两个元素,预期不应该是2吗?
如果我们按照list底层使用;
的来进行分割的方式去解释,我们就会得到原因:
CMake 将字符串按;
分割为列表元素,连续的分号会产生空元素。
对"a;b;c;;d"分割后,得到的元素是:
第 1 个:a
第 2 个:b
第 3 个:c
第 4 个:(空字符串,因为两个分号之间无内容)
第 5 个:d
当然list除了追加功能外,还有许多其它功能:
Reading
list(LENGTH <list> <out-var>)
list(GET <list> <element index> [<index> ...] <out-var>)
list(JOIN <list> <glue> <out-var>)
list(SUBLIST <list> <begin> <length> <out-var>)
Search
list(FIND <list> <value> <out-var>)
Modification
list(APPEND <list> [<element>...])
list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>)
list(INSERT <list> <index> [<element>...])
list(POP_BACK <list> [<out-var>...])
list(POP_FRONT <list> [<out-var>...])
list(PREPEND <list> [<element>...])
list(REMOVE_ITEM <list> <value>...)
list(REMOVE_AT <list> <index>...)
list(REMOVE_DUPLICATES <list>)
list(TRANSFORM <list> <ACTION> [...])
Ordering
list(REVERSE <list>)
list(SORT <list> [...])
3.18 常用宏
在实际的开发中,我们可能会使用相关的日志管理系统来输出我们的日志消息,而我们可以通过设置对应的“宏”来控制日志输出的等级;
比如我们将控制日志输出等级的“宏” 设置为为warning ,那么就意味着warning以上的日志类型才会被输出,warning以下的就不会被输出;
而这个“宏”的值,我们可以在输入编译命令的时候来指定;
eg:
运行结果:
当然除了上面的宏,还有其它的宏:
宏 | 功能 |
---|---|
PROJECT_SOURCE_DIR | 使用cmake命令后紧跟的目录,一般是工程的根目录 |
PROJECT_BINARY_DIR | 执行cmake命令的目录 |
CMAKE_CURRENT_SOURCE_DIR | 当前处理的CMakeLists.txt所在的路径 |
CMAKE_CURRENT_BINARY_DIR | target 编译目录 |
EXECUTABLE_OUTPUT_PATH | 重新定义目标二进制可执行文件的存放位置 |
LIBRARY_OUTPUT_PATH | 重新定义目标链接库文件的存放位置 |
PROJECT_NAME | 返回通过PROJECT指令定义的项目名称 |
CMAKE_BINARY_DIR | 项目实际构建路径,假设在build目录进行的构建,那么得到的就是这个目录的路径 |
3.19 add_subdirectory
前面的示例都是一些比较简单的示例,但是在实际的工程中我们的项目肯定是巨复杂的,我们再通过顶层的CMakeLists.tx去管理整个项目就会让顶层的CMakeLists.txt文件变的比较臃肿,同时也难以维护,各个模块之间耦合性也会变强;
根据"高内聚,低耦合"的开发思想,我们可以在每个子文件中都写一个CMakeLists.txt文件,每个子文件中的CMakeLists.txt文件就管自己本文件下的工作,各个子文件之间彼此独立,耦合性就会大大降低,而顶层的CMakeLists.txt文件就可以作为这些子文件的“父节点”来定义一些公共变量和“连接”子文件的CMakeLists.txt的工作等,而实现“父节点”与“字节点”的连接纽带的就是"add_subdirectory";
语法:
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
功能:
将子目录添加到构建中。source_dir 指定子目录 CMakeLists.txt 和代码文件所在的目录。如果它是相对路径,则将相对于当前目录(典型用法)进行评估,但它也可能是绝对路径。binary_dir 指定放置输出文件的目录。如果它是相对路径,则将相对于当前输出目录进行计算,但它也可能是绝对路径。如果未指定 binary_dir,则 在扩展任何相对路径之前,将使用 source_dir(典型用法)。CMake 将立即处理指定源目录中的 CMakeLists.txt 文件,然后在此命令之后继续处理当前输入文件。
实战:目前的文件结构如下:
bin目录:存放生成的可执行程序;
build目录:构建目录;
include/cacl:存放计算器相关的头文件目录;
include/sort: 存放排序相关的头文件目录;
lib:存放生成的动态库或静态库的目录;
src/cacl:存放计算器相关的源文件目录;
src/sort: 存放排序相关的源文件目录;
test1:使用排序算法的main函数;
test2:使用计算器相关功能的main函数;
紧接着我们来说明一下实验开始的思路:
我们希望,通过连接动态库的方式来生成可执行程序MainSort和MainCacl;
因此src/sort/CMakeLists.txt文件就负责打包sort动态库;
src/cacl/CMakeLists.txt文件复制打包cacl动态库;
test1/CMakeLists.txt负责构建MainSort可执行程序;
test2/CMakeLists.txt负责构建MainCacl可执行程序;
顶层CMakeLists.txt负责进行定义一些公共变量和管理子目录CMakeLists.txt文件的工作:
接下来我们来看看各个目录下的CMakeLists.txt文件的编写:
- 顶层CMakeLists.txt:
- src/cacl/CMakeLists.txt
- src/sort/CMakeLists.txt
- test1/CMakeLists.txt
- test2/CMakeLists.txt
执行Cmake命令并执行make命令:
查看bin目录和lib目录是否有对应文件生成:
运行结果:
4. 流程控制
4.1 条件判断
if(<condition>)
<commands>
elseif(<condition>) # 可选快, 可以重复
<commands>
else() # 可选快
<commands>
endif()
在进行条件判断的时候,如果有多个条件,可以写多个elseif 这一点和主流编程语言的语法一致;
需要注意的是if结束后需要写endif();
4.1.1 基本表达式
if(<expression>)
expression可以是常量、字符串、变量;
当expression为以下值时,if条件为true:
非0数字
、YES
、Y
、ON
、非空字符串
、TRUE
当expression为以下值时,if条件为false
0
、NO
、N
、OFF
、空字符串
、FALSE
4.1.2 逻辑判断
// NOT 取反操作
if(NOT <condition>)
// 并且
if(<cond1> AND <cond2>)
// 或者
if(<cond1> OR <cond2>)
4.1.3 比较
if(<variable|string> LESS <variable|string>)
if(<variable|string> GREATER <variable|string>)
if(<variable|string> EQUAL <variable|string>)
if(<variable|string> LESS_EQUAL <variable|string>)
if(<variable|string> GREATER_EQUAL <variable|string>)
以上是基于数值的比较:
LESS: 小于
GREATER:大于
EQUAL:等于
LESS_EQUAL:小于等于
GREATER_EQUAL:大于等于
比较运算符用于数值比较,CMake 会自动将字符串尝试转换为数字;
如果比较非数值(且无法转换为数字的字符串),会被视为 0,可能导致不符合预期的结果,因此建议仅对明确的数值变量使用这些运算符。
if(<variable|string> STRLESS <variable|string>)
if(<variable|string> STRGREATER <variable|string>)
if(<variable|string> STREQUAL <variable|string>)
if(<variable|string> STRLESS_EQUAL <variable|string>)
if(<variable|string> STRGREATER_EQUAL <variable|string>)
以上是基于字符串的比较
STRLESS:如果左侧字符串小于右侧,返回True
STRGREATER:如果左侧字符串大于右侧,返回True
STREQUAL:如果左侧字符串等于右侧,返回True
STRLESS_EQUAL:如果左侧字符串小于等于右侧,返回True
STRGREATER_EQUAL:如果左侧字符串大于等于右侧,返回True
4.1.4 文件操作
- 判断文件或目录是否存在:
if(EXISTS path-to-file-or-directory)
- 判断是不是目录
//此处目录的 path 必须是绝对路径
if(IS_DIRECTORY path)
- 判断是不是软连接
if(IS_SYMLINK file-name)
- 判断是不是绝对路径
if(IS_ABSOLUTE path)
- 判断某个元素是否在列表中
if(<variable|string> IN_LIST <variable>)
- 比较两个路径是否相等
if(<variable|string> PATH_EQUAL <variable|string>)
4.2 循环
在cmake中有两种循环:foreach、while
4.2.1 foreach
foreach(<loop_var> <items>)
<commands>
endforeach()
4.2.2 while
while(<condition>)
<commands>
endwhile()