目录
CMake是什么
CMake是一个跨平台的项目构建工具,它使用一种名为CMakeLists.txt的脚本文件来定义编译过程。CMake的主要目的是解决直接使用make+Makefile方式时无法实现跨平台编译的问题。通过CMake,开发者可以编写一种与平台无关的CMakeLists.txt文件来制定整个工程的编译流程,然后CMake会根据具体的编译平台生成本地化的Makefile或工程文件,最后通过make或其他构建工具来编译整个工程。CMake的最大特点是其跨平台性,它支持多种操作系统和编译器,使得开发者可以更加专注于代码本身,而不是编译环境的配置。
CMake的优点主要有:跨平台、能够管理大型项目、简化了编译构建过程和编译过程、可扩展,即可以为cmake编写特定功能的模块,扩充cmake功能。
如何配置
1、在官网官网下载cmake的安装包
wget wget https://cmake.org/files/v3.24/cmake-3.24.1-linux-aarch64.tar.gz
tar -zxvf cmake-3.24.1-linux-aarch64.tar.gz
2、然后把cmake/bin/cmake软链接到/bin目录下
ln -s /usr/cmake-3.24.1-linux-aarch64/bin/* /usr/bin/
3、检查是否安装成功
cmake --version
使用方法
-
基本流程
编写CMakeLists.txt文件
CMakeLists.txt是CMake的核心配置文件,它定义了项目的编译规则。以下是一个简单的CMakeLists.txt文件的示例:
cmake_minimum_required(VERSION 3.10) # 指定CMake的最低版本要求 project(HelloWorld) # 定义项目名称 # 添加一个可执行文件 add_executable(hello main.cpp) # 指定生成的可执行文件名称和依赖的源文件
使用CMake生成构建文件
cmake .
或者,如果你希望将生成的构建文件放在与源代码分离的目录中(即所谓的out-of-source构建),可以先创建一个构建目录,然后在该目录中执行cmake命令,并指定源代码目录的路径:
mkdir build cd build cmake ..
使用构建工具编译项目
生成构建文件后,你可以使用make或其他构建工具来编译项目。
make
编译完成后,将在构建目录下找到生成的可执行文件或库文件。
-
使用cmake的示例
只有源文件
在当前目录(CMake_test)下有4个测试文件,分别为:
head.h
#ifndef _HEAD_H
#define _HEAD_H
#include <iostream>
int add(int a, int b);
int sub(int a, int b);
#endif
add.cpp
#include "head.h"
int add(int a, int b)
{
return a+b;
}
sub.cpp
#include "head.h"
int sub(int a, int b)
{
return a-b;
}
main.cpp
#include "head.h"
int main()
{
int a = 20;
int b = 30;
int sum = add(a, b);
int ans = sub(a, b);
std::cout << "a + b = " << sum << std::endl;
std::cout << "a - b = " << ans << std::endl;
return 0;
}
在上述源文件所在目录下添加一个新文件CMakeLists.txt,文件内容如下
# 设置CMake的最低版本要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名称和版本
project(CMAKE_TEST)
#:定义工程会生成一个可执行程序
add_executable(app main.cpp add.cpp sub.cpp)
在这个例子中 ,add_executable命令告诉CMake,根据main.cpp、add.cpp和sub.cpp这三个源文件来编译并链接生成一个名为app的可执行文件。
add_executable(可执行程序名 源文件名称)
这里的可执行程序名和project中的项目名没有任何关系
源文件名可以是一个也可以是多个,如有多个可用空格或 ; 间隔,例如:
add_executable(app main.cpp;add.cpp;sub.cpp)
add_executable(app main.cpp add.cpp sub.cpp)
作用:用于指定从哪些源文件(source files)生成一个可执行程序(executable)
将CMakeLists.txt 文件编辑好后,就可以执行cmake命令了。当执行cmake命令之后,CMakeLists.txt 中的命令就会被执行,所以一定要注意给cmake 命令指定路径的时候一定不能出错。
在当前目录(CMake_test)下执行cmake .
执行命令之后,源文件所在目录中会多了一些文件:
可以看到在对应的目录下生成了一个makefile文件,此时再执行make命令,就可以对项目进行构建得到所需的可执行程序了。
会发现可执行程序已经生成
最终可执行程序app就被编译出来了(这个名字是在CMakeLists.txt中指定的)。
为了便于后期的管理和维护项目,可以将生成的构建文件放在与源代码分离的目录中。故在当前目录(CMake_test)下新一个名为build的文件夹。存放生成的构建文件。
-
搜索文件
如果一个项目下有很多源文件,一个一个添加太过于麻烦,可以使用cmake中搜索文件的命令,自动化地收集项目中的源文件、头文件等,以提高项目管理的效率和可维护性。其中最常用的是aux_source_directory
和file。
1. aux_source_directory(<dir> <variable>)
<dir>
:指定要搜索的目录。<variable>
:用于存储搜索到的文件列表的变量。作用:把dir目录中的所有源文件都储存在variable变量中,然后需要用到源文件的地方用变量variable来代替。
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST)
aux_source_directory
命令搜索src
目录下的所有源文件,并将它们存储在变量SRC_LIST
中。
CMAKE_CURRENT_SOURCE_DIR
是一个变量,它表示当前正在处理的CMakeLists.txt文件所在的目录的完整路径。
# 设置CMake的最低版本要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名称和版本
project(CMAKE_TEST)
#源文件均在当前目录(CMake_test)下
aux_source_directory(. SRC_LIST)
#定义工程会生成一个可执行程序
add_executable(app ${SRC_LIST})
2. file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
- GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
- GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
# 搜索当前目录的src目录下所有的.cpp源文件 file(GLOB MAIN_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") # 搜索当前目录的include目录下所有的头文件 file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h") # 根据搜索到的源文件和头文件创建可执行文件 # 注意:通常不推荐将头文件直接传递给add_executable,这里仅为示例 add_executable(app ${MAIN_SRC} ${MAIN_HEAD})
然而,需要注意的是,虽然
file(GLOB ...)
和file(GLOB_RECURSE ...)
命令在查找文件时非常方便,但它们也有一些缺点。例如,当添加新文件到项目中时,CMake不会自动重新运行以更新文件列表,除非CMakeLists.txt文件本身被修改。因此,在某些情况下,可能需要手动重新运行CMake以确保所有新文件都被包含在内。相比之下,aux_source_directory
命令在某些方面可能更加可靠,因为它会在每次CMake运行时都重新搜索目录。
-
set的使用
1. 定义变量
在上面的例子中一共提供了3个源文件,假设这3个源文件需要反复被使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串存储起来,在cmake里定义变量需要使用set
set(<variable> <value>... [PARENT_SCOPE])
<variable>
:你想要设置的变量的名称。
<value>
...:你想要赋给该变量的值。对于列表类型的变量,你可以通过空格分隔多个值,或者使用分号;
作为分隔符
[PARENT_SCOPE]
(可选):如果你在一个函数或宏中调用set
,并且想要设置父作用域中的变量,可以使用这个选项。默认情况下,set
命令在当前作用域中设置变量。例如:
set(SRC_LIST main.cpp add.cpp sub.cpp)
set(SRC_LIST main.cpp;add.cpp;sub.cpp)
# 设置CMake的最低版本要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名称和版本
project(CMAKE_TEST)
#定义变量
set(SRC_LIST main.cpp add.cpp sub.cpp)
#定义工程会生成一个可执行程序
add_executable(app ${SRC_LIST})
2. 指定C++标准
在编写C++程序的时候,可能会用到C++11、C++14、C++17、C++20等新特性,那么就需要在编译的时候在编译命令中制定出要使用哪个标准:
# 指定C++标准 C++11
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)
第一行:用于指定C++项目的编译标准应为C++11
第二行:用于指定CMake在构建项目时是否要求C++编译器必须支持指定的C++标准。当这个变量被设置为True
时,CMake会检查编译器是否支持通过CMAKE_CXX_STANDARD
变量指定的C++标准,如果不支持,CMake将报错并停止配置过程。
3. 指定输出的路径
在CMake中指定可执行程序输出的路径,对应一个宏,叫做EXECUTABLE_OUTPUT_PATH,它的值还是通过set命令进行设置:
#指定输出的路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
其中,PROJECT_SOURCE_DIR
是一个变量,这个变量在CMake处理project()
命令时自动被设置,它包含了当前CMake项目源代码目录的完整路径。这行代码表示将生成的可执行程序放入源代码目录下的bin目录中。或者可以自定义路径,如下:
#定义一个变量用于存储一个绝对路径
set(HOME /home/robin/Linux/Sort)
#将拼接好的路径值设置给EXECUTABLE_OUTPUT_PATH宏
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
如果这个路径中的子目录不存在,会自动生成,无需自己手动创建。
由于可执行程序是基于 cmake 命令生成的 makefile 文件然后再执行 make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的 ./ 对应的就是 makefile 文件所在的那个目录。
这样,修改上面例子的CMakeLists.txt,执行可以发现CMake_test/bin中有一个可执行程序文件app。
头文件和源文件分离
-
包含头文件
在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。
在CMake中,
target_include_directories
和include_directories
都是用来指定头文件搜索路径的命令,但它们之间存在一些关键差异。1. include_directories ( dir )
- 他的作用是自动去dir目录下寻找头文件,相当于 gcc中的 gcc -I dir
举例说明,目录结构如下图所示:
CMakeLists.txt文件内容如下:
# 设置CMake的最低版本要求 cmake_minimum_required(VERSION 3.10) # 设置项目名称和版本 project(CMAKE_TEST) # 指定C++标准 C++14 set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED True) #指定输出的路径 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) #在include目录下寻找头文件 include_directories(${PROJECT_SOURCE_DIR}/src/include) #搜索当前目录的src目录下所有.cpp源文件 file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") #定义工程会生成一个可执行程序 add_executable(app ${SRC_LIST})
include_directories
命令用于为后续的add_executable
或add_library
命令添加全局的头文件搜索路径。这意味着所有后续添加的目标(可执行文件或库)都会继承这些搜索路径。2.
target_include_directories(
<target> [SYSTEM] [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...]
)
<target>
: 要修改的目标名称。SYSTEM
: 可选参数,表示这些包含目录是系统级别的,这可能会影响编译器的行为(例如,在某些情况下,编译器会忽略这些目录中的警告)。BEFORE
: 可选参数,表示这些包含目录应该被添加到目标已有的包含目录之前。<INTERFACE|PUBLIC|PRIVATE>
: 指定包含目录的作用域。
INTERFACE
:这些目录将仅对使用该目标的其他目标可见(即,它们被导出)。PUBLIC
:这些目录不仅对目标本身可见,而且对使用该目标的其他目标也可见(即,它们被导出)。PRIVATE
:这些目录仅对目标本身可见,不会影响到使用该目标的其他目标。[items...]
:一个或多个包含目录的路径。- 作用:用于向指定的目标(如可执行文件、库等)添加包含目录(include directories)。这些包含目录会被编译器用来查找头文件(.h, .hpp 等)。
target_include_directories
命令则更加精细,它允许你为特定的目标(如可执行文件或库)指定头文件搜索路径。这些路径的作用域可以是私有的(仅对该目标可见),公共的(对该目标及其链接者可见),或接口的(仅对链接该目标的其他目标可见)。
-
变量操作
有时项目的源文件并不一定在同一个目录中,但这些源文件最终却需要一起进行编译来生成可执行文件/库。当需要将多个目录下的源文件列表合并为一个时,可以使用file(GLOB ...)
命令配合变量拼接来实现。
1. 使用set拼接
set(变量名1 ${变量名1} ${变量名2} ...)
- 作用:将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖。
示例:源文件列表拼接
file(GLOB SRC_FILES1 "${CMAKE_CURRENT_SOURCE_DIR}/src1/*.cpp") file(GLOB SRC_FILES2 "${CMAKE_CURRENT_SOURCE_DIR}/src2/*.cpp") set(ALL_SRC_FILES "${SRC_FILES1} ${SRC_FILES2}") add_executable(MyExe ${ALL_SRC_FILES})
构建目录路径拼接
set(BUILD_DIR "${CMAKE_BINARY_DIR}/build") set(OUTPUT_DIR "${BUILD_DIR}/output")
2. 使用list拼接
list(APPEND <list> [<element> ...])
<list>
:要追加元素的列表变量名。<element>...
:一个或多个要追加到列表末尾的元素。- 作用:将元素追加到列表中。如果列表不存在,则首先创建该列表。
list命令的功能比set要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作(如:APPEND、FIND、GET.....),后边的参数和set就一样了。
示例:
file(GLOB SRC_FILES1 "${CMAKE_CURRENT_SOURCE_DIR}/src1/*.cpp") file(GLOB SRC_FILES2 "${CMAKE_CURRENT_SOURCE_DIR}/src2/*.cpp") list(APPEND ALL_SRC_FILES "${SRC_FILES1} ${SRC_FILES2}") add_executable(MyExe ${ALL_SRC_FILES})
set(LIST1 "a" "b") set(LIST2 "c" "d") list(APPEND LIST1 ${LIST2}) message(STATUS "${LIST1}") # 输出: a;b;c;d
注意:在CMake中,列表的默认分隔符是分号(
;
),但在打印或引用时可能看起来像是空格分隔的字符串。
生成动态库和静态库
静态库和动态库的区别:
- 静态库:在程序编译时进行链接。静态库的代码和数据会被完整地复制到目标程序中,因此每个目标程序都有自己的一份库的副本。静态库文件的扩展名在Linux系统中通常为
.a
,在Windows系统中通常为.lib
。
- 动态库:在程序运行时进行链接。这意味着当可执行文件被加载到内存中时,操作系统会查找并加载所需的动态库文件。动态库文件的扩展名在Linux系统中通常为
.so
,在Windows系统中通常为.dll
。
-
制作静态库
在CMake中制作静态库,主要通过add_library
命令实现。以下是详细的步骤和说明:
add_library(库名称 STATIC 源文件1 [源文件2] ...)
- 库名称:这是你想要创建的静态库的名字(在Linux中,静态库名字分为三部分:lib+库名字+.a,注意,这里不需要加
lib
前缀和后缀,CMake会自动处理)。- STATIC:这个关键字指示CMake生成一个静态库。
- 源文件:这是构成静态库的源文件列表。
项目目录结构如下:
编写CMakeLists.txt文件来生成静态库:
# 设置CMake的最低版本要求 cmake_minimum_required(VERSION 3.10) # 设置项目名称和版本 project(CMAKE_TEST) # 指定C++标准 C++14 set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED True) #指定输出的路径 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) #在include目录下寻找头文件 include_directories(${PROJECT_SOURCE_DIR}/src/include) #搜索当前目录的src目录下所有.cpp源文件 file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") #定义工程会生成一个可执行程序 add_executable(app ${SRC_LIST}) #指定库文件的输出路径 set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib) #添加静态库 add_library(mylib STATIC ${SRC_LIST})
这样最终就会生成对应的静态库文件mylib.a,并将生成的静态库文件会被放置在根目录下的lib目录。
其中,LIBRARY_OUTPUT_PATH
用于直接指定库(包括静态库和动态库)的输出路径。
-
制作动态库
在CMake中制作动态库(也称为共享库),主要通过add_library
命令实现,并指定库的类型为SHARED
。以下是详细的步骤和说明:
add_library(库名称 SHARED 源文件1 [源文件2] ...)
- 在Linux中,动态库名字分为三部分:lib+库名字+.so,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
根据上面的目录结构,编写CMakeLists.txt文件来生成动态库:
# 设置CMake的最低版本要求 cmake_minimum_required(VERSION 3.10) # 设置项目名称和版本 project(CMAKE_TEST) # 指定C++标准 C++14 set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED True) #指定输出的路径 set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin) #在include目录下寻找头文件 include_directories(${PROJECT_SOURCE_DIR}/src/include) #搜索当前目录的src目录下所有.cpp源文件 file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") #定义工程会生成一个可执行程序 add_executable(app ${SRC_LIST}) #指定库文件的输出路径 set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib) #添加动态库 add_library(mylib SHARED ${SRC_LIST})
这样最终就会生成对应的动态库文件libcalc.so。
-
链接库文件
在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,以下是几种常见的方法来包含库文件:
target_link_libraries(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
<target>
:目标名称,即你想要链接库的可执行文件或库。<PRIVATE|INTERFACE|PUBLIC>
:动态库的访问权限,默认为PUBLIC。
- 如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可。
PRIVATE
:链接项仅对当前目标及其依赖项(如果有的话)可见,并且仅在当前目标链接时使用。INTERFACE
:链接项对当前目标的接口可见,即它会影响链接到当前目标的其他目标,但不会在当前目标自身链接时使用。PUBLIC
:链接项对当前目标和链接到当前目标的其他目标都可见,并且会在当前目标链接时使用。<item>
:要链接的库名称或目标名称。如果是库名称,CMake 会在链接器的搜索路径中查找该库。如果是目标名称,CMake 会确保该目标的输出(如库文件)被链接到当前目标。set(SRC test.cpp) add_executable(cal ${SRC}) target_link_libraries(cal pthread)
cal: 是最终生成的可执行程序的名字
pthread:这是可执行程序要加载的动态库,这个库是系统提供的线程库,全名为libpthread.so,在指定的时候一般会掐头(lib)去尾(.so)。find_library(<VAR> name1 [path1 path2 ...])
<VAR>
:找到的库文件路径将被存储在这个变量中。name1
:要查找的库名称(不包括前缀和后缀)。[path1 path2 ...]
:库可能存在的额外路径。- 用途:查找库文件并设置变量。
find_library(MY_LIB NAMES mylib PATHS /usr/local/lib) if(MY_LIB) target_link_libraries(my_app PRIVATE ${MY_LIB}) else() message(FATAL_ERROR "mylib not found") endif()
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE] [REQUIRED] [[COMPONENTS] [components...]])
<PackageName>
:要查找的包名。[version]
:期望的包版本。[EXACT]
:要求版本号完全匹配。[QUIET]
:不打印查找信息。[MODULE]
:指定查找模式(模块模式)。[REQUIRED]
:如果未找到包,则停止处理 CMakeLists.txt 并报错。[COMPONENTS ...]
:指定需要的包组件。#查找Protobuf包 find_package(Protobuf REQUIRED) #${PROTOBUF_LIBRARIES}是Protobuf库(或库集)的占位符,CMake在find_package(Protobuf)成功后设置这个变量。 target_link_libraries(app ${PROTOBUF_LIBRARIES})
日志
CMake中的日志使用方法主要涉及使用message
命令来显示消息,以及通过设置CMake的日志级别来控制输出的信息量。
1. 使用message命令显示消息
message([<类型>] "消息内容")
- 其中,
<类型>
是可选的,用于指定消息的重要性或类型。如果不指定类型,则默认为重要消息。CMake支持的消息类型包括:
- 无类型(默认为重要消息)
- STATUS:显示非重要消息,通常用于输出构建过程中的状态信息。
- WARNING:显示警告信息,但构建过程会继续执行。
- AUTHOR_WARNING:开发者警告,构建过程也会继续执行。
- SEND_ERROR:错误消息,会跳过生成步骤,但构建过程不会立即终止。
- FATAL_ERROR:致命错误消息,会立即终止所有处理过程。
# 输出一般信息 message(STATUS "这是一个普通消息。") # 输出警告信息 message(WARNING "这是一个警告消息。")
这样会在执行cmake时在终端上打印出消息内容。
2. 设置CMake的日志级别
CMake的日志级别决定了在构建过程中输出的信息量。通过设置日志级别,可以控制哪些信息被输出到控制台或日志文件。CMake的日志级别包括:
- NOTHING:不输出任何信息。
- SEND_ERROR:仅在发生错误时输出信息。
- STATUS:输出基本的构建状态信息。
- VERBOSE:输出详细的构建过程信息。
- DEBUG:输出调试级别的信息,包括CMake的内部操作和变量设置等。
直接在CMakeLists.txt文件中设置日志级别并不直接支持,因为CMake本身没有提供直接的命令来设置全局日志级别。但是,可以通过命令行选项来控制CMake的日志输出。
- 使用
-v
或--verbose
选项可以增加输出的详细程度,但这不是直接设置日志级别。- 使用
--trace
选项可以输出CMake在解析构建文件时展开的所有变量和函数,这类似于DEBUG级别的日志输出。