CMake基础用法
CMake介绍
CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述安装(编译)过程。他能够输出各种各样的makefile或者project文件。
笔者主要用CMake来构建CPP项目(但不仅限于CPP)。由于makefile通常依赖于当前的编译平台,编写makefile相较于麻烦一些,依赖也容易出错,而CMake恰好能解决上述问题。
使用CMake编译项目流程如下:
- 项目源码
- 编写CMakeLists.txt文件
- 执行cmake命令
- 生成makefile文件
- 执行make命令
- 生成目标文件
前提
一个项目的目录大概是这样的
.
├── bin
├── build
├── CMakeLists.txt
├── include
├── lib
└── src
下面对其介绍:
bin
目标文件生成目录build
cmake命令生成的文件存放目录include
项目自己编写的头文件目录lib
库的目录src
源文件的目录CMakeLists.txt
CMake配置文件,注意这个名称必须这样写(大小写也一致)
我们接下来就根据这个目录结构来进行编写CMake的配置文件
CMake使用
注释
- 注释行
# 这是个行注释
- 注释块
#[[
这是块注释
]]
指定cmake版本
如指定Cmake最低版本为3.26
cmake_minimum_required(VERSION 3.26)
该命令并不是必须的,但是在编写项目的时候使用高版本的CMake命令通常会向下兼容,但使用低版本的CMake却没有高版本的特性,所以通常指定一个最低版本CMake以减少不必要的错误。
设置项目名称
例如名称设置为HelloWorld
project(HelloWorld)
set的使用
set命令可以自定义变量,而使用这些变量需要用${}
将其名称包括进去
例如我指定变量名为VAR_NAME
,其值为var_value
set(VAR_NAME var_value) # 定义变量
message(${VAR_NAME}) # 使用变量并输出它的值
- 设置CPP标准
同理编译器可能默认使用低标准的C++,但是项目中包含高标准的特性,所以一般指定一个项目所使用的C++ 表准。
例如项目中使用了auto
关键字,这个是C++11标准里的,编译器可能默认C++98标准,这时候编译就会报错。
set(CMAKE_CXX_STANDARD 11)
- 设置可执行文件输出路径
设置EXECUTABLE_OUTPUT_PATH
宏来指定可执行程序的输出命令
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/bin)
其中CMAKE_SOURCE_DIR
为CMakeLists.txt
的目录
这句话的含义是将可执行文件放到了./bin
目录下
- 设置库的输出目录
设置LIBRARY_OUTPUT_PATH
宏来指定生成库的输出目录
set(LIBRARY_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/lib)
搜索文件
搜索源文件,有两种方式
我们项目的源文件都在./src
目录下
将搜索到的源文件路径存放到SRC
这个变量里
- 利用
aux_source_directory
aux_source_directory(${CMAKE_SOURCE_DIR}/src SRC)
- 使用
file
file(GLOB SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
这里file
命令的第一个参数如果是GLOB
则只搜索当前目录,如果是GLOB_RECURSE
则进行递归搜索
设置头文件目录
include_directories(${PROJECT_SOURCE_DIR}/include)
这里宏PROJECT_SOURCE_DIR
对应项目根目录
打印日志信息
# 打印HelloWorld(重要信息)
message("HelloWorld")
# 打印Hello(非重要信息)
message(STATUS "Hello")
message
命令第一个参数可指定消息类型:
- 如果没有第一个参数,则这是重要信息
STATUS
:则这是非重要信息WARNING
:警告信息,但会继续执行后面的命令AUTHOR_WARNING
:警告信息(dev),但会继续执行后面的命令SEND_ERROR
:错误信息,继续执行,但会跳过生成的步骤FATAL_ERROR
:错误信息,中止所有处理的过程
字符串追加
- 使用
set
拼接
# 省略号表示参数数量不固定
set(变量名 ${变量1} ${变量2} ...)
用法如下
# 定义一些字符串
set(VAR1 hello)
set(VAR2 world)
# 拼接字符串,FINAL_STR的值为helloworldhaha123
set(FINAL_STR ${VAR1} ${VAR2} haha123)
- 使用
list
拼接
# FINAL_STR的值为helloworldhaha123
list(APPEND FINAL_STR ${VAR1} ${VAR2} "haha123")
删除子字符串
CMake的删除子字符串,是将上述字符串拼接中每一个字符串看作一个单位进行操作的。可以把它类比为一个list<string>
类型,其删除操作是删除其中的一个元素,而非一个真正意义上的子串。
以前文拼接好的FINAL_STR
为例子:
list(REMOVE_ITEM FINAL_STR "haha")
# 此时FINAL_STR仍为helloworldhaha123,因为并没haha这个元素
list(REMOVE_ITEM FINAL_STR "haha123")
# 此时为helloworld
list的其他用法
在前面已经提到过了list
命令可以追加、删除子字符串,list第一个参数还能决定其他用法。
- 获取list长度(字符串元素个数)
list(LENGTH FINAL_STR RESULT_NUMBER)
# RESULT_NUMBER为接受结果的变量,FINAL_STR是需要获取长度的字符串(列表)
- 指定索引位置元素
# 获取第一个元素,FIRST_ITEM为接受结果的变量
list(GET 0 FINAL_STR FIRST_ITEM)
# 获取最后一个元素,索引可以为负数;-1是倒数第一个,-2是倒数第二个,以此类推
list(GET -1 FINAL_STR LAST_ITEM)
生成库文件
库文件一般有两种,动态库(共享库)和静态库,二者的区别我们将在下文中的引入库文件部分说明。
我们设SRC
为源文件目录,libName
为库的名称
- 生成静态库可用如下命令
add_library(libName STATIC ${SRC})
- 生成动态库可用如下命令
add_library(libName SHARED ${SRC})
生成可执行程序
源文件地址在SRC
中,生成app
名称的可执行文件可用以下命令
#生成可执行文件
add_executable(app ${SRC})
引入库文件
如果调用静态库会被打包进可执行文件中,操作系统不知道它们可以复用,因此每次启动都会随可执行程序加载到内存中(如果用的话,取决于操作系统的内存管理技术);而动态库在可以在多个进程共享,它们只会被加载到内存一次,且不会打包进可执行文件中。(详情参考操作系统,此处不过多解释)
- 我们以链接
lib
目录下的静态库为例
#设置非系统提供的库目录
link_directories(${PROJECT_SOURCE_DIR}/lib)
#链接静态库
link_libraries(model)
如果库是系统给出的,则一般无需使用link_directories
命令,如果是自己或第三方提供的则需要。
注意link_libraries
的参数,这里参数为库名称,可以是全名libmodel.a
,也可以是lib
与后缀名.a
之间的部分。(在Linux下后缀为.a
而Windows下为.lib
,以Linux为例Win同理)
- 然后是动态库,同样以
lib
目录为例
假设我们生成的可执行文件为app,源文件目录变量为SRC
,只需在add_executable
之后添加target_link_libraries
即可
#设置非系统提供的库目录
link_directories(${PROJECT_SOURCE_DIR}/lib)
#生成可执行文件
add_executable(app ${SRC})
#链接动态库
target_link_libraries(app model)
对于target_link_libraries
第一个参数为需要加载动态库的目标文件(不是动态库的文件,是需要将动态库加载进去的文件),后面可跟多个参数,为动态库文件。
而每个动态库文件参数之前可设置动态库访问权限,有PUBLIC
、PRIVATE
和INTERFACE
三种,默认为PUBLIC
:
PUBLIC
: 在PUBLIC后面的库会被链接到目标文件,并且符号会被导出提供给第三方。也就是说动态库链接有传递性,如果目标程序引入了一个动态库,而这个动态库引入了其他库,目标程序也会引入其他库PRIVATE
: 在PRIVATE后面的库仅会被链接到目标文件,传递性会被终结,中间动态库为PRIVATE不会将它引入的其他库导入到目标程序。INTERFACE
:在INTERFACE后面的库不会被链接,只会导出符号
CMake嵌套
在大型项目中,通常分为多个模块,会有很多目录,CMake允许子目录下拥有相应的CMakeLists.txt
文件。
在这些文件中的变量有着各自的作用域:
- 父节点的变量可以在子节点使用
- 子节点变量只能在当前文件中使用
- 根节点变量全局有效
总结一下就是子节点能使用在它之上层级的变量
我们首先来看一个项目的目录结构
.
├── build
├── CMakeLists.txt
├── include
│ ├── model2.h
│ └── model.h
├── model
│ ├── CMakeLists.txt
│ └── model.cpp
├── model2
│ ├── CMakeLists.txt
│ └── model2.cpp
├── testModel
│ ├── CMakeLists.txt
│ └── test.cpp
└── testModel2
├── CMakeLists.txt
└── test2.cpp
其中,include
存放头文件,model
为模块1,model2
为模块2,testModel
与testModel2
分别对模块1和模块2进行测试。
在此之前我们在根目录文件下设置一些变量
#静态库路径
set(LIBPATH ${PROJECT_SOURCE_DIR}/lib)
#可执行程序的存储目录
set(EXECPATH ${PROJECT_SOURCE_DIR}/bin)
#头文件路径
set(HEADPATH ${PROJECT_SOURCE_DIR}/include)
#库文件名字
set(MODEL model)
set(MODEL2 model2)
#可执行文件名称
set(TEST1 test1)
set(TEST2 test2)
为了构建CMake目录的父子关系,我们需要使用一条命令add_subdirectory
#添加子目录,注意名称与目录名称一致
add_subdirectory(model)
add_subdirectory(model2)
add_subdirectory(testModel)
add_subdirectory(testModel2)
接下来对子目录下的CMakeLists.txt
来进行配置
我们以模块1下的为例,模块2同理,只不过要改成对应的变量名
cmake_minimum_required(VERSION 3.26)
project(model)
#搜索源文件,子节点变量SRC不能给父文件
aux_source_directory(./ SRC)
#制定头文件,使用父节点的变量HEADPATH
include_directories(${HEADPATH})
#指定输出目录,使用父节点变量LIBPATH
set(LIBRARY_OUTPUT_PATH ${LIBPATH})
#生成静态库
add_library(${MODEL} STATIC ${SRC})
然后对测试部分的CMakeLists.txt
文件配置,以testModel为例,其余同理
cmake_minimum_required(VERSION 3.26)
project(test)
#搜索源文件,子节点变量SRC不能给父文件
aux_source_directory(./ SRC)
#制定头文件,使用父节点的变量HEADPATH
include_directories(${HEADPATH})
#指定输出目录,使用父节点变量EXECPATH
set(EXECUTABLE_OUTPUT_PATH ${EXECPATH})
#生成可执行文件
add_executable(${TEST1} ${SRC})
其它
CMake还有一些流程控制语句,有些与版本有关,这部分内容放在以后的文章写
生成
生成很简单,假设当前为项目文件根目录,执行以下命令,然后你就可以看到生成的文件了(cmake会先生成makefile文件,然后再编译)
mkdir build
cd build
cmake ..
make