Discuz! Board

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 1634|回复: 0
打印 上一主题 下一主题

CMake使用教程和原理

[复制链接]

1272

主题

2067

帖子

7958

积分

认证用户组

Rank: 5Rank: 5

积分
7958
跳转到指定楼层
楼主
发表于 2023-3-3 23:46:37 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
一、什么是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配置和生成阶段
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的 外部输入变量

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

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


图5, 构建目标的文件夹结构
depend.make和DependInfo.make:所有object的依赖关系。DependInfo.cmake保存了语言和对象文件的关系。


图6 depend.cmake文件内容

图7 DependInfo.make的文件内容
flags.make保存了编译选项,如果编译选项改变了,也会触发重建构建


图8、flags.make的文件内容
最后这些信息都会汇总成build.make


图9、build.make的文件内容
三、Cmake怎么使用
CMakeLists.txt定义了所有编译规则的入口。CMakeLists的常用编译指令按照目的分类有:
我们联想从最简单的编译规则说起:
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 定义编译选项(或者编译特征)target_compile_features(target PRIVATE|PUBLIC|INTERFACE feature1 [feature2 ...])复制
PRIVATE的意思是这个target的编译选项只对该target有效,如果需要对引用该target的上级target也有效,那么这里需要用PUBLIC。
样例:
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个大类:
  • 编译最低要求:版本号什么的
  • 编译选项:
SET(CMAKE_CXX_STANDARD 14):为什么是CXX复制
  • 条件编译:
如果开启了CXX_VARIADIC_TEMPLATES#if Foo_COMPILER_CXX_VARIADIC_TEMPLATES#else#endif复制
3.2 找到编译头文件
让CMake找到我的头文件, include_directories(/home/include)。常见的也有这样写,把工程的include文件夹加到包含路径。
include_directories(${CMAKE_CURRENT_LIST_DIR}/include),复制
CMAKE_CURRENT_LIST_DIR这个变量,它表示当前CMakeLists所在的路径.或者PROJECT_SOURCE_DIR,这个命令的原型是
命令: include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])复制
作用是把dir1, [dir2 …]这(些)个路径添加到当前CMakeLists及其子CMakeLists的头文件包含路径中;
AFTER 或者 BEFORE 指定了要添加的路径是添加到原有包含列表之前或之后
若指定 SYSTEM 参数,则把被包含的路径当做系统包含路径来处理
如果需要递归include文件夹及子文件夹的所有目录,用
add_subdirectory()复制
那target_inlucde_directories()是指什么,库的所有者都可以使用
外部的target
#include(TARGET),它会去子文件夹cmake/TARGET文件夹,搜索TARGET.cmake的文件。
3.3、找到源文件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 找到库文件link_directories(${CMAKE_CURRENT_LIST_DIR}/lib)复制
link_directories(directory1 directory2 ...)和include_directories()类似他,添加库包含路径。
3.5 链接库文件target_link_libraries(${PROJECT_NAME} util)复制
命令: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<ackage>.cmake
3.6生成target
Target包括3种: executable、 library、自定义command
指令分别为
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、打印调试日志消息message(STATUS “my custom debug info”)复制
3.7.2、操作文件FILE()复制
3.7.3、循环控制foreach()endforeach()复制
3.7.4、定义宏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、工程包名字
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的例子如下:
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_PREFIXATH=${DMP_CLIENT_SOURCE_DIR}/third/gtest/build 设置 cmake 的参数来实现。
  • ExternalProject_Get_Property()是获取工程的一些属性。
  • add_dependencies增加依赖编译项目
五、总结
这些变量和指令不好记,怎么快速记忆。
  • 全为大写
  • 大小写混用
  • 规则指令add_xxxxxx等
  • token之间没有逗号,用空格隔断两个token
5.1 cmake开启详细信息调试模式
--trace-expand


https://cloud.tencent.com/developer/article/1561162

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|firemail ( 粤ICP备15085507号-1 )

GMT+8, 2024-11-22 17:37 , Processed in 0.061881 second(s), 20 queries .

Powered by Discuz! X3

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表