CMake学习笔记(一)基本概念介绍、入门教程及CLion安装配置

什么是构建系统

在软件开发中,构建系统build system)是用来从源代码生成用户可使用的目标自动化工具。目标能够包括库、可执行文件、或者生成的脚本等等。html

一般每一个构建系统都有一个对应的构建文件(也能够叫配置文件项目文件之类的),来指导构建系统如何编译、连接生成可执行程序等等。构建文件中一般描述了要生成的目标生成目标所须要的源代码文件依赖库等等内容。不一样构建系统使用的构建文件的名称和内容格式规范一般各不相同。ios

常见的构建系统

  • GNU Make:类Unix操做系统下的构建系统,构建文件名称一般是Makefilemakefile
  • NMake:能够理解为Windows平台下的GNU Make,是微软Visual Studio早期版本使用的构建系统,好比VC++6.0。构建文件的后缀名是.mak
  • MSBuildNMake的替代品,一开始是与.net框架绑定的。Visual Studio2013版本开始使用它做为构建系统。Visual Studio的项目构建依赖于MSBuild,但MSBuild并不依赖前者,能够独立运行。构建文件的后缀名是.vcproj(以C++项目为例)。
  • Ninja:一个专一于速度的小型构建系统,Chrome团队开发。

CMake

CMake是一个开源的跨平台构建系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个库。虽然CMake一样用构建文件控制构建过程(名称是CMakeLists.txt),但它却并不直接构建并生成目标,而是产生其余构建系统所须要的构建文件,而后再由它们来构建生成最终目标。支持MSBuildGNU MakeMINGW Make等等构建系统。c++

qmake

qmake是一个协助简化跨平台进行项目开发的构建过程的工具程序,Qt附带的工具之一 。与CMake相似,qmake一样不是直接构建并生成目标,而是依赖其余构建系统。它可以自动生成MakefileVisual Studio项目文件 和 XCode项目文件。无论项目是否使用Qt框架,都能使用qmake,所以qmake能用于不少软件的构建过程。git

qmake使用的构建文件是.pro项目文件,开发者可以自行撰写项目文件或是由qmake自己产生。qmake包含额外的功能来方便 Qt 开发,如自动包含mocuic的编译规则。值得一提的是,CMake一样支持Qt开发,但并不依赖于qmakegithub

CMake入门教程(Step By Step)

这个入门教程我主要是参考了CMake官方的Tutorial以及网上的一些资料,并进行了一些更改和补充,使得理解起来更加容易。正则表达式

1. 安装、配置开发环境

在开始以前,咱们能够选择一个本身喜欢的IDE(集成开发环境)来做为C/C++开发工具,CMake支持Visual StudioQtCreatorEclipseCLion等多个IDE,固然你也可使用像是VSCodeVim这些文本编辑器并配合一些插件来做为开发工具。因为我我的的习惯和偏好,加上主要是Linux系统下开发,最终选择了JetBrains家族的CLion做为开发工具。shell

  • 安装构建工具链(CMake、编译器、调试器、构建系统):后端

    CMake:Debian系Linux系统可使用apt安装:sudo apt install cmake cmake-qt-gui,若是嫌apt里的版本过低,能够手动编译安装最新版本,参考编译安装cmake及cmake-gui;Windows系统能够前往官网下载安装程序。安全

    其余Linux系统下直接使用自带的GNU套件(make、gcc、gdb),Windows系统可使用MinGW-w64MSVC或者WSL等工具。bash

  • 下载安装:CLion官网,能够免费试用30天。

  • 首次运行:登陆账号进行激活受权,偏好配置。学生可使用edu邮箱注册JetBrains账号,而后能够无偿使用JetBrains家族全部IDEULTIMATE版本。

  • 界面汉化(可选):平方X JetBrains系列软件汉化包。我英文不太行,因此汉化仍是挺有必要的。

  • 配置构建工具链(设置 -> 构建,执行,部署 -> 工具链):这一步是在CLion中配置构建须要的工具的路径。CMake能够直接使用CLion自带绑定的一个版本,固然也能够选择本身安装的版本。

    配置构建工具链

  • 配置CMake选项(设置 -> 构建,执行,部署 -> CMake):设置构建类型(Debug/Release),CMake构建选项参数、构建目录等等。通常保持默认的就能够,等到须要修改CMake构建相关选项的时候再去配置。

    配置CMake选项

2. 建立一个CLion C++项目

打开CLion,新建一个C++可执行程序项目C++标准版本我选择了C++17。其实像是标准版本、构建目标类型这些选项,在新建项目时选好了,后面仍是能够经过CMakeLists.txt文件随时进行更改的,不用太过纠结。

CLion新建项目

3. 项目结构及CMakeLists.txt内容分析

建立一个项目后,初始结构是这样的:

CLion项目结构分析

  • CMakeLearnDemo:项目源目录,包含项目源文件的顶级目录

  • main.cpp:自动生成的main函数源文件,没什么好说的。

  • cmake-build-debugCLion调用CMake生成的默认构建目录。什么是构建目录呢,用于存储构建系统文件(好比makefile以及其余一些cmake相关配置文件)和构建输出文件(编译生成的中间文件、可执行程序、库)的顶级目录。由于咱们确定不想把构建生成的文件和项目源文件混在一块,这样会使项目结构变得混乱,因此通常都会单首创建一个构建目录。固然若是你喜欢,能够直接将项目源目录做为构建目录。使用CLion咱们不须要手动在命令行调用CMake来生成构建目录以及构建项目,CLionCMakeLists.txt的内容发生改变时会自动从新生成构建目录,构建项目也只须要点击构建按钮就能够了。可是在学习阶段,了解CMake的基本用法仍是很重要的,等到熟悉了以后,再使用IDE也就驾轻就熟了。咱们在项目源目录下新建一个mybuild目录,做为咱们本身手动调用CMake命令时所指定的构建目录,如图:

建立mybuild构建目录

  • CMakeLists.txtcmake项目配置文件,准确点说是项目顶级目录的cmake配置文件,由于一个项目在多个目录下能够有多个CMakeLists.txt文件。这个应该是cmake的核心配置文件了,基本上更改项目构建配置都是围绕着这个文件进行。咱们来看看CLion为咱们自动生成的CMakeLists.txt是什么内容:

    CMakeLists.txt初始内容

    cmake_minimum_required(VERSION 3.15)
    复制代码

    设置cmake的最低版本要求,若是cmake的运行版本低于最低要求版本,它将中止处理项目并报告错误。

    project(CMakeLearnDemo)
    复制代码

    设置项目的名称,并将其值存储在cmake内置变量PROJECT_NAME中。当从顶级CMakeLists.txt调用时,还将其存储在内置变量CMAKE_PROJECT_NAME中。

    set(CMAKE_CXX_STANDARD 17)
    复制代码

    设置C++标准的版本。目前支持的版本值是98, 11, 14, 1720

    add_executable(CMakeLearnDemo main.cpp)
    复制代码

    添加一个可执行文件类型的构建目标到项目中。CMakeLearnDemo是文件名,后面是生成这个可执行文件所须要的源文件列表。

4. 一个最基础项目的配置、构建和运行

首先咱们将main.cpp的内容修改一下,代码以下:

#include <cmath>
#include <iostream>

int main(int argc, char *argv[]) {
    if(argc < 2)
    {
        std::cerr << "Must have at least 2 command line arguments." << std::endl;
        return 1;
    }

    try
    {
        double inputValue = std::stof(argv[1]);
        double outputValue = std::sqrt(inputValue);
        std::cout << "the square root of " << inputValue 
                  << " is " << outputValue << std::endl;
    }
    catch(const std::invalid_argument& e)
    {
        std::cerr << e.what() << std::endl;
        return 1;
    }

    return 0;
}
复制代码

main函数中,主要作了如下操做:读取命令行参数值,计算它的算术平方根并输出,同时包含了一些错误判断。

明确须要某个C++标准版本

以前设置的CMAKE_CXX_STANDARD只是一个可选属性,若是编译器不支持此标准版本,则仍是有可能会退化为之前的版本。若是咱们想要明确表示须要某个C++标准,则能够经过:

set(CMAKE_CXX_STANDARD_REQUIRED True)
复制代码

来实现。这样的话,若是编译器不支持此标准,则CMake会直接报错,中止运行。

生成项目构建系统

这一步能够理解为一个项目配置过程,并无发生任何的编译工做。cmake根据项目的CMakeLists.txt文件在构建目录中生成对应构建系统构建文件,同时也包含了不少cmake相关的配置文件,不过这些自动生成文件的内容其实咱们目前不须要去关心。

生成项目构建系统的命令有3种形式:

使用当前目录做为构建目录,<path-to-source>做为项目源目录。能够是相对或者绝对路径。

cmake [<options>] <path-to-source>

# 举例,..表明当前目录的上级目录
cd mybuild
cmake ..
复制代码

使用<path-to-existen-build>做为构建目录,并从其CMakeCache.txt文件中获取到项目源目录的路径,该文件必须是在之前的CMake运行时生成的,也就是说从以前已经生成过构建系统的构建目录中获取到以前的项目源目录,并从新生成一次。能够是相对或者绝对路径。

cmake [<options>] <path-to-existing-build>

# 举例
cmake mybuild
复制代码

使用<path-to-source>做为项目源目录,<path-to-build做为构建目录。能够是相对或者绝对路径。

cmake [<options>] -S <path-to-source> -B <path-to-build>

# 举例,. 表明当前目录
cmake -S . -B mybuild
复制代码

我更喜欢第三种方式,由于这样更直观。接下来就让咱们亲自生成项目构建系统吧:

生成项目构建系统

切换构建系统

对于每种构建系统,都对应着一个cmake生成器,负责为此构建系统生成原生的相关构建文件,能够在调用cmake命令行工具时经过-G <generator_name>来指定,也能够在CMakeLists.txt中经过设置CMAKE_GENERATOR变量的值来指定。

cmake生成器是特定于平台的,所以每一个生成器只能在特定的平台上使用。可使用cmake --helo命令行来查看当前平台上可用的生成器,个人Linux mint 1903输出以下图:

切换构建类型

构建类型有DebugReleaseRelWithDebInfoMinSizeRel等,能够经过CMAKE_BUILD_TYPE变量来指定,好比:

set(CMAKE_BUILD_TYPE Release)
复制代码

构建项目

生成项目构建系统后,接下来就能够选择构建项目了。咱们能够直接调用相应的构建系统来构建项目,好比GNU make,也能够调用cmake来让它自动选择相对应的构建系统来构建项目。以下:

使用make构建项目

或者:

使用cmake调用构建系统来构建项目

运行可执行文件

构建完成了,接下来让咱们运行可执行文件,看看运行结果:

运行CMakeLearnDemo

5. 为项目添加一个版本号

虽然能够直接在源文件里定义版本号,可是在CMakeLists.txt里设置会更加灵活方便。配置版本号是做为project命令的一个可选择项,语法以下:

project(<PROJECT-NAME>
				 [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
复制代码

不难看出,版本号最少1个层级,最多4个层级。以版本号0.8.1.23为例,各个层级的值及含义以下:

  • major:主版本号,值为0。能够经过cmake内置变量PROJECT_VERSION_MAJOR或者<PROJECT-NAME>_VERSION_MAJOR来获取到它的值。
  • minor:次版本号,值为8。能够经过cmake内置变量PROJECT_VERSION_MINOR或者<PROJECT-NAME>_VERSION_MAJOR来获取到它的值。
  • patch:补丁版本号,值为1。能够经过cmake内置变量PROJECT_VERSION_PATCH或者<PROJECT-NAME>_VERSION_PATCH来获取到它的值。
  • tweak:微小改动版本号,值为23。能够经过cmake内置变量PROJECT_VERSION_TWEAK或者<PROJECT-NAME>_VERSION_TWEAK来获取到它的值。
  • 完整版本号0.8.1.23能够经过cmake内置变量PROJECT_VERSION或者<PROJECT-NAME>_VERSION来获取到它的值。

如今,让咱们建立一个autoGeneratedHeaders文件夹,用于存放由cmake自动生成或更新的头文件。接着,建立一个projectConfig.h.in文件,内容以下:

#ifndef CMAKELEARNDEMO_PROJECTCONFIG_H
#define CMAKELEARNDEMO_PROJECTCONFIG_H

#define PROJECT_VERSION "@CPROJECT_VERSION@"
#define PROJECT_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define PROJECT_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define PROJECT_VERSION_PATCH @PROJECT_VERSION_PATCH@

#endif //CMAKELEARNDEMO_PROJECTCONFIG_H
复制代码

接着将CMakeLists.txt里添加或更改以下内容:

project(CMakeLearnDemo VERSION 1.0.0)

configure_file(
        autoGeneratedHeaders/projectConfig.h.in
        ${PROJECT_SOURCE_DIR}/autoGeneratedHeaders/projectConfig.h
)

target_include_directories(CMakeLearnDemo PUBLIC
        autoGeneratedHeaders
)
复制代码

解释:configure_file(<input> <output> ...)命令的做用是将指定的文件复制到另外一个位置并替换某些内容。<input>若是使用相对路径,则相对于项目源目录;<output>若是使用相对路径,则相对于项目构建目录,因此在<output>的值中我使用了PROJECT_SOURCE_DIR这个cmake变量来获取项目源目录。若是但愿直接替换某个文件中的内容,能够将<input><output>的值指向同一文件。

再说替换,在projectConfig.h.in中,@VARIABLE_NAME@这个语法是引用某个cmake变量的值,执行configure_file命令后,它就会被替换为相应变量的值。而具体定义多少个项目层级,这由你本身决定,我这里就只定义了3个项目层级和一个完整版本号。

target_include_directories(...)命令是将指定目录添加到指定构建目标编译器搜索包含文件目录中。经过添加autoGeneratedHeaders到包含文件目录,在main.cpp里能够直接使用

#include "projectConfig.h"
// 或者
#include <projectConfig.h>
复制代码

而不用

#include "autoGeneratedHeaders/projectConfig.h"
// 或者
#include <autoGeneratedHeaders/projectConfig.h>
复制代码

接下来,让咱们在main.cpp里输出项目版本信息,以下:

#include "projectConfig.h"

int main(int argc, char *argv[]) {
    std::cout << "Project Version: " << PROJECT_VERSION << std::endl;
    std::cout << "Project Version Major: " << PROJECT_VERSION_MAJOR << std::endl;
    std::cout << "Project Version Minor: " << PROJECT_VERSION_MINOR << std::endl;
    std::cout << "Project Version Patch: " << PROJECT_VERSION_PATCH << std::endl;
    ...
复制代码

从新生成构建系统,不出意外会新增一个projectConfig.h文件。

构建而后运行可执行文件,查看输出内容:

版本号输出

6. 添加库

以前咱们在main函数中是使用标准库中的std::sqrt来计算平方根。如今,让咱们本身实现一个计算平方根的函数,并将其构建生成静态库,最后在main函数中使用咱们本身的平方根库函数来替代标准库。

代码实现、库生成及使用

建立一个mymath文件夹,存放咱们本身实现的平方根函数的.h.cpp以及CMakeLists.txt文件。结构以下:

mymath结构

mymath.h内容以下:

#ifndef CMAKELEARNDEMO_MYMATH_H
#define CMAKELEARNDEMO_MYMATH_H

#include <stdexcept>

namespace mymath
{
    // calculate the square root of number
    double sqrt(double number) noexcept(false);
}

#endif // CMAKELEARNDEMO_MYMATH_H
复制代码

mymath.cpp内容以下:

#include "mymath.h"

namespace mymath
{
    double sqrt(double number) {
        static constexpr double precision = 1e-6;
        static constexpr auto abs = [](double n) ->double { return n > 0? n:-n; };

        if(number < 0)
        {
            throw std::invalid_argument("Cannot calculate the square root of a negative number!");
        }

        double guess = number;
        while( abs(guess * guess - number) > precision)
        {
            guess = ( guess + number / guess ) / 2;
        }

        return guess;
    }
}
复制代码

CMakeLists.txt内容以下:

add_library(mymath STATIC mymath.cpp)
复制代码

add_library与以前的add_execuable相似,只不过它添加的是库目标,而不是可执行文件目标。mymath是库名,STATIC指明生成静态库,mymath.cpp是生成这个库所须要的源文件。

接下来咱们须要在项目根目录的CMakeLists.txt中添加或更改以下内容:

add_subdirectory(mymath)

target_include_directories(CMakeLearnDemo PUBLIC
        autoGeneratedHeaders
        mymath
)

target_link_libraries(CMakeLearnDemo PUBLIC
        mymath
)
复制代码

add_subdirectory的做用是将一个包含CMakeLists.txt的子目录添加到构建中,由于子CMakeLists.txt若是没有被添加到根CMakelists.txt中,它是不会参与构建的。

mymath添加到target_include_directories中,由于在main.cpp中须要包含mymath.h头文件;target_link_libraries的做用是将依赖库连接到指定目标,由于main.cpp中须要用到mymath库。

如今让咱们在main.cppstd::sqrt换成咱们本身的平方根函数,改动的地方以下:

- #include <cmath>
+ #include "mymath.h"
...
int main(int argc ,char* argv[]) {
    ...
    double outputValue = mymath::sqrt(inputValue);
    ...
}
复制代码

从新生成构建系统并构建运行,输出结果应该是与以前同样的;打开mybuild目录,能够发现里面多了一个mymath子构建目录,其目录下有生成的库文件libmymath.a,如图:

mymath子构建目录

将库设置为可选的

如今让咱们将mymath库设置为用户可选择的,虽然对一个教程来讲不是颇有必要,可是在一个大型项目中这种行为仍是很常见的。

首先让咱们在CMakeLists.txt中加入或更改以下内容:

option(USE_MYMATH "Use CMakeLearnDemo provided math implementation" ON)
message("value of USE_MYMATH is : " ${USE_MYMATH})

configure_file(
        autoGeneratedHeaders/projectConfig.h.in
        ${PROJECT_SOURCE_DIR}/autoGeneratedHeaders/projectConfig.h
)

if(USE_MYMATH)
    add_subdirectory(mymath)
    list(APPEND EXTRA_INCLUDES mymath)
    list(APPEND EXTRA_LIBS mymath)
endif()

add_executable(CMakeLearnDemo main.cpp)

target_include_directories(CMakeLearnDemo PUBLIC
        autoGeneratedHeaders
        ${EXTRA_INCLUDES}
)

target_link_libraries(CMakeLearnDemo PUBLIC
        ${EXTRA_LIBS}
)
复制代码

解释:

option(<variable> "<help_text>" [value])
复制代码

添加一个选项供用户选择ON开启或者OFF关闭,用户最终选择的值会存放到变量<variable>中,"<help_text"是该选项的描述信息,[value]是默认开启/关闭值。

message命令会在生成构建系统时输出一条信息,通常是用来查看变量的值之类的。

if()endif()条件语句和其余高级语言中的做用相似,能够阅读cmake if条件判断语法来了解哪些变量值会被if断定为True,哪些会被断定为False

cmake中的列表型变量是指用;分隔开的字符串,好比"a;b;c;d;e"list()命令的做用则是对列表型变量进行一系列操做,好比添加、插入、删除、获取长度等等。在本教程中,咱们使用EXTRA_INCLUDESEXTRA_LIBS这2个变量来单独存放由用户勾选的可选库的包含路径和库路径。

可使用set()命令来建立一个列表型变量,尝试在CMakeLists.txt中加入以下几行,并观察输出:

set(testVariable "a" "b" "c")
message(${testVariable})
list(LENGTH testVariable testLength)
message(${testLength})

set(testVariable "a;b;c")
message(${testVariable})
list(LENGTH testVariable testLength)
message(${testLength})

set(testVariable "a b c")
message(${testVariable})
list(LENGTH testVariable testLength)
message(${testLength})
复制代码

接下来让咱们在projectConfig.h.in中加入以下一行:

#cmakedefine USE_MYMATH
复制代码

configure_file命令会根据USE_MYMATH变量的值来将这一行替换为相应的内容。若是是被if命令断定为True的值,则会被替换为:

#define USE_MYMATH
复制代码

不然则会被替换为:

/* #undef USE_MYMATH */
复制代码

这样的话,咱们就能经过使用条件编译来判断是否认义USE_MYMATH宏,从而在代码中选择使用标准库仍是本身的库。在main.cpp中咱们须要修改如下几个地方:

#ifdef USE_MYMATH
#include "mymath.h"
#else
#include <cmath>
#endif
...
int main(int argc ,char* argv[]) {
    ...
#ifdef USE_MYMATH
        double outputValue = mymath::sqrt(inputValue);
#else
        double outputValue = std::sqrt(inputValue);
#endif
    ...
}
复制代码

为库添加使用需求

使用需求能够在CMake中更好地控制库或可执行文件的连接和包含目录,以及构建目标之间的属性传递。影响使用需求的主要命令有:

  • target_compile_definitions
  • target_compile_options
  • target_include_directories
  • target_link_libraries

如今让咱们重构一下咱们的代码,以使用现代的CMake方法来添加使用需求。咱们首先要求,任何连接了mymath库的构建目标都须要包含mymath目录,而mymath库自己固然不须要,因此这能够是一个接口型(INTERFACE)使用需求。

接口型是指消费者(其余使用此库的构建目标)须要而生产者(库自身)不须要的需求。如今让咱们在mymath/CMakeLists.txt中加入以下内容:

target_include_directories(mymath
        INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
复制代码

CMAKE_CURRENT_SOURCE_DIR变量的值是当前由CMake处理的源目录的完整路径,在本例中也就是mymath目录的完整路径。这样,全部连接了mymath库的构建目标就自动包含了mymath目录,如今能够将EXTRA_INCLUDES安全地删除掉:

if(USE_MYMATH)
    add_subdirectory(mymath)
#删除 list(APPEND EXTRA_INCLUDES mymath)
    list(APPEND EXTRA_LIBS mymath)
endif()

target_include_directories(CMakeLearnDemo PUBLIC
        autoGeneratedHeaders
#删除 ${EXTRA_INCLUDES}
)
复制代码

使用cmake-gui配置项目、生成构建系统

如今咱们的项目有了1个构建选项,而GUI图形界面能使构建选项更直白地向用户体现出来,因此咱们此次不使用命令行cmake,而是使用cmake-gui来配置项目并生成构建系统。

打开cmake-gui,首先选择项目源目录和构建目录,如图:

点击Configure,选择 Unix Makefiles做为构建系统,而后肯定,如图:

肯定并等待配置完成后,出现了若干个红色项,这表示当前项目中可由用户进行配置的可选项。咱们勾选USE_MYMATH选项,如图:

再次点击Configure,直到没有红色选项了以后,点击Generate来生成通过用户配置的项目构建系统,如图:

构建系统生成以后,观察projectConfig.hmain.cpp文件的变化,而后就能够进行项目构建了,方法跟以前同样,好比cmake --build mybuild

若是不想使用图形工具,也能够直接在调用命令行cmake工具时传递变量值,好比:

cmake -B mybuild -S . -D USE_MYMATH:BOOL=ON
复制代码

7. 安装

所谓安装,能够简单地理解为将软件或程序所须要的若干文件复制到指定位置。那么,对于咱们的CMakeLearnDemo项目来讲,须要安装哪些文件呢?对于mymath,须要安装库和头文件;对于整个应用程序,须要安装可执行程序和projectConfig.h头文件。

安装规则肯定好以后,咱们在mymath/CMakeLists.txt的末尾加入:

install(TARGETS mymath DESTINATION lib)
install(FILES mymath.h DESTINATION include)
复制代码

在根CMakeLists.txt的末尾加入:

install(TARGETS CMakeLearnDemo DESTINATION bin)
install(FILES autoGeneratedHeaders/projectConfig.h DESTINATION include)
复制代码

install命令用于生成项目的安装规则,即指明须要安装哪些内容。TARGETS是安装构建目标;FILES是安装文件,若是使用相对路径,则相对于当前cmake处理的源目录;DESTINATION <directory>是安装路径,若是使用相对路径,则相对于CMAKE_INSTALL_PREFIX变量的值。

接下来,让咱们打开cmake-gui,配置方法和以前同样,只不过此次咱们须要设置一下CMAKE_INSTALL_PREFIX变量,也就是项目的安装前缀路径,如图:

配置项目、生成项目构建系统、构建项目,这3项都完成以后,就能够进行安装了,命令以下:

cmake --install mybuild
复制代码

完成以后,打开你以前设置的安装前缀路径,不出意外已经有了相应的文件,如图:

8. 测试

接下来让咱们测试CMakeLearnDemo应用程序。在根CMakeLists.txt的末尾,咱们能够启用测试,而后添加一些基本测试来验证应用程序是否正常工做,以下:

enable_testing()

function(do_test target arg result)
    add_test(NAME sqrt${arg} COMMAND ${target} ${arg})
    set_tests_properties(sqrt${arg} PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endfunction()

do_test(CMakeLearnDemo 4 "2")
do_test(CMakeLearnDemo 2 "1.414")
do_test(CMakeLearnDemo 5 "2.236")
do_test(CMakeLearnDemo 123.456 "11.111")
do_test(CMakeLearnDemo -4 "NaN|NULL|Null|null|[Ee]rror|[Nn]ot [Ee]xist|[Nn]egative")
复制代码

解释:

function(<name> [arg1 arg2 ...])
		do sth ...
endfunction()		
复制代码

顾名思义,定义一个函数,必需要有一个函数名,参数可选。

add_test命令的做用是添加一个测试,NAME <name>指定这个测试的名字,COMMAND <command> [arg ...]指定测试时调用的命令行,若是<command>是一个由add_execuable()建立的可执行文件目标,它将自动被替换为构建时生成的可执行文件的路径。

set_tests_properties命令的做用是为指定的测试设置属性,PASS_REGULAR_EXPRESSION属性的含义是:为了经过测试,命令的输出结果必须匹配这个正则表达式,好比"\d+(\.\d+)?",输出结果是"result is 1.5",则测试经过。要想详细地了解cmake中的测试有哪些属性,能够阅读Properties on Tests

项目构建完成后,使用cd mybuild跳转到构建目录下,输入ctest -N来查看将要运行的测试,但并不实际运行它们,如图:

接着运行ctest -VV运行测试,并输出详细测试信息,如图:

9. 添加系统自检

如今,让咱们向mymath::sqrt函数中添加一些代码,这些代码所依赖的功能可能在某些目标平台上不支持,因此咱们须要检查。咱们想要添加的代码是经过下面这个数学公式来计算平方根,须要用到log对数函数和exp指数函数,若是目标平台不支持这2个函数,则仍是使用以前的方法计算:

\sqrt{x} = e^{ \ln{ \sqrt{x} } } = e^{ 0.5 \times \ln{x} }

如今让咱们在mymath/CMakeLists.txt中加入以下内容:

include(CheckCXXSymbolExists)
check_cxx_symbol_exists(log "cmath" HAVE_LOG)
check_cxx_symbol_exists(exp "cmath" HAVE_EXP)

if(HAVE_LOG AND HAVE_EXP)
    target_compile_definitions(mymath
            PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()
复制代码

解释:

cmake中有不少可选功能模块,咱们能够经过include命令来在项目中启用指定模块。

check_symbol_exists(<symbol> <files> <variable>)
复制代码

启用了CheckCXXSymbolExists模块后,此命令才会生效,做用是检查<symbol>符号是否在指定的files C++头文件中可用,符号能够被定义为宏、变量或者函数名,若是是Class、Struct、enum或基本类型名,则没法被识别;检查的结果值放在<variable>中。

target_compile_definitions命令的做用是向目标添加编译定义,即gcc中的-D选项:

g++ -D HAVE_LOG -D HAVE_EXP -o mymath.o -c mymath.cpp
复制代码

至关于在mymath.cpp中手动定义了2个宏:

#define HAVE_LOG
#define HAVE_EXP
复制代码

归纳一下,新添加内容的做用是:检查cmath头文件中logexp函数是否存在定义而且可用,若是都存在而且可用,则向mymath中添加HAVE_LOGHAVE_EXP这2个编译定义。

也可使用configure_file()的方式,不过要更麻烦些。

接下来让咱们对mymath::sqrt函数稍做修改,以下:

#include "mymath.h"

# if defined(HAVE_LOG) && defined(HAVE_EXP)
#include <cmath>
#endif

namespace mymath
{
    double sqrt(double number) {
        static constexpr double precision = 1e-6;
        static constexpr auto abs = [](double n) ->double { return n > 0? n:-n; };

        if(number < 0)
        {
            throw std::invalid_argument("Cannot calculate the square root of a negative number!");
        }

# if defined(HAVE_LOG) && defined(HAVE_EXP)
        return std::exp(0.5 * std::log(number) );
#endif

        double guess = number;
        while( abs(guess * guess - number) > precision)
        {
            guess = ( guess + number / guess ) / 2;
        }

        return guess;
    }
}
复制代码

CLion中从新加载CMakeLists.txt,不出意外被条件编译包含的那2行如今应该会处于高亮状态,也就是说log函数和exp函数在当前平台中可用;从新构建运行一下,观察结果是否正常。

10. 添加自定义命令和生成文件

假设出于本教程的目的,咱们决定再也不使用log函数和exp函数,而是但愿生成一个可在mymath::sqrt函数中使用的预计算值表。在本节中,咱们将在构建过程当中建立表,而后将其编译到mymath库中。

首先,让咱们从mymath/CMakeLists.txtmymath/mysqrt.cpp中删除上一节新增的全部内容,而后在mymath目录下新增一个makeSqrtTable.cpp源文件,用于生成预计算值表的头文件,内容以下:

#include <iostream>
#include <fstream>
#include <cmath>

int main(int argc, char* argv[]) {
    // argv[1] : output header file path
    // argv[2] (optional) : max value of precomputed square root

    if(argc < 2)
    {
        std::cerr << "Must have at least 2 command line arguments." << std::endl;
        return 1;
    }

    int maxPrecomputedSqrtValue = 100;
    if(argc >= 3)
    {
        try
        {
            maxPrecomputedSqrtValue = std::stoi(argv[2]);
        }
        catch(const std::invalid_argument& e)
        {
            std::cerr << e.what() << std::endl;
            return 1;
        }
    }

    std::ofstream ofstrm(argv[1], std::ios_base::out | std::ios_base::trunc);
    if(!ofstrm.is_open())
    {
        std::cerr << "Can not open " << argv[1] << " to write!" << std::endl;
        return 1;
    }

    ofstrm << "namespace mymath\n{\n\tstatic constexpr int maxPrecomputedSqrtValue = " << maxPrecomputedSqrtValue << ";\n";
    ofstrm << "\tstatic constexpr double sqrtTable[] =\n\t{\n";

    for(int i = 0; ; i++)
    {
        double precomputedSqrtValue = std::sqrt(i);
        ofstrm << "\t\t" << precomputedSqrtValue;
        if(i == maxPrecomputedSqrtValue)
        {
            ofstrm << "\n\t};\n}";
            break;
        }
        else
        {
            ofstrm << ",\n";
        }
    }

    ofstrm.close();

    return 0;
}
复制代码

接着在mymath/CMakeLists.txt中新增以下内容:

add_executable(MakeSqrtTable makeSqrtTable.cpp)

add_custom_command(
        OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/sqrtTable.h
        COMMAND MakeSqrtTable ${CMAKE_CURRENT_SOURCE_DIR}/sqrtTable.h 1000
        DEPENDS MakeSqrtTable
)

add_library(mymath STATIC mymath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/sqrtTable.h )
复制代码

解释:

第一行不用多说了,后面的add_custom_command命令的做用是将自定义构建规则添加到构建系统的生成中,有多种用法,在本例中它的做用是定义用于生成指定输出文件的命令。OUTPUT output1 [...]声明了有哪些输出文件;COMMAND commands [args ...]声明了在构建时所要执行的命令,在本例中咱们调用MakeSqrtTable并传递了2个参数,一个是输出文件路径,另外一个是最大预计算平方根数值,因为MakeSqrtTable是一个构建目标,因此会自动建立一个目标级别的依赖项,以确保在调用此命令以前先构建目标;DEPENDS [depend ...]声明了执行此命令所依赖的文件,当依赖是构建目标时,它也会建立一个目标级别的依赖项,除此以外,若是构建目标是可执行文件或库,它还会建立一个文件级别的依赖项,以在从新编译此构建目标时从新运行自定义命令。

有人可能会有疑问,既然已经在COMMAND中将输出文件路径做为参数传过去了,那么OUTPUT output1 [...]选项的做用是什么?事实上,自定义命令是在构建目标在构建过程当中被调用的,是哪一个构建目标?是指在同一个CMakeLists.txt中将add_custom_command中的OUTPUT选项中声明的任意输出文件指定为源文件的构建目标,在本例中就是mymath库,若是没有任何构建目标用到输出文件,则此自定义命令也不会被调用,虽然头文件是在cpp中被包含使用的,不须要在添加构建目标命令中显示指明,可是为了使自定义命令被调用,这种状况下就须要显示指明了;同时,不要在1个以上可能并行构建的独立目标中列出输出文件,不然产生的输出实例可能会有冲突。

最后,修改mymath::sqrt函数为以下内容:

#include "mymath.h"
#include "sqrtTable.h"
#include <iostream>

namespace mymath
{
    double sqrt(double number) {
        static constexpr double precision = 1e-6;
        static constexpr auto abs = [](double n) ->double { return n > 0? n:-n; };

        if(number < 0)
        {
            throw std::invalid_argument("Cannot calculate the square root of a negative number!");
        }

        int integerPart = static_cast<int>(number);
        if(integerPart <= maxPrecomputedSqrtValue && abs(number - integerPart) <= precision)
        {
            std::cout << "use precomputed square root : " << integerPart << std::endl;
            return sqrtTable[integerPart];
        }

        double guess = number;
        while( abs(guess * guess - number) > precision)
        {
            guess = ( guess + number / guess ) / 2;
        }

        return guess;
    }
}
复制代码

从新构建并运行,检查mymath::sqrt是否使用了预计算表,结果示例以下图:

11. 打包项目

这是整个教程的最后一步,咱们要作的是使用cpack工具打包项目,打包有2种形式:源代码包二进制安装包

源代码包是指将软件某个版本的源代码打包,这样发布出去后,下载的用户就能够根据本身的需求进行配置、构建和安装。软件包能够是多种形式:tar.gz.zip.7z等。

二进制安装包是指做者预先将某个版本的软件构建好,并将安装文件(由install()命令所指定的)打包成一个软件包供用户安装。软件包能够是多种形式:简单的tar.gz压缩包形式、.shshell脚本形式、debian系下的.deb安装包形式、Windows系统下的安装包形式等等。

如今咱们来简单说一下在项目中使用cpack打包的工做流程:

  • 对于每种安装程序或软件包格式,cpack都有一个特定的后端处理程序,称为“生成器”,它负责生成所需的安装包并调用特定的程序包建立工具。

  • 咱们能够在CMakeLists.txt中设置相关cmake变量的值来控制所生成软件包的各类属性,也就是所谓的“定制化”。全部形式软件包的都有一些公共的属性,好比CPACK_PACKAGE_NAME软件包名、CPACK_PACKAGE_VERSION软件包版本等等,固然每一个软件包也有它们独有的一些属性能够设置。设置完相关属性后,最后包含cpack模块:

    include(CPack)
    复制代码
  • 在生成项目构建系统的过程当中,cmake会根据咱们上述设置的一些属性,在构建目录下生成2个配置文件:CPackConfig.cmakeCPackSourceConfig.cmake,一个用于控制二进制安装包的生成,一个用于控制源代码包的生成。

  • 项目构建系统生成后,就能够在构建目录下使用cpack命令行工具生成源代码包;项目构建完成后,就能够生成二进制安装包。默认是生成二进制安装包,若是要生成源代码包,则须要指定,如:

    cpack --config CPackSourceConfig.cmake -G tar.gz
    复制代码

cpack还有许多其余选项能够设置,具体请参考cpack options

虽然本项目只是一个教程,但既然是要生成软件包供用户使用,仍是添加一个软件许可证说明文件会显得更正规哈,在项目根目录下新建一个LICENSE文件,内容以下:

Copyright (c) 2019: Siwei Zhu

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
复制代码

设置软件包的各类属性可能会占据不少行,这会使CMakeLists.txt文件中的内容增长不少,若是将这些内容单独放到一个文件中则会更加便于项目配置管理。事实上,cmake代码除了放在CMakeLists.txt中,还能够放在以.cmake做为扩展名的文件中。include()命令能够包含cmake模块或者.cmake文件,跟c++中的#include相似,其实就是将其余文件中的cmake代码包含进来,每一个cmake模块都对应着一个<module_name>.cmake文件,因此包含模块与包含.cmake文件的本质实际上是同样的。接下来让咱们在项目根目录下建立一个ProjectCPack.cmake文件,用于配置项目的安装及打包,而后在根CMakeLists.txt文件中删掉install(xxx)的那几行,并替换为include(ProjectCPack.cmake)。最后,ProjectCPack.cmake的内容以下:

# 安装内容
install(TARGETS CMakeLearnDemo DESTINATION bin)
install(FILES autoGeneratedHeaders/projectConfig.h DESTINATION include)
install(FILES LICENSE DESTINATION .)

# 设置包的名称
set(CPACK_PACKAGE_NAME "${PROJECT_NAME}")
# 设置包的提供商
set(CPACK_PACKAGE_VENDOR "siwei Zhu")
# 设置包描述信息
set(CPACK_PACKAGE_DESCRIPTION "a simple cmake learn demo.")
# 设置LICENSE许可证
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
# 设置包版本信息
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
# 设置要生成的包文件的名称,不包括扩展名
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CPACK_SYSTEM_NAME}")
# 设置源代码包忽略文件,相似gitignore
set(CPACK_SOURCE_IGNORE_FILES "${PROJECT_BINARY_DIR};/cmake-build-debug/;/.git/;.gitignore")
# 设置源代码包生成器列表
set(CPACK_SOURCE_GENERATOR "ZIP;TGZ")
# 设置二进制包生成器列表
set(CPACK_GENERATOR "ZIP;TGZ")
# 设置包安装前缀目录
set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/${PROJECT_NAME}")

if(UNIX AND CMAKE_SYSTEM_NAME MATCHES "Linux")
    # 添加deb安装包的生成器
    list(APPEND CPACK_GENERATOR "DEB")
    # 包维护者
    set(CPACK_DEBIAN_PACKAGE_MAINTAINER "siwei Zhu")
    # 包分类,devel是指开发工具类软件
    set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
    # 包依赖
    set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6")
elseif(WIN32 OR MINGW)
    # 添加Windows NSIS安装包的生成器
    list(APPEND CPACK_GENERATOR "NSIS")
    # 设置包安装目录
    set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}")
    # NSIS安装程序提供给最终用户的默认安装目录位于此根目录下。
    # 呈现给最终用户的完整目录是:${CPACK_NSIS_INSTALL_ROOT}/${CPACK_PACKAGE_INSTALL_DIRECTORY}
    set(CPACK_NSIS_INSTALL_ROOT "C:\\Program Files\\")
    # 设置有关安装过程的问题和意见的联系信息
    set(CPACK_NSIS_CONTACT "siwei Zhu")
    # 首先询问卸载之前的版本。若是设置为ON,
    # 则安装程序将查找之前安装的版本,若是找到,则在继续安装以前询问用户是否要卸载它。
    set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
endif()

include(CPack)
复制代码

在上述代码中,咱们先设置了要安装的文件,软件包的若干属性,最后包含CPack模块。其中,源代码包和二进制包都须要生成.zip包和.tar.gz包,若是当前系统是Linux,还生成.deb包,若是是Windows系统或者编译器是MINGW,还生成NSIS安装包(Windows下的一款安装包制做工具)。

从新生成项目构建系统并构建,完成后,跳转到mybuild目录下,运行以下2条命令:

cpack
cpack --config CPackSourceConfig.cmake
复制代码

来生成源代码包和二进制包(若是没有使用-G选项,则生成全部由CPACK_GENERATORCPACK_SOURCE_GENERATOR变量所指定的软件包类型)。

打开mybuild目录,不出意外应该已经生成了若干软件包,如图:

咱们双击CMakeLearnDemo-1.0.0-Source.tar.gz文件,使用归档管理器查看该源代码包的目录结构,以下:

咱们双击CMakeLearnDemo-1.0.0-.deb来安装咱们的二进制软件包,如图:

安装完成后,来到/opt目录,能够看到已经有了安装文件:

打开终端,输入:

sudo dpkg -l | grep cmakelearndemo
复制代码

结果如图:

能够看到已经有了此软件包的安装记录,最后输入:

sudo apt remove cmakelearndemo
复制代码

来卸载此软件包,如图:

CMake学习心得及资源分享

最后,来讲一下学习CMake的一些方法及资源分享:

首先,上述给出的入门教程应该已经囊括大部分常见的cmake命令及方法了,官方的cmake tutorial总共有十几个steps,本文章只包括了前7个,缘由是cmake的官方文档写得不怎么样,倒不是说内容不详细,主要是缺少使用案例,并且有些地方不合逻辑,特别是这个cmake tutorial,刚开始的几个steps看起来比较顺畅,一鼓作气的感受,越看到后面越让人头大,有些地方莫名其妙就新增了一个文件,结果它一句也不提,文件内容也不给,真是使人窒息;还有一个缘由是后面的steps的一些功能用的比较少,也不适合放在入门教程里,有空的话我能够单独拿出来写一篇文章。

第二,说一下官方文档的一些使用方法,文档首页是一些topics,你能够针对性的去看,好比说我要看一下哪些cmake变量能够在项目配置时用到,那就选择cmake-variables,如图:

更加细化一点,若是想搜要索特定的某个命令或者变量的详细用法和做用,能够在左边的搜索框里直接输入你想要搜索的内容,如图:

最后,是我偶然发现的,CMake Cookbook这本书的民间中文翻译版本gitbook形式的,直接在线阅读,很是方便。讲解cmake的书自己就很稀少,更不用说中文的了,好好珍惜吧。

入门教程项目源代码仓库

码云 - CMakeLearnDemo

相关文章
相关标签/搜索