在軟件開發中,構建系統(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
的書自己就很稀少,更不用說中文的了,好好珍惜吧。