cmake 腳本編程簡介

前言

學習,其實就是用已有的知識去理解未知的過程,如果能找到已有知識和未知之間的相似之處,那麼學習將事半功倍。接下來,我將嘗試用找尋已經學會的編程語言和cmake之間的相似點。
以前,我只能被動的去記憶cmake的一條條命令,把一cmake看作是一個工具,我要去用一條條命令去指揮cmake去工作,可我最後發現,根本記不住。後來我調整了想法,cmake應該被看做一個編譯器,cmake那一條條命令其實是一種新語言的語法,我編寫的一條條命令最終會被編譯成另外一個腳本。那麼,讓我們開啓愉快的編程之旅。
聲明一下,我沒有去了解過cmake內部的工作原理,好奇心得有個限度,先專注於目前需要解決的事情。這裏不管它最終不是以編譯的方式進行,在這裏都認爲它是一個編譯器,cmake的命令就是語法。

預備知識

cmake是一個用於管理源碼編譯的工具,雖然常見的使用場景是用於生成Makefile便於使用make構建工程,但其實它也可以用於其他構建系統以及IDE,例如生成Visual Studio等的工程文件。
爲了能方便例子的講解,這裏先給出一些編寫例子需要的預備知識。

  1. cmake程序必須有的兩行,聲明最低的版本要求和確定工程名字,並且需要放在文件的開頭:
cmake_minimum_required(VERSION 2.8)
project(Test)

其中VERSION後面的值可以換成已有的任何cmake版本值,形式是主版本號.次版本號,例如我的機子上的cmake版本是cmake version 3.5.1,那麼我可以改成cmake_minimum_required(VERSION 3.5)Test也可以改成任何名字。
2. 註釋使用井號#,可以獨立一行或者和代碼共用同一行。
3. 代碼塊類似於Python,使用縮進表示。
4. 假設讀者有一定的shell編程知識,因爲很多語法和shell語法類似。
5. 關鍵字都以英文輸入狀態的括號()結尾,表達式爲於括號內。

語法語義

使用cmake,其實也就是在編寫腳本。既然是腳本,萬變不離其宗,編程上的一套東西其實是通用的,只要你瞭解它的語法,就可以開始編程。
篇(wo)幅(bi)有(jiao)限(lan),這裏只介紹能跑起一個簡單的程序的語法而不是cmake的所有語法,畢竟有些語法使用頻率也很低。編程的目的是按照一定的算法對數據進行處理,最終得到我們想要的結果。對語法的介紹將從下面幾個方面進行介紹:

  1. 變量定義;
  2. 數據操作;
  3. 程序結構;
  4. 代碼複用;
  5. 輸入輸出。

變量定義

cmake中用於定語變量的語法有兩種方式:set()option()

  1. set用於定義數值型變量,理論上它定義的變量都是字符串,但是有些特殊的變量如果你願意也可把他看成數值類型,例如set(var 10),你可以把它看成數值10也可看成字符串"10"。其原型爲set(<variable> <value>... [PARENT_SCOPE])。例如set(VAR helloword)就定義了一個值是helloworld的變量VAR。這裏字符串加不加雙引號都行,但是有細小的區別。一般情況下使用它對源文件進行歸類,所以一般不加引號。
  2. option可以用於定語布爾變量,其作用是可以給用戶提供編譯的選項。其原型爲option(<variable> "<help_text>" [value])。例如option(TRUE "boolean value true" ON)定義一個叫TRUE`的布爾變量,它的值爲真。

數據操作

定義數據後,我們可以對數據進行操作,主要有兩種:

  1. 數學運算;
  2. 字符串操作;
  3. 數組操作。

先說第一種,數學運算。其原型爲math(EXPR <variable> "<expression>" [OUTPUT_FORMAT <format>]),需要注意的是,它的表達式需要的是帶雙引號的字符串表示,例如"10 + 2 * 3",支持的數學運算有+, -, *, /, %, |, &, ^, ~, <<, >>,這些符號的含義和他們在C++中的數學運算含義一樣。例如math(EXPR mul "10 * 20")
接下來說說字符串操作。字符串操作也基本和主流編程語言相似,支持的操作有查找、替換、小寫轉大寫、大寫轉小寫、拼接以及去頭去尾等,甚至還支持正則表達式。其原型有多個,這裏只列舉幾個,詳細的列表請看文末參考文檔:

  string(TOLOWER <string> <out-var>)
  string(TOUPPER <string> <out-var>)
  string(LENGTH <string> <out-var>)

例如string(TOLOWER "helloworld" var),則var的值爲HELLOWORLD
而數組操作,則是通過list()語句實現的。list()語句也包括對數組增刪改查等基本操作。詳細信息參閱參考文檔。

基本結構

學計算機的都知道,任何算法,不論多麼簡單或者複雜,都可以由順序結構、選擇結構和循環結構這三種基本結構組合而成。因此,每一種語言都必須提供這三種操作的語法。

  1. 順序結構:這個沒什麼好說的,cmake會從文件開始,順序執行程序,以值到文件結束。如果在執行的過程中遇到include()包含的其他文件或者函數調用,那麼就進入該文件或者函數內部去繼續執行,執行完畢或者遇到return語句在返回到原來的地方繼續執行;
  2. 選擇結構:
if(<condition>)
  <commands>
elseif(<condition>) # 可以沒有
  <commands>
else()              # 可以沒有
  <commands>
endif()

與一般語言不同的是,else()也可以有條件判斷語句,例如else(3 EQUAL 3)。可以用if()語句的一元操作符有EXISTS, COMMAND, DEFINED,二元操作符有EQUAL, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL, STREQUAL, STRLESS, STRLESS_EQUAL, STRGREATER, STRGREATER_EQUAL, VERSION_EQUAL, VERSION_LESS, VERSION_LESS_EQUAL, VERSION_GREATER, VERSION_GREATER_EQUAL, MATCHES。布爾操作符有NOT, AND, OR。舉個例子:

cmake_minimum_required(VERSION 3.5)
project(Test)

option(BOOLEAN_FALSE "boolean false" OFF)
if(TRUE AND BOOLEAN_FALSE)
    message(STATUS "Hello World!")
endif()
if(UNIX)
    message(STATUS "UNIX system")
else()
    message(STATUS "Other system")
endif()

則輸出爲:

-- UNIX system
  1. 循環結構:循環結構有兩種方式:
while(<condition>)
  <commands>
endwhile()

foreach(<loop_var> <items>)
  <commands>
endforeach()

用於if()的條件語句中的操作符,也一樣適用於while()語句。此外,break(), continue()使得你可以終止當前的控制流程。你一定好奇怎麼定義個列表,這裏舉個栗子:

set(LIST one two three four five)

foreach(item ${LIST})
    message(STATUS ${item})
endforeach()

輸出爲:

-- one
-- two
-- three
-- four
-- five

代碼複用

編程,除了Hello World這一類非常簡單的栗子,肯定會有重複性的工作,因此,編程語言需要提供代碼複用的方法。爲了實現代碼複用,cmake提供了三種代碼複用的方法,分別是1)函數,2)宏,3)包含其他cmake文件,函數和文件包含都可以通過return()語句將控制權返回給調用者。

函數

在cmake中定義函數和調用的方法爲,函數調用是大小寫敏感的:

# 定義
function(<name> [<arg1> ...])
  <commands>
endfunction()
# 例子
function(foo)
  <commands>
endfunction()
foo()
Foo()
fOo()

宏的定義和函數定義看起來及其相似,使用上也是大小寫不敏感。但是宏和C++裏面的宏一樣,其實做的是字符替換而不是傳遞真的值。

macro(<name> [<arg1> ...])
  <commands>
endmacro()
文件包含

使用include()去包含另外一個文件或者模塊到當前文件中。

include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>]
                      [NO_POLICY_SCOPE])

輸入輸出

這裏說的輸入輸出,講的是輸出信息給用戶,讓用戶知道程序做了什麼或者處於什麼狀態,而不是說輸出編譯工程的文件腳本,注意區別。
有於cmake的目的是生成做在平臺的編譯腳本,所以這裏只說如何輸出信息。
輸出信息使用message()語句:message([<mode>] "message text" ...)>。上面的例子中我們已經多次使用這個語句。

到此,我們所學的語法基本可以編寫一個程序了,雖然這樣的程序其實並沒有什麼用。爲了讓程序變得在實際中有用,我們需要學習另一部分——“系統調用”。

系統調用

這裏所謂“系統調用”,並不是調用OS,而是使用一些語句,結合我們上節所介紹的語法所定義、處理的一些數據,真正能夠生成對應平臺的編譯腳本。這裏,也不會對所有的命令做一一的介紹,這裏只簡單介紹一些我認爲常用的,想要獲取全部的命令,參閱參考文檔。

1. 編譯可執行文件

想要將源代碼編譯成一個可執行文件,使用add_executable()命令,例如我有一個名字叫main.cpp的源文件,則我的編譯命令爲:

cmake_minimum_required(VERSION 3.5)
project(Test)

set(SRC main.cpp)
set(CMAKE_CXX_FLAGS -std=c++11)
add_executable(make_out ${SRC})

其中CMAKE_CXX_FLAGS相當於一個內部變量,對於內部變量,我們只需要設置它的值,不需要顯試的使用它。

2. 編譯庫文件

編譯庫文件使用add_library()命令。原型如下:

add_library(<name> [STATIC | SHARED | MODULE]
            [EXCLUDE_FROM_ALL]
            [source1] [source2 ...])

3. 鏈接文件

編譯庫文件使用target_link_libraries()命令,相當於在gcc命令中通過-I指定鏈接的時候所依賴的庫文件。原型如下:

target_link_libraries(<target>
                      <PRIVATE|PUBLIC|INTERFACE> <item>...
                     [<PRIVATE|PUBLIC|INTERFACE> <item>...]...)

類似的還有link_libraries()

4. 添加編譯參數

內部變量

關於cmake還有一類值得一提的是內不變量,也可以說是於定義變量,例如上面提到的CMAKE_CXX_FLAGS。預定義變量有幾百個,一一講解是不可能的,也沒有必要。對於編程,我一直主張的先對語言有個大概瞭解,一邊使用一邊深入,想學完在使用是不可能的,因爲根本學不完。誰用誰知道,只需要有這麼個概念,等到要用的時候查參考手冊就行。

References

[1] cmake-commands
[2] cmake-variables

本文首發於個人微信公衆號TensorBoy。如果你覺得內容還不錯,歡迎分享並關注我的微信公衆號TensorBoy,掃描下方二維碼獲取更多精彩原創內容!
公衆號二維碼

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