CMake

cmake的关键词大小写不敏感,在cmake中,变量名直接给出,不需要声明,和shell语言一样,引用变量时用变量名,大多数变量见到的都是字符串,如果不是却又想转成字符串,则用"{变量名},大多数变量见到的都是字符串,如果不是却又想转成字符串,则用"{变量名}",这和shell也是如出一辙。

cmake_minimum_required(VERSION 3.10) 指定CMake的最低版本要求

project(Tutorial VERSION 1.0) 指定该编译项目的名字和迭代版本

各种路径解释(均为绝对路径)

PROJECT_SOURCE_DIR

是最近给出project命令的CMakeLists.txt所在的路径。一般为顶层CMakeLists.txt所在的目录

PROJECT_BINARY_DIR

是cmake构建时的文件路径,例如如果我们在build目录下构建,则build目录就是一个该路径。

CMAKE_CURRENT_SOURCE_DIR

指CMakeLists.txt所在的源文件目录

CMAKE_CURRENT_BINARY_DIR

指构建时CMakeLists.txt所在的目录

CMAKE_SOURCE_DIR

顶层CMakeLists.txt所在的源文件目录,所谓的顶层source tree目录

CMAKE_BINARY_DIR

顶层CMakeLists.txt在构建时所在的目录,所谓的顶层binary tree目录

简单来说,*_binary_dir是指构建的目录,例如如果在build目录下构建,则会是...../build/

如果在最顶层的当前目录下构建,则这些目录都会相等。

source和源文件相关,即没有执行cmake前的文件;而binary则和构建的文件相关,即执行了cmake后的文件。由于源文件和构建后的文件存在很多相同文件,需要特此注意,自己留意下build目录的文件和原始文件就能发现该现象。

add_library(Name [STATIC|SHARED] [EXCLUDE_FROM_ALL] source1 [source2...])

add_library使cmake编译一个库,要么是STATIC静态库,要么是SHARED动态库,EXCLUDE_FROM_ALL指代这个库不会被默认构建,除非有组件依赖或手工构建。

编译出来的库文件名为libName.a或libName.so

重名的add_library命令只会执行第一条,例如add_library(mylib staic helloworld.cc), add_library(mylib shared helloworld.cc),编译出来只有libmylib.a。

SET_TARGET_PROPERTIES(mylib-shared PROPERTIES OUTPUT_NAME "mylib")

可以设置库的输出文件名。

SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1)

#VERSION 指代动态库版本,SOVERSION 指代 API 版本。

add_executable(OutPutName [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2...])

add_executable生成指定OutPutName的可执行文件,编译source1...等一系列源文件,该文件名必须是在项目中全局唯一的,可以理解为类似makefile中的一个target名。该名字会和target_link_libraries、target_include_directories相对应,指定编译出这个target所需的库文件和库目录。add_executable和add_library都是会生成ELF文件的,也即都是一个target,所以target_XXX_XXX命令都需要在这两者之后给出。

编译的CMake的版本需求即可通过cmake_minimum_required给出,也可以通过configure_file()命令给出,配置文件中的相应宏会被替换并注入到源文件中,配置文件会写入到bin目录中.

.in后缀的文件中写好cmake能够识别的替换代码,执行该语句后会生成.h头文件,此时相应的需要替换的内容已经完成

#cmakedefine这句会在cmake执行后在相应的目标头文件(这里为TutorialConfig.h)中#define指定宏名(这里为#define USE_MYMATH),需要注意的是,#cmakedefine是指如果在当前的可见域中,定义了某个量,则在config文件中才会去定义同名的宏

源文件需要引用,所以需要把搜索路径也添加到项目的当前搜索路径中,使用

target_include_directories(targetname <PUBLIC,PRIVATE,INTERFACE> search_dir)

要注意这个不等同于g++中的-L选项,-L是指定库文件的搜索路径,而include_directories是指定头文件的搜索路径,两者有区别。

target_link_libraries(targetname libname)

指定target需要链接的库文件

add_subdirectory(sourcedir [binary_dir] [EXCLUDE_FROM_ALL])

sourcedir告诉cmake还有哪个路径下也有CMakeLists.txt和源文件,binary_dir指定在哪个目录下放置输出文件(指sourcedir下的CMakeLists.txt编译出来的输出文件),sourcedir和binary_dir可用绝对/相对路径。如果不指定binary_dir,输出文件则直接生成在sourcedir中。

该语句还会将该目录添加到库文件搜索路径中:

上图中MathFunctions库文件在./MathFunctions/下,而extra_libs并没有指定该路径,却能找到该库,是因为add_subdirectory已经把该路径添加到库文件搜索路径了。

值得注意的是:在上层CMakeLists.txt中执行到add_subdirectory时,会立刻去执行子目录的CMakeLists.txt,执行完之后才会回到上层继续执行,所以需要注意该语句的顺序,例如如果要先编译静态库再进行主函数的编译,则应在主函数cc文件编译之前给出这句语句。

option(optionName "annotation" ON)

指定一个选项名,双引号给出其注释,ON表示打开该选项,可用于控制选择性编译某些文件。

e.x. option(USE_MYMATH “use my math implementation” ON)

默认情况下,若不指定选项的值,则以CMakeLists.txt文件中的值为准,例如如果要关闭该选项可执行: cmake ../ -DUSE_MYMATH=off 命令,注意是在build之前执行的cmake命令行。


指定C++标准号

set(CMAKE_CXX_STANDARD 11)

设置项目需要设置C++标准号

set(CMAKE_CXX_STANDARD_REQUIRED True)

CMake中的语句有顺序要求,例如上面这两句必须在add_executable之前输入,而target_include_directories在文件最后输入。具体命令具体分析

我们一般都会建立一个build文件夹,进入build目录,如果这个目录在最顶层CMakeLists.txt文件所在的目录下,那么执行

cmake ..

该命令行实际作用是用cmake程序执行一些预处理,例如将源代码中注入配置文件的代码,还有设置编译参数等一系列设置操作,也可以理解为用cmake对CMakeLists.txt转换成makefile:(CMakeFiles目录下有处理后的源文件等内容)

而且build目录中会有预处理后的所有文件,在获取这些文件之后,在build目录下执行:

cmake --build .

实现真正的编译、链接,得到最终的可执行文件(ELF文件)


如果一个库或者可执行文件的编译不需要依赖别的文件,只需要自己就可以编译,而别人又需要依赖这个库或者可执行文件,那么这个库或可执行文件就称作是一个INTERFACE,对于这种INTERFACE,在他的CMakeLists.txt文件下可将target_include_directories指定为:

e.x. target_include_directories(MathFunctions INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

该语句会为target将其指定目录填充到INTERFACE_INCLUDE_DIRECTORIES。

如果别的文件A依赖这个target,使用target_link_libraries后,cmake会自动去INTERFACE_INCLUDE_DIRECTORIES找这些库文件进行链接,保证成功编译(在A文件编译的CMakeLists.txt文件中就不需要再把这个依赖文件的路径写到include_directories里了)。

https://cmake.org/cmake/help/latest/guide/tutorial/index.html#adding-usage-requirements-for-library-step-3 Step3

CMAKE_CURRENT_SOURCE_DIR指代当前正在执行的CMakeLists.txt文件所在的目录。

Install Rules

我们一致使用的build是构建整个项目,不包括install和测试的内容,而install的主要作用是将某些文件指定安装到CMAKE_INSTALL_DIR里面,两者不冲突。

可以用来指定安装ELF文件或者头文件:

install(TARGETS MathFunctions DESTINATION lib)

install(FILES MathFunctions.h DESTINATION include)

没有指定绝对路径的话,路径前缀使用CMAKE_INSTALL_PREFIX,在UNIX下默认为/usr/local.

使用cmake --install . 进行安装,若要指定安装的前缀路径则命令为 cmake --install . --prefix "/home/rex/"

Testing Support (cmake支持测试)

在顶层CMakeLists.txt中使能测试,并指定测试的方法。PASS_REGULAR_EXPRESSION是传递一个正则表达式,查看测试单元输出的结果中是否有包含给定的字符串。

照常编译即可,在编译后的可执行文件目录下,执行ctest -N查看所有测试的内容,ctest -VV执行这些测试


增加系统检查

由于不同的平台可能有些函数并不存在,需要在编译的时候进行系统检查,判断函数是否存在,不存在应使用哪种函数替代。

使用CheckSymbolExits模块实现该功能

e.x.

include(CheckSymbolExists)

check_symbol_exists(log "math.h" HAVE_LOG)

在使用该模块前,需要在CMakeLists.txt文件中include该模块,随后使用它提供的check_symbol_exists函数检查指定名字(log)是否存在文件中(math.h),把检查结果1或0赋给变量(HAVE_LOG)

打印变量值

set(variable1 value)

message(STATUS "the value is ${variable1}")

status表示一般的打印,还有error可以指代,用于打印错误信息

变量作用域

和c++一样,外层作用域的量对内层可以,反之不可见,当前可以简单的理解成CMakeLists.txt的层级关系就是所谓的内层和外层。

例如如果顶层CMakeLists.txt定义了变量A,则在下层CMakeLists.txt中能够引用该变量

SET与UNSET

1.SET可用来设置普通变量、CACHE变量、环境变量,后两者具体应用时再补充

SET(variableName <values... > [PARENT_SCOPE])

在当前作用域或函数中定义一个变量并赋值,若变量未定义则定义一个新变量,若已定义,则对该变量赋值为value,如果value是一系列值,则该变量是一个列表,每个值都会用分号分隔开。如果values为空,其作用与unset一样,相当于取消该变量的定义,该变量变为未定义状态。

指定PARENT_SCOPE代表该变量可被其上层作用域可见,否则上层均不可见。

2.UNSET用来取消普通变量、CACHE变量、环境变量的定义

unset(variableName [CACHE | PARENT_SCOPE])

取消普通变量的定义,指定CACHE则取消CACHE变量的定义,指定PARENT_SCOPE则同时取消上层作用域的该同名变量的定义。

使用${variable}时,cmake先查普通变量中是否有该变量,若无则继续在CACHE变量中查找。所以如果unset一个变量A,则可以在CACHE变量中暴露出来,因为最终找到的是CACHE变量中的A变量。

unset(ENV{variable})

cmake保留变量名规范

cache变量含义

cmake环境变量

增加编译定义

target_compile_definitions(targetName [PUBLIC|PRIVATE|INTERFACE] item1,item2...)

给指定的目标文件注入宏定义

e.x.

target_compile_definitions(MathFunctions PRIVATE HAVE_LOG)

向目标文件注入HAVE_LOG宏定义

若被指定item没有使用=号,则默认定义为1,若想指定则需item=value来指定。


添加自定义命令和生成的文件

add_custom_command(OUTPUT filename COMMAND targetname args DEPENDS target)

上面的命令只给出了该命令的一个示例,其有很多参数,需要具体应用时查询。

COMMAND是让cmake在构建时执行的命令,DEPENDS指执行COMMAND依赖的东西(只有这个依赖已存在了才能执行COMMAND)。

OUTPUT是指定COMMAND命令语句生成的文件名,可以看待成是一个target,如果有别的语句用到了这个OUTPUT后的变量,cmake才会去执行add_custom_command语句,也即被依赖了才会去执行。

https://www.cnblogs.com/dangerman/p/6529742.html

这个命令的作用是在构建时生成一些文件,而这些文件可以被其他的源文件引用,这样可以增加编译的灵活性。

编译文件的顺序

如果A库依赖B库,则在g++命令行中,B文件名应在A文件名右边。g++优先编译右侧文件生成obj文件,再来编译左侧的文件。有时候发现未定义的引用,但明明给定的文件有该引用,往往是因为这个问题。

在cmake中也有一样的问题,add_library和add_executable的源文件列表也要注意相应的顺序问题。


include(ModuleName)

在cmake中引用某个模块

源码/可执行文件的分发

include(InstallRequiredSystemLibraries)

set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")

set(CPACK_PACKAGE_VERSION_MAJOR "TutorialVERSIONMAJOR")set(CPACKPACKAGEVERSIONMINOR"{Tutorial_VERSION_MAJOR}") set(CPACK_PACKAGE_VERSION_MINOR "{Tutorial_VERSION_MINOR}")
include(CPack)

InstallRequiredSystemLibraries模块是用来包含项目在当前平台下所需的运行时库,FILE_LICENSE是指定一个文件,在他人使用到我们分发的包时,会提醒别人我们的许可证情况。CPack模块是用来打包项目的

在执行完cmake --build .命令后,再执行cpack,即可将可执行文件打包成tar格式,其中打包的规则是由CMakeList.txt中的install命令指定的,如果不指定,则打包后为空。

若想改成其他打包文件格式可以-G指定:

cpack -G ZIP

如果想打包源文件则

cpack --config CPackSourceConfig.cmake

打包的都是我们build之后的所有文件


测试结果在面板上显示

将enable_testing()替换成include(CTest),该语句会自动执行enable_testing()。

在顶层目录新建CTestConfig.cmake文件,指定我们测试的参数和想要在哪里展示测试结果

set(CTEST_PROJECT_NAME "CMakeTutorial")

set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")

​

set(CTEST_DROP_METHOD "http")

set(CTEST_DROP_SITE "my.cdash.org")

set(CTEST_DROP_LOCATION "/submit.php?project=CMakeTutorial")

set(CTEST_DROP_SITE_CDASH TRUE)

CTest在执行时会读取该文件

ctest [-VV] -D Experimental

在网页上dashboard显示测试结果


静态、动态库混合

可以通过变量BUILD_SHARED_LIBS来控制是编译动态库还是静态库,该变量可作为选项在顶层CMakeLists.txt中给出

option(BUILD_SHARED_LIBS "Build shared libraries" ON)

打开该选项后,项目的所有库都会默认以动态库的进行编译,若要指定某个库为静态库,可以

add_library(libName STATIC source_file...)

但要注意的是,动态库一般都是位置无关代码position independent code(PIC),而静态库不是,所以在混合编译时,需要将静态库的属性指定PIC

set_target_properties(staticLibName PROPERTIES POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS})

若不指定,则会:


PUBLIC,PRIVATE,INTERFACE总结

基本上所有命令有这几个参数的都可以用下面的方式去理解。

先了解一个interface library:

一个interface library可以被当成一般的library使用,一些有library的命令可以使用这个库,但是这个库并没有实际文件对应,可以看成是一个标志性的库。在cmake中,每个target都有自己的编译属性,编译参数,例如c++标准,所链接的库,include的目录等等,这些信息都是可以传递给使用者的,例如:

target_link_libraries(使用者(target名) [PUBLIC|PRIVATE|INTERFACE] 生产者(库名))

这样,一个target的编译信息就可以传递给依赖这各target的其他target,而这些参数的传递是通过PUBLIC,PRIVATE,INTERFACE来控制的。

PUBLIC和PRIVATE为一类,INTERFACE为一类

对于在命令中被指定INTERFACE的对象obj,表示使用者(A)不需要这个对象,但这个对象的信息可以传递给别的使用者(假设为B),则B可以通过使用A来获得对象obj的编译信息。

例如,一个源文件编译所需的头文件目录就在其当前目录下,所以该头文件的路径本质上是不需要用target_include_directories指定的,也即使用者A不需要的,但可以用INTERFACE来指定该路径,那么,当别的target使用A时,该路径便可被别的target继承,别的target自然而然就把这个目录添加到头文件搜索路径中去了。

而PUBLIC,PRIVATE指定的对象obj,使用者A都会使用到对象obj,两者的区别在于,PUBLIC指定下,obj的信息给传递给别的使用者B,而PRIVATE指定下,obj的信息将无法传递给别的使用者。

PUBLIC和PRIVATE的逻辑可以和类的公有和私有继承想类比。而INTERFACE为单独一种逻辑,需要额外记忆。

下面的例子可用于了解三者区别。

add_library(archive archive.cpp)

target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)

​

add_library(serialization serialization.cpp)

target_compile_definitions(serialization INTERFACE USING_SERIALIZATION_LIB)

​

add_library(archiveExtras extras.cpp)

target_link_libraries(archiveExtras PUBLIC archive)

target_link_libraries(archiveExtras PRIVATE serialization)

archiveExtras is compiled with -DUSING_ARCHIVE_LIB

and -DUSING_SERIALIZATION_LIB

​

add_executable(consumer consumer.cpp)

consumer is compiled with -DUSING_ARCHIVE_LIB

target_link_libraries(consumer archiveExtras)


Generator Expression

$<command: ...>

生成器表达式是在构建的时候才会求值,表达式有很多种类型,可用于条件、逻辑、变量评估等等,具体参考官方文档

https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#introduction

对于条件表达式

<0:...>则得到的是空字符串,若<0: ...>则得到的是空字符串,若<1: ...>则得到的是...指定的内容

确定编译的语言和编译平台:

修改编译属性和参数

target_compile_features(targetName INTERFACE cxx_std_11)

使用上述命令,可以替代

target_compile_options(targetName INTERFACE -Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused)

用于指定编译参数


添加导出配置

可以将我们自己项目构建的库给别的项目使用,导出我们项目的信息,别的项目include该信息文件即可使用我们指定导出的target

install(TARGETS ${installable_libs} DESTINATION lib EXPORT MathFunctionsTargets)

该命令相应的会生成一个MathFunctionsTargets.cmake文件和安装指定的库到指定的目录中,别的项目使用include命令include该路径即可引用我们安装的这些库了。

打包debug和release版本

https://cmake.org/cmake/help/latest/guide/tutorial/index.html#packaging-debug-and-release-step-12