一、什么是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生成targetTarget包括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
|