完整的CMake指南。 第二部分:构建系统


引言


本文讨论了在大量C / C ++项目中使用的CMake构建系统的用法。 强烈建议您阅读本手册的第一部分,以免误解了CMake语言的语法,该语法在整篇文章中都明确提到。


CMake发布


以下是使用应练习的CMake语言的示例。 通过修改现有命令并添加新命令来试验源代码。 要运行这些示例,请从官方网站安装CMake。


工作原理


CMake构建系统是其他依赖于平台的实用程序(例如NinjaMake )的包装。 因此,在组装过程本身中,无论听起来多么矛盾,它都不会直接参与。


CMake构建系统接受CMakeLists.txt文件并以正式的CMake语言描述构建规则,然后在平台上接受的同一目录中生成中间构建文件和本机构建文件。


生成的文件将包含系统实用程序,目录和编译器的特定名称,而CMake命令仅使用编译器的抽象概念,并不依赖于平台相关的工具,这些工具在不同的操作系统上有很大的不同。


检查CMake版本


cmake_minimum_required命令检查CMake的运行版本:如果它小于指定的最小值,则CMake终止并出现致命错误。 一个示例,该示例演示在任何CMake文件的开始处此命令的典型用法:


 #     CMake: cmake_minimum_required(VERSION 3.0) 

注释中所述, cmake_minimum_required命令设置所有兼容性标志(请参阅cmake_policy )。 一些开发人员有意设置较低版本的CMake,然后手动调整功能。 这使您可以同时支持较旧版本的CMake,并且在某些地方可以利用新功能。


项目设计


在任何CMakeLists.txt的开头CMakeLists.txt应该与项目团队一起指定项目特征,以便使用集成环境和其他开发工具进行更好的设计。


 #    "MyProject": project(MyProject VERSION 1.2.3.4 LANGUAGES C CXX) 

值得注意的是,如果省略LANGUAGES关键字,则默认语言为C CXX 。 您还可以通过将关键字NONE编写为语言列表来禁用任何语言的指示,或者只留下一个空列表。


运行脚本文件


include命令用指定文件的代码替换其调用行,其作用类似于C / C ++预处理程序include命令。 本示例MyCMakeScript.cmake描述MyCMakeScript.cmake命令运行脚本文件MyCMakeScript.cmake


 message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]") #   `MyCMakeScript.cmake`  : include(MyCMakeScript.cmake) message("'TEST_VARIABLE' is equal to [${TEST_VARIABLE}]") 

在此示例中,第一条消息将通知尚未定义TEST_VARIABLE变量,但是,如果MyCMakeScript.cmake脚本MyCMakeScript.cmake此变量,则第二条消息将已通知测试变量的新值。 因此, include命令包含的脚本文件不会创建自己的作用域,这在上一篇文章的注释中已提到


可执行文件的编译


add_executable命令从源列表中使用给定名称编译可执行文件。 重要的是要注意,最终文件名取决于目标平台(例如, <ExecutableName>.exe或仅<ExecutableName> )。 调用此命令的典型示例:


 #    "MyExecutable"  #  "ObjectHandler.c", "TimeManager.c"  "MessageGenerator.c": add_executable(MyExecutable ObjectHandler.c TimeManager.c MessageGenerator.c) 

图书馆汇编


add_library命令使用源中指定的视图和名称来编译库。 重要的是要注意,最终的库名称取决于目标平台(例如, lib<LibraryName>.a<LibraryName>.lib )。 调用此命令的典型示例:


 #    "MyLibrary"  #  "ObjectHandler.c", "TimeManager.c"  "MessageConsumer.c": add_library(MyLibrary STATIC ObjectHandler.c TimeManager.c MessageConsumer.c) 

  • 静态库由STATIC关键字定义为第二个参数,并且是在编译时与可执行文件和其他库关联的目标文件的存档;
  • 动态库由SHARED关键字指定为第二个参数,并且是操作系统在程序执行期间加载的二进制库。
  • 模块化库由MODULE关键字定义为第二个参数,并且是由可执行文件本身使用执行技术加载的二进制库。
  • 对象库由OBJECT关键字定义为第二个参数,并且是在编译时与可执行文件和其他库关联的一组对象文件。

向目标添加源


在某些情况下,需要向目标多次添加源文件。 为此,提供了target_sources命令,该命令可以将源多次添加到目标。


target_sources命令的第一个参数是先前使用add_libraryadd_executable指定的目标的名称,后续参数是要添加的源文件的列表。


重复调用target_sources将源文件按照被调用的顺序添加到目标中,因此底部的两个代码块在功能上是等效的:


 #    "MyExecutable"   # "ObjectPrinter.c"  "SystemEvaluator.c": add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c) #    "MyExecutable"  "MessageConsumer.c": target_sources(MyExecutable MessageConsumer.c) #    "MyExecutable"  "ResultHandler.c": target_sources(MyExecutable ResultHandler.c) 

 #    "MyExecutable"   # "ObjectPrinter.c", "SystemEvaluator.c", "MessageConsumer.c"  "ResultHandler.c": add_executable(MyExecutable ObjectPrinter.c SystemEvaluator.c MessageConsumer.c ResultHandler.c) 

生成的文件


仅在生成阶段才能确定由add_executableadd_library生成的输出文件的位置,但是,可以使用确定二进制文件最终位置的几个变量来更改此规则:



可执行文件始终被视为执行目标,静态库被视为归档目标,而模块化库则被视为库目标。 对于“非DLL”平台,动态库被视为库目标,对于“ DLL平台”,动态库被视为执行目标。 此类变量未提供给对象库,因为此类库是在CMakeFiles目录的肠道中生成的。


重要的是要注意,所有基于Windows的平台,包括Cygwin,都被视为“ DLL平台”。


图书馆布局


target_link_libraries命令与提供的其他库一起target_link_libraries库或可执行文件。 该命令的第一个参数是add_executableadd_library生成的目标的名称,后续参数是库目标的名称或库的完整路径。 一个例子:


 #    "MyExecutable"  #  "JsonParser", "SocketFactory"  "BrowserInvoker": target_link_libraries(MyExecutable JsonParser SocketFactory BrowserInvoker) 

值得注意的是,模块化库不能与可执行文件或其他库链接,因为它们仅供执行技术加载。


与目标合作


正如评论中提到的,CMake中的目标也需要手动操作,但是非常有限。


可以控制旨在设置项目组装过程的目标的属性。 get_target_property命令get_target_property target属性get_target_propertyget_target_property为提供的变量。 本示例在屏幕上显示C_STANDARD目标的C_STANDARD属性的值:


 #   "VALUE"   "C_STANDARD": get_target_property(VALUE MyTarget C_STANDARD) #      : message("'C_STANDARD' property is equal to [${VALUE}]") 

set_target_properties命令将指定的目标属性设置为指定的值。 该命令接受将为其设置属性值的目标列表,然后接受关键字PROPERTIES ,后跟格式为< > < >


 #   'C_STANDARD'  "11", #   'C_STANDARD_REQUIRED'  "ON": set_target_properties(MyTarget PROPERTIES C_STANDARD 11 C_STANDARD_REQUIRED ON) 

上面的示例设置了影响编译过程的MyTarget目标属性,即:在编译MyTarget目标时MyTarget CMake将MyTarget编译器使用C11标准。 此页面上列出了所有已知的目标属性命名。


也可以使用if(TARGET <TargetName>)构造来验证先前定义的目标:


 #  "The target was defined!"   "MyTarget"  , #    "The target was not defined!": if(TARGET MyTarget) message("The target was defined!") else() message("The target was not defined!") endif() 

添加子项目


add_subdirectory命令提示CMake立即处理指定的子项目文件。 下面的示例演示了所描述机制的应用:


 #   "subLibrary"    , #       "subLibrary/build": add_subdirectory(subLibrary subLibrary/build) 

在此示例中, add_subdirectory命令的第一个参数是add_subdirectory子项目,第二个参数是可选的,它通知CMake用于包含子项目的生成文件的文件夹(例如CMakeCache.txtcmake_install.cmake )。


值得注意的是,父作用域中的所有变量都由添加的目录继承,并且在此目录中定义和重新定义的所有变量将仅对它可见(如果set命令参数未指定关键字PARENT_SCOPE )。 在上一篇文章的评论中提到了此功能。


套餐搜索


find_package命令查找并加载外部项目的设置。 在大多数情况下,它用于后续链接外部库(例如BoostGSL) 。 本示例调用描述的命令来搜索GSL库,然后链接:


 #     "GSL": find_package(GSL 2.5 REQUIRED) #      "GSL": target_link_libraries(MyExecutable GSL::gsl) #      "GSL": target_include_directories(MyExecutable ${GSL_INCLUDE_DIRS}) 

在上面的示例中, find_package命令接受包的名称作为find_package第一个参数,然后接受所需的版本。 如果找不到所需的包装,则REQUIRED选项需要打印致命错误并终止CMake。 相反的是QUIET选项,即使未找到软件包,也要求CMake继续工作。


接下来,使用GSL::gsl变量,使用target_link_libraries命令将MyExecutable链接到GSL库,该变量封装了已编译的GSL的位置。


最后,将target_include_directories命令,将GSL库头文件的位置通知编译器。 请注意, GSL_INCLUDE_DIRS变量用于GSL_INCLUDE_DIRS我描述的标头GSL_INCLUDE_DIRS位置(这是导入的程序包设置的示例)。


如果您指定了QUIET选项,则可能要检查软件包搜索的结果。 这可以通过检查<PackageName>_FOUND来完成,该<PackageName>_FOUNDfind_package命令find_package后自动确定。 例如,如果您成功将GSL设置导入到项目中,则GSL_FOUND变量将变为true。


通常, find_package命令具有两种启动方式:模块化和配置。 上面的示例应用了模块化形式。 这意味着调用该命令时,CMake在CMAKE_MODULE_PATH目录中搜索形式为Find<PackageName>.cmake的脚本文件,然后启动它并导入所有必需的设置(在这种情况下,CMake启动了标准的FindGSL.cmake文件)。


包含标题的方式


您可以使用两个命令: include_directoriestarget_include_directories来告知编译器所包含标头的位置。 您可以决定使用哪一个,但是,值得考虑一下它们之间的一些差异(该想法已在注释中提出 )。


include_directories命令会影响目录的范围。 这意味着此命令指定的所有标头目录将用于当前CMakeLists.txt所有目的,以及已处理的子项目(请参见add_subdirectory )。


target_include_directories命令target_include_directories影响第一个参数指定的目标,而不影响其他目标。 下面的示例演示了这两个命令之间的区别:


 add_executable(RequestGenerator RequestGenerator.c) add_executable(ResponseGenerator ResponseGenerator.c) #     "RequestGenerator": target_include_directories(RequestGenerator headers/specific) #    "RequestGenerator"  "ResponseGenerator": include_directories(headers) 

在注释中提到 ,在现代项目中,不希望使用include_directorieslink_libraries 。 另一种选择是target_include_directoriestarget_link_libraries ,它们仅对特定目标起作用,而不对整个当前范围起作用。


项目安装


install命令为您的项目生成安装规则。 此命令能够处理目标,文件,文件夹等。 首先,考虑设定目标。


要设置目标,必须将TARGETS关键字作为所描述函数的第一个参数传递,然后传递要设置的目标的列表,然后传递DESTINATION关键字以及将在其中设置这些目标的目录的位置。 此示例演示了一个典型的目标设置:


 #   "TimePrinter"  "DataScanner"   "bin": install(TARGETS TimePrinter DataScanner DESTINATION bin) 

描述文件安装的过程类似,只是必须指定FILES而不是TARGETS关键字。 演示文件安装的示例:


 #   "DataCache.txt"  "MessageLog.txt"   "~/": install(FILES DataCache.txt MessageLog.txt DESTINATION ~/) 

描述文件夹安装的过程类似,除了必须指定DIRECTORY而不是FILES关键字。 重要的是要注意,在安装过程中将复制文件夹的全部内容,而不仅仅是其名称。 安装文件夹的示例如下:


 #   "MessageCollection"  "CoreFiles"   "~/": install(DIRECTORY MessageCollection CoreFiles DESTINATION ~/) 

完成所有文件的CMake处理后,您可以使用sudo checkinstall安装所有描述的对象(如果CMake生成Makefile ),或者在支持CMake的集成开发环境中执行此操作。


可视项目示例


如果没有演示使用CMake构建系统的真实示例,本指南将是不完整的。 考虑一个使用CMake作为唯一构建系统的简单项目图:


 + MyProject - CMakeLists.txt - Defines.h - StartProgram.c + core - CMakeLists.txt - Core.h - ProcessInvoker.c - SystemManager.c 

主体程序文件CMakeLists.txt描述了整个程序的编译:首先, add_executable命令编译可执行文件,然后add_subdirectory命令,这刺激了子项目的处理,最后,可执行文件链接到已编译的库:


 #    CMake: cmake_minimum_required(VERSION 3.0) #   : project(MyProgram VERSION 1.0.0 LANGUAGES C) #      "MyProgram": add_executable(MyProgram StartProgram.c) #    "core/CMakeFiles.txt": add_subdirectory(core) #    "MyProgram"  #    "MyProgramCore": target_link_libraries(MyProgram MyProgramCore) #    "MyProgram"   "bin": install(TARGETS MyProgram DESTINATION bin) 

core/CMakeLists.txt文件由主程序集文件调用,并编译MyProgramCore静态库,用于与可执行文件链接:


 #    CMake: cmake_minimum_required(VERSION 3.0) #      "MyProgramCore": add_library(MyProgramCore STATIC ProcessInvoker.c SystemManager.c) 

经过一系列的cmake . && make && sudo checkinstall命令cmake . && make && sudo checkinstall cmake . && make && sudo checkinstall CMake构建系统成功完成。 第一个命令开始处理项目根目录中的CMakeLists.txt文件,第二个命令最终编译所需的二进制文件,第三个命令将编译后的MyProgram安装到系统中。


结论


现在,您可以编写自己的文件并了解其他人的CMake文件,并且可以在官方网站上详细了解其他机制。


本指南的下一篇文章将重点介绍如何使用CMake测试和创建软件包,并将在一周内发布。 待会见!

Source: https://habr.com/ru/post/zh-CN432096/


All Articles