第三章 关键概念
3.1 主结构
本章介绍了CMake的关键概念。当您开始使用CMake时,您将遇到各种各样的概念,例如目标,生成器和命令。在CMake中,这些概念是作为C++类来实现的,并且在许多CMake的命令中被引用。理解这些概念将为您提供创建高效率的CMakeLists文件所需的工作知识。
在详细介绍CMake的课程之前,有必要了解他们的基本关系。最底层有源文件。这些对应于典型的C或C++源代码文件。源文件被合并到目标中。目标通常是可执行文件或库。目录表示源代码树中的目录,并且通常含有CMakeLists文件以及与其关联的一个或多个目标。每个目录都有一个本地生成器,负责生成该目录的Makefile或项目文件。所有的本地生成器共享一个共同的全局的生成器,监督构建过程。最后,全局生成器是由cmake类本身创建和驱动的。
图4显示了CMake的基本类结构。现在我们将更详细地介绍CMake的概念。CMake的执行过程以先创建一个cmake类的实例并将命令行参数传递给它开始。该类管理整个配置过程,并保存构建过程的全局信息,例如缓存值。cmake类所做的第一件事就是根据用户选择使用什么生成器(如Visual Studio 10,Borland Makefiles或UNIX Makefiles)来创建正确的全局生成器。此时,cmake类通过调用configure和generate方法将控制权交给它创建的全局生成器。
Figure 4 - CMake Internals
全局生成器负责管理项目的所有Makefile(或项目文件)的配置和生成。在实践中,大部分工作实际上是由全局生成器创建的本地生成器完成的。为处理的项目的每个目录创建一个本地生成器。所以当一个项目只有一个全局生成器时,它可能有许多本地生成器。例如,在Visual Studio 7下,全局生成器为整个项目创建一个解决方案文件,而本地生成器为其目录中的每个目标创建一个项目文件。
在“Unix Makefiles”生成器的情况下,本地生成器创建大部分Makefile,全局生成器简单地编排进程并创建顶层Makefile。生成器之间的实施细节差别很大。 Visual Studio 6生成器使用.dsp和.dsw文件模板,并对它们执行变量替换。 Visual Studio 7及更高版本的生成器直接生成XML输出,而不使用任何文件模板。包括UNIX,NMake,Borland等的Makefile生成器使用一组规则模板和替换来生成它们的Makefile。
Figure 5 - Sample Directory Tree
每个本地生成器都有一个类cmMakefile的实例,cmMakefile是存储解析CMakeLists文件结果的地方。具体而言,对于项目中的每个目录,都会有一个cmMakefile实例,这就是cmMakefile类通常称为目录的原因。这对于不使用Makefiles的构建系统更为清晰。该实例将包含解析该目录的CMakeLists文件的所有信息(参见图5)。一种考虑cmMakefile类的方法是以一种初始化其父目录中的一些变量的结构,然后在处理CMakeLists文件时填充该结构。读取CMakeLists文件只是CMake按照遇到的顺序执行命令的一个步骤。
CMake中的每个命令都作为一个单独的C++类来实现,并且有两个主要部分。命令的第一部分是InitialPass方法。 lnitialPass方法接收当前正在处理的目录的参数和cmMakefile实例,然后执行其操作。在set命令的情况下,它处理它的参数,如果参数是正确的,它调用cmMakefile上的一个方法来设置变量。命令的结果总是存储在cmMakefile实例中。信息永远不会存储在一个命令中。一个命令的最后一部分是FinalPass。所有命令(对于整个CMake项目)都调用了InitialPass之后,执行FinalPass命令。大多数命令没有FinalPass,但是在极少数情况下,命令必须使用全局信息进行处理,这些信息在初始阶段可能不可用。
一旦所有的CMakeLists文件都被处理完毕,生成器会使用收集到cmMakefile实例的信息为目标构建系统(如Makefiles)生成相应的文件。
3.2 目标
现在我们已经讨论了CMake的整个过程,让我们考虑一下cmMakefile实例中存储的一些关键项目。可能最重要的是目标。目标表示由CMake构建的可执行文件,库和实用程序。每个add_library,add_executable,和add_custom_target命令创建一个目标。例如,下面的命令将创建一个名为foo的目标,它是一个静态库,foo1.c和f002.c作为源文件。
add_library (foo STATIC foo1.c foo2.c)
- 1
现在,foo可以被用作项目中其他任何地方的库,CMake将知道如何在需要时将名称扩展到库中。库可以被声明为特定的类型,例如STATIC,SHARED,MODULE,或者不被声明。 STATIC表示库必须被构建为一个静态库。同样SHARED表示它必须被构建为一个共享库。 MODULE指示该库必须被创建,以便它可以被动态加载到可执行文件中。在许多操作系统上,这与SHARED相同,但在其他系统(如Mac OS X)上则不同。如果没有指定这些选项,则表示可以将该库构建为共享或静态。在这种情况下,CMake通过设置BUILD_SHARED_LIBS变量来确定库应该是SHARED还是STATIC。 如果没有被设置,那么CMake默认构建静态库。
同样可执行文件有一些选项。默认情况下,可执行文件将是一个具有main(int argc, const char *argv [])的传统控制台应用程序。如果在可执行文件名称之后指定了wint32,则可执行文件将被编译为MS Windows的可执行文件,操作系统将在启动时调用WinMain而不是main。 WIN32对非Windows系统没有影响。
除了存储他们的类型,目标也跟踪一般属性。可以使用set target_properties和get target_property命令或者更一般的set_property和get_property命令来设置和检索这些属性。最常用的属性是LINK_FLAGS,它用来指定特定目标的链接标志。目标存储它们链接的库列表,这些库使用target_link _libraries命令设置。传递给此命令的名称可以是库,库的完整路径,也可以是来自add_llbrary命令的库的名称。它们还存储链接时使用的链接目录,目标的安装位置和链接后执行的自定义命令。
对于CMake创建的每个库,它都会跟踪该库所依赖的所有库。由于静态库不链接到它所依赖的库,因此CMake必须跟踪这些库,以便可以在正在创建的可执行文件的链接行上指定它们。例如,
add_library (foo foo.cxx)
target_link_libraries (foo bar)
add_executable (foobar foobar.cxx)
target_link_libraries (foobar foo)
- 1
- 2
- 3
- 4
- 5
这将链接图书馆foo和bar到可执行文件foobar,虽然只有foo明确链接到了foobar。使用共享或DLL构建这个链接并不总是需要的,但额外的链接是无害的。对于静态构建,这是必需的。由于foo库使用bar库中的符号,所以foobar很可能也需要bar,因为它使用了foo。
3.3 源文件
源文件结构在很多方面与目标类似。它存储与源文件相关的文件名,扩展名和一些常规属性。与目标类似,您可以使用set_source_files_properties和get_source_file_property或更通用的版本来设置和获取属性。最常见的属性包括:
COMPILE FLAGS
编译特定于此源文件的标志。这些可以包括源特定的-D和-I标志。
- 1
GENERATED
GENERATED属性表示源文件是作为构建过程的一部分生成的。 在这种情况下,CMake会以不同的方式计算依赖性,因为当CMake首次运行时,源文件可能不存在。
- 1
OBJECT DEPENDS
添加此源文件应该依赖的其他文件。CMake自动执行依赖关系分析来确定通常的C,C++和Fortran依赖关系。这个参数在有非常规依赖,或者在依赖分析时不存在源文件的情况下很少使用。
- 1
ABSTRACT
WRAP_EXCLUDE
CMake不直接使用这些属性。一些加载的命令和CMake的扩展看这些属性来确定如何以及何时将C++类包装成Tcl,Python等语言。
- 1
3.4 目录, 生成器, Tests, and 属性
除了目标和源文件之外,您可能偶尔会用到目录,生成器和测试等其他类。通常这样的交互是对这些对象设置或获取属性的形式。所有这些类都具有与它们相关的属性,源文件和目标也是如此。属性是连接到特定对象(如目标)的键值对。访问属性的最普通的方式是通过set_property和get_property命令。这些命令允许你设置或获取CMake中任何具有属性类的属性。目标和源文件的一些属性已经被覆盖。目录的一些有用的属性包括:
ADDITIONAL_MAKE_CLEAN_FILES
该属性指定了将作为“清洁”阶段的一部分进行清理的附加文件的列表。默认情况下,CMake会清除它所知道的任何生成的文件,但是你的构建过程可能会使用其他的工具来留下文件。这个属性可以设置为这些文件的列表,以便它们也将被正确地清理。
- 1
EXCLUDE_FROM_ALL
该属性指示是否应将此目录中的所有目标和所有子目录从默认构建目标中排除。如果不是这样,那么用一个Makefile,当输入make的时候,会导致这些目标被构建。同样的概念适用于其他生成器的默认构建。
- 1
LlSTFILE_STACK
当你试图在CMake脚本中调试错误时,这个属性是非常有用的。它将按顺序返回当前正在处理的列表文件的列表。 所以如果一个CMakeLists文件执行了一个include命令,那么这个文件就有效地将所包含的CMakeLists文件推送到堆栈上。
- 1
CMake支持属性的完整列表可以通过运行cmake时带有-help-property-list选项来获得。当CMake处理你的源码树时,生成器和目录会自动为你创建。
3.5 变量和缓存条目
CMakeLists文件使用变量很像任何编程语言。变量用于存储值供以后使用,可以是单个值,如“ON”或“OFF”,或者可以表示一个列表,例如(/usr/include /home/foo/include /usr/local/include)。一些有用的变量由CMake自动定义,并在附录A-变量中讨论。
CMake中的变量是用$ {VARIABLE}表示法引用的,它们是按照执行set命令的顺序定义的。考虑下面的例子:
# FOO is undefined
set (FOO 1)
# FOO is now set to 1
set (FOO 0)
# FOO is now set to 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这看起来很简单,但请考虑以下示例:
set (FOO 1)
if (${FOO) LESS 2)
set (FOO 2)
else (${FOO) LESS 2)
set (FOO 3)
endif (${FOO} LESS 2)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
很明显if语句是真的,这意味着if语句的主体将被执行。这会将变量FOO设置为2,所以当遇到else语句时,FOO的值将是2.通常在CMake中将使用FOO的新值,但else语句对于规则来说是罕见的例外,并且总是 在执行if语句时指向变量的值。 所以在这种情况下,else子句的主体将不会被执行。为了进一步理解变量的范围,考虑这个例子:
set (foo 1)
# process the dir1 subdirectory
add subdirectory (dir1)
# include and process the commands in file1.cmake
include (file1.cmake)
set (bar 2)
# process the dir2 subdirectory
add_subdirectory (dir2)
# include and process the commands in file2.cmake
include (file2.cmake)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
在这个例子中,因为变量foo是在开始时定义的,所以在处理dirl和dir2时foo将会被定义。对比栏只会在处理dir2时被定义。同样,在处理filel.cmake和file2.cmake时,foo将被定义,而bar只会在处理file2.cmake时被定义。
CMake中的变量的范围与大多数语言有一点不同。设置变量时,它对当前CMakeLists文件或函数,以及任何子目录的CMakeLists文件,调用的任何函数或宏以及使用INCLUDE命令包含的任何文件都是可见的。当一个新的子目录被处理时(或者一个被调用的函数),一个新的变量作用域被创建并且被初始化为调用作用域中所有变量的当前值。 在子作用域中创建的任何新变量或对现有变量所做的更改都不会影响父作用域。考虑下面的例子:
function (foo)
message (${test}) # test is 1 here
set (test 2)
message (${test}) # test is 2 here, but only in this scope
endfunction()
set (test 1)
foo ()
message (${test}) # test will still be 1 here
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在某些情况下,您可能需要一个函数或子目录在其父级范围内设置一个变量。 这是CMake从函数返回值的一种方法,可以通过在set命令中使用PARENT_SCOPE选项来完成。我们可以修改前面的例子,以便函数foo在其父级的范围中更改测试的值,如下所示:
function (foo)
message (${test}) # test is 1 here
set (test 2 PARENT SCOPE)
message (${test}) # test still 1 in this scope
endfunction ()
set (test 1)
foo ()
message (${test}) # test will now be 2 here
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
变量也可以表示一个值列表。在这些情况下,当变量被展开时,它将被展开成多个值。考虑下面的例子:
# set a list of items
set (items to buy apple orange pear beer)
# loop over the items
foreach (item ${items to buy})
message ( "Don't forget to buy one ${item}" )
endforeach ()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
在某些情况下,您可能希望允许构建项目的用户从CMake用户界面设置变量。在这种情况下,变量必须是缓存条目。只要CMake运行,它就会在要写入二进制文件的目录中生成一个缓存文件。这个缓存文件的值由CMake用户界面显示。这个缓存有几个目的。首先是存储用户的选择,如果他们再次运行CMake,他们将不需要重新输入这些信息。例如,option命令创建一个布尔变量并将其存储在缓存中。
option (USE_JPEG "Do you want to use the jpeg library")
- 1
上面的代码会创建一个名为USE_JPEG的变量并将其放入缓存中。 这样用户就可以从用户界面设置这个变量,并且它的值将保持到用户将来再次运行CMake的时候。要在缓存中创建变量,可以使用选项find_file等命令,也可以使用带有CACHE选项的标准set命令。
set (USE_JPEG ON CACHE BOOL "include jpeg support?")
- 1
当您使用缓存选项时,您还必须提供变量的类型和文档字符串。 GUT使用变量的类型来控制变量的设置和显示方式。变量类型包括BOOL,PATH,FILEPATH和STRING。GUI使用文档字符串提供联机帮助。
缓存的另一个目的是存储要确定的关键变量。这些变量可能不可见或可由用户调整。通常这些值是系统相关的变量,如CMAKE_WORDS_BIGENDIAN,它要求CMake编译并运行一个程序来确定它们的值。一旦确定了这些值,就将它们存储在缓存中,以避免每次运行CMake时重新计算它们。通常CMake会尝试将这些变量限制为不可改变的属性(例如,您所在机器的字节顺序)。如果您通过更改操作系统或切换到其他编译器来更改计算机,则需要删除缓存文件(可能还包括所有二叉树的目标文件,库和可执行文件)。
缓存中的变量也有一个属性,指示它们是否高级属性。默认情况下,运行CMake GUI(例如ccmake或cmake-gui)时,不会显示高级缓存条目。这样用户就可以专注于他们应该考虑改变的缓存条目。高级缓存条目是用户可以修改的其他选项,但通常不会被更改。一个大型软件项目有五十个或更多的选项并不罕见,高级属性可以让一个软件项目将它们分为大多数用户的关键选项和高级用户的高级选项。项目可能没有任何非高级缓存条目。为使缓存条目高级化,使用mark_as_advanced命令与变量名(a.k.a.高速缓存条目)进行高级化。
在某些情况下,您可能希望将缓存条目限制为一组有限的预定义选项。您可以通过在缓存条目上设置STRINGS属性来完成此操作。下面的CMakeLists代码通过像往常一样创建一个名为CRYPTOBACKEND的属性来说明这一点,然后将其上的STRINGS属性设置为一组三个选项。
set (CRYPTOBACKEND "OpenSSL" CACHE STRING "Select a cryptography backend")
set property (CACHE CRYPTOBACKEND PROPERTY STRINGS "OpenSSL" "LibTomCrypt" "LibDES")
- 1
- 2
当运行cmake-gui并且用户选择了CRYPTOBACKEND缓存条目时,下拉菜单选项会显示他们想要的选项,如图6所示。
Figure 6 - Cache Value Options in cmake-gui
应该对变量及其与缓存的交互作出几个最后的观点。如果一个变量在缓存中,它仍然可以在没有CACHE选项的情况下使用set命令在CMakeLists文件中覆盖。 只有在CMakeLists文件处理开始之前在当前cmMakefle实例中未找到该变量时,才检查缓存值。 set命令将设置用于处理当前CMakeLists文件(和子目录)的变量,而不更改缓存中的值。
# assume that FOO is set to ON in the cache
set (FOO OFF)
# sets foo to OFF for processing this CMakeLists file
# and subdirectories; the value in the cache stays ON
- 1
- 2
- 3
- 4
- 5
一旦变量在缓存中,它的“缓存”值通常不能从CMakeLists文件修改。这背后的原因是,一旦CMake已经把变量放入缓存中,它的初始值,用户就可以从GUI中修改这个值。如果CMake的下一次调用将其改变重写为设定值,则用户将永远无法进行更改。因此,一个集合(FOO ON CACHE BOOL“doc”)命令通常只会在缓存中没有变量时才会执行某些操作。一旦变量在缓存中,该命令将不起作用。
如果您真的想要更改缓存变量的值,您可以将FORCE选项与CACHE选项结合使用以设置命令。FORCE选项将导致set命令覆盖并更改变量的缓存值。
3.6 构建配置
构建配置允许项目以不同的方式构建,用于调试,优化或其他任何特殊标志。 CMake默认支持Debug,Release,MinSizeRel和RelWithDebInfo配置。Debug打开了基本的调试标志。Release已打开基本的优化。 MinSizeRel具有产生最小目标代码的标志,但不一定是最快的代码。 RelWithDeblnfo也使用调试信息构建了一个优化版本。
CMake根据正在使用的生成器以稍微不同的方式处理配置。尽可能遵循本地构建系统的约定。这意味着在使用Makefiles和使用Visual Studio项目文件时,配置会以不同的方式影响构建。
Visual Studio IDE支持构建配置的概念。 Visual Studio中的默认项目通常具有调试和发布配置。您可以从IDE中选择构建Debug,并使用Debug标记构建这些文件。 IDE将所有二进制文件放入具有活动配置名称的目录中。这为构建需要作为构建过程一部分从自定义命令运行的程序的项目带来了额外的复杂性。有关如何处理此问题的更多信息,请参阅CMAKE_CFG_INTDIR变量和自定义命令部分。变量CMAKE_CONFIGURATION_TYPES用于告诉CMake将哪些配置放入工作区。
使用基于Makefile的生成器,只有一个配置在CMake运行时处于活动状态,并且由CMAKE_BUILD_TYPE变量指定。如果变量是空的,那么没有标志被添加到构建。如果变量被设置为配置的名称,那么相应的变量和规则(例如CMAKE_CXX_FLAGS_)被添加到编译行中,Makefile不使用特殊的配置子目录作为目标文件。无论是调试还是释放树,用户都需要使用CMake的源代码编译特性创建多个编译目录,并将CMAKE_BUILD_TYPE设置为每个编译所需的选项,例如,
# With source code in the directory MyProject
# to build MyProject-debug create that directory, cd into it and (ccmake .. /MyProject -DCMAKE_BUILD TYPE:STRING=Debug)
# the same idea is used for the release tree MyProject-release (ccmake .. /MyProject -DCMAKE BUILD TYPE:STRING=Release)
- 1
- 2
- 3
<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-60ecaf1f42.css" rel="stylesheet">
</div>