在软件开发中,构建系统(build system)是用来从源代码生成用户可使用的目标的自动化工具。目标能够包括库、可执行文件、或者生成的脚本等等。html
一般每一个构建系统都有一个对应的构建文件(也能够叫配置文件、项目文件之类的),来指导构建系统如何编译、连接生成可执行程序等等。构建文件中一般描述了要生成的目标
、生成目标所须要的源代码文件
、依赖库
等等内容。不一样构建系统使用的构建文件的名称和内容格式规范一般各不相同。ios
GNU Make
:类Unix操做系统下的构建系统,构建文件名称一般是Makefile
或makefile
。NMake
:能够理解为Windows平台下的GNU Make
,是微软Visual Studio
早期版本使用的构建系统,好比VC++6.0
。构建文件的后缀名是.mak
。MSBuild
:NMake
的替代品,一开始是与.net
框架绑定的。Visual Studio
从2013
版本开始使用它做为构建系统。Visual Studio
的项目构建依赖于MSBuild
,但MSBuild
并不依赖前者,能够独立运行。构建文件的后缀名是.vcproj
(以C++
项目为例)。Ninja
:一个专一于速度的小型构建系统,Chrome
团队开发。CMake
是一个开源的跨平台构建系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个库。虽然CMake
一样用构建文件控制构建过程(名称是CMakeLists.txt
),但它却并不直接构建并生成目标,而是产生其余构建系统所须要的构建文件,而后再由它们来构建生成最终目标。支持MSBuild
、GNU Make
、MINGW Make
等等构建系统。c++
qmake
是一个协助简化跨平台进行项目开发的构建过程的工具程序,Qt
附带的工具之一 。与CMake
相似,qmake
一样不是直接构建并生成目标,而是依赖其余构建系统。它可以自动生成Makefile
、Visual Studio
项目文件 和 XCode
项目文件。无论项目是否使用Qt
框架,都能使用qmake
,所以qmake
能用于不少软件的构建过程。git
qmake
使用的构建文件是.pro
项目文件,开发者可以自行撰写项目文件或是由qmake
自己产生。qmake
包含额外的功能来方便 Qt
开发,如自动包含moc
和uic
的编译规则。值得一提的是,CMake
一样支持Qt
开发,但并不依赖于qmake
。github
这个入门教程我主要是参考了CMake
官方的Tutorial以及网上的一些资料,并进行了一些更改和补充,使得理解起来更加容易。正则表达式
在开始以前,咱们能够选择一个本身喜欢的IDE
(集成开发环境)来做为C/C++
开发工具,CMake
支持Visual Studio
、QtCreator
、Eclipse
、CLion
等多个IDE
,固然你也可使用像是VSCode
、Vim
这些文本编辑器并配合一些插件来做为开发工具。因为我我的的习惯和偏好,加上主要是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-w64
、MSVC
或者WSL
等工具。bash
下载安装:CLion官网,能够免费试用30天。
首次运行:登陆账号进行激活受权,偏好配置。学生可使用edu
邮箱注册JetBrains账号,而后能够无偿使用JetBrains家族全部IDE
的ULTIMATE
版本。
界面汉化(可选):平方X JetBrains系列软件汉化包。我英文不太行,因此汉化仍是挺有必要的。
配置构建工具链(设置 -> 构建,执行,部署 -> 工具链):这一步是在CLion
中配置构建须要的工具的路径。CMake
能够直接使用CLion
自带绑定的一个版本,固然也能够选择本身安装的版本。
配置CMake
选项(设置 -> 构建,执行,部署 -> CMake):设置构建类型(Debug/Release),CMake
构建选项参数、构建目录等等。通常保持默认的就能够,等到须要修改CMake
构建相关选项的时候再去配置。
打开CLion
,新建一个C++可执行程序项目,C++
标准版本我选择了C++17
。其实像是标准版本、构建目标类型这些选项,在新建项目时选好了,后面仍是能够经过CMakeLists.txt
文件随时进行更改的,不用太过纠结。
建立一个项目后,初始结构是这样的:
CMakeLearnDemo
:项目源目录,包含项目源文件的顶级目录。
main.cpp
:自动生成的main
函数源文件,没什么好说的。
cmake-build-debug
:CLion
调用CMake
生成的默认构建目录。什么是构建目录呢,用于存储构建系统文件(好比makefile以及其余一些cmake相关配置文件)和构建输出文件(编译生成的中间文件、可执行程序、库)的顶级目录。由于咱们确定不想把构建生成的文件和项目源文件混在一块,这样会使项目结构变得混乱,因此通常都会单首创建一个构建目录。固然若是你喜欢,能够直接将项目源目录做为构建目录。使用CLion
咱们不须要手动在命令行调用CMake
来生成构建目录以及构建项目,CLion
在CMakeLists.txt
的内容发生改变时会自动从新生成构建目录,构建项目也只须要点击构建按钮就能够了。可是在学习阶段,了解CMake
的基本用法仍是很重要的,等到熟悉了以后,再使用IDE
也就驾轻就熟了。咱们在项目源目录下新建一个mybuild目录,做为咱们本身手动调用CMake
命令时所指定的构建目录,如图:
CMakeLists.txt
:cmake
项目配置文件,准确点说是项目顶级目录的cmake
配置文件,由于一个项目在多个目录下能够有多个CMakeLists.txt
文件。这个应该是cmake
的核心配置文件了,基本上更改项目构建配置都是围绕着这个文件进行。咱们来看看CLion
为咱们自动生成的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
, 17
和20
。
add_executable(CMakeLearnDemo main.cpp)
复制代码
添加一个可执行文件类型的构建目标到项目中。CMakeLearnDemo
是文件名,后面是生成这个可执行文件所须要的源文件列表。
首先咱们将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
函数中,主要作了如下操做:读取命令行参数值,计算它的算术平方根并输出,同时包含了一些错误判断。
以前设置的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
输出以下图:
构建类型有Debug
、Release
、RelWithDebInfo
、MinSizeRel
等,能够经过CMAKE_BUILD_TYPE
变量来指定,好比:
set(CMAKE_BUILD_TYPE Release)
复制代码
生成项目构建系统后,接下来就能够选择构建项目了。咱们能够直接调用相应的构建系统来构建项目,好比GNU make
,也能够调用cmake
来让它自动选择相对应的构建系统来构建项目。以下:
或者:
构建完成了,接下来让咱们运行可执行文件,看看运行结果:
虽然能够直接在源文件里定义版本号,可是在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
文件。
构建而后运行可执行文件,查看输出内容:
以前咱们在main
函数中是使用标准库中的std::sqrt
来计算平方根。如今,让咱们本身实现一个计算平方根的函数,并将其构建生成静态库,最后在main
函数中使用咱们本身的平方根库函数来替代标准库。
建立一个mymath
文件夹,存放咱们本身实现的平方根函数的.h
、.cpp
以及CMakeLists.txt
文件。结构以下:
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.cpp
将std::sqrt
换成咱们本身的平方根函数,改动的地方以下:
- #include <cmath>
+ #include "mymath.h"
...
int main(int argc ,char* argv[]) {
...
double outputValue = mymath::sqrt(inputValue);
...
}
复制代码
从新生成构建系统并构建运行,输出结果应该是与以前同样的;打开mybuild
目录,能够发现里面多了一个mymath
子构建目录,其目录下有生成的库文件libmymath.a
,如图:
如今让咱们将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_INCLUDES
和EXTRA_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}
)
复制代码
如今咱们的项目有了1个构建选项,而GUI图形界面能使构建选项更直白地向用户体现出来,因此咱们此次不使用命令行cmake
,而是使用cmake-gui
来配置项目并生成构建系统。
打开cmake-gui
,首先选择项目源目录和构建目录,如图:
Unix Makefiles
做为构建系统,而后肯定,如图:
肯定并等待配置完成后,出现了若干个红色项,这表示当前项目中可由用户进行配置的可选项。咱们勾选USE_MYMATH
选项,如图:
再次点击Configure
,直到没有红色选项了以后,点击Generate
来生成通过用户配置的项目构建系统,如图:
构建系统生成以后,观察projectConfig.h
和main.cpp
文件的变化,而后就能够进行项目构建了,方法跟以前同样,好比cmake --build mybuild
。
若是不想使用图形工具,也能够直接在调用命令行cmake
工具时传递变量值,好比:
cmake -B mybuild -S . -D USE_MYMATH:BOOL=ON
复制代码
所谓安装,能够简单地理解为将软件或程序所须要的若干文件复制到指定位置。那么,对于咱们的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
复制代码
完成以后,打开你以前设置的安装前缀路径,不出意外已经有了相应的文件,如图:
接下来让咱们测试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
运行测试,并输出详细测试信息,如图:
如今,让咱们向mymath::sqrt
函数中添加一些代码,这些代码所依赖的功能可能在某些目标平台上不支持,因此咱们须要检查。咱们想要添加的代码是经过下面这个数学公式来计算平方根,须要用到log
对数函数和exp
指数函数,若是目标平台不支持这2个函数,则仍是使用以前的方法计算:
如今让咱们在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
头文件中log
和exp
函数是否存在定义而且可用,若是都存在而且可用,则向mymath
中添加HAVE_LOG
和HAVE_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
函数在当前平台中可用;从新构建运行一下,观察结果是否正常。
假设出于本教程的目的,咱们决定再也不使用log
函数和exp
函数,而是但愿生成一个可在mymath::sqrt
函数中使用的预计算值表。在本节中,咱们将在构建过程当中建立表,而后将其编译到mymath
库中。
首先,让咱们从mymath/CMakeLists.txt
和mymath/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
是否使用了预计算表,结果示例以下图:
这是整个教程的最后一步,咱们要作的是使用cpack
工具打包项目,打包有2种形式:源代码包和二进制安装包:
源代码包是指将软件某个版本的源代码打包,这样发布出去后,下载的用户就能够根据本身的需求进行配置、构建和安装。软件包能够是多种形式:tar.gz
、.zip
、.7z
等。
二进制安装包是指做者预先将某个版本的软件构建好,并将安装文件(由install()
命令所指定的)打包成一个软件包供用户安装。软件包能够是多种形式:简单的tar.gz
压缩包形式、.sh
shell脚本形式、debian
系下的.deb
安装包形式、Windows
系统下的安装包形式等等。
如今咱们来简单说一下在项目中使用cpack
打包的工做流程:
对于每种安装程序或软件包格式,cpack
都有一个特定的后端处理程序,称为“生成器”,它负责生成所需的安装包并调用特定的程序包建立工具。
咱们能够在CMakeLists.txt
中设置相关cmake
变量的值来控制所生成软件包的各类属性,也就是所谓的“定制化”。全部形式软件包的都有一些公共的属性,好比CPACK_PACKAGE_NAME
软件包名、CPACK_PACKAGE_VERSION
软件包版本等等,固然每一个软件包也有它们独有的一些属性能够设置。设置完相关属性后,最后包含cpack
模块:
include(CPack)
复制代码
在生成项目构建系统的过程当中,cmake
会根据咱们上述设置的一些属性,在构建目录下生成2个配置文件:CPackConfig.cmake
和CPackSourceConfig.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_GENERATOR
或CPACK_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 tutorial
总共有十几个steps,本文章只包括了前7个,缘由是cmake
的官方文档写得不怎么样,倒不是说内容不详细,主要是缺少使用案例,并且有些地方不合逻辑,特别是这个cmake tutorial
,刚开始的几个steps看起来比较顺畅,一鼓作气的感受,越看到后面越让人头大,有些地方莫名其妙就新增了一个文件,结果它一句也不提,文件内容也不给,真是使人窒息;还有一个缘由是后面的steps的一些功能用的比较少,也不适合放在入门教程里,有空的话我能够单独拿出来写一篇文章。
第二,说一下官方文档的一些使用方法,文档首页是一些topics
,你能够针对性的去看,好比说我要看一下哪些cmake
变量能够在项目配置时用到,那就选择cmake-variables
,如图:
更加细化一点,若是想搜要索特定的某个命令或者变量的详细用法和做用,能够在左边的搜索框里直接输入你想要搜索的内容,如图:
最后,是我偶然发现的,CMake Cookbook
这本书的民间中文翻译版本,gitbook
形式的,直接在线阅读,很是方便。讲解cmake
的书自己就很稀少,更不用说中文的了,好好珍惜吧。