一篇带你掌握cmake入门|了解cmake|掌握cmake基本操作
一篇带你掌握cmake入门
内容出处:
作者: 苏丙榅
链接: https://subingwen.cn/cmake/CMake-primer/
来源: 爱编程的大丙
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我是在这位博主的博客上学习的,学习过程中我用了我自己的例子,用了自己的代码,都整理好在demo文件夹里提供给大家。源代码见博客绑定的资源。
简单认识Cmake
(因为我是先学会makefile才抽空学的这个cmake,所以下面的解释都是基于我自己的理解的
不一定适合所有初学者)
其实功能和makefile是一样的,但是Cmake可以跨平台
CmakeLists.txt->makefile->make
就可以了,其实就是多一步前面的Cmake而已
和makefile一样,可以生成可执行,也可以生成库文件,动态库静态库等等
(为什么要库:学习bit的时候学过了)
Cmake的简单demo
把文件准备好
先创建一下简单的文件,弄个小demo
add.cc
#include "head.hpp"
int add(int a, int b)
{
return a + b;
}
head.hpp
#pragma once
int add(int a, int b);
main.cc
#include <iostream>
#include "head.hpp"
int main()
{
int a = 10;
int b = 20;
std::cout << add(a, b) << std::endl;
return 0;
}
然后在这些文件的同级目录下创建一个CMakeLists.txt
,注意,不要拼错!
先看看写好的cmake是怎么写的。
cmake_minimum_required(VERSION 3.0) # 指定使用cmake的最低版本
project(ADD) # 指定项目的名字
add_executable(app add.cc main.cc)
首先第一句是指定要使用的cmake的最低版本,如果本地的cmake版本不符合要求,就不能完成cmake编译,这个可以不写,但是写上是一个良好的习惯,避免一些潜在问题。
第二句就是项目的名字,定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定出工程名字即可。
# PROJECT 指令的语法是:
project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
add_executable:定义工程会生成一个可执行程序
add_executable(可执行程序名 源文件名称)
中间可以用空格来分开也可以用;来分开
使用cmake编译
因为如果直接在当前目录下面用cmake编译,所有东西包括可执行,都会堆到一个目录下,很乱,所以我们一般现在当前目录下创建一个build
目录,然后在里面cmake ..
SET命令
其实就相当于makefile里面的$@ $^
是一个道理。
定义变量
# SET 指令的语法是:
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
VAR就是变量名,VALUE就是变量的值
SET(list main.cc add.cc)
表示list代表main.cc
和add.cc
# 定义一个COMPILE_LIST变量
set(COMPILE_LIST main.cc add.cc)
add_executable(app ${COMPILE_LIST})
如何指定-std=c++11
这种选项
C++标准对应有一宏叫做CMAKE_CXX_STANDARD
有两种方式:
- 在txt文件里面配置好
set(CMAKE_CXX_STANDARD 11)
- cmake的时候带上宏
cmake .. -DCMAKE_CXX_STANDARD=11
指定可执行的路径
set(HOME /home/robin/Linux/Sort)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
- 第一行:定义一个变量用于存储一个绝对路径
- 第二行:将拼接好的路径值设置给
EXECUTABLE_OUTPUT_PATH
宏 - 如果这个路径中的子目录不存在,会自动生成,无需自己手动创建
搜索文件
使用aux_source_directory
搜索
这里我用demo2,把目录结构给大家展示出来先。
其实我们知道很多项目都是这样的目录结构的。
在 CMake 中使用aux_source_directory
命令可以查找某个路径下的所有源文件,命令格式为:
aux_source_directory(<dir> <variable>)
- dir:要搜索的目录
- variable:将从dir目录下搜索到的源文件列表存储到该变量中
使用file
搜索符合条件的文件
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
- GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
- GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
关于要搜索的文件路径和类型可加双引号,也可不加。
file(GLOB MAIN_SRC ${PROJECT_SOURCE_DIR}/src/*.cpp)
file(GLOB MAIN_HEAD ${PROJECT_SOURCE_DIR}/include/*.h)
tips: 如果我们在
.cc
文件里面如果指定了头文件路径的话,其实是不用在cmake的txt里面添加头文件的。如果没有指定路径,直接"head.hpp"
这样就要在cmake里面添加一下头文件了
file(GLOB MAIN_SRC ${PROJECT_SOURCE_DIR}/src/*.cc)
# file(GLOB MAIN_HEAD ${PROJECT_SOURCE_DIR}/include/*.hpp)
include_directories(${PROJECT_SOURCE_DIR}/include)
add_executable(app ${MAIN_SRC})
制作动静态库
制作库的时候,就可以很好的表现出cmake这个工具的跨平台能力。
- 在Windows下, 动态库以.dll结尾, 静态库以.lib结尾。
- 在Linux下, 动态库文件以.so结尾, 静态库以.a结尾。
- 在MacOS下, 动态库以.dylib结尾, 静态库以.a结尾
如果在makefile里面写,我们就要按照不同操作系统对应的不同后缀来进行脚本的编写,但是在cmake上就不需要知道后缀是什么,只需要指定库的名字就可以了。
制作静态库
用这个命令就可以了
add_library(库名称 STATIC 源文件1 [源文件2] ...)
制作动态库
用这个命令即可
add_library(库名称 SHARED 源文件1 [源文件2] ...)
其实就是STATIC
和SHARED
的区别而已
指定库的输出路径
平时肯定是要指定路径的,放到名字为lib的文件夹下。
动态库可以用这个两个宏:
EXECUTABLE_OUTPUT_PATH
LIBRARY_OUTPUT_PATH
静态库只能用这一个:
LIBRARY_OUTPUT_PATH
demo3
cmake_minimum_required(VERSION 3.0)
project(ADD)
# 包含一些文件
file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/src/*.cc)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 输出库
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
add_library(add_static STATIC ${SRC_LIST})
add_library(add_shared SHARED ${SRC_LIST})
使用库文件
先准备好demo4的文件目录,把demo3生成的库拿过来
.
├── CMakeLists.txt
├── build
├── include
│ └── head.hpp
├── lib
│ ├── libadd_shared.dylib
│ └── libadd_static.a
└── main.cc
使用静态库
在cmake中,链接静态库的命令如下:
link_libraries(<static lib> [<static lib>...])
- 参数1:指定出要链接的静态库的名字可以是全名 libxxx.a
- 也可以是掐头(lib)去尾(.a)之后的名字 xxx
- 参数2-N:要链接的其它静态库的名字
如果该静态库不是系统提供的(自己制作或者使用第三方提供的静态库)可能出现静态库找不到的情况,此时可以将静态库的路径也指定出来:
link_directories(<lib path>)
然后demo4的txt就这样写就行了
cmake_minimum_required(VERSION 3.0)
project(ADD)
# 搜索指定目录下源文件
file(GLOB SRC_LIST ${PROJECT_SOURCE_DIR}/main.cc)
# 包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 添加库的目录
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 添加静态库
link_libraries(add_static)
# 生成可执行
add_executable(app ${SRC_LIST})
使用动态库
在cmake中链接动态库的命令如下:
target_link_libraries(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
target:指定要加载动态库的文件的名字
- 该文件可能是一个源文件
- 该文件可能是一个动态库文件
- 该文件可能是一个可执行文件
PRIVATE|PUBLIC|INTERFACE
:
- 动态库的访问权限,默认为PUBLIC
- 如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可。
- 动态库的链接具有传递性,如果动态库 A 链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库B、C,并可以使用动态库B、C中定义的方法。
target_link_libraries(A B C)
target_link_libraries(D A)
PUBLIC
:在public后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用。PRIVATE
:在private后面的库仅被link到前面的target中,并且终结掉,第三方不能感知你调了啥库INTERFACE
:在interface后面引入的库不会被链接到前面的target中,只会导出符号。
注意: 如果自己写的动态库调用,你不加路径,cmake是不会报错!因为动态库是一定要被可执行调用之后才会进入内存。所以会在执行可执行的报错,cmake是没反应的。
动态库的链接和静态库是完全不同的:
静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。
动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存。
日志
在CMake中可以用用户显示一条消息,该命令的名字为message:
message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)
- (无) :重要消息
STATUS
:非重要消息WARNING:CMake
警告, 会继续执行AUTHOR_WARNING
:CMake 警告 (dev), 会继续执行SEND_ERROR
:CMake 错误, 继续执行,但是会跳过生成的步骤FATAL_ERROR
:CMake 错误, 终止所有处理过程
CMake的命令行工具会在stdout上显示STATUS消息,在stderr上显示其他所有消息。CMake的GUI会在它的log区域显示所有消息。
CMake警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。
# 输出一般日志信息
message(STATUS "source path: ${PROJECT_SOURCE_DIR}")
# 输出警告信息
message(WARNING "source path: ${PROJECT_SOURCE_DIR}")
# 输出错误信息
message(FATAL_ERROR "source path: ${PROJECT_SOURCE_DIR}")
变量操作
追加
有时候项目中的源文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过file命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用set命令也可以使用list命令。
使用set拼接
如果使用set进行字符串拼接,对应的命令格式如下:
set(变量名1 ${变量名1} ${变量名2} ...)
关于上面的命令其实就是将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖。
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src/*.cc)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/*.cc)
include_directories(${PROJECT_SOURCE_DIR}/include)
# 拼接一下这个字符串
SET(SRCLIST ${SRC_1} ${SRC_2})
add_executable(app ${SRCLIST})
使用list拼接
如果使用list进行字符串拼接,对应的命令格式如下:
list(APPEND <list> [<element> ...])
list命令的功能比set要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作,APPEND表示进行数据追加,后边的参数和set就一样了。
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src/*.cc)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/*.cc)
include_directories(${PROJECT_SOURCE_DIR}/include)
# SET(SRCLIST ${SRC_1} ${SRC_2})
list(APPEND SRCLIST ${SRC_1} ${SRC_2})
add_executable(app ${SRCLIST})
在CMake中,使用set命令可以创建一个list。一个在list内部是一个由分号;分割的一组字符串。例如,set(var a b c d e)命令将会创建一个list:a;b;c;d;e,但是最终打印变量值的时候得到的是abcde。
set(tmp1 a;b;c;d;e)
set(tmp2 a b c d e)
message(${tmp1})
message(${tmp2})
这两者输出的结果都是abcde
字符串移除
我们在通过file搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如:
(base) ➜ demo5 tree ./src
./src
├── add.cc
└── sub.cc
1 directory, 2 files
(base) ➜ demo5
在当前这么目录有2个源文件,我用file添加进来之后,可以用list命令把sub.cc去掉
list(REMOVE_ITEM <list> <value> [<value> ...])
通过上面的命令原型可以看到删除和追加数据类似,只不过是第一个参数变成了REMOVE_ITEM。
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src/*.cc)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/*.cc)
list(APPEND SRCLIST ${SRC_1} ${SRC_2})
message(STATUS "message ${SRCLIST}") # 去掉之前打印一下
list(REMOVE_ITEM SRCLIST ${PROJECT_SOURCE_DIR}/src/sub.cc)
message(STATUS "message ${SRCLIST}") # 去掉之后打印一下
可以看到,在第8行把将要移除的文件的名字指定给list就可以了。但是一定要注意通过 file 命令搜索源文件的时候得到的是文件的绝对路径(在list中每个文件对应的路径都是一个item,并且都是绝对路径),那么在移除的时候也要将该文件的绝对路径指定出来才可以,否是移除操作不会成功。
关于list命令还有其它功能,但是并不常用。
list这些功能我在demo6中展现
获取 list 的长度
list(LENGTH <list> <output variable>)
LENGTH
:子命令LENGTH用于读取列表长度<list>
:当前操作的列表<output variable>
:新创建的变量,用于存储列表的长度
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src/*.cc)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/main.cc)
set(SRCLIST ${SRC_1} ${SRC_2})
list(LENGTH SRCLIST list_size) # 看看列表长度
message(STATUS ${list_size})
读取列表中指定索引的的元素,可以指定多个索引
list(GET <list> <element index> [<element index> ...] <output variable>)
<list>
:当前操作的列表<element index>
:列表元素的索引- 从0开始编号,索引0的元素为列表中的第一个元素;
- 索引也可以是负数,-1表示列表的最后一个元素,-2表示列表倒数第二个元素,以此类推
- 当索引(不管是正还是负)超过列表的长度,运行会报错
<output variable>
:新创建的变量,存储指定索引元素的返回结果,也是一个列表。
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src/*.cc)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/main.cc)
set(SRCLIST ${SRC_1} ${SRC_2})
# list(LENGTH SRCLIST list_size) # 看看列表长度
list(GET SRCLIST -1 0 1 NEW_SRC_LIST)
message(WARNING ${NEW_SRC_LIST})
message用warning打印比较明显一点
将列表中的元素用连接符(字符串)连接起来组成一个字符串
list (JOIN <list> <glue> <output variable>)
<list>
:当前操作的列表<glue>
:指定的连接符(字符串)<output variable>
:新创建的变量,存储返回的字符串
cmake_minimum_required(VERSION 3.0)
project(TEST)
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src/*.cc)
file(GLOB SRC_2 ${PROJECT_SOURCE_DIR}/main.cc)
set(SRCLIST ${SRC_1} ${SRC_2})
# list(LENGTH SRCLIST list_size) # 看看列表长度
# list(GET SRCLIST -1 0 1 NEW_SRC_LIST)
list(JOIN SRCLIST "-" JOINED_STING)
message(WARNING ${JOINED_STING})
查找列表是否存在指定的元素,若果未找到,返回-1
list(FIND <list> <value> <output variable>)
<list>
:当前操作的列表<value>
:需要再列表中搜索的元素<output variable>
:新创建的变量
如果列表<list>
中存在<value>
,那么返回<value>
在列表中的索引,如果未找到则返回-1。
其他功能
将元素追加到列表中
list (APPEND <list> [<element> ...])
在list中指定的位置插入若干元素
list(INSERT <list> <element_index> <element> [<element> ...])
将元素插入到列表的0索引位置
list (PREPEND <list> [<element> ...])
将列表中最后元素移除
list (POP_BACK <list> [<out-var>...])
将列表中第一个元素移除
list (POP_FRONT <list> [<out-var>...])
将指定的元素从列表中移除
list (REMOVE_ITEM <list> <value> [<value> ...])
将指定索引的元素从列表中移除
list (REMOVE_AT <list> <index> [<index> ...])
移除列表中的重复元素
list (REMOVE_DUPLICATES <list>)
列表翻转
list(REVERSE <list>)
列表排序
list (SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
COMPARE
:指定排序方法。有如下几种值可选:STRING
:按照字母顺序进行排序,为默认的排序方法FILE_BASENAME
:如果是一系列路径名,会使用basename进行排序NATURAL
:使用自然数顺序排序CASE
:指明是否大小写敏感。有如下几种值可选:SENSITIVE
: 按照大小写敏感的方式进行排序,为默认值INSENSITIVE
:按照大小写不敏感方式进行排序ORDER
:指明排序的顺序。有如下几种值可选:ASCENDING
:按照升序排列,为默认值DESCENDING
:按照降序排列
宏定义
用一个demo7写代码
在进行程序测试的时候,我们可以在代码中添加一些宏定义,通过这些宏来控制这些代码是否生效,如下所示:
#include <iostream>
int main()
{
#ifndef MY_DEBUG
std::cout << "Release" << std::endl;
#else
std::cout << "DEBUG" << std::endl;
#endif
return 0;
}
如果这样编译直接运行,是肯定是Release的
cmake_minimum_required(VERSION 3.0)
project(TEST)
add_executable(app ${PROJECT_SOURCE_DIR}/main.cc)
我们可以带上宏
add_definitions(-D宏名称)
cmake_minimum_required(VERSION 3.0)
project(TEST)
add_definitions(-DMY_DEBUG)
add_executable(app ${PROJECT_SOURCE_DIR}/main.cc)
这样是DEBUG模式了
预定义宏
下面的列表中为大家整理了一些CMake中常用的宏:
宏 功能 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 目录进行的构建, 那么得到的就是这 \begin{array}{|c|c|} \hline \text { 宏 } & \text { 功能 } \\ \hline \text { PROJECT\_SOURCE\_DIR } & \text { 使用cmake命令后紧跟的目录, 一般是工程的根目录 } \\ \hline \text { PROJECT\_BINARY\_DIR } & \text { 执行cmake命令的目录 } \\ \hline \text { CMAKE\_CURRENT\_SOURCE\_DIR } & \text { 当前处理的CMakeLists.txt所在的路径 } \\ \hline \text { CMAKE\_CURRENT\_BINARY\_DIR } & \text { target 编译目录 } \\ \hline \text { EXECUTABLE\_OUTPUT\_PATH } & \text { 重新定义目标二进制可执行文件的存放位置 } \\ \hline \text { LIBRARY\_OUTPUT\_PATH } & \text { 重新定义目标链接库文件的存放位置 } \\ \hline \text { PROJECT\_NAME } & \text { 返回通过PROJECT指令定义的项目名称 } \\ \hline \text { CMAKE\_BINARY\_DIR } & \text { 项目实际构建路径, 假设在 build 目录进行的构建, 那么得到的就是这 } \\ \hline \end{array} 宏 PROJECT_SOURCE_DIR PROJECT_BINARY_DIR CMAKE_CURRENT_SOURCE_DIR CMAKE_CURRENT_BINARY_DIR EXECUTABLE_OUTPUT_PATH LIBRARY_OUTPUT_PATH PROJECT_NAME CMAKE_BINARY_DIR 功能 使用cmake命令后紧跟的目录, 一般是工程的根目录 执行cmake命令的目录 当前处理的CMakeLists.txt所在的路径 target 编译目录 重新定义目标二进制可执行文件的存放位置 重新定义目标链接库文件的存放位置 返回通过PROJECT指令定义的项目名称 项目实际构建路径, 假设在 build 目录进行的构建, 那么得到的就是这