前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CMake使用教程和原理

CMake使用教程和原理

原创
作者头像
mariolu
修改2019-12-28 17:23:34
12.6K0
修改2019-12-28 17:23:34
举报
文章被收录于专栏:CDN及云技术分享

一、什么是CMake

CMake是一个主要用于CPP的构建工具。CMake语言是平台无关的中间编译工具。同一个CMake编译规则在不同系统平台构建出不同的可执行构建文件。在Linux产生MakeFile,在Windows平台产生Visual Studio工程等。CMake旨在解决各平台的不同Make工具的产生的差异(比如GNU Make, QT的qmake,微软的nmake, BSD的pmake)。

其实除了CMake构建系统之外,CMake已经发展出一系列开发工具:CMake,CTest,CPack和CDash。

- CMake是负责构建软件的构建工具。

- CTest是一个测试驱动程序工具,用于运行回归测试。

- CPack是一种打包工具,用于为使用CMake构建的软件创建特定于平台的安装程序。

- CDash是一个Web应用程序,用于显示测试结果并执行连续的集成测试。

- 其他还有Doxygen和BullseyeCoverage

1.1 CMake的前世今生

项目的通常做法是为Unix平台提供配置脚本和Makefile,为Windows提供Visual Studio项目文件。autoconf / libtool构建软件的方法不能满足跨平台的要求。

历史上曾经出现的1999年的VTK构建系统。该系统由Unix的配置脚本和pcmaker Windows 的可执行文件组成。pcmaker是一个C程序,可以读取Unix Makefile文件并为Windows创建NMake文件。

另一种是是gmake针对Sun工作站上C ++计算机视觉环境。Sun工作站使用该imake系统创建Makefile。但是,有时需要Windows端口时,gmake才创建了系统。Unix编译器和Windows编译器均可与此gmake基于此的系统一起使用。

这两个系统都存在严重缺陷:它们迫使Windows开发人员使用命令行。有经验的Windows开发人员更喜欢使用集成开发环境(IDE)。

1.2 Cmake的使命

  • 创建和源代码库隔离的构建目录,分离开发和构建目录。易于进行源代码版本控制。
  • CMake是具有管理依赖项,依赖之间的关系。如果变更了源文件,必须重新构建所有依赖该源文件的脚本。
  • 并且要求高效的依赖关系解析是耗时短的。
  • CMake提供一些易于操作的API,向开发人员屏蔽平台细节。

二、CMake怎么解决问题

CMake有两个阶段,配置和生成阶段。

图1、CMake配置和生成阶段
图1、CMake配置和生成阶段

2.1 配置阶段

配置阶段解析所有的输入变量,并存储在CMakeCache.txt这个文件。这个阶段解决了用户构建一个项目需要依赖的各种输入参数。

在项目的构建过程中都使用shell级别的环境变量。通常,项目具有指向根目录位置的PROJECT_ROOT环境变量。还有配置可选或外部程序包。要使构建正常进行,每次执行构建时都需要设置所有这些外部变量。所有CMakeFile在配置阶段解决了这个问题。

先来窥探下CMakeCache.txt的构成,CmakeCache.txt由两部分构成:External Cache Entries和Internal Cache Entries。而CMakeCache.txt是由解析器Parser生成。解析器的匹配器找到各种token。CMakeLists也可以解析外部的CMake语法,他是由“include” 或者“add_subdirectory”包含进来,两者的区别后面会说到。

解析完这些变量,cmake在内存中有了项目(可执行程序、库、用户自定义Command)的构建表达方法。在代码中一个target用cmTarget对象表示,所有的cmTarget构成了cmMakefile对象。

图2、CMakeCache.txt的 外部输入变量
图2、CMakeCache.txt的 外部输入变量
图3、CMakeCache.txt的内部输入变量
图3、CMakeCache.txt的内部输入变量

2.2 生成阶段

在生成阶段,cmake使用了一套语法解析系统,关键的类图如下。cmMakefile对象存错了CMakeLists.txt的所有输入变量。解析器使用了lex/yacc语法解析器,执行构建动作。cmCommand定义了命令的执行动作,并且该动作的注释在代码也有注释。这些关键类 是抽象类,CMake的跨平台实现依赖于这些类的平台实现类。

图4、生成阶段的关键类
图4、生成阶段的关键类

2.3 依赖管理和更新构建

CMake在使用IDE的平台不生成依赖,这些依赖由IDE自己完成。在Unix系统,CMake做了依赖管理,并把这些信息写在depend.make,flags.make, build.make,DependInfo.cake。当这些文件有变化,都会从cmake的重新构建。

图5, 构建目标的文件夹结构
图5, 构建目标的文件夹结构

depend.make和DependInfo.make:所有object的依赖关系。DependInfo.cmake保存了语言和对象文件的关系。

图6 depend.cmake文件内容
图6 depend.cmake文件内容
图7 DependInfo.make的文件内容
图7 DependInfo.make的文件内容

flags.make保存了编译选项,如果编译选项改变了,也会触发重建构建

图8、flags.make的文件内容
图8、flags.make的文件内容

最后这些信息都会汇总成build.make

图9、build.make的文件内容
图9、build.make的文件内容

三、Cmake怎么使用

CMakeLists.txt定义了所有编译规则的入口。CMakeLists的常用编译指令按照目的分类有:

我们联想从最简单的编译规则说起:

代码语言:javascript
复制
gcc -Wall -std=c++11 -DMY_MACRO -I/home/lib [-Ldir] -llibname main.c -o main 

比如gcc 这里的-Wall是编译选项,-DMY_MACRO定义了MY_MACRO宏,-L指库的搜索路径,-l指链接libname库,源文件是main.c,最终生成的二进制可执行文件是main 

那么怎么用CMake表示这个规则。

3.1 定义编译选项(或者编译特征)

代码语言:javascript
复制
target_compile_features(target PRIVATE|PUBLIC|INTERFACE feature1 [feature2 ...])

PRIVATE的意思是这个target的编译选项只对该target有效,如果需要对引用该target的上级target也有效,那么这里需要用PUBLIC。

样例:

代码语言:javascript
复制
target_compile_features(main PRIVATE “-Wall”)
set_target_properties(main PROPERITES
    COMPILE_FLAGS "-Wall"
)
target_compile_features(mylib PUBLIC cxx_std_11)

还有个target_compile_option()是什么区别

另外提一下,这里C++在这里是CXX? 因为涉及到不同平台下C++程序的后缀名不一样,在Windows下我们常用的就是一个.cpp扩展名,还有gcc一般用c.cc.cxx 等等都是C++文件的扩展名。

有些c++就是直接用语言的名字命名的扩展名,但有些系统可能不支持在文件名里放入加号"+",或许这里用cxx的x有点像+,当时设计意图可能是这边吧。

编译命令可以归结为以下3个大类:

  • 编译最低要求:版本号什么的
  • 编译选项:
代码语言:javascript
复制
SET(CMAKE_CXX_STANDARD 14):为什么是CXX
  • 条件编译:
代码语言:javascript
复制
如果开启了CXX_VARIADIC_TEMPLATES
#if Foo_COMPILER_CXX_VARIADIC_TEMPLATES
#else
#endif

3.2 找到编译头文件

让CMake找到我的头文件, include_directories(/home/include)。常见的也有这样写,把工程的include文件夹加到包含路径。

代码语言:javascript
复制
include_directories(${CMAKE_CURRENT_LIST_DIR}/include),

CMAKE_CURRENT_LIST_DIR这个变量,它表示当前CMakeLists所在的路径.或者PROJECT_SOURCE_DIR,这个命令的原型是

代码语言:javascript
复制
命令: include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

作用是把dir1, [dir2 …]这(些)个路径添加到当前CMakeLists及其子CMakeLists的头文件包含路径中;

AFTER 或者 BEFORE 指定了要添加的路径是添加到原有包含列表之前或之后

若指定 SYSTEM 参数,则把被包含的路径当做系统包含路径来处理

如果需要递归include文件夹及子文件夹的所有目录,用

代码语言:javascript
复制
add_subdirectory()

那target_inlucde_directories()是指什么,库的所有者都可以使用

外部的target

#include(TARGET),它会去子文件夹cmake/TARGET文件夹,搜索TARGET.cmake的文件。

3.3、找到源文件

代码语言:javascript
复制
aux_source_directory(./src ${hello_src})

作用: 把当前路径下src目录下的所有源文件路径放到变量hello_src中

命令:aux_source_directory(<dir> <variable>)

作用:查找dir路径下的所有源文件,保存到variable变量中.

上面的例子中,hello_src是一个自定义变量,在执行了aux_source_directory(./src ${hello_src})之后,我就可以像这样来添加一个可执行文件:add_executable(hello ${hello_src}), 意思是用hello_src里面的所有源文件来构建hello可执行程序, 不用手动列出src目录下的所有源文件了。

值得注意的是:aux_source_directory 不会递归包含子目录,仅包含指定的dir目录

CMake官方不建议用aux_source_directory及类似命令(file(GLOB_RECURSE …))搜索源文件。因为这样子文件夹的变化不容易被感知到,从而无法触发重新构建。比如被搜索的路径下添加源文件,此时没有修改CMakeLists脚本,但是CMakeLists并不需要(没有)变化,构建系统无法察觉到新加的文件,除非手动重新运行cmake,否则新添加的文件就不会被编译到项目结果中。

3.4 找到库文件

代码语言:javascript
复制
link_directories(${CMAKE_CURRENT_LIST_DIR}/lib)

link_directories(directory1 directory2 ...)和include_directories()类似他,添加库包含路径。

3.5 链接库文件

代码语言:javascript
复制
target_link_libraries(${PROJECT_NAME} util)
代码语言:javascript
复制
命令:target_link_libraries(<target> [item1 [item2 [...]]] [[debug|optimized|general] <item>] ...)

这个target需要链接util这个库,会优先搜索libutil.a(windows上就是util.lib), 如果没有就搜索libutil.so(util.dll, util.dylib)’

类似于与pkg-config去文件夹找*.pc,cmake也提供了find_package(),它会去cmake安装目录module文件夹执行Find<Package>.cmake

3.6生成target

Target包括3种: executable、 library、自定义command

指令分别为

代码语言:javascript
复制
add_custom_command()
add_library(archive archive.cpp zip.cpp lzma.cpp)
add_executable(zipapp zipapp.cpp)

链接库和最终target:target_link_libraries(zipapp archive)

3.7 其他命令等

3.7.1、打印调试日志消息

代码语言:javascript
复制
message(STATUS “my custom debug info”)

3.7.2、操作文件

代码语言:javascript
复制
FILE()

3.7.3、循环控制

代码语言:javascript
复制
foreach()
endforeach()

3.7.4、定义宏

代码语言:javascript
复制
macro()
endmacro()

3.7.5、设置cmake最低版本

设置要求版本>=3.5:CMAKE_MINIMUM_REQUIRED(VERSION 3.5)

CMAKE_MODULE_PATH:

什么是工程MODULE,多个工程连接

编译选项:

SET(CMAKE_CXX_STANDARD 14):为什么是CXX

3.7.6、包含外部子target

#include(TARGET),它会去子文件夹cmake/搜索TARGET.cmake的文件。也可能去cmake的安装目录下搜索。

3.7.8、工程包名字

代码语言:javascript
复制
PROJECT(output_binary_name CXX)

四、高级特性 - 在线下载编译工程

ExternalProject在构建时从另一个项目填充内容。这意味着在构建主项目之前,本地没有其他项目的库。首先需要add_dependencies()声明,ExternalProject才会下载,配置或构建。最主要外部下载引用是 ExternalProject_Add,功能很强大,支持不同的地址去获取依赖,可以是打包文件的 URL,比如 github 上的某个项目的 tag,或者像 boost 这种,在官网提供的下载链接,也可以直接是 GIT_REPOSITORY,一般建议直接使用打包的 tag,因为比较快,而且有固定的 tag,比较好做版本管理,但是有些项目引用了外部项目需要执行 git submodule update --init,这种就比较适合用 git 地址,会自动下载依赖模块

一个ExternalProject_ADD的例子如下:

代码语言:javascript
复制
ExternalProject_ADD(
  #--External-project-name------
  antlr4cpp
  #--Depend-on-antrl-tool-----------
  # DEPENDS antlrtool
  #--Core-directories-----------
 # PREFIX             ${ANTLR4CPP_EXTERNAL_ROOT}
  PREFIX             ${ANTLR4CPP_LOCAL_ROOT}
  #--Download step--------------
 # GIT_REPOSITORY     ${ANTLR4CPP_EXTERNAL_REPO}
  URL                ${ANTLR4CPP_LOCAL_REPO}
  # GIT_TAG          ${ANTLR4CPP_EXTERNAL_TAG}
  TIMEOUT            10
  LOG_DOWNLOAD       ON
  #--Update step----------
  UPDATE_COMMAND     ${GIT_EXECUTABLE} pull
  #--Patch step----------
  # PATCH_COMMAND sh -c "cp <SOURCE_DIR>/scripts/CMakeLists.txt <SOURCE_DIR>"
  #--Configure step-------------
  CONFIGURE_COMMAND  ${CMAKE_COMMAND} -DCMAKE_BUILD_TYPE=Release -DANTLR4CPP_JAR_LOCATION=${ANTLR4CPP_JAR_LOCATION} -DBUILD_SHARED_LIBS=ON -BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_SOURCE_DIR:PATH=<SOURCE_DIR>/runtime/Cpp <SOURCE_DIR>/runtime/Cpp
  LOG_CONFIGURE ON
  #--Build step-----------------
  # BUILD_COMMAND ${CMAKE_MAKE_PROGRAM}
  LOG_BUILD ON
  #--Install step---------------
  # INSTALL_COMMAND    ""
  # INSTALL_DIR ${CMAKE_BINARY_DIR}/
  #--Install step---------------
  # INSTALL_COMMAND    ""
)

下载完之后编译这个过程,基本不需要额外的配置,会自动编译,也许会按照个人习惯设置一个编译后的 install 目录,可以通过 CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=${DMP_CLIENT_SOURCE_DIR}/third/gtest/build 设置 cmake 的参数来实现。

  • ExternalProject_Get_Property()是获取工程的一些属性。
  • add_dependencies增加依赖编译项目

五、总结

这些变量和指令不好记,怎么快速记忆。

  • 全为大写
  • 大小写混用
  • 规则指令add_xxxxxx等
  • token之间没有逗号,用空格隔断两个token

5.1 cmake开启详细信息调试模式

--trace-expand

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么是CMake
    • 1.1 CMake的前世今生
      • 1.2 Cmake的使命
      • 二、CMake怎么解决问题
        • 2.1 配置阶段
          • 2.2 生成阶段
            • 2.3 依赖管理和更新构建
            • 三、Cmake怎么使用
              • 3.1 定义编译选项(或者编译特征)
                • 3.2 找到编译头文件
                  • 3.3、找到源文件
                    • 3.4 找到库文件
                      • 3.5 链接库文件
                        • 3.6生成target
                          • 3.7 其他命令等
                            • 3.7.1、打印调试日志消息
                            • 3.7.2、操作文件
                            • 3.7.3、循环控制
                            • 3.7.4、定义宏
                            • 3.7.5、设置cmake最低版本
                            • 3.7.6、包含外部子target
                        • 四、高级特性 - 在线下载编译工程
                        • 五、总结
                          • 5.1 cmake开启详细信息调试模式
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档