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_DIRCMakeLists.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第一个参数为需要加载动态库的目标文件(不是动态库的文件,是需要将动态库加载进去的文件),后面可跟多个参数,为动态库文件。

而每个动态库文件参数之前可设置动态库访问权限,有PUBLICPRIVATEINTERFACE三种,默认为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,testModeltestModel2分别对模块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
最后修改:2023 年 09 月 09 日
如果觉得我的文章对你有用,请随意赞赏