CMake基础
一、准备知识
1.1 C++的编译过程
使用g++
等编译工具,从源码生成最终的可执行文件一般有这几步:预处理(Preprocess)、编译(Compile)、汇编(assemble)、链接(link)。

输入
g++ --help
可以看到对应命令:
-E Preprocess only; do not compile, assemble or link.
-S Compile only; do not assemble or link.
-c Compile and assemble, but do not link.
-o <file> Place the output into <file>.
以下面程序为例:
#include <iostream>
int main() {
std::cout << "Hello World!" << std::endl;
return 0;
}
-
第一步:预处理
C++中预处理指令以#
开头。在预处理阶段,会对#define
进行宏展开,处理#if,#else
等条件编译指令,递归处理#include
。这一步需要我们添加所有头文件的引用路径。# 将xx.cpp源文件预处理成xx.i文件(文本文件) g++ -E main.cpp -o main.i
-
第二步:编译
检查代码的规范性和语法错误等,检查完毕后把代码翻译成汇编语言文件。
# 将xx.i文件编译为xx.s的汇编文件(文本文件) g++ -S main.i -o main.s
-
第三步:汇编
基于汇编语言文件生成二进制格式的目标文件。# 将xx.s文件汇编成xx.o的二进制目标文件 g++ -c main.s -o main.o
-
第四步:链接
将目标代码与所依赖的库文件进行关联或者组装,合成一个可执行文件
# 将xx.o二进制文件进行链接,最终生成可执行程序 g++ main.o -o main
-
最后一步执行可执行文件
./main
1.2 静态链接库和动态链接库
所谓静态和动态,其区别是链接的阶段不一样。
-
静态链接库名称一般是
lib库名称.a
(.a
代表archive library
),其链接发生在编译环节。一个工程如果依赖一个静态链接库,其输出的库或可执行文件会将静态链接库*.a
打包到该工程的输出文件中(可执行文件或库),因此生成的文件比较大,但在运行时也就不再需要库文件了。 -
而动态链接库的链接发生在程序的执行过程中,其在编译环节仅执行链接检查,而不进行真正的链接,这样可以节省系统的开销。动态库一般后缀名为
*.so
(.so
代表shared object
,Linux:lib库名称.so
,macOS:lib库名称.dylib
)。动态链接库加载后,在内存中仅保存一份拷贝,多个程序依赖它时,不会重复加载和拷贝,这样也节省了内存的空间。 -
以下图为例
-
工程
A
和B
依赖静态链接库static library
,A
和B
在运行时,内存中会有多份static library
; -
工程
A
和B
依赖动态链接库shared library
,A
和B
在运行时,内存中只有一份shared library
(shared:共享)。
-
以上只是非常简单的一个解释以区分动态链接库和静态链接库。更多底层的知识需要单独进行深入讲解。
1.3 为什么需要CMake
1.3.1 g++ 命令行编译
main.cpp 代码:
#include <iostream>
int main(int argc, char** argv)
{
std::cout << "Hello World!" << std::endl;
return 0;
}
当我们编译附件中1.hello_world的main.cpp
时,我们可以运行,
g++ main.cpp -o main
当我们需要引入外部库时,如附件中的2.external_libs
,代码如下,需要引入gflags
(Google开源的命令行参数处理库),我们则需要运行:
main.cpp代码:
#include <iostream>
#include <gflags/gflags.h> // 引入gflags头文件
DEFINE_string(name, "", "姓名"); // 定义name参数,类型为string,缺省值为空,描述为“姓名”
DEFINE_int32(age, 0, "年龄"); // 定义age参数,类型为int32,缺省值为0,描述为“年龄”
int main(int argc, char **argv)
{
gflags::ParseCommandLineFlags(&argc, &argv, true); // 解析命令行参数
std::cout << "name: " << FLAGS_name << std::endl; // 输出name参数
std::cout << "age: " << FLAGS_age << std::endl; // 输出age参数
return 0;
}
安装和运行命令:
# 安装gflags
sudo apt-get install libgflags-dev libgflags2.2
# 安装opencv
sudo apt-get install libopencv-dev
// -lgflags表示链接gflags库,-o main表示输出文件名为main
g++ main.cpp -lgflags -o main
# 或者:
# 安装pkg-config
sudo apt-get install pkg-config
// pkg-config是一个工具,用于查找和管理安装在系统上的库文件,--cflags --libs gflags表示查找gflags库的头文件和库文件的路径,-o main表示输出文件名为main
g++ main.cpp `pkg-config --cflags --libs gflags` -o main
# 测试输出
./main --age 31 --name alice
输出:
name: hn
age: 19
有些时候有一些常用库我们也不用手动添加头文件或链接库路径,通常g++能在默认查询路径中找到他们。当我们的项目文件变得多起来,引入的外部库也多起来时,g++就不能找到外部库了,命令行编译这种方式就会变得十分臃肿,也不方便调试和编辑,所以这是为什么需要使用cmake的原因。通常在测试单个文件时会使用命令行进行编译,但不推荐在一个实际项目中使用命令行编译。
1.3.2 CMake简介
在实际工作中推荐使用CMake构建C++项目,CMake是用于构建、测试和软件打包的开源跨平台工具;
特性:
- 自动搜索可能需要的程序、库和头文件的能力;
- 独立的构建目录(如
build
),可以安全清理 - 支持复杂的自定义命令(下载、生成各种文件)
- 自定义配置可选组件
- 从简单的文本文件(
CMakeLists.txt
)自动生成工作区和项目的能力 - 在主流平台上自动生成文件依赖项并支持并行构建
- 几乎支持所有的IDE
二、CMake基础知识
Cmake的项目代码链接如下:
链接:https://pan.baidu.com/s/1M3r4pGzdr4Kp1VKQeMob8g
提取码:4skv
2.1 安装
ubuntu上请执行,但是版本不一定是最新版,
sudo apt install cmake -y
或者编译安装:
# 以v3.25.1版本为例
git clone -b v3.25.1 https://github.com/Kitware/CMake.git
cd CMake
# 你使用`--prefix`来指定安装路径,或者去掉`--prefix`,安装在默认路径。
./bootstrap --prefix=<安装路径> && make && sudo make install
# 验证
cmake --version
2.2 第一个CMake例子
附件位置:3.first_cmake
,包括CMakeLists.txt和main.cpp2个文件,
CMakeLists.txt代码:
# 单行注释
# 单行注释1
#[[
多行注释1
多行注释2
多行注释3
]]
# 指定版本
cmake_minimum_required(VERSION 3.10)
# 设置项目,项目名称、版本、描述、语言
project(main_test
VERSION 1.0.0
DESCRIPTION "main_test的项目描述"
LANGUAGES CXX
)
# 设定target目标:可执行文件、库文件、自定义命令
add_executable(main_test main.cpp)
main.cpp代码:
#include <iostream>
int main()
{
std::cout << "Hello World!" << std::endl;
return 0;
}
编译过程:
# 第一步:配置,-S 指定源码目录,-B 指定构建目录
cmake -S . -B build
# 第二步:生成,--build 指定构建目录
cmake --build build
# 运行
./build/first_cmake
输出:
Hello World!
vs code插件:
- 安装
twxs.cmake
做代码提示; - 安装
ms-vscode.cmake-tools
界面操作。
2.3 语法基础
2.3.1 指定版本
以附件:3.first_cmake/CMakeLists.txt
为例:
# CMake 最低版本号要求
cmake_minimum_required(VERSION 3.10)
# first_cmake是项目名称,VERSION是项目的版本号,DESCRIPTION是项目描述,LANGUAGES是项目语言
project(first_cmake
VERSION 1.0.0
DESCRIPTION "项目描述"
LANGUAGES CXX)
# 添加一个可执行程序,first_cmake是可执行程序名称,main.cpp是源文件
add_executable(first_cmake main.cpp)
命令cmake_minimum_required
来指定当前工程所使用的CMake最小版本,不区分大小写,通常用小写。VERSION
是这个函数的一个特殊关键字,版本的值在关键字之后。CMake中的命令大多和cmake_minimum_required
相似,不区分大小写,并有很多关键字来引导命令的参数输入(类似函数传参)。
2.3.2 设置项目
以附件:3.first_cmake/CMakeLists.txt
为例:
project(ProjectName
VERSION 1.0.0
DESCRIPTION "项目描述"
LANGUAGES CXX)
在CMakeLists.txt
的开头,都会使用project
来指定本项目的名称、版本(注意:是自己项目的版本)、介绍、与使用的语言。在project
中,第一个ProjectName
(例子中用的是first_cmake
)不需要参数,其他关键字都有参数。
2.3.3 添加可执行文件目标
以附件:3.first_cmake/CMakeLists.txt
为例:
# 设定target目标,target可以是可执行文件,库文件,自定义命令
add_executable(first_cmake main.cpp)
这里我们用到add_executable
,其中第一个参数是最终生成的可执行文件名以及在CMake中定义的Target
名。我们可以在CMake中继续使用Target
的名字为Target
的编译设置新的属性和行为。命令中第一个参数后面的参数都是编译目标所使用到的源文件。
2.3.4 生成静态库并链接
附件位置:4.static_lib_test
,其中文件夹:
account_dir
是创建静态链接库,
test_account
文件夹是调用静态链接库,
account_dir/CMakeLists.txt
是用来生成相应的静态库,
A.生成静态库
add_library(Account STATIC Account.cpp Account.h) 参数解释:
Account:库的名称,
STATIC :用于指定链接库为动态链接库(SHARED
)还是静态链接库(STATIC
),
Account.cpp Account.h:用的源码的路径,
account_dir/CMakeLists.txt 代码:
#account_dir/CMakeLists.txt
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目信息
project(Account)
# 添加静态库,Linux下会生成libAccount.a,也可以不用 Account.h
add_library(Account STATIC Account.cpp Account.h)
通过以下命令生成,
cmake -S . -B build
cmake --build build
# 编译静态库后,会在build下生成 build/libAccount.a 静态库文件
account_dir/
├── Account.cpp
├── Account.h
├── build
│ └── libAccount.a
└── CMakeLists.txt
这里我们用到add_library
, 和add_executable
一样,Account
为最终生成的库文件名(lib库名称.a
),第二个参数是用于指定链接库为动态链接库(SHARED
)还是静态链接库(STATIC
),后面的参数是需要用到的源文件,libAccount.a 就是生成的静态库文件。
B.链接
test_account/CMakeLists.txt 代码:
# test_account/CMakeLists.txt
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目名称
project(test_account)
# 添加执行文件
add_executable(test_account test_account.cpp)
# 添加头文件目录,如果不添加,找不到头文件
target_include_directories(test_account PUBLIC "../account_dir")
# 添加库文件目录,如果不添加,找不到库文件,test_account可换成Account
target_link_directories(test_account PUBLIC "../account_dir/build")
# 添加目标链接库,用的 Account,而不是 libAccount.a,因为如果需要跨平台名称只能用Account,
target_link_libraries(test_account PRIVATE Account)
# 编译后目录如下
4.static_lib_test/
├── account_dir
│ ├── Account.cpp
│ ├── Account.h
│ ├── build
│ │ └── libAccount.a
│ └── CMakeLists.txt
└── test_account
├── build
│ └── test_account
├── CMakeLists.txt
└── test_account.cpp
我们通过add_library
和add_executable
定义了Target
,我们可以通过Target
的名称为其添加属性,例如:
# 指定目标包含的头文件目录
target_include_directories(test_account PUBLIC "../account_dir")
# 添加库文件目录,如果不添加,找不到库文件
target_link_directories(test_account PUBLIC "../account_dir/build")
# 指定目标链接的库
target_link_libraries(test_account PRIVATE Account)
输出:
构造函数Account::Account()
test Account 的main函数
析构函数Account::~Account()
- 通过
target_include_directories
,我们给test_account
添加了头文件引用路径"../account_dir"
。上面的关键词PUBLIC
,PRIVATE
用于说明目标属性的作用范围,更多介绍参考下节。 - 通过
target_link_libraries
,将前面生成的静态库libAccount.a
链接给对象test_account
,但此时还没指定库文件的目录,CMake无法定位库文件 - 再通过
target_link_directories
,添加库文件的目录即可。
2.3.5 生成动态库并连接
附件位置:5.dynamic_lib_test
A.生成动态库
#account_dir/CMakeLists.txt
# 添加动态库,Linux下会生成libAccount.so
add_library(Account SHARED Account.cpp Account.h)
# 编译动态库后,会在build下生成 build/libAccount.so 动态库文件
account_dir/
├── Account.cpp
├── Account.h
├── build
│ └── libAccount.so
└── CMakeLists.txt
B.链接
和静态库的 CMakeLists.txt 一样,操作不变。
# ldd查看可执行文件所依赖的动态库
libAccount.so => /home/enpei/Documents/course_cpp_tensorrt/course_5/src/5.dynamic_lib_test/test_account/../account_dir/build/libAccount.so (0x00007fb692cf1000)
当然,也可以用一个CMakeLists.txt
来一次性编译,参考附件6.build_together
#6.build_together/CMakeLists.txt
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目信息
project(test_account)
# 添加动态库
add_library(Account SHARED "./account_dir/Account.cpp" "./account_dir/Account.h")
# 添加可执行文件
add_executable(test_account "./test_account/test_account.cpp")
# 添加头文件,test_account可换成Account
target_include_directories(test_account PUBLIC "./account_dir")
# 添加链接库,因为 6.build_together/CMakeLists.txt 和 6.build_together/build 在同一个文件夹下,所以不需要添加库文件目录
target_link_libraries(test_account Account)
输出:
构造函数Account::Account()
test Account 的main函数
析构函数Account::~Account()
2.3.6 CMake 中的 PUBLIC、PRIVATE、INTERFACE
CMake中经常使用target_...()
类似的命令,一般这样的命令支持通过PUBLIC
、PRIVATE
、INTERFACE
关键字来控制传播。
以target_link_libraries(A B)
为例,从理解的角度来看
PRIVATE
:依赖项B
仅链接到目标A
,如果有C
链接了A
,C
不会链接B
INTERFACE
:依赖项B
并不链接到目标A
,如果有C
链接了A
,C
会链接B
PUBLIC
:依赖项B
链接到目标A
,如果有C
链接了A
,C
也会链接B
其实就是对象属性的传递,打个散烟的比方:
PRIVATE
: 就是自己抽,不给别人抽INTERFACE
:就是自己不抽,给别人抽PUBLIC
:就是自己抽,也给别人抽
从使用的角度来说,如果有C
链接了目标A
- 如果
B
仅用于A
的实现,且不在头文件中提供给C
使用,使用PRIVATE
- 如果
B
不用于A
的实现,仅在头文件中作为接口给C
使用,使用INTERFACE
- 如果
B
既用于A
的实现,也在头文件中提供给C
使用,使用PUBLIC
举例:
# 创建库
add_library(C c.cpp)
add_library(D d.cpp)
add_library(B b.cpp)
# C是B的PUBLIC依赖项
target_link_libraries(B PUBLIC C)
# D是B的PRIVATE依赖项
target_link_libraries(B PRIVATE D)
# 添加可执行文件
add_executable(A a.cpp)
# 将B链接到A
target_link_libraries(A B)
- 因为
C
是B
的PUBLIC
依赖项,所以C
会传播到A
- 因为
D
是B
的PRIVATE
依赖性,所以D
不会传播到A
2.3.7 变量
附件位置:7.message_var_demo
像其他编程语言一样,我们应该将CMake理解为一门编程语言。我们也需要设定变量来储存我们的选项,信息。有时候我们通过变量来判断我们在什么平台上,通过变量来判断我们需要编译哪些Target
,也通过变量来决定添加哪些依赖。
代码:
cmake_minimum_required(VERSION 3.10)
project(message_var_demo)
# 输出消息
message("输出消息") # 输出消息
message("输出1" "输出2" 输出3) # 会做拼接,输出1输出2输出3
# 设置变量
set(VAR1 "变量1")
message("VAR1=" ${VAR1}) # 外部访问, VAR1=变量1
message("输出变量VAR1:${VAR1}") # 内部拼接, 输出变量VAR1:变量1
message("\${VAR1}=${VAR1}") # 使用\转义, ${VAR1}=变量1
unset(VAR1) # 删除变量
message("\${VAR1}=${VAR1}") # 删除变量后,输出为空, ${VAR1}=+++
# 设置变量缓存,可以在命令行中修改(通过-D参数), 强制修改CACHE_VARIABLE_TEST的值 set(CACHE_VARIABLE_TEST "value" CACHE STRING "变量缓存的描述"
# FORCE),或者通过命令 cmake -S . -B build -DCACHE_VARIABLE_TEST=new value
# 通过后面加FORCE强制修改CACHE_VARIABLE_TEST的值,
set(CACHE_VARIABLE_TEST "new value" CACHE STRING "变量缓存的描述" FORCE)
message("变量缓存的值:${CACHE_VARIABLE_TEST}")
# 常见的内置的变量,更多访问:https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html#variables-that-provide-information
# 第一类:提供信息的变量
message("${PROJECT_NAME}") # 项目名称,message_var_demo
message("${CMAKE_SOURCE_DIR}") # 源码目录,/root/cmake_project/src/7.message_var_demo
message("${CMAKE_BINARY_DIR}") # 编译目录,/root/cmake_project/src/7.message_var_demo/build
message("${CMAKE_CURRENT_LIST_FILE}") # 当前CMakeLists.txt文件路径,/root/cmake_project/src/7.message_var_demo/CMakeLists.txt
# 第二类:控制CMake运行的变量,更多:https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html#variables-that-change-behavior
set(BUILD_SHARED_LIBS ON) # 设置是否构建动态库,默认为OFF,即构建静态库,设置为ON后,构建动态库
# 第三类:描述系统的变量,更多:https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html#variables-that-describe-the-system
message("是否是Windows系统:${WIN32}")
message("是否是Linux系统:${UNIX}")
message("系统名称:${CMAKE_SYSTEM_NAME}")
# 生成库
add_library(${PROJECT_NAME} Account.cpp Account.h)
2.3.8 include引入其他代码
附件位置:8.include_demo
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(include_demo)
message("调用include前的信息")
# include,引用一次就导入一次
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/module_1.cmake")
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/module_1.cmake")
message("调用include后的信息")
module_1.cmake:
message("模块内部被调用")
输出:
调用include前的信息
模块内部被调用
模块内部被调用
调用include后的信息
2.3.9 条件控制
附件位置:9.if_demo
正如前面所讲,应该把CMake当成编程语言,除了可以设置变量以外,CMake还可以写条件控制。
if(variable)
# 为true的常量:ON、YES、TRUE、Y、1、非0数字
else()
# 为false的常量:OFF、NO、FALSE、N、0、空字符串、NOTFOUND
endif()
可以和条件一起使用的关键词有
NOT, TARGET, EXISTS (file), DEFINED等
STREQUAL, AND, OR, MATCHES (regular expression), VERSION_LESS, VERSION_LESS_EQUAL等
CMakeLists.txt代码:
cmake_minimum_required(VERSION 3.10)
project(syntax_test)
message("====== if ======")
#[[
if 测试,语法如下:
if (<condition>)
<commands>
elseif (<condition>)
<commands>
else()
<commands>
endif() # 表示if语句结束
<condition>:可以是常量、变量、字符串
为true的常量:ON、YES、TRUE、Y、1、非0数字
为false的常量:OFF、NO、FALSE、N、0、空字符串、NOTFOUND
]]
# 常量 ===============================
if(1)
message("1 是 true")
endif()
if(0)
message("0 是 true")
else()
message("0 是 false")
endif()
# 未定义的变量 ===============================
if(UNDEFINED)
message("UNDEFINED 是 true")
else()
message("UNDEFINED 是 false")
endif()
# 定义了的变量 ===============================
set(DEFINED 1)
if(DEFINED)
message("DEFINED 是 true")
else()
message("DEFINED 是 false")
endif()
# 字符串 ===============================
# 把字符串当成常量
if("Y")
message("字符串是 true")
else()
message("字符串是 false")
endif()
message("====== if 配合逻辑运算符 ======")
#[[
if 配合逻辑运算符(AND、OR、NOT、括号)使用
if (<condition1> AND <condition2>) # 两个条件都为true
<commands>
endif()
if (<condition1> OR <condition2>) # 两个条件有一个为true
<commands>
endif()
if (NOT <condition>) # 条件为false
<commands>
endif()
if (<condition1> AND (<condition2> OR <condition3>)) # 先算括号里的
<commands>
endif()
]]
# AND ===============================
if(1 AND YES)
message("1 AND YES 是 true")
else()
message("1 AND YES 是 false")
endif()
# OR ===============================
if(1 OR NO)
message("1 OR NO 是 true")
else()
message("1 OR NO 是 false")
endif()
# NOT ===============================
if(NOT 0)
message("NOT 0 是 true")
else()
message("NOT 0 是 false")
endif()
# 括号 ===============================
if(1 AND (0 OR 1))
message("1 AND (0 OR 1) 是 true")
else()
message("1 AND (0 OR 1) 是 false")
endif()
输出:
====== if ======
1 是 true
0 是 false
UNDEFINED 是 false
DEFINED 是 true
字符串是 true
====== if 配合逻辑运算符 ======
1 AND YES 是 true
1 OR NO 是 true
NOT 0 是 true
1 AND (0 OR 1) 是 true
-- Configuring done
-- Generating done
-- Build files have been written to: /root/cpp_project/course_5_cmake/9.if_demo/build
2.3.10 CMake分步编译
附件位置:10.steps_demo
CMakeLists.txt代码:
# 查看所有目标
$ cmake -S . -B build
$ cd build
$ cmake --build . --target help
The following are some of the valid targets for this Makefile:
... all (the default if no target is provided)
... clean
... depend
... rebuild_cache
... edit_cache
... steps_demo
... main.o
... main.i
... main.s
# 1.预处理
$ cmake --build . --target main.i
# 输出:Preprocessing CXX source to CMakeFiles/steps_demo.dir/main.cpp.i
# 可以打开滑到底部
# 2.编译
$ cmake --build . --target main.s
# 输出汇编代码:Compiling CXX source to assembly CMakeFiles/steps_demo.dir/main.cpp.s
# 3.汇编
$ cmake --build . --target main.o
# 输出二进制文件:Building CXX object CMakeFiles/steps_demo.dir/main.cpp.o
# 链接
$ cmake --build .
Scanning dependencies of target steps_demo
[ 50%] Linking CXX executable steps_demo
[100%] Built target steps_demo
# 运行
./steps_demo
2.3.11 生成器表达式
附件位置:11.generator_expression
生成器表达式简单来说就是在CMake生成构建系统的时候根据不同配置动态生成特定的内容。有时用它可以让代码更加精简,我们介绍几种常用的。
需要注意的是,生成表达式被展开是在生成构建系统的时候,所以不能通过解析配置
CMakeLists.txt
阶段的message
命令打印,可以用类似file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<BOOL:TRUE>:TEST>")
生成文件的方式间接测试。
在其最一般的形式中,生成器表达式是$<...>
,尖括号中间可以是如下几种类型:
- 条件表达式
- 变量查询(Variable-Query)
- 目标查询(Target-Query)
- 输出相关的表达式
# 1.条件表达式:$<condition:true_string>,当condition为真时,返回true_string,否则返回空字符串
$<0:TEST>
$<1:TEST>
$<$<BOOL:TRUE>:TEST>
# 2.变量查询(Variable-Query)
$<TARGET_EXISTS:target>:判断目标是否存在
$<CONFIG:Debug>:判断当前构建类型是否为Debug
# 3.目标查询(Target-Query)
$<TARGET_FILE:target>:获取编译目标的文件路径
$<TARGET_FILE_NAME:target>:获取编译目标的文件名
# 4.输出相关表达式:用于在不同的环节使用不同参数,比如需要在`install`和`build`环节分别用不同的参数,我们可以这样写:
add_library(Foo ...)
target_include_directories(Foo
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
其中$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
仅在build
环节生效;而$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
仅在install
环节生效。通过设定不同阶段不同的参数,我们可以避免路径混乱的问题。
CMakeLists.txt代码:
cmake_minimum_required(VERSION 3.10)
project(generator_expression)
#[[
====================================================
1.条件表达式:$<condition:true_string>,当condition为真时,返回true_string,否则返回空字符串
示例:
$<0:TEST>
$<1:TEST>
$<$<BOOL:TRUE>:TEST>
]]
# 需要注意的是,生成表达式被展开是在生成构建系统的时候,所以不能通过解析配置`CMakeLists.txt`阶段的`message`命令打印,可以用类似`file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<BOOL:TRUE>:TEST>")`生成文件的方式间接测试。
# 用来测试
# file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<BOOL:TRUE>:TEST>")
#[[
====================================================
2.变量查询(Variable-Query)
$<TARGET_EXISTS:target>:判断目标是否存在
$<CONFIG:Debug>:判断当前构建类型是否为Debug
]]
# 添加一个可执行文件
# add_executable(generator_expression main.cpp)
# file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<TARGET_EXISTS:generator_expression>")
# file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<TARGET_EXISTS:generator_expression>:目标已存在>") # 嵌套
# 设置构建类型,Debug/Release/...
# SET(CMAKE_BUILD_TYPE "Debug")
# file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<CONFIG:Debug>:--coverage>") # 嵌套
#[[
====================================================
3.目标查询(Target-Query)
$<TARGET_FILE:target>:获取编译目标的文件路径
$<TARGET_FILE_NAME:target>:获取编译目标的文件名
]]
# 添加一个可执行文件
# add_executable(generator_expression main.cpp)
# 输出:/root/cmake_project/src/11.generator_expression/build/generator_expression
# file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<TARGET_FILE:generator_expression>")
2.3.12 函数和宏
附件位置:12.function_macro
# 定义一个宏,宏名为my_macro,没有参数
macro(my_macro)
message("宏内部的信息")
set(macro_var "宏内部变量test")
endmacro(my_macro)
# 定义一个函数,函数名为second_func,有两个参数
function(second_func arg1 arg2)
message("第一个参数:${arg1}, 第二个参数:${arg2}")
endfunction(second_func)
CMakeLists.txt代码:
cmake_minimum_required(VERSION 3.10)
project(function_macro_test)
# ====================================
# 定义一个宏,宏名为my_macro,没有参数
macro(my_macro)
message("宏内部的信息")
set(macro_var "宏内部变量test")
endmacro(my_macro) # 结尾需要用到的endmacro参数
# 调用宏
my_macro() # 宏内部的信息
my_macro() # 宏内部的信息
# 输出宏内部的信息,也能访问到变量,理解为代码替换
message(${macro_var}) # 宏内部变量test
# ====================================
# 定义一个宏,宏名为second_macro,有两个参数
macro(second_macro arg1 arg2)
message("第一个参数:${arg1}, 第二个参数:${arg2}")
endmacro(second_macro)
# 调用宏
second_macro("hello" "world") # 第一个参数:hello, 第二个参数:world
# ====================================
# 定义一个函数,函数名为my_func,没有参数
function(my_func)
message("函数内部的信息")
set(func_var "变量test")
endfunction(my_func)
# 调用函数
my_func() # 函数内部的信息
my_func() # 函数内部的信息
# 访问不了函数内部的变量,因为函数是一个独立的作用域
# message(${func_var})
# ====================================
# 定义一个函数,函数名为second_func,有两个参数
function(second_func arg1 arg2)
message("第一个参数:${arg1}, 第二个参数:${arg2}")
endfunction(second_func)
# 调用函数
second_func("hello" "world") # 第一个参数:hello, 第二个参数:world
输出:
宏内部的信息
宏内部的信息
宏内部变量test
第一个参数:hello, 第二个参数:world
函数内部的信息
函数内部的信息
第一个参数:hello, 第二个参数:world
-- Configuring done
-- Generating done
-- Build files have been written to: /root/cpp_project/course_5_cmake/12.function_macro/build
2.3.13 设置安装
附件位置:13.install_demo
当需要发布项目时你需要指定项目文件的安装路径。下面的代码片段中,使用install
安装demo_test
,并分别将可执行文件安装在bin
中,动态链接库和静态链接库都安装在lib
,公共头文件安装在include
。这里的路径都将添加${CMAKE_INSTALL_PREFIX}
作为前缀(如果不设置CMAKE_INSTALL_PREFIX
,则会安装到/usr/local
目录下)。实现安装的功能在你需要发布你项目给其他人使用时,非常有用。
参数解释:
TARGETS :可执行文件,动态链接库,静态链接库都可以看成 TARGETS
RUNTIME :代表可执行文件
LIBRARY :动态库
ARCHIVE:静态库
DESTINATION :目的地,安装在哪里
bin,lib:自定义的路径
# 设置安装
install(TARGETS demo_test slib dlib
RUNTIME DESTINATION bin # 可执行文件
LIBRARY DESTINATION lib # 动态库
ARCHIVE DESTINATION lib # 静态库
PUBLIC_HEADER DESTINATION include # 公共头文件
)
CMakeLists.txt 文件代码:
其中文件结构为:
CMakeLists.txt代码:
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目信息
project(install_demo)
# 添加公共头文件目录,代替12行和15行
include_directories(include)
# 添加静态库
add_library(slib STATIC src/slib.cpp include/slib.h)
# target_include_directories(slib PUBLIC include) # 这里只能用 slib,不能用 install_demo
# 添加动态库
add_library(dlib SHARED src/dlib.cpp include/dlib.h)
# target_include_directories(dlib PUBLIC include) # 这里只能用 dlib,不能用 install_demo
# 设置RPATH,否则install后,运行可执行文件时找不到动态库
SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
SET(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")
# 添加可执行文件
add_executable(${PROJECT_NAME} main.cpp)
# 链接库
target_link_libraries(${PROJECT_NAME} slib dlib)
# install安装头文件的2个方法,其实不安装头文件也可以运行可执行文件,
# 方法一 使用install将文件夹安装到指定目录
# install(DIRECTORY include/ DESTINATION include) # include/:被安装的文件夹中的文件,DESTINATION:安装目录
# 方法二 设置公共头文件:PUBLIC_HEADER,以便install时,将头文件一起安装,或者使用install(DIRECTORY include/ DESTINATION include)
set_target_properties(slib PROPERTIES PUBLIC_HEADER include/slib.h)
set_target_properties(dlib PROPERTIES PUBLIC_HEADER include/dlib.h)
message(STATUS "CMAKE_INSTALL_PREFIX的默认值:${CMAKE_INSTALL_PREFIX}")
# 设置安装
install(TARGETS install_demo slib dlib
RUNTIME DESTINATION bin # 可执行文件
LIBRARY DESTINATION lib # 动态库
ARCHIVE DESTINATION lib # 静态库
PUBLIC_HEADER DESTINATION include # 公共头文件
)
#[[
如果不设置DCMAKE_INSTALL_PREFIX ,则会安装到 /usr/local 目录下,例如bin的完整路径是 /usr/local/bin
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=./installed
或者其他目录
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=~/Documents/install_demo
cmake --build build
cmake --install build
]]
输出:
main函数被调用
静态库的slib_test函数被调用
动态库的dlib_test函数被调用
2.3.14 寻找依赖 find_package
对于大部分支持了CMake的项目来说,均可以通过find_package
找到对应的依赖库,参考附件:14.find_demo
# 使用find_package寻找<LibaryName>库,如果找到,一般都会有以下变量(库作者设置)
<LibaryName>_FOUND:表示是否找到
<LibaryName>_INCLUDE_DIR:表示头文件目录
<LibaryName>_LIBRARIES:表示库文件目录
输出:
name: hn
age: 20
假设我们编写了一个新的函数库,我们希望别的项目可以通过find_package
对它进行引用,我们有两种办法:
- 编写一个
Find<LibraryName>.cmake
,适用于导入非cmake安装的项目,参考附件:15.custom_find
,如果没有Find<LibraryName>.cmake
文件的话,会报如下图的错误,
CMakeLists.txt代码:
# 最低版本
cmake_minimum_required(VERSION 3.10)
# 项目名称
project(main)
# 设置CMAKE_MODULE_PATH,以便find_package查找,cmake下需要有Finddlib.cmake文件
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake/)
message("cmake_module_path:${CMAKE_MODULE_PATH}")
# 设置一个缓存变量,用于在命令行设置dlib的安装路径,给Finddlib.cmake使用
set(DLIB_INSTALL_PATH "./" CACHE PATH "dlib的安装路径")
message(STATUS "dlib的安装路径为:${DLIB_INSTALL_PATH}")
#使用find_package查找dlib(这里会从CMAKE_MODULE_PATH设置的路径中查找)
find_package(dlib REQUIRED)
if (dlib_FOUND)
message("dlib found")
message("dlib include dir: ${dlib_INCLUDE_DIR}")
message("dlib lib: ${dlib_LIBRARY}")
message("dlib version: ${dlib_VERSION}")
message("dlib author: ${dlib_AUTHOR}")
message("dlib lib dir: ${dlib_LIBRARY_DIR}")
else()
message("dlib not found")
endif()
# 设置RPATH,否则install后,运行时找不到动态库
SET(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
SET(CMAKE_INSTALL_RPATH "${dlib_LIBRARY_DIR}")
# 添加动态库
add_executable(main main.cpp)
# 添加头文件
target_include_directories(main PUBLIC ${dlib_INCLUDE_DIR})
# 链接动态库
target_link_libraries(main ${dlib_LIBRARY})
# 设置安装
install(TARGETS main
RUNTIME DESTINATION bin # 可执行文件安装路径
)
#[[
CMAKE_INSTALL_PREFIX为安装路径(系统内置),DLIB_INSTALL_PATH为dlib安装路径
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=./installed -DDLIB_INSTALL_PATH=~/Documents/course_lib
cmake --build build
cmake --install build
]]
Finddlib.cmake代码:
#寻找 dlib.h
find_path(dlib_INCLUDE_DIR dlib.h PATHS ${DLIB_INSTALL_PATH}/include)
#寻找 libdlib.so
find_library(dlib_LIBRARY dlib PATHS ${DLIB_INSTALL_PATH}/lib)
# 如果dlib_INCLUDE_DIR和dlib_LIBRARY都找到了,那么就设置dlib_FOUND为TRUE
if(dlib_INCLUDE_DIR AND dlib_LIBRARY)
set(dlib_FOUND TRUE)
set(dlib_VERSION 1.0.0) # dlib的版本号
set(dlib_AUTHOR "enpei") # dlib的作者
# lib文件所在目录
get_filename_component(dlib_LIBRARY_DIR ${dlib_LIBRARY} DIRECTORY)
endif()
输出:
main函数被调用
动态库dlib_test()被调用
- 使用
install
安装,生成<LibraryName>Config.cmake
文件,适用于导入自己开发的cmake项目,参考附件:16.custom_install_demo
三、opencv CMake示例
附件位置:17.demo_opencv/
安装OpenCV:sudo apt install libopencv-dev
依赖和链接OpenCV与常规的添加依赖并没有太多不同,同时OpenCV提供了cmake find package
的功能,因此我们可以通过find_package
方便的定位opencv在系统中的位置和需要添加的依赖。
find_package(OpenCV REQUIRED)
message("OPENCV INCLUDE DIRS: ${OpenCV_INCLUDE_DIRS}")
message("OPENCV LINK LIBRARIES: ${OpenCV_LIBS}")
如果cmake找到了OpenCV,配置cmake后,命令行会有如下输出:
OPENCV INCLUDE DIRS: /usr/include/opencv4
OPENCV LINK LIBRARIES: opencv_calib3d;opencv_core;opencv_dnn;opencv_features2d;opencv_flann;opencv_highgui;opencv_imgcodecs;opencv_imgproc;opencv_ml;opencv_objdetect;opencv_photo;opencv_stitching;opencv_video;opencv_videoio;opencv_aruco;opencv_bgsegm;opencv_bioinspired;opencv_ccalib;opencv_datasets;opencv_dnn_objdetect;opencv_dnn_superres;opencv_dpm;opencv_face;opencv_freetype;opencv_fuzzy;opencv_hdf;opencv_hfs;opencv_img_hash;opencv_line_descriptor;opencv_optflow;opencv_phase_unwrapping;opencv_plot;opencv_quality;opencv_reg;opencv_rgbd;opencv_saliency;opencv_shape;opencv_stereo;opencv_structured_light;opencv_superres;opencv_surface_matching;opencv_text;opencv_tracking;opencv_videostab;opencv_viz;opencv_ximgproc;opencv_xobjdetect;opencv_xphoto
CMakeLists.txt代码:
cmake_minimum_required(VERSION 3.10)
project(demo_opencv)
find_package(OpenCV REQUIRED)
if (OpenCV_FOUND)
message(STATUS "OpenCV library status:")
message(STATUS " libraries: ${OpenCV_LIBS}")
message(STATUS " include path: ${OpenCV_INCLUDE_DIRS}")
else ()
message(FATAL_ERROR "Could not find OpenCV")
endif ()
add_executable(demo_opencv main.cpp)
target_include_directories(demo_opencv PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(demo_opencv ${OpenCV_LIBS})
main.cpp代码:
#include <iostream>
#include <opencv2/opencv.hpp>
int main(int argc, char **argv)
{
if (argc != 2)
{
std::cout << "请输入图片路径" << std::endl;
return -1;
}
else
{
std::cout << "图片路径为:" << argv[1] << std::endl;
cv::Mat image = cv::imread(argv[1]);
cv::Mat image_resized;
cv::resize(image, image_resized, cv::Size(400, 400));
cv::cvtColor(image_resized, image_resized, cv::COLOR_BGR2GRAY);
cv::imwrite("resized.png", image_resized);
std::cout << "图片已处理完毕,并保存为resized.png" << std::endl;
}
return 0;
}
备注:声明此博客为修改后的转载,没有转载链接