CMake使用
CMake
CMake是一个开源、跨平台的编译、测试和打包工具,它使用比较简单的语言描述编译、安装的过程,输出Makefile或者project文件,再去执行构建。
1 gcc、make和CMake的关系
gcc(GNU Compiler Collection)将源文件编译(Compile)成可执行文件或者库文件;而当需要编译的东西很多时,需要说明先编译什么,后编译什么,这个过程称为构建(Build)。常用的工具是make,对应的定义构建过程的文件为Makefile;而编写Makefile对于大型项目又比较复杂,通过CMake就可以使用更加简洁的语法定义构建的流程,CMake定义构建过程的文件为CMakeLists.txt。
CMake通过CMakeLists.txt配置项目的构建系统,配合使用cmake命令行工具生成构建系统并执行编译、测试,相比于手动编写构建系统(如Makefile)要高效许多。对于C/C++项目开发,非常值得学习掌握。这里的GCC只是示例,也可以是其他的编译工具。这里的Bin表示目标文件,可以是可执行文件或者库文件.
2. CMake一般使用流程
使用cmake一般流程为:生成构建系统(buildsystem,比如make工具对应的Makefile); 执行构建(比如make),生成目标文件;和执行测试、安装或打包。
2.1 生成构建系统
生成构建系统(buildsystem,比如make工具对应的Makefile), 通过cmake命令生成构建系统.
参数 | 含义 |
---|---|
-S | 指定源文件根目录,必须包含一个CMakeLists.txt文件 |
-B | 指定构建目录,构建生成的中间文件和目标文件的生成路径 |
-D | 指定变量,格式为-D =,-D后面的空格可以省略 |
指明使用当前目录作为源文件目录,其中包含CMakeLists.txt文件;使用build目录作为构建目录;设定变量CMAKE_BUILD_TYPE的值为Debug,变量AUTHOR的值为RealCoolEngineer:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DAUTHOR=RealCoolEngineer
2.2 执行构建(比如make),生成目标文件;
使用cmake --build [
参数 | 含义 |
---|---|
–target | 指定构建目标代替默认的构建目标,可以指定多个 |
–parallel/-j [<jobs>] | 指定构建目标时使用的进程数 |
2.3 执行测试、安装或打包。
3. CMake具体操作流程
- 写CMakeLists.txt: 在具有源码的工作空间建立CMakeLists.txt文件.
- 生成构建系统: 在工作空间目录中生成构建系统, 执行
cmake -B build
, 执行完成后,在项目的根目录下会创建build目录,可以看到其中生成了Makefile文件。 - 执行构建: 在工作空间目录下执行
cmake --build build
, 使用make工具也可cd bbuild && make
. - build文件中有生成的可执行文件或者静态库.
4. CMake语法核心概念
CMake的命令有不同类型,包括脚本命令、项目配置命令和测试命令,细节可以查看官网cmake-commands。
CMake语言在项目配置中组织为三种源文件类型:
目录:CMakeLists.txt,针对的是一个目录,描述如何针对目录(Source tree)生成构建系统,会用到项目配置命令;
脚本:
4.1 注释
行注释使用"#“;块注释使用”#[[Some comments can be multi lines or in side the command]]"。
4.2 变量
CMake中使用set和unset命令设置或者取消设置变量。CMake中有以下常用变量类型:
4.2.1一般变量
设置的变量可以是字符串,数字或者列表(直接设置多个值,或者使用分号隔开的字符串格式为"v1;v2;v3"),比如:
# Set variable
set(AUTHOR_NAME Farmer)
set(AUTHOR "Farmer Li")
set(AUTHOR Farmer\ Li)
# Set list
set(SLOGAN_ARR To be) # Saved as "To;be"
set(SLOGAN_ARR To;be)
set(SLOGAN_ARR "To;be")
set(NUM 30) # Saved as string, but can compare with other number string
set(FLAG ON) # Bool value
注意: 如果要设置的变量值包含空格,则需要使用双引号或者使用"“转义,否则可以省略双引号; 如果设置多个值或者字符串值的中间有”;“,则保存成list,同样是以”;"分割的字符串; 变量可以被list命令操作,单个值的变量相当于只有一个元素的列表; 引用变量:${},在if()条件判断中可以简化为只用变量名。
4.2.2 Cache变量
Cache变量(缓存条目,cache entries)的作用主要是为了提供用户配置选项,如果用户没有指定,则使用默认值,设置方法如下:
# set(<variable> <value>... CACHE <type> <docstring> [FORCE])
set(CACHE_VAR "Default cache value" CACHE STRING "A sample for cache variable")
注意: 主要为了提供可配置变量,比如编译开关; 引用CACHE变量 $CACHE{<varialbe>}
。
Cache变量会被保存在构建目录下的CMakeCache.txt中,缓存起来之后是不变的,除非重新配置更新
4.2.3 环境变量
修改当前处理进程的环境变量,设置和引用格式为:
# set(ENV{<variable>} [<value>])
set(ENV{ENV_VAR} "$ENV{PATH}")
message("Value of ENV_VAR: $ENV{ENV_VAR}")
和CACHE变量类似,要引用环境变量,格式为:$ENV{}。
4.3 条件语句
支持的语法有:
字符串比较,比如:STREQUAL、STRLESS、STRGREATER等;
数值比较,比如:EQUAL、LESS、GREATER等;
布尔运算,AND、OR、NOT;
路径判断,比如:EXISTS、IS_DIRECTORY、IS_ABSOLUTE等;
版本号判断;等等;
使用小括号可以组合多个条件语句,比如:(cond1) AND (cond2 OR (cond3))。
对于常量:
- ON、YES、TRUE、Y和非0值均被视为True;
- 0、OFF、NO、FALSE、N、IGNORE、空字符串、NOTFOUND、及以"-NOTFOUND"结尾的字符串均视为False。
对于变量,只要其值不是常量中为False的情形,则均视为True。
5. CMake常用的脚本命令
有了前面的总体概念,下面掌握一些常用的CMake命令,对于CMake脚本编写就可以有不错的基础。
5.1 消息打印
前面已经有演示,即message命令,其实就是打印log,用来打印不同信息,常用命令格式为:
message([<mode>] "message text" ...)
其中mode就相当于打印的等级,常用的有这几个选项:
空或者NOTICE:比较重要的信息,如前面演示中的格式
DEBUG:调试信息,主要针对开发者
STATUS:项目使用者可能比较关心的信息,比如提示当前使用的编译器
WARNING:CMake警告,不会打断进程
SEND_ERROR:CMake错误,会继续执行,但是会跳过生成构建系统
FATAL_ERROR:CMake致命错误,会终止进程
5.2 条件分支
这里以if()/elseif()/else()/endif()举个例子,for/while循环也是类似的:
set(EMPTY_STR "")
if (NOT EMPTY_STR AND FLAG AND NUM LESS 50 AND NOT NOT_DEFINE_VAR)
message("The first if branch...")
elseif (EMPTY_STR)
message("EMPTY_STR is not empty")
else ()
message("All other case")
endif()
5.3 列表操作
list也是CMake的一个命令,有很多有用的子命令,比较常用的有:
APPEND,往列表中添加元素;
LENGTH,获取列表元素个数;
JOIN,将列表元素用指定的分隔符连接起来;
5.4 文件操作
CMake的file命令支持的操作比较多,可以读写、创建或复制文件和目录、计算文件hash、下载文件、压缩文件等等。 使用的语法都比较类似,以笔者常用的递归遍历文件为例,下面是获取src目录下两个子目录内所有c文件的列表的示例:GLOB_RECURSE表示执行递归查找,查找目录下所有符合指定正则表达式的文件。
file(GLOB_RECURSE ALL_SRC
src/module1/*.c
src/module2/*.c
)
5.5 配置文件生成
使用configure_file命令可以将配置文件模板中的特定内容替换,生成目标文件。 输入文件中的内容@VAR@或者${VAR}在输出文件中将被对应的变量值替换。 使用方式为:
set(VERSION 1.0.0)
configure_file(version.h.in "${PROJECT_SOURCE_DIR}/version.h")
假设version.in.h的内容为:
#define VERSION "@VERSION@"
那么生成的version.h的内容为:
#define VERSION "1.0.0"
5.6 执行系统命令
使用execute_process命令可以执行一条或者顺序执行多条系统命令,对于需要使用系统命令获取一些变量值是有用的。比如获取当前仓库最新提交的commit的commit id:
execute_process(COMMAND bash "-c" "git rev-parse --short HEAD" OUTPUT_VARIABLE COMMIT_ID)
5.7 查找库文件
通过find_library在指定的路径和相关默认路径下查找指定名字的库,常用的格式如下:
find_library (<VAR> name1 [path1 path2 ...])
找到的库就可以被其他target使用,表明依赖关系。
5.8 include其他模块
include命令将cmake文件或者模块加载并执行。比如:
include(CPack) # 开启打包功能
include(CTest) # 开启测试相关功能
6. CMakeLists详解
介绍如何书写一个完备的CMakeLists.txt文件,满足一般项目的基础构建要求,CMake的语法将会更多介绍项目配置命令,主要有以下内容:
(1)设置一些自定义编译控制开关和自定义编译变量控制编译过程
(2)根据不同编译类型配置不同的编译选项和链接选项
(3)添加头文件路径、编译宏等常规操作
(4)编译生成不同类型的目标文件,包括可执行文件、静态链接库和动态链接库
(5)安装、打包和测试
6.1基础配置
6.1.1设置项目版本和生成version.h
一般来说,项目一般需要设置一个版本号,方便进行版本的发布,也可以根据版本对问题或者特性进行追溯和记录。
通过project命令配置项目信息,如下:
project(CMakeTemplate VERSION 1.0.0 LANGUAGES C CXX)
第一个字段是项目名称;通过VERSION指定版本号,格式为main.minor.patch.tweak,并且CMake会将对应的值分别赋值给以下变量(如果没有设置,则为空字符串):
PROJECT_VERSION, <PROJECT-NAME>_VERSION
PROJECT_VERSION_MAJOR, <PROJECT-NAME>_VERSION_MAJOR
PROJECT_VERSION_MINOR, <PROJECT-NAME>_VERSION_MINOR
PROJECT_VERSION_PATCH, <PROJECT-NAME>_VERSION_PATCH
PROJECT_VERSION_TWEAK, <PROJECT-NAME>_VERSION_TWEAK
因此,结合前一篇文章提到的configure_file命令,可以配置自动生成版本头文件,将头文件版本号定义成对应的宏,或者定义成接口,方便在代码运行的时候了解当前的版本号, 比如:
configure_file(src/c/cmake_template_version.h.in "${PROJECT_SOURCE_DIR}/src/c/cmake_template_version.h")
假如cmake_template_version.h.in内容如下:
#define CMAKE_TEMPLATE_VERSION_MAJOR @CMakeTemplate_VERSION_MAJOR@
#define CMAKE_TEMPLATE_VERSION_MINOR @CMakeTemplate_VERSION_MINOR@
#define CMAKE_TEMPLATE_VERSION_PATCH @CMakeTemplate_VERSION_PATCH@
执行cmake配置构建系统后,将会自动生成文件:cmake_template_version.h,其中@@将会被替换为对应的值:
#define CMAKE_TEMPLATE_VERSION_MAJOR 1
#define CMAKE_TEMPLATE_VERSION_MINOR 0
#define CMAKE_TEMPLATE_VERSION_PATCH 0
6…1.2指定编程语言版本
为了在不同机器上编译更加统一,最好指定语言的版本,比如声明C使用c99标准,C++使用c++11标准:
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)
这里设置的变量都是CMAKE_开头(包括project命令自动设置的变量),这类变量都是CMake的内置变量,正是通过修改这些变量的值来配置CMake构建的行为。CMAKE_、_CMAKE或者以下划线开头后面加上任意CMake命令的变量名都是CMake保留的。
6.1.3配置编译选项
通过命令add_compile_options命令可以为所有编译器配置编译选项(同时对多个编译器生效); 通过设置变量CMAKE_C_FLAGS可以配置c编译器的编译选项; 而设置变量CMAKE_CXX_FLAGS可配置针对c++编译器的编译选项。 比如:
add_compile_options(-Wall -Wextra -pedantic -Werror)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c++11")
6.1.4 配置编译类型
通过设置变量CMAKE_BUILD_TYPE
来配置编译类型,可设置为:Debug
、Release
、RelWithDebInfo
、MinSizeRel
等,比如:
set(CMAKE_BUILD_TYPE Debug)
当然,更好的方式应该是在执行cmake命令的时候通过参数-D指定:
cmake -B build -DCMAKE_BUILD_TYPE=Debug
可以针对不同的编译类型设置不同的编译选项,比如对于Debug版本,开启调试信息,不进行代码优化:
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
对于Release版本,不包含调试信息,优化等级设置为2:
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
6.1.5添加全局宏定义
通过命令add_definitions可以添加全局的宏定义,在源码中就可以通过判断不同的宏定义实现相应的代码逻辑。用法如下:
add_definitions(-DDEBUG -DREAL_COOL_ENGINEER)
6.1.6添加include目录
通过命令include_directories来设置头文件的搜索目录,比如:
include_directories(src/c)
6.2编译目标文件
一般来说,编译目标(target)的类型一般有静态库、动态库和可执行文件。 这时编写CMakeLists.txt主要包括两步:
编译:确定编译目标所需要的源文件
链接:确定链接的时候需要依赖的额外的库
下面以开源项目(cmake-template)来演示。项目的目录结构如下:
./cmake-template
├── CMakeLists.txt
├── src
│ └── c
│ ├── cmake_template_version.h
│ ├── cmake_template_version.h.in
│ ├── main.c
│ └── math
│ ├── add.c
│ ├── add.h
│ ├── minus.c
│ └── minus.h
└── test
└── c
├── test_add.c
└── test_minus.
项目的构建任务为:
将math目录编译成静态库,命名为math
编译main.c为可执行文件demo,依赖math静态库
编译test目录下的测试程序,可以通过命令执行所有的测试
支持通过命令将编译产物安装及打包
6.2.1编译静态库
这一步需要将项目目录路径src/c/math下的源文件编译为静态库,那么需要获取编译此静态库需要的文件列表,可以使用set命令,或者file命令来进行设置。比如:
在src/c/math里的CMakeLists.txt里面编译libmath.o, 静态链接库 add_library, STATIC:
cmake_minimum_required(VERSION 3.5.1)
file(GLOB_RECURSE MATH_LIB_SRC *.c)
add_library(math STATIC ${MATH_LIB_SRC})
使用file命令获取src/c/math目录下所有的*.c文件,然后通过add_library命令编译名为math的静态库,库的类型是第二个参数STATIC指定的. 如果指定为SHARED则编译的就是动态链接库。
6.2.2 编译可执行文件
通过add_executable命令来往构建系统中添加一个可执行构建目标,同样需要指定编译需要的源文件。但是对于可执行文件来说,有时候还会依赖其他的库,则需要使用target_link_libraries命令来声明构建此可执行文件需要链接的库。
在示例项目中,main.c就使用了src/c/math下实现的一些函数接口,所以依赖于前面构建的math库。所以在CMakeLists.txt中添加以下内容:
add_executable(demo src/c/main.c)
target_link_libraries(demo math)
第一行说明编译可执行文件demo需要的源文件(可以指定多个源文件,此处只是以单个文件作为示例);第二行表明对math库存在依赖。
此时可以在项目的根目录下执行构建和编译命令,并执行demo.
6.3安装和打包
6.3.1安装
对于安装来说,其实就是要指定当前项目在执行安装时,需要安装什么内容:通过install命令来说明需要安装的内容及目标路径;通过设置CMAKE_INSTALL_PREFIX变量说明安装的路径;v3.15往后的版本可以使用cmake --install --prefix <install-path>
覆盖指定安装路径。
比如,在示例项目中,把math和demo两个目标按文件类型安装:
install(TARGETS math demo
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
这里通过TARGETS参数指定需要安装的目标列表;参数RUNTIME DESTINATION、LIBRARY DESTINATION、ARCHIVE DESTINATION分别指定可执行文件、库文件、归档文件分别应该安装到安装目录下个哪个子目录。
如果指定CMAKE_INSTALL_PREFIX为/usr/local,那么math库将会被安装到路径/usr/local/lib/目录下;而demo可执行文件则在/usr/local/bin目录下。
CMAKE_INSTALL_PREFIX在不同的系统上有不同的默认值,使用的时候最好显式指定路径。
同时,还可以使用install命令安装头文件:
file(GLOB_RECURSE MATH_LIB_HEADERS src/c/math/*.h)
install(FILES ${MATH_LIB_HEADERS} DESTINATION include/math)
假如将安装到当前项目的output文件夹下,可以执行:
➜ # cmake -B cmake-build -DCMAKE_INSTALL_PREFIX=./output
➜ # cmake --build cmake-build
➜ # cd cmake-build && make install && cd -
Install the project...
-- Install configuration: ""
-- Installing: .../cmake-template/output/lib/libmath.a
-- Installing: .../gitee/cmake-template/output/bin/demo
-- Installing: .../gitee/cmake-template/output/include/math/add.h
-- Installing: .../gitee/cmake-template/output/include/math/minus.h
可以看到安装了前面install命令指定要安装的文件,并且不同类型的目标文件安装到不同子目录。
6.3.2 打包
6.4测试
6.4.1编写测试程序
6.4.2 添加测试
6.4.3 执行测试
------------------------------------end------------------------------------------
Reference:
转自https://zhuanlan.zhihu.com/p/371257515
参考https://blog.csdn.net/rangfei/article/details/108756768
本文仅作个人学习记录, 如有侵权, 立即删除.