文章目錄
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的小技能