一篇很靠譜的CMake入門教程

CMake是什麼

CMake的全稱是:Cross Platform Make,即跨平臺Make;CMake到底是什麼呢?WikiPedia的解釋是這樣的:

CMake是個一個開源跨平臺自動化建構系統,用來管理軟件建置的程序,並不依賴於某特定編譯器,並可支持多層目錄、多個應用程序與多個庫。

? 鏈接:https://zh.wikipedia.org/wiki/CMake

比較難以理解吼,使用過命令行編譯或者Unix的Makefile進行構建的同學應該大概可以類比一下,CMake就是一個寫腳本然後可以生成Makefile一類的文件的程序。

只用IDE進行過工程開發的同學可能不太能理解,但是可以注意一下:大部分IDE需要新建工程,新建工程後會多出來一些工程文件(比如Eclipse,Visual Studio……都會自己建),裏邊描述了依賴的頭文件路徑啊,依賴的庫文件的路徑啊,文件之間的依賴關係啊什麼的,這其實就是CMake會生成的東西。

所以簡單地說, CMake是用來進行構建的一個東西(隨便你叫它語言也好、程序也好,反正就是這麼一個東西),寫CMake的腳本,就可以生成Makefile,就可以用Unix的Make進行編譯了。

聽起來還挺酷的!那麼問題來了,怎麼寫這些東西呢?、

首先我們來介紹一下CMake的文件編排方式和結構。

CMake工程的文件和結構

CMake的文件大概可以分爲幾類:

  • CMakeLists.txt:描述構建信息。CMake中負責構建的文件都命名爲 CMakeLists.txt。通常來說,每個模塊處於一個單獨的文件夾中,這樣的每個文件夾都有一個專職的CMakeLists.txt負責描述它的構建需求。
  • .cmake文件:通常會放一些函數啊,或者專門負責定義一類變量什麼的,可以在CMakeLists.txt中用include命令調用這一類文件,會順序執行其中的語句。

也就是說,一個項目的工程目錄可能是這樣的:

RootDir
├─CMakeLists.txt		# 一個根目錄下的頂層CMakeLists.txt
├─cmake/				# cmake文件夾,存放一些構建需要的腳本什麼的
|  ├─utils.cmake		# 一個.cmake文件,utils表示存放了一些工具性的函數
├─build/				# build文件夾,用作存放生成構建中間+最終文件的目錄
├─server/				# 一個子模塊文件夾,比如:服務器程序,Server
│  ├─CMakeLists.txt		# 子模塊通常也要一個CMakeLists.txt
|  ├─include/
│  |   ├─a.hpp
│  |   └─b.hpp
│  └─src/
│      ├─a.cpp
│      └─b.cpp
└─client/				# 另一個子模塊,比如:客戶端程序,Client
   ├─CMakeLists.txt		# 另一個子模塊的CMakeLists.txt
   ├─include/
   |   └─c.hpp
   └─src/
       └─c.cpp

根目錄下的CMakeLists.txt是整個CMake程序的入口,而子目錄下的CMakeLists.txt會編譯出兩個可執行文件來(當然

那麼CMakeLists.txt怎麼寫呢?

寫一個頂層的CMakeLists.txt文件

頂層CMakeLists.txt文件是整個CMake構建的入口,因此通常來說會設置一些全局通用的變量啊,還要將所有的子目錄包含進來。

頂層文件比子目錄下的CMakeLists.txt多出來的(也是必不可少的)是如下的兩句話:

cmake_minimum_required(VERSION 3.12)
# cmake_minimum_required(VERSION major[.minor[.patch[.tweak]]] [FATAL_ERROR])
project(M_PROJECT	C)
# project(<PROJECT-NAME>
#        [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
#        [LANGUAGES <language-name>...])

然後我們可以使用add_subdirectory來添加我們的子目錄,比如:

add_subdirectory(./client)
add_subdirectory(./server)

這樣,CMake就會自動的讀取這兩個子目錄下的CMakeLists.txt文件,然後進行解析。

使用CMakeLists.txt描述一個構建目標

在子目錄下的每個CMakeLists.txt文件一般會用來描述一個具體的構建目標(當然,如果工程比較簡單的話,直接放在頂層的CMakeLists.txt裏也是完全OK的)。

我們來舉個簡單的栗子,比如如下的這條編譯命令:

$ g++ main.cpp lib.cpp -o demo -I../include -lpthread -DA_PREDEF_MACRO=STH

解釋一下,是將兩個cpp源文件一起編譯成一個可執行文件,叫做demo;我們還給這個編譯命令添加了一個包含目錄,是**…/include**;還有一個預定義的宏,即A_PREDEF_MACRO,值爲STH;同時還指定了一個鏈接庫,即pthread

我們把這個改編成一個CMakeLists.txt來看看。

首先,我們需要明確,我們的構建目標是一個可執行文件,叫做demo。所以,我們需要添加一個target。

這裏的**目標(target)**是一個很重要的概念,不管是CMake還是Makefile,整個構建都是圍繞着target來的。這個target可以是可執行文件,也可以是一個.a的靜態庫文件,或者.so的動態庫,都是可以的。

這個target肯定需要指定源文件,因此CMake將添加源文件的操作與添加target放在一起,如下:

add_executable(demo		main.cpp lib.cpp) 
# <=>
# g++ main.cpp lib.cpp -o demo

然後我們處理我們加入的其他的選項,比如添加包含目錄-I../include,如下:

target_include_directories(demo	PRIVATE ../include)
# <=>
# g++ ... -I../include

我們還給他添加了鏈接庫,鏈接到pthread-lpthread,如下:

target_link_libraries(demo	PRIVATE pthread)
# <=>
# g++ ... -lpthread

我們還添加了編譯期定義的宏變量-DA_PREDEF_MACRO=STH,如下:

target_compile_options(demo	PRIVATE -DA_PREDEF_MACRO=STH)
# OR
# target_compile_definitions(demo PRIVATE A_PREDEF_MACRO=STH)
# <=>
# g++ ... -DA_PREDEF_MACRO=STH

把上邊這幾句話組合到一個CMakeLists.txt文件中,就完成了對如何編譯demo這個可執行程序的完整描述。

CMake的一些基礎語法

顯示幫助信息 message()

當需要打印信息時,我們會使用這個函數,舉個栗子:

message("Hello,world")
message(STATUS "A status code") 		  # STATUS 會在打印消息前加上兩個-,比如 "-- ..."
message(AUTHOR_WARNING "A user warning")  # AUTHOR WARNING 會把這條消息按照警告的格式輸出
message(FATAL_ERROR	"An error")           # FATAL_ERROR 會發送一個error然後退出程序

其實還有很多其他的標記,去看看文檔吧:https://cmake.org/cmake/help/v3.3/command/message.html?highlight=message

變量

變量一般會用set來設置,比如

set(target	demo)

這樣,我們就定義了一個變量,叫做target,其值爲demo

那麼如何訪問這個變量的值呢?這一點上cmake和shell是非常相似的,我們可以通過${}來訪問。比如:

message(STATUS ${target}) # 打印:-- demo
# OR
message(STATUS "${target}") # 加雙引號不影響解析,還是打印:-- demo

${target}外邊加個""是沒有影響的,只是將其變成了一個真·字符串。

如果需要使用列表的話,有兩種不同的寫法,比如:

set(aList  "demo1;demo2;demo3")
# or
set(bList  demo1 demo2 demo3) # 完全等價

條件語句

就是if啦,這是一個簡單又不簡單的語句,舉個栗子:

if (A EQUAL B)
	message("A equal to B")
elseif(A EQUAL C)
	message("A equal to C")
endif()

if裏的那個判斷語句只是一個簡單的栗子,CMake還提供了好多的判斷方式,多給幾個栗子嘗一嘗:

if(IS_DIRECTORY ../include) ...
if(${aString} MATCHES regex) ...
if(${aString} IN_LIST ${a_list}) ...
...

好多好多,具體去看官網的說明吧:https://cmake.org/cmake/help/v3.3/command/if.html?highlight=#command:if

foreach循環

循環語句也好理解吼,舉個栗子:

foreach(element IN LISTS a_list)
    message("${arg}")
endforeach()

簡單的循環了一個列表,foreach還有不少其他的遍歷方式,比如:

foreach(loop_var arg1 arg2 ...) ...
foreach(loop_var RANGE total) ...
foreach(loop_var IN ITEMS item1 item2) ...

還是蠻好理解的,也就不多做解釋了

老規矩,看官網:https://cmake.org/cmake/help/v3.0/command/foreach.html?highlight=foreach

函數

函數有兩種寫法,一種是macro,另一種則是function。其實效果上是差不多的,我們都來舉幾個栗子。

macro(m_macro cpu_type linux_name)
  # 假設就按照 m_macro(x86, ubuntu, a_unknown_parameter) 這樣來調用
  message("${cpu_type}, ${linux_name}")  # x86, ubuntu
  # ARGC 保存了傳入的參數數量
  message("${ARGC}")                     # 3
  # 可以使用 ARGV#n 來訪問第n個參數
  message("${ARGV0}")                    # x86
  # ARGN 保存了我們在聲明中沒有聲明的變量,就是最後的幾個
  message("${ARGN}")                     # a_unknown_parameter
  # ARGV 保存了所有的參數列表
  message("${ARGV}")                     # x86;ubuntu;a_unknown_parameter
endmacro(m_macro)

function(m_function cpu_type linux_name)
  # 假設就按照 m_function(x86, ubuntu, a_unknown_parameter) 這樣來調用
  message("${cpu_type}, ${linux_name}")  # x86, ubuntu
  # 和macro一樣,也有那幾個參數的控制
  message("${ARGC} & ${ARGV0} & ${ARGN} & ${ARGV}")
  # 3 & x86 & a_unknown_parameter & x86;ubuntu;a_unknown_parameter
endfunction(m_function)

macro的官網說明:https://cmake.org/cmake/help/v3.0/command/macro.html

function的官網說明:https://cmake.org/cmake/help/v3.0/command/function.html

根據官網的說法,在我們調用函數時,會首先將函數裏定義的命令裏的變量替換爲我們傳入的值,然後順序執行所有的命令。

當然,函數和宏還是有一定的區別的,從官網找到的差別大概是函數會打開一個新的命名作用域, 而宏的話則是一個純粹的替換,和上下文使用的作用域是完全相同的。這也是和我們平常使用的宏和函數是可以類比的。應該還是可以理解的。

這裏有一篇文章講的非常好,可以看一哈:https://juejin.im/post/5a8ab0e4f265da4e9d223972

find_package函數

這是一個感覺需要講的東西,用來尋找依賴的庫

這個應用的場景通常是在我們需要自己指定依賴的第三方庫時,比如我們需要依賴哪個公司提供給我們的一個so文件,我們會把這個so存放在我們的一個目錄下(比如,openGL的libGL.so,在windows下叫做opengl.lib),那麼如何讓我們的CMake程序找到這個依賴的so文件呢?這時候就要用到這個命令了。

我們先來回顧一下之前是如何寫依賴庫的,我們使用的是target_link_libraries()命令。

add_executable(demo		main.cpp lib.cpp) 
target_link_libraries(demo  PRIVATE pthread)

這個pthread是系統庫,不需要進行進一步的描述,如果不是系統自帶的庫的話,理所當然的,我們需要告訴CMake這個庫所在的位置。比如使用libGL.so時,我們直接寫

target_link_libraries(demo  PRIVATE GL)

CMake十有八九是很懵逼的,什麼GL,哪裏來的GL?因此我們可以使用一個如下這樣的命令告訴CMake這個庫是什麼,在哪裏存放着。

find_libraries函數

我們之前說過了一個很重要的概念叫target,我們可以編譯出一個可執行文件或者一個庫(lib或者so什麼的)作爲target。CMake也提供了一組函數,供我們導入現有的庫作爲CMake裏的target。下面我們來舉栗子:

find_libraries(lib_gl GL ../libs) # 會去../libs目錄下尋找名爲libGL.so的文件,
                                  # 然後放在lib_gl這個變量裏
add_library(opengl SHARED IMPORTED GLOBAL
            IMPORTED_LOCATION ${lib_gl}) # 表明是導入的庫,會直接導入位置指定的文件作爲目標

重點就是上面的哪個IMPORTED,表明是導入的庫,無需編譯構建。

這樣我們就可以使用opengl這個名字作爲依賴庫了,舉個栗子嘗一嘗:

find_libraries(lib_gl GL ../libs)
add_library(opengl SHARED IMPORTED GLOBAL
            IMPORTED_LOCATION ${lib_gl})
# 然後就可以使用了
target_link_libraries(demo  PRIVATE opengl)

就可以順暢的將這個添加到我們的工程裏。

一種更通用的寫法

通用的寫法就是使用find_package命令了。

當我們使用的庫比較多的時候,比如一個產品會提供好多個so文件,那麼此時我們通常會使用一個類似命名空間的方法來控制,比如:

target_link_libraries(demo  PRIVATE opengl::GL)

即將Opengl這個大命名空間下的GL庫作爲依賴。

隨便怎麼理解啦,理解爲一個叫做**“opengl::GL”**的庫大概也是可以的

find_package(XXX ...)命令會去一些目錄裏尋找一個命名爲findXXX.cmake的腳本文件並運行它,我們通常會在這樣的一個文件裏寫我們的尋找庫文件的具體過程。

這些目錄被定義在CMAKE_MODULE_PATH裏,我們可以自己編輯這個變量,添加我們自己的目錄進去

這個文件是可以完全由我們自己定義的,裏邊寫什麼都是很隨意的了,CMake沒有對這個做什麼很嚴格的要求。但是通常來說,我們會定義以下的一些變量:

  • XXX_FOUND:表明包已經找到了,完全OK,在下文中可以正常使用;
  • XXX_LIBRARIES:一個列表,寫明瞭所有導入的庫在cmake中的名稱;
  • XXX::lib_name:每個都是一個單獨的庫文件,比如我們上面提到的opengl::GL

還有一個常用的函數是:FindPackageHandleStandardArgs,方法如下:

FIND_PACKAGE_HANDLE_STANDARD_ARGS(NAME [REQUIRED_VARS <var1>...<varN>])
# 舉個栗子
find_package_handle_standard_args(OPENGL REQUIRED_VARS lib_GL)

當然這個方法的參數還有很多,請看官網鏈接:https://cmake.org/cmake/help/v3.0/module/FindPackageHandleStandardArgs.html

這個函數做了什麼呢?其實就是驗證和lib_GL放一起的這些變量有沒有都找到,如果都找到的話,就會把OPENGL_FOUND設置爲真,否則會報錯表示缺少了一些需要的庫。

具體找包的過程和上述的find_libraries是一樣的,也就不太贅述具體的過程了,直接給個栗子嚐嚐(請詳細的看下注釋):

# findOPENGL.cmake
find_libraries(lib_gl GL ../libs)
find_package_handle_standard_args(OPENGL REQUIRED_VARS lib_GL)
add_library(OPENGL::GL SHARED IMPORTED GLOBAL
            IMPORTED_LOCATION ${lib_gl})

到了我們真正的CMakeLists.txt文件中,栗子如下:

...
find_package(OPENGL REQUIRED GLOBAL)
...
if (NOT OPENGL_FOUND)
	...
endif()
...
target_link_libraries(demo  PRIVATE opengl::GL)

實際中,我們通常會在頂層文件中把所有依賴的包都找出來,會比較好統一管理

CMake常用變量

CMake內置了很多變量,比如我們上一節提到的CMAKE_MODULE_PATH就是一個,還有一些常用的,我們直接列下來:

全局編譯選項

  • CMAKE_C_FLAGS:編譯C程序時加入的編譯器選項;
  • CMAKE_CXX_FLAGS:同理,編譯C++程序時的編譯選項;

工作目錄信息

  • CMAKE_CURRENT_SOURCE_DIR:當前CMakeLists.txt所在的目錄
  • EXECUTABLE_OUTPUT_PATH:輸出可執行文件的目錄
  • LIBRARY_OUTPUT_PATH:輸出庫文件的目錄
  • CMAKE_CURRRENT_BINARY_DIR:當前正在編譯的目標要輸出的目錄

以上的這些變量通常都可以自己設置,比如可以自己指定可執行文件輸出到哪裏,自己指定要使用哪些全局的編譯選項……

CMake版本

  • CMAKE_MAJOR_VERSION,CMAKE 主版本號,比如 2.4.6 中的 2
  • CMAKE_MINOR_VERSION,CMAKE 次版本號,比如 2.4.6 中的 4
  • CMAKE_PATCH_VERSION,CMAKE 補丁等級,比如 2.4.6 中的 6

系統信息

  • CMAKE_SYSTEM,系統名稱,比如 Linux-2.6.22
  • CMAKE_SYSTEM_NAME,不包含版本的系統名,比如 Linux
  • CMAKE_SYSTEM_VERSION,系統版本,比如 2.6.22
  • CMAKE_SYSTEM_PROCESSOR,處理器名稱,比如 i686.

CMake常用方法

message函數

最常用的當然就是我們提過的message啦,不在此處贅述了

FILE函數

file方法可以對文件系統進行很多操作,比如讀寫,新建,重命名,哈希校驗……具體的還是要看文檔

https://cmake.org/cmake/help/v3.0/command/file.html?highlight=file

有一個方法GLOB是我曾經踩過的坑,在這裏提一嘴

file(GLOB variable [RELATIVE path] [globbing expressions]...)

這裏的globbing expressions和正則很像,但是真的不是正則,可以到維基上看一下什麼叫globbing,千萬不要簡單的當作正則來用了。

INSTALL方法

這個東西一說就很多了,但是也不是很難,大家直接看官網就好了

The end but may be not the end

感覺想到的東西都已經說的差不多了,那就先到這裏吧,想起來了會繼續補充的,希望大家栗子喫的開心,也學到了一些CMake的小技能

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章