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

相關文章
相關標籤/搜索