提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、编译构建相关的核心概念及它们之间的关系
gcc(GNU Compiler Collection)
将源文件编译(Compile)成可执行文件或者库文件;
而当需要编译的东西很多时,需要说明先编译什么,后编译什么,这个过程称为构建(Build)。
常用的工具是make,对应的定义构建过程的文件为Makefile;
而编写Makefile对于大型项目又比较复杂,通过CMake就可以使用更加简洁的语法定义构建的流程,CMake定义构建过程的文件为CMakeLists.txt。
它们的大致关系如下图: 这里的GCC只是示例,也可以是其他的编译工具。
这里的Bin表示目标文件,可以是可执行文件或者库文件。
二、CMake使用流程
CMake提供cmake、ctest和cpack三个命令行工具分别负责构建、测试和打包。本文主要介绍cmake命令。
使用cmake一般流程为:
生成构建系统(buildsystem,比如make工具对应的Makefile);
执行构建(比如make),生成目标文件;
执行测试、安装或打包。
本文先介绍前面两个步骤。
1.生成构建系统
通过cmake命令生成构建系统。
通过cmake --help可以看到cmake命令支持的详细参数,常用的参数如下:
参数 | 含义 |
---|---|
-S | 指定源文件根目录,必须包含一个CMakeLists.txt文件 |
-B | 指定构建目录,构建生成的中间文件和目标文件的生成路径 |
-D | 指定变量,格式为-D =,-D后面的空格可以省略 |
比如,指明使用当前目录作为源文件目录,其中包含CMakeLists.txt文件;使用build目录作为构建目录;设定变量CMAKE_BUILD_TYPE的值为Debug,变量AUTHOR的值为RealCoolEngineer:
cmake -S . -B build -D CMAKE_BUILD_TYPE=Debug -D AUTHOR=RealCoolEngineer
使用-D设置的变量在CMakeLists.txt中生效,可以设置cmake的内置支持的一些变量控制构建的行为;当然也可以使用自定义的变量,在CMakeLists.txt中自行判断做不同的处理。
2.执行构建
使用cmake --build [
这里要指定的目录就是生成构建系统时指定的构建目录。常用的参数如下
在这一步,如果使用的是make构建工具,则可以在构建目录下直接使用make命令。
2.简单实例
1、编写CMakeLists.txt
2、生成构建系统(Makefile)
3、执行构建 (上图中的Make部分)
三、 核心语法篇
1、CMake语法核心概念
CMake的命令有不同类型,包括脚本命令、项目配置命令和测试命令,细节可以查看官网cmake-commands。
CMake语言在项目配置中组织为三种源文件类型:
目录:CMakeLists.txt,针对的是一个目录,描述如何针对目录(Source tree)生成构建系统,会用到项目配置命令;
脚本:< script>.cmake就是一个CMake语言的脚本文件,可使用cmake -P直接执行,只能包含脚本命令;
模块:< module>.cmake,实现一些模块化的功能,可以被前面两者包含,比如include(CTest)启用测试功能。
1、注释
行注释使用"#“;块注释使用”#[[Some comments can be multi lines or in side the command]]"
2、变量
CMake中使用set和unset命令设置或者取消设置变量。CMake中有以下常用变量类型。
一般变量
设置的变量可以是字符串,数字或者列表(直接设置多个值,或者使用分号隔开的字符串格式为"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()条件判断中可以简化为只用变量名。
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{}。
Cache变量会被保存在构建目录下的CMakeCache.txt中,缓存起来之后是不变的,除非重新配置更新
环境变量
修改当前处理进程的环境变量,设置和引用格式为:
# set(ENV{<variable>} [<value>])
set(ENV{ENV_VAR} "$ENV{PATH}")
message("Value of ENV_VAR: $ENV{ENV_VAR}")
和CACHE变量类似,要引用环境变量,格式为:$ENV{}。
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。
2、常用的脚本命令
1、消息打印
即message命令,其实就是打印log,用来打印不同信息,常用命令格式为:
message([<mode>] "message text" ...)
其中mode就相当于打印的等级,常用的有这几个选项:
空或者NOTICE:比较重要的信息,如前面演示中的格式
DEBUG:调试信息,主要针对开发者
STATUS:项目使用者可能比较关心的信息,比如提示当前使用的编译器
WARNING:CMake警告,不会打断进程
SEND_ERROR:CMake错误,会继续执行,但是会跳过生成构建系统FATAL_ERROR:CMake致命错误,会终止进程
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()
3、列表操作
list也是CMake的一个命令,有很多有用的子命令,比较常用的有:
APPEND,往列表中添加元素;
LENGTH,获取列表元素个数;
JOIN,将列表元素用指定的分隔符连接起来;
set(SLOGAN_ARR To be) # Saved as "To;be"
set(SLOGAN_ARR To;be)
set(SLOGAN_ARR "To;be")
set(WECHAT_ID_ARR Real Cool Eengineer)
list(APPEND SLOGAN_ARR a) # APPEND sub command
list(APPEND SLOGAN_ARR ${WECHAT_ID_ARR}) # Can append another list
list(LENGTH SLOGAN_ARR SLOGAN_ARR_LEN) # LENGTH sub command
# Convert list "To;be;a;Real;Cool;Engineer"
# To string "To be a Real Cool Engineer"
list(JOIN SLOGAN_ARR " " SLOGEN_STR)
message("Slogen list length: ${SLOGAN_ARR_LEN}")
message("Slogen list: ${SLOGAN_ARR}")
message("Slogen list to string: ${SLOGEN_STR}\n")
对于列表常用的操作,list命令都基本实现了,需要其他功能直接查阅官方文档即可。
4、文件操作
CMake的file命令支持的操作比较多,可以读写、创建或复制文件和目录、计算文件hash、下载文件、压缩文件等等。
使用的语法都比较类似,以笔者常用的递归遍历文件为例,下面是获取src目录下两个子目录内所有c文件的列表的示例:
file(GLOB_RECURSE ALL_SRC
src/module1/*.c
src/module2/*.c
)
GLOB_RECURSE表示执行递归查找,查找目录下所有符合指定正则表达式的文件。
5、配置文件生成
使用configure_file命令可以将配置文件模板中的特定内容替换,生成目标文件。
输入文件中的内容@VAR@或者${VAR}在输出文件中将被对应的变量值替换。
使用方式为:
set(VERSION 1.0.0)
configure_file(version.h.in "${PROJECT_SOURCE_DIR}/version.h")
假设version.h.in的内容为:
#define VERSION "@VERSION@"
那么生成的version.h的内容为:
#define VERSION "1.0.0"
6、执行系统命令
使用execute_process命令可以执行一条或者顺序执行多条系统命令,对于需要使用系统命令获取一些变量值是有用的。
比如获取当前仓库最新提交的commit的commit id:
execute_process(COMMAND bash "-c" "git rev-parse --short HEAD" OUTPUT_VARIABLE COMMIT_ID)
7、查找库文件
通过find_library在指定的路径和相关默认路径下查找指定名字的库,常用的格式如下:
find_library (<VAR> name1 [path1 path2 ...])
找到的库就可以被其他target使用,表明依赖关系。
8、include其它模块
include命令将cmake文件或者模块加载并执行。
比如:
include(CPack) # 开启打包功能
include(CTest) # 开启测试相关功能
CMake自带有很多有用的模块,可以看看官网的链接:cmake-modules,对支持的功能稍微有所了解,后续有需要再细看文档。
四 、CMakeLists.txt完全指南
1、基础配置
1、设置项目版本和生成version.h
项目一般需要设置一个版本号,方便进行版本的发布,也可以根据版本对问题或者特性进行追溯和记录。
通过project命令配置项目信息,如下:
project(CMakeExample VERSION 1.0.0 LANGUAGES C CXX)
第一个字段是项目名称;通过VERSION指定版本号,格式为major.minor.patch.tweak,并且CMake会将对应的值分别赋值给以下变量(如果没有设置,则为空字符串):
PROJECT_VERSION, <PROJECT-NAME>_VERSION
PROJECT_VERSION_MAJOR, <PROJECT-NAME>_VERSION_MAJOR //1
PROJECT_VERSION_MINOR, <PROJECT-NAME>_VERSION_MINOR //0
PROJECT_VERSION_PATCH, <PROJECT-NAME>_VERSION_PATCH //0
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
2、指定编程语言版本
为了在不同机器上编译更加统一,最好指定语言的版本,比如声明C使用c99标准,C++使用c++11标准:
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)
这里设置的变量都是CMAKE_开头(包括project命令自动设置的变量),这类变量都是CMake的内置变量,正是通过修改这些变量的值来配置CMake构建的行为。
CMAKE_、_CMAKE或者以下划线开头后面加上任意CMake命令的变量名都是CMake保留的。
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")
主要目的是为了提供更多的警告和错误检查,并设置C和C++的编译器标志,以便使用最新的标准进行编译。
4、配置编译类型
通过设置变量CMAKE_BUILD_TYPE来配置编译类型,可设置为:Debug、Release、RelWithDebInfo、MinSizeRel等,比如:
set(CMAKE_BUILD_TYPE Debug)
如果设置编译类型为Debug,那么对于c编译器,CMake会检查是否有针对此编译类型的编译选项CMAKE_C_FLAGS_DEBUG,如果有,则将它的配置内容加到CMAKE_C_FLAGS中。
可以针对不同的编译类型设置不同的编译选项,比如对于Debug版本,开启调试信息,不进行代码优化:
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
5、添加全局宏定义
通过命令add_definitions可以添加全局的宏定义,在源码中就可以通过判断不同的宏定义实现相应的代码逻辑。
用法如下:
add_definitions(-DDEBUG -DREAL_COOL_ENGINEER)
6、添加include目录
通过命令include_directories来设置头文件的搜索目录,比如:
include_directories(src/c)
编译目标文件
一般来说,编译目标(target)的类型一般有静态库、动态库和可执行文件。 这时编写CMakeLists.txt主要包括两步:
编译:确定编译目标所需要的源文件
链接:确定链接的时候需要依赖的额外的库
下面以开源项目(cmake-template)来演示。项目的目录结构如下:
项目的构建任务为:
将math目录编译成静态库,命名为math
编译main.c为可执行文件demo,依赖math静态库
编译test目录下的测试程序,可以通过命令执行所有的测试
支持通过命令将编译产物安装及打包
1、编译静态库
这一步需要将项目目录路径src/c/math下的源文件编译为静态库,那么需要获取编译此静态库需要的文件列表,可以使用set命令,或者file命令来进行设置。比如:
file(GLOB_RECURSE MATH_LIB_SRC
src/c/math/*.c
)
add_library(math STATIC ${MATH_LIB_SRC})
使用file命令获取src/c/math目录下所有的*.c文件,然后通过add_library命令编译名为math的静态库,库的类型是第二个参数STATIC指定的。
如果指定为SHARED则编译的就是动态链接库。
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:
安装和打包
1、安装
对于安装来说,其实就是要指定当前项目在执行安装时,需要安装什么内容:
通过install命令来说明需要安装的内容及目标路径;
通过设置CMAKE_INSTALL_PREFIX变量说明安装的路径;
3.15往后的版本可以使用cmake --install --prefix 覆盖指定安装路径。
比如,在示例项目中,把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命令指定要安装的文件,并且不同类型的目标文件安装到不同子目录。
2、打包
要使用打包功能,需要执行include(CPack)启用相关的功能,在执行构建编译之后使用cpack命令行工具进行打包安装;对于make工具,也可以使用命令make package。
打包的内容就是install命令安装的内容,关键需要设置的变量有:
CPACK_GENERATOR | 打包使用的压缩工具,比如"ZIP" |
CPACK_OUTPUT_FILE_PREFIX | 打包安装的路径前缀 |
CPACK_INSTALL_PREFIX | 打包压缩包的内部目录前缀 |
CPACK_PACKAGE_FILE_NAME | 打包压缩包的名称,由CPACK_PACKAGE_NAME、CPACK_PACKAGE_VERSION、CPACK_SYSTEM_NAME三部分构成 |
include(CPack)
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_NAME "CMakeTemplate")
set(CPACK_SET_DESTDIR ON)
set(CPACK_INSTALL_PREFIX "")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
假如: CPACK_OUTPUT_FILE_PREFIX设置为/usr/local/package; CPACK_INSTALL_PREFIX设置为real/cool/engineer; CPACK_PACKAGE_FILE_NAME设置为CMakeTemplate-1.0.0; 那么执行打包文件的生成路径为:
/usr/local/package/CMakeTemplate-1.0.0.zip
解压这个包得到的目标文件则会位于路径下:
/usr/local/package/real/cool/engineer/
此时重新执行构建,使用cpack命令执行打包:
➜ # cmake -B cmake-build -DCPACK_OUTPUT_FILE_PREFIX=`pwd`/output
➜ # cmake --build cmake-build
➜ # cd cmake-build && cpack && cd -
CPack: Create package using ZIP
CPack: Install projects
CPack: - Run preinstall target for: CMakeTemplate
CPack: - Install project: CMakeTemplate
CPack: Create package
CPack: - package: /Users/Farmer/gitee/cmake-template/output/CMakeTemplate-1.0.0-Darwin.zip generated.
cpack有一些参数是可以覆盖CMakeLists.txt设置的参数的,比如这里的-G参数就会覆盖变量CPACK_GENERATOR,具体细节可使用cpack --help查看。