Cmake教程

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、编译构建相关的核心概念及它们之间的关系

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 [

| --preset ]执行构建。

这里要指定的目录就是生成构建系统时指定的构建目录。常用的参数如下
在这一步,如果使用的是make构建工具,则可以在构建目录下直接使用make命令。


2.简单实例

1、编写CMakeLists.txt

2、生成构建系统(Makefile)

3、执行构建 (上图中的Make部分)

Cmake构建简单实例(参考上图来分析每一步的目的)

三、 核心语法篇

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查看。

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值