CMake-02:核心概念

本篇将会介绍一些CMake中的关键概念, 在开始使用CMake时,您将遇到各种各样的概念,例如target,generator,commands等等, 这些概念在CMake中以C++类的形式被实现, 并且被众多CMake命令广泛的引用着。理解这些概念,会使你在工作中能够更高效的创建CMakeLists文件。

2.1 CMake的基础架构

在深入CMake的细节之前, 我们首先需要理清一些对象之间的基本关系。CMake流程最开始阶段是一些典型的C/C++源文件, 这些源文件被结合成一个个target, 一个target就是一个典型的可执行程序或者一个directory代表的是源码树中的目录, 这个目录中有一个与之相关的CMakeLists.txt文件, 同时也会有一个或者多个target与之关联。每一个directory都有一个local generator, 它会为这个目录生成标准的Makefile文件或者vs studio相关的工程文件, 所有的local generator都共享同一个公共的global generator,这个generator会监控着整个构建过程。这个global generator是由CMake本身创建和驱动。

下面然我们来更进一步的讨论这些概念,CMake程序是由一个CMake类的实例创建,同时传递一些命令行参数,该实例管理着CMake的整个配置过程, 同时保存一些构建中的全局信息, 例如缓存数据。CMake的要做的第一件事就是根据用户选择的generator去正确的创建global generator. 生成global generator是通过调用configure,和generate函数。此时,CMake就将控制权移交给了global generator.

global generator负责管理整个工程中的所有配置和生成的Makefile, 事实上,很多工作是有local generator完成的,而local generator是由global generator创建完成的,global generator会为工程中每一个要处理的目录生成一个local generator.因此一个工程只有一个global generator,但却有很多个local generator

当用户选择Unix makefile global generator的时,local generator负责为各个目录创建Makefile文件,而global generator负责生成顶层的Makefile同时协调整个处理过程。同时不同的目标generator在实现细节上具有很大差异。

每一个local generator都是一个cmMakefile类的实例,cmMakefileCMakeLists文件解析结果存储的地方。对于directory中的每个目录都有一个单独的cmMakefile实例,这也是cmMakefile类经常被引用为的directory原因。cmMakefile实例将会存储该directoryCMakeLists的全部解析结果。我们也可以将cmMakefile看成一个结构体,它在创建时被其父对象中的一些变量初始化,然后被当前目录中的cmakelists处理结果填充。对于CMake而言,读取CMakeLists文件还是很简单的,CMake按着它在Cmakelist文件中遇到的顺序去执行这些命令。

CMake中的每一个命令都由一个独立的C++的类来实现。它有两个主要部分,第一部分是InitialPass方法,这个方法接收一些参数和当前正在处理目录的cmMakefile实例,然后执行该函数。例如,set命令,它接收并处理传递给其的参数,如果参数正确,它将会调用cmMakefile对象的方法将该参数设置到cmMakefile实例中,需要注意的是,CMake命令的处理结果始终保存在cmMakefile实例中,而不是存放在该命令的本身当中。命名的最后一部分是FinalPass, 当CMake工程的中的所有命令都执行完InitalPassFinalPass才会执行,但是不是所有的命令都会有FinalPass部分,同时还有一些其他的情况,在一些极少数的情况下,某些命令在执行InitalPass部分需要一些全局的必要信息,但是这些信息可能无法取得。

2.2 Targets

Targets可能是存储在cmMakefile对象核心组件中最重要的一个了,Targets代表的是CMake构建输出对象,如executeables,libraries,utilities。每一个add_library,add_executable,add_custom_target命令都会创建生成一个target。例如下面的命令就会生成一个名为footarget,这个目标是一个静态库。

add_library(foo STATIC foo1.c foo2.c)

现在foo可以作为库名在项目的其他地方使用了,并且CMake知道如何在需要时将名称扩展到库中,可以被声明成一些特定的类型:STATICSHAREDMODULESTATIC表示该库必须作为静态库构建,同样,SHARED表示该库必须作为动态库或者共享库构建,MODULE表示这个库必须被创建,只有这样才能被动态加载到一个可执行程序中。在很多操作系统上在构建SHARED库上是相同的,但是MAC OS例外,如果如果你不指定库的构建类型的话,它可能按着STATIC或者SHARED类型中一种构建,此时CMake会根据BUILD_SHARED_LIBS变量设定的值来决定是构建SHARED还是STATIC,如果这个值没有被设置,CMake将会默认构建静态库。

同样,构建可执行程序也有很多选项,默认情况下,一个可执行程序就是一个典型的控制台应用程序,它有一个main(int argc, const char *argv)函数,如果在可执行程序的名称后面指定了WIN32,则该程序就会构建成MS Windows可执行程序。而且操作系统会调用WinMain而不是main作为入口函数。WIN32在非windows操作系统上不会其任何作用。

除了存储类型之外,target还会记录一些通用属性,set_target_propertiesget_target_properties命令可以设置和获取target上的属性,或者使用更通用的set_propertyget_property命令。最常见的属性可能就是LINK_FLAGS了,它常被用来为某个特殊的目标指定链接时需要的一些flagtarget_link_libraries命令可以在target上存储一个库列表,以供target链接时使用,列表可以是库名称,可以是库的路径,或者是由add_library命令定义的库名称。Target上也能存储链接时需要的链接目录,安装位置,以及一些在链接后执行的自定义命令等。

对于每一个CMake创建的库,CMake都会记录追踪该库需要的所有依赖库,由于静态库不能链接到依赖的库,所以cmake必须跟踪这些库,这样才能在创建的可执行程序的链接时上指定它们。例如:

add_library (foo foo.cpp)
target_link_libraries(foo bar)

add_executable(foobar foobar.cpp)
target_link_libraries(foobar foo)

该例虽然只有foo库被明确的指定链接到可执行程序foobar中,但foo库和bar库都会被可执行程序foobar链接,当使用动态库或者dll构建时,这些链接不总是一定要指定的,但是多余额外的链接却是无害的没有任何副作用的。对于静态库构建,指定连接却是必要的。因为foo使用的函数符号来自于bar库,foobar很可能也需要bar库,因为它需要依赖foo。

2.3 Source Files

源文件结构与target在许多地方都有着相似之处,它存储filename,extension,以及一些和源文件相关的一般属性。类似于Target,你可以通过set_source_files_propertiesget_source_files_properties命令去setget这些属性,或者使用通用版的命令也行,一些常用的属性如下:

  • COMPILE_FLAGS : 为源文件指定编译flag,例如-D或者-I flags.
  • GENERATED: 这个属性表面该源文件在构建过程生成,在这种情况下,CMake在计算依赖时会区别对待该源文件,因为它可能在CMake第一次运行时,不存在。
  • OBJECT_DEPENDS:源码文件的附加依赖文件,CMake自动执行依赖项分析,以确定常见的c、c++依赖项。这个参数一般很少使用,一般是在非常规的依赖或者在依赖分析时源文件不存在时可能需要。
  • ABSTRACT
  • WRAP_EXCLUDE:CMake不会直接使用这些属性,某些加载命令或者CMake的一些插件需要用到该属性,用来决定如何或者何时将C++的类包裹封装成其他的语言,如Python。

2.4 Directories , Generators , Tests, and Properties

除了targetssource files,在工作中你可能偶尔也会遇到一些其他的类对象,例如:directories,generators,以及tests。通常,这些交互作用的形式是setget这些对象的属性。所有的这些对象都有一些相关的属性与之关联。就如同targetssource files对象一样。属性就是依附到某个特殊对象上的key-value,访问这些属性值的一种通用的方式可以使用set_propertyget_property命令,这两个命令可以让你能从任何CMake类对象中set或者get其拥有的属性。targetssource files的有些属性已经被重写。

下面是一些directory对象的很有用的属性。

  • ADDITIONAL_MAKE_CLEAN_FILES:

    这个属性指定了一个附加的文件list,这个列表里的文件将会在Make clean阶段被清理。默认情况下,CMake 会清理掉所有它识别的生成文件。但是可能在构建过程中你使用了其他的工具,这些工具也会生成一些文件,这个属性可以让你告诉CMake在执行清理任务时,将该list中的文件也清理掉

  • EXCLUDE_FROM_ALL

    这个属性表示当前目录或者其子目录中的所有target在执行默认构建目标时是否应该被移除。如果没有移除,则该目录就会生成一个与之相关Makefile文件,对于其他的generator而言也是如此。

  • LISTFILE_STACK

    该属性主要用于调试CMake脚本中的错误,它将会按序返回一个CMake当前正在处理的文件列表。所以,如果一个CMakeLists文件执行了一个include命令,这就将包含的CMakeLists文件压入堆栈了。

如果你想要了解CMake支持的全部属性列表,可以通过运行cmake -help-property-list。同时generatorsdirectoriesCMake在处理源码时自动创建的。

2.5 Variables and Cache Entries

就如同许多编程语言一样,CMakeLists file也需要使用很多变量。变量常用来存储一些值,以后后续使用,这些值可以是一个单独的值如:“NO"或者”OFF",也能代表一个list如(/usr/include /home/foo/include /usr/local/include).许多有用的变量已经被CMake定义,你可以查阅附录;

变量在CMake中通过${VARIABLE}这样的形式被引用,他们也能依序通过执行set命令被定义,例如:

# FOO is undefined
set (FOO 1)
# FOO is now set to 1
set (FOO 0)
# foo is now set to 0

上面的例子看起来很简单,让我们来看下面的一个例子

set(FOO 1)
if (${FOO} LESS 2)
    set(FOO 4)
else (${FOO} GREATER 2)
    set(FOO 3)
endif (${FOO} LESS 2)

很明显 if 语句是true,将会执行 if 语句体下的内容,然后FOO将会被设置成4, 当遇到else 语句时,FOO的内容是4,通常情况下CMake会使用FOO更新后的值,但是else 语句极少的例外,它读取的值是if语句被执行时FOO的值。所以在这个例子中,else语句不会执行。为了更进一步不理解变量的作用域,我们来看下一个例子:

set(foo 1)
#处理dir1子目录
add_subdirectory (dir1)
# 包含并处理file1.cmake文件中的命令
include (file1.cmake)

set(bar 2)
#处理dir2子目录
add_subdirectory (dir2)
#包含并处理file2.cmake中的命令
include (file2.cmake)

在这个例子中,由于foo在一开始的位置就被定义,因此在执行处理dir1dir2时都是可用的。但是 bar这个变量只有在处理dir2时才有效,同理,foo在处理file1.cmakefile2时都是有效的,而bar这个变量只是在处理file2.cmake才是有效的。

CMake中的变量作用域和其他语言略有不同,当你在CMakeList文件或者某个函数里定义一个变量时,它同样的在其子目录中CMakeList文件,函数,宏块,以及使用include包含的命令中都是可以被调用的。当在处理一个新的子目录时,将会创建一个新的变量作用域,同时用当前可调用的变量的的值进行初始化。任何在子作用域中创建的变量,或者对已有变量的更改,都不会影响到父作用域里变量的值。看下面的例子

function (foo)
    message (${test}) # 这里test是1
    set(test 2)
    message (${test}) # 这里test是2
endfunction()

set(test 1)
foo()
message (${test}) #这里test是1

在某些情况下,你可能需要在函数内部或者子目录中定义的变量在其父作用域中也能使用,有两种方式可以完成满足这个需求,第一种是从这个函数中返回这个值,第二种方式是使用set命令时加上PARENT_SCOPE选项。我们来更改一下之前的例子

function(foo)
    message(${test}) # test is 1
    set(test 2 PARENT_SCOPE)
    message(${test}) #test is 2
endfunction()

set(test 1)
foo()
message (${test}) # test is 2

变量既可以代表一个单独的值,也能表示一个值的列表(list),当你使用一个value list变量时,它将会展开成多个值。看下面的例子:

#set a list of items
set (items_to_buy apple orange pear bear)

#loop over the items
foreach(item ${items_to_buy})
    message("Don't forget to buy one ${item}")
endforeach()

在某些情况下,你能希望用户通过CMake界面让用户通过设置变量来构建工程,此时,这个变量必须是一个缓存项,每当Cmake运行时,它都会在构建目录中生成一个缓存文件,这个缓存文件的值将会通过CMake UI界面呈现给用户。首先它会保存用户的选择和输入的选项值,这样当用户再次运行时就不需要再次输入,例如,option命令将会创建一个Boolean类型的变量,然后存储在缓存中。

option (USER_JPEG "DO you want to use the jpeg library")

上面的代码将会创建一个叫"USE_JPEG"的变量,然后这个变量将会被存放到缓存中,未来,用户可以通过用户界面的方式设置这个变量的值。在缓冲中创建变量,你可以使用如下的一些命令:optionfind,file.或者你也可以使用标准的set命令加上缓冲选项。

set (USER_JPEG ON CACHE BOOL "include jpeg support?")

当你使用缓存选项时,你必须提供一个变量的类型和一个文档说明字符串,变量的类型将会控制CMake GUI呈现给你这个变量的形式。变量可以是如下一些类型:BOOL,PATH,FILEPATH以及STRING.文档说明字符串被GUI用来提供用户帮助。

缓存的另一个目的是存储一些关键变量,一般获取这些变量的值需要做很多工作。这些变量通常是用户不可见的或者不能更改的。通常他们的一些值是一些系统变量,例如:CMAKE_WORDS_BIGENDIAN,这需要CMake在编译运行程序时才能决定。一旦这些变量的值已经被确定下来,他们就会被存储在缓存文件中,从而避免了每一次运行cmake时都需重新编译。通常,CMake会给这些变量添加一些限制属性,让这些变量一旦确定就永远不能被改变。如果改变了操作系统,或者切换成不同的编译器,你需要删除这些缓存文件。

通常一些应用程序的设置界面,会有两部分组成,尤其是设置选项较多的情况下更是如此,总会有些高级选项,留给用户进行选择性配置,一般情况下,用户不需要去设置这些选项,但总有一些例外的情况,有些用需要这设置这部分。CMake同样也是如此,存储在缓存中的变量也有一个属性,标识着这个变量是否是一个高级配置,默认情况下,CMake GUI不会显示这些高级变量的缓存项,这样用户就可以专注于那些他们需要考虑的缓存项。高级缓存项就是有些用户可以设置的其他选项,但是通常条件下不需要用户设置。对于一个大型的项目工程,拥有50多个或者更多的选项配置是很正常的,通常我们可以将这些选项配置分割成两部分,一部分是大多数用户需要的核心配置,另一部分是满足一些少数高级用户需要的高级配置部分。当然,根据项目的不同,也可能不存在任何非高级缓存项。如果你想要创建高级选项,可以使用mark_as_advanced命令。

在某些情况下,你可能需要限制某个缓存项只能设置一些预定义的选项,你可以在这个缓存项上设置一个STRINGS属性,下面的例子描述的就是创建了一个名为CRYPTOBACKEND选项,并且设置了STRINGS属性,为这个选项提供了三个候选值。

set (CRYPTOBACKEND "OpenSSL" CACHE STRING "Select a cryptography backend")
set_property (CACHE CRYPTOBACKEND PROPERTY STRINGS "openSSL" "LibTOmCrypt" "LibDES")

当你运行CMake GUI时就会出现类似于如下界面:

cmake01.png

关于变量和缓存的交互方式,最后几点需要注意的是,如果一个变量在缓存中,在CMakeLists中使用不加CACHE选项的set命令可以覆写这个变量。在开始处理当前CMakeLists之前,如果cmMakefile对象没有这个变量的值时,才会去检测缓存。在当前正在处理的CMakeLists中使用set命令更改该变量的值或者其子目录中更改该变量的值,不会影响到缓存中的原始值。

一旦一个变量已经存在缓存中,则缓存中的值通常是无法被CMakeLists更改的。原因是因为,一旦CMake将变量的值初始化并放入缓存,用户可能会从GUI更改它的值。当下一次调用CMake时,CMake会将用户的更改恢复回来。因此set (Foo ON CACHE BOOL "doc")这个命令,通常只有在缓存中没有缓存这个变量的时候才有效,一旦这个变量已经被缓存,则这个命令就不起任何作用了。

在极少数情况下,你可能真的想要改变缓存中变量的值,你可以使用在set命令上加上FORCE选项结合CACHE选项。FORCE会覆写并改变缓存该变量的值。

2.6 Build Configurations

Build Configurations可以让一个工程项目能够以不同的方式构建:如debug,optimized或者其他的一些特殊的配置。CMake默认支持的构建配置有Debug,Release,MinSizeRel以及RelwithDebInfo, Debug会打开一些基本的调试flag,Release会开启一些基本的优化选项,MinSizeRel则会添加一些flag让生成的程序对象体积最小,但是代码运行不一定是最快的。RelwithDebInfo是优化和调试并存的构建方式。

CMake对不同的构建配置处理方式略有不同,这完全取决于使用哪个generator,通常是跟随当前的构建系统。这就表示使用Makefiles配置和使用Visual Studio配置在构建方式上大相径庭。

Visual Studio中有构建配置的概念,一个工程中通常默认有DebugRelease两种配置,当我们选择Debug构建时,源文件就被添加上debug flag,IDE会将所以的二进制文件放入一个名为当前活动的文件夹中。这就为构建工程增加了一些额外的复杂性,你需要为构建工程执行一些自定义的命令,你可以查阅CMAKE_CFG_INTDIR变量和自定义命令部分去了解更多信息,CMAKE_CONFIGURATION_TYPES可以告诉CMake应该在工作空间中选择哪种配置。

当选择Makefile generator构建时,只能同时执行一种构建方式,你可以使用CMAKE_BUILD_TYPE变量配置构建方式,如果这个变量是空的,则不会有任何flag被添加到构建过程中。当你配置了一个指定了一个构建方式后,Cmake选择合适的变量和flag加入到构建过程中,Makefile不会为目标文件配置指定的子目录。因此Debug构建和release构建是目录是一样的。如果用户想要创建多个构建目录,可以用CMake的源外构建特性,通过设置CMAKE_BUILD_TYPE为每一个构建类型选择做出合适的选择。例如:

cmake ../myProject -DCMAKE_BUILD_TYPE:STRING=Debug
cmake ../myProject -DCMAKE_BUILD_TYPE:STRING=Release

你可能感兴趣的:(CMake-02:核心概念)