1 Makefile和CMake實踐
1.1 Makefile
參考
簡介:
Makefile是一種用于自動化構建和管理程序的工具。它通常用于編譯源代碼、鏈接對象文件以生成可執行文件或庫文件。Makefile以文本文件的形式存在,其中包含了一系列規則和指令,用于描述程序的依賴關系以及構建步驟,指定哪些文件需要先編譯,哪些文件需要后編譯,哪些文件需要重新編譯。核心思想是根據文件的最后修改時間來確定哪些部分需要重新編譯,以及以什么順序來執行編譯步驟。每個規則由一個目標(target)和一組依賴項(dependencies)組成,以及執行指令(commands)。
? makefile帶來的好處就是自動化編譯,一旦寫好就只需要一個make指令來實現該文件。當執行make命令時,Makefile解析并執行其中的規則。它首先檢查目標文件和依賴文件的時間戳,如果目標文件不存在或其依賴項的時間戳較新,則執行該規則所定義的指令來生成目標文件。這種方式可以避免不必要的重新編譯,提高構建效率。Makefile中常見的指令包括編譯源代碼、鏈接對象文件、復制文件、清理生成的文件等。通過定義變量和規則,可以使Makefile更加靈活和可維護。
? Makefile廣泛應用于C、C++等編程語言的項目中,但也可以用于其他類型的項目。它是一種跨平臺的工具,可以在不同的操作系統上使用,例如Linux、Unix和Windows等。
? 總而言之,Makefile是一種用于自動化構建和管理程序的工具,通過描述依賴關系和構建步驟,可以有效地管理大型項目的編譯過程,并提高開發效率。
1.1.1 安裝
sudo apt install make
sudo apt install make-guile
1.1.2 簡單Makefile
重點:
- 目標、依賴、命令
- all有什么意義
- all和test的順序問題
- 空格符號的影響
1.1.3 Makefile三要素
假如說目標all有兩個依賴test1和test2,那么先執行最底層依賴。
再例如這個:
對于simple的兩個依賴,他是從左向右執行的。
1.1.4 Makefile工作
現在,我們需要寫一個用于創建simple 可執行程序的 Makefile 了,這個 Makefile 需要如何去寫?還記得目標、依賴關系和命令嗎?
此時的目錄:Makefile/simple
foo.c
#include <stdio.h>
void foo ()
{printf ("This is foo ()!\n");
}
main.c
extern void foo();
int main ()
{foo();return 0;
}
寫一個 Makefile 文件的第一步不是一個猛子扎進去試著寫一個規則,而是先用面向依賴關系的方法想清楚,所要寫的 Makefile 需要表達什么樣的依賴關系,這一點非常的重要。通過不斷的練習,我們最終能達到很自然的運用依賴關系去思考問題。到那時,你在寫 Makefile 時,頭腦會非常的清楚自己在寫什么,以及后面要寫什么。現在拋開 Makefile,我們先看一看 simple 程序的依賴關系是什么。
我想是第一個躍入我們腦海中的依賴關系圖,其中 simple 可執行文件顯然是通過main.c 和 foo.c 最后編譯并連接生成的。通過這個依賴圖,其實我們就可以寫出一個 Makefile 來了,這個任務交給讀者你。這樣的依賴關系所寫出來的 Makefile,在現實中不是很可行,就是你得將所有的源程序都放在一行中讓 GCC 為我們編譯。如果是這樣,那我們希望什么樣的依賴關系呢?讓我們想想看下圖中缺了什么。目標文件?對了!
下圖是 simple 程序的依賴關系更為精確的表達,其中我們加入了目標文件。對于 simple 可執行程序來說, 表示的就是它的“依賴樹”。接下來需要做的是將其中的每一個依賴關系,即其中的每一個帶箭頭的虛線,用 Makefile 中的規則來表示。
有了“依賴樹”,寫 Makefile 就會相對的輕松了。下面是所對應的 Makefile。
Makefile
Makefile
all: main.o foo.o
gcc -o simple main.o foo.o
main.o: main.c
gcc -o main.o -c main.c
foo.o: foo.c
gcc -o foo.o -c foo.c
clean:
rm simple main.o foo.o
而下圖則是依賴關系與規則的映射圖。在這個 Makefile 中,我還增加了一個 clean 目標用于刪除所生成的文件,包括目標文件和 simple 可執行程序,這在現實的項目中很是常見。
編譯結果:
$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o$./simple
This is foo ()!$make clean
rm simple main.o foo.o
首先我們解釋一下這個$make的輸出結果:
gcc -c main.c -o main.o
這個是將main.c編譯成main.o
gcc -c foo.c -o foo.o
這個是將foo.c編譯成foo.o
gcc -o simple main.o foo.o
輸出到simple中,然后執行的時候就是./simple
這個如果不懂的話可以看看這個鏈接
如果我們在不改變代碼的清況下再編譯會出現什么現象呢?下面給出了結果,注意到了第二次編譯并沒有構建目標文件的動作嗎?但為什么有構建simple可執行程序的動作呢?為了明白為什么,我們需要了解 make 是如何決定哪些目標(這里是文件)是需要重新編譯的。為什么 make會知道我們并沒有改變 main.c 和 foo.c 呢?答案很簡單,通過文件的時間戳!當 make 在運行一個規則時,我們前面已經提到了目標和先決條件之間的依賴關系,make 在檢查一個規則時,采用的方法是:如果先決條件中相關的文件的時間戳大于目標的時間戳,即先決條件中的文件比目標更新,則知道有變化,那么需要運行規則當中的命令重新構建目標。這條規則會運用到所有與我們在 make時指定的目標的依賴樹中的每一個規則。比如,對于 simple 項目,其依賴樹中包括三個規則,make 會檢查所有三個規則當中的目標(文件)與先決條件(文件)之間的時間先后關系,從而來決定是否要重新創建規則中的目標。
$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o$make
gcc -o simple main.o foo.o
知道了 make 是如何工作以后,我們不難想明白,為什么前面進行第二次 make 時,還會重新構建 simple 可執行文件,因為 simple 文件不存在。我們將 Makefile 做一點小小的改動,下面代碼所示。其最后的運行結果則是這樣的。為什么還是和以前一樣呢?哦,因為 Makefile 中的第一條規則中的目標是 all,而 all 文件在我們的編譯過程中并不生成,即 make 在第二次編譯時找不到,所以又重新編譯了一遍。
Makefile
all: main.o foo.ogcc -o simple main.o foo.o
main.o: main.cgcc -o main.o -c main.c
foo.o: foo.cgcc -o foo.o -c foo.c
clean:rm simple main.o foo.o
執行
$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o$make
gcc -o simple main.o foo.o
再一次更改后的 Makefile 如圖下面代碼所示,其最終的運行結果,它的確是發現了不需要進行第二次的編譯。這正是我們所希望的!
Makefile
simple: main.o foo.ogcc -o simple main.o foo.o
main.o: main.cgcc -o main.o -c main.c
foo.o: foo.cgcc -o foo.o -c foo.c
clean:rm simple main.o foo.o
執行
$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o$make
make: 'simple' is up to date.
下面我們來驗證一下如果對 foo.c 進行改動,是不是 make 能正確的發現并從新構建所需。對于make 工具,一個文件是否改動不是看文件大小,而是其時間戳。在我的環境中只需用 touch 命令來改變文件的時間戳就行了,這相當于模擬了對文件進行了一次編輯,而不需真正對其進行編輯。圖1.28 列出了所有相關的命令操作,從最終的結果來看,make 發現了 foo.c 需要重新被編譯,而這,最終也導致了 simple 需要重新被編譯。
執行
$ls -l foo.c
-rw-r--r-- 1 Administrator None 70 Aug 14 07:38 foo.c$touch foo.c$ls -l foo.c
-rw-r--r-- 1 Administrator None 70 Aug 14 08:48 foo.c$make
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o
至此,你完全明白了什么是目標的依賴關系以及 make 選擇哪些目標需要重新編譯的工作原理。掌握如果在頭腦中勾畫(當然初學時,可以用紙畫一畫)出我們想讓 make 做的事的“依賴樹”是編寫 Makefile 最為重要和關鍵的一步。后面我們需要做的是讓 Makefile 更加的簡單但卻更加的強大。
1.4.5 編譯程序-偽對象.PHONY
- 在程序所在的目錄創建一個 clean 文件
.PHONY: main clean - 執行make clean
- 提示:make: ‘clean’ is up to date
在前面的 sample 項目中,現在假設在程序所在的目錄下面有一個 clean 文件,這個文件也可以通過 touch 命令來創建。創建以后,運行 make clean 命令,你會發現 make 總是提示 clean 文件是最新的,而不是按我們所期望的那樣進行文件刪除操作,如圖 1.29 所示。從原理上我們還是可以理解的,這是因為 make 將 clean 當作文件,且在當前目錄找到了這個文件,加上 clean 目標沒有任何先決條件,所以,當我們要求 make 為我們構建 clean 目標時,它就會認為 clean 是最新的。
執行
$ls -l clean
ls: cannot access clean: No such file or directory$touch clean$ls -l clean
-rw-r--r-- 1 Administrator None 70 Aug 13:01 clean$make clean
make: `clean' is up to date.
那對于這種情況,在現實中也難免存在所定義的目標與所存在的文件是同名的,采用 Makefile如何處理這種情況呢?Makefile 中的假目標(phony target)可以解決這個問題。假目標可以采用.PHONY 關鍵字來定義,需要注意的是其必須是大寫字母。圖 1.30 是將 clean 變為假目標后的Makefile,更改后運用 make clean 命令的結果。
Makefile
.PHONY: clean
simple: main.o foo.ogcc -o simple main.o foo.o
main.o: main.cgcc -o main.o -c main.c
foo.o: foo.cgcc -o foo.o -c foo.c
clean:rm simple main.o foo.o
執行
$make clean
rm simple main.o foo.o
正如你所看到的,采用.PHONY
關鍵字聲明一個目標后,make
并不會將其當作一個文件來處理,而只是當作一個概念上的目標。對于假目標,我們可以想像的是由于并不與文件關聯,所以每一次 make 這個假目標時,其所在的規則中的命令都會被執行。
1.5 變量
只要是從事程序開發的,對于變量都很熟悉,因為每一個編程語言都有變量的概念。為了方便使用,Makefile 中也有變量的概念,我們可以在 Makefile 中通過使用變量來使得它更簡潔、更具可維護性。下面,我們來看一看如何通過使用變量來提高 simple 項目 Makefile 的可維護性,例如:
.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)$(CC) -o $(EXE) $(OBJS)
main.o: main.c$(CC) -o main.o -c main.c
foo.o: foo.c$(CC) -o foo.o -c foo.c
clean:$(RM) $(EXE) $(OBJS)
一個變量的定義很簡單,就是一個名字(變量名)后面跟上一個等號,然后在等號的后面放這個變量所期望的值。對于變量的引用,則需要采用 ( 變量名 ) 或者 (變量名)或者 (變量名)或者{變量名}這種模式。在這個 Makefile 中,我們引入了 CC 和 RM 兩個變量,一個用于保存編譯器名,而另一個用于指示刪除文件的命令是什么。還有就是引入了 EXE 和 OBJS 兩個變量,一個用于存放可執行文件名,可另一個則用于放置所有的目標文件名。采用變量的話,當我們需要更改編譯器時,只需更改變量賦值的地方,非常方便,如果不采用變量,那我們得更改每一個使用編譯器的地方,很是麻煩。顯然,變量的引入增加了 Makefile 的可維護性。你可能會問,既然定義了一個 CC 變量,那是不是要將-o 或是-c 命令參數也定義成為一個變量呢?好主意!的確,如果我們更改了一個編譯器,那么很有可能其使用參數也得跟著改變。現在,我們不急著這么去做,為什么?因為后面我們還會對 Makefile 進行簡化,到時再改變也來得及,現在我們只是將注意力放在變量的使用上。
1.5.1 自動變量
對于每一個規則,不知你是否覺得目標和先決條件的名字會在規則的命令中多次出,每一次出現都是一種麻煩,更為麻煩的是,如果改變了目標或是依賴的名,那得在命令中全部跟著改。有沒有簡化這種更改的方法呢?這我們需要用到 Makefile 中的自動變量,它們包括:
● $@
用于表示一個規則中的目標。當我們的一個規則中有多個目標時,$@
所指的是其中任何造成命令被運行的目標。
● $^
則表示的是規則中的所有先擇條件。
● $<
表示的是規則中的第一個先決條件。
除了上面的兩個自動變量,在 Makefile 中還有其它的動變量,我們在需要的時候再提及,就simple 項目的 Makefile 而言,為了簡化它,采用這些變量就足夠了。下面是用于測試上面三個自動變量的值的 Makefile,其運行結果中可以找到。需要注意的是,在 Makefile 中$
具有特殊的意思,因此,如果想采用 echo 輸出$
,則必需用兩個連著的$
。還有就是,$@
對于 Shell 也有特殊的意思,我們需要在$$@
之前再加一個脫字符\
。如果你還有困惑,你可以通過改一改 Makefile 來驗證它。
Makefile
.PHONY: all
all: first second third@echo "\$$@ = $@"@echo "$$^ = $^"@echo "$$< = $<"
first second third:
執行
$make
$@ = all
$^ = first second third
$< = first
采用自動變量后 simple 項目的 Makefile 可以被重寫為下面所示,用了自動變量以后這個Makefile 看起來有點怪怪的,有些什么^
、@
,等等。這就對了,你所看到的 Makefile 看起來不都很奇怪嗎?我們要的就是這個“味”!
Makefile
.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)$(CC) -o $@ $^
main.o: main.c$(CC) -o $@ -c $^
foo.o: foo.c$(CC) -o $@ -c $^
clean:$(RM) $(EXE) $(OBJS)
自動變量在對它們還不熟悉時,看起來可能有那么一點吃力,但熟悉了你就會覺得其簡捷(潔),那時也會覺得它們好用。這有點像我們用慣了 Windows 操作系統,剛開始用 Linux 時很不適應,比如在 Linux 中,ls 表示的是查看目錄的文件,這名字就是有那么一點怪。但當我們對于 Linux 熟悉了,你會發現在 Linux 平臺上工作,非常的自由和方便,遠比 Windows 上靈活。從這個問題來看,設計是有一個平衡點的,Windows 容易上手,但它把用戶當作“傻子”,對于高級用戶來說卻不方便;而 Linux 則更難上手,但一旦上手后,功能卻更強大。
至此,你有沒有覺得我們的 Makefile 更加的酷了呢?當然,我們寫 Makefile 的目的不是為了讓
其酷得不能理解,相反是為了更簡單和易于維護,這里的酷是指其看起來更專業。
1.5.2 依賴第三方庫
如果想更深入了解可以看看這個
2 cmake
CMake是一個開源、跨平臺的工具系列,是用來構建、測試和打包軟件。
CMake使用平臺無關的配置文件來控制軟件編譯過程,并生成可在您選擇的編譯器環境中使用項目文件,比如可以生成vs項目文件或者makefile。CMake工具套件由Kitware公司創建,以滿足ITK和VTK等開源項目對跨平臺構建環境的需求。Kitware是一家從事醫療計算,高性能的可視化和計算,數據和分析,計算機視覺的公司。該公司成立于1998年。
2.1 CMake介紹
假如我們有一個深度學習框架的部分工程列表,里面有超過40個互相調用的工程共同組成,一些用于生成庫文件,一些用于實現邏輯功能。他們之間的調用關系復雜而嚴格,如果我想在這樣復雜的框架下進行二次開發,顯然只擁有它的源碼是遠遠不夠的,還需要清楚的明白這幾十個項目之間的復雜關系,在沒有原作者的幫助下進行這項工作幾乎是不可能的。
??即使是原作者給出了相關的結構文檔,對新手來說建立工程的過程依舊是漫長而艱辛的,因此CMake的作用就凸顯出來了。原作者只需要生成一份CMakeLists.txt文檔,框架的使用者們只需要在下載源碼的同時下載作者提供的CMakeLists.txt,就可以利用CMake,在”原作者的幫助下“進行工程的搭建。
??打個更通俗易懂的比喻,小利在路邊撿瓶蓋賺了500萬準備買房,但是小利這一麻袋的5毛、一塊、十塊、五十、一百售樓處的小姐姐嫌麻煩不想收這些錢,那怎么辦呢?小姐姐建議小利把錢拿到銀行去換成一張銀行卡,然后直接來刷卡就行啦!CMake這里就扮演銀行的角色,幫你去整理你的資產。
2.1.1 CMake主要功能
配置和生成各大平臺的工程(VS的vcxproj,Qt的Pro):
??比如設置輸出目錄,設置編譯對象的debug后綴,設置源碼在工程中的那個文件夾(Filter),配置需要依賴的第三方的頭文件目錄,庫目錄等等屬性。
生成makefile文件
??計算機編譯源文件的時候是一條指令一條指令的發送給編譯器執行的,這樣效率很低下,所以就產生了一種文件,把所有的命令寫到一個文件中,這個文件就是makefile。CMake生成了這個makefile之后,各大平臺的編譯器都會拿到這個makefile然后解析它。將他的命令解析出來一條一條執行。
2.1.2 什么是makefile?
或許很多Winodws的程序員都不知道這個東西,因為那些Windows的IDE都為你做了這個工作,但要作一個好的和professional的程序員,makefile還是要懂。這就好像現在有這么多的HTML的編輯器,但如果你想成為一個專業人士,你還是要了解HTML的標識的含義。
特別在Unix下的軟件編譯,你就不能不自己寫makefile了,會不會寫makefile,從一個側面說明了一個人是否具備完成大型工程的能力。因為,makefile關系到了整個工程的編譯規則。一個工程中的源文件不計數,其按類型、功能、模塊分別放在若干個目錄中,makefile定義了一系列的規則來指定,哪些文件需要先編譯,哪些文件需要后編譯,哪些文件需要重新編譯,甚至于進行更復雜的功能操作,因為makefile就像一個Shell腳本一樣,其中也可以執行操作系統的命令。makefile帶來的好處就是——“自動化編譯”,一旦寫好,只需要一個make命令,整個工程完全自動編譯,極大的提高了軟件開發的效率。
使用 GCC 的命令行進行程序編譯在單個文件下是比較方便的,當工程中的文件逐漸增多,甚至變得十分龐大的時候,使用 GCC 命令編譯就會變得力不從心。這種情況下我們需要借助項目構造工具 make 幫助我們完成這個艱巨的任務。 make 是一個命令工具,是一個解釋 makefile 中指令的命令工具,一般來說,大多數的 IDE 都有這個命令,比如:Visual C++ 的 nmake,QtCreator 的 qmake 等。
make 工具在構造項目的時候需要加載一個叫做 makefile 的文件,makefile 關系到了整個工程的編譯規則。一個工程中的源文件不計數,其按類型、功能、模塊分別放在若干個目錄中,makefile 定義了一系列的規則來指定哪些文件需要先編譯,哪些文件需要后編譯,哪些文件需要重新編譯,甚至于進行更復雜的功能操作,因為 makefile 就像一個 Shell 腳本一樣,其中也可以執行操作系統的命令。
makefile 帶來的好處就是 ——“自動化編譯”,一旦寫好,只需要一個 make 命令,整個工程完全自動編譯,極大的提高了軟件開發的效率。
makefile 文件有兩種命名方式 makefile 和 Makefile,(注意:makefile文件就叫這個名字,是前面這兩種文件名,并且沒有擴展名或者后綴),構建項目的時候在哪個目錄下執行構建命令 make 這個目錄下的 makefile 文件就會別加載,因此在一個項目中可以有多個 makefile 文件,分別位于不同的項目目錄中。
2.1.3 為什么要用makefile?
對于一個大型軟件,其編譯、維護是一個復雜而耗時的過程。它涉及到大量的文件、目錄,這些文件可能是在不同的時間、由不同的人、在不同的地方分別寫的,其中一些是程序,有些是數據,有些是文檔,有些是衍生文件。
甚至參與開發的人員也不一定清楚所有文件的細節,包括如何處理它們。此外,構成軟件的文件數目可能達到成百上千,甚至成千上萬個,開發過程中當修改了少量幾個文件后,往往只需要重新編譯、生成少數幾個文件。有效地描述這些文件之間的依賴關系以及處理命令,當個別文件改動后僅執行必要的處理,而不必重復整個編譯過程,可以大大提高軟件開發的效率。
2.1.4 CMakeLists.txt介紹
CMakelists.txt是Cmake的配置文件,用于描述編譯方式和項目依賴。CMakeLists.txt文件是CMake編譯系統編譯軟件包過程的輸入文件,任何CMake兼容包都包含一個或多個CMakeLists.txt文件,這些文件描述了如何編譯代碼以及將其安裝到哪里。在windows下CMake 會讀取 CMakeLists.txt 中的設置,并生成項目的 makefile 或 Visual Studio 工程文件。
makefile文件的編寫實在是個繁瑣的事,于是,CMake出現了,使得這一切變得簡單,CMake通過CMakeLists.txt讀入所有源文件自動生成makefile,進而將源文件編譯成可執行文件或庫文件。
2.2 初試 cmake – cmake 的 helloworld
本節選擇了一個最簡單的例子Helloworld 來演練一下cmake 的完整構建過程,本節并不會深入的探討cmake,僅僅展示一個簡單的例子,并加以粗略的解釋。
2.2.1 準備工作
首先,建立一個cmake 目錄,用來放置我們學習過程中的所有練習。 mkdir -p cmake
以后我們所有的cmake
練習都會放在cmake
的子目錄下(你也可以自行安排目錄,這個并不是限制,僅僅是為了敘述的方便)
然后在cmake
建立第一個練習目錄t1
cd cmake
mkdir t1
cd t1
在t1(hello-world)
目錄建立main.c
和CMakeLists.txt
(注意文件名大小寫):
main.c 文件內容:
//main.c
#include <stdio.h>
int main()
{printf("Hello World from t1 Main!\n"); return 0;
}
CmakeLists.txt 文件內容:
PROJECT (HELLO)
SET(SRC_LIST main.c)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})
ADD_EXECUTABLE(hello2 ${SRC_LIST})
2.2.2 開始構建
所有的文件創建完成后,t1
目錄中應該存在main.c
和 CMakeLists.txt
兩個文件接下來我們來構建這個工程,在這個目錄運行:
cmake .
(注意命令后面的點號,代表本目錄)。
輸出大概是這個樣子:
再讓我們看一下目錄中的內容, 你會發現,系統自動生成了:
CMakeFiles, CMakeCache.txt, cmake_install.cmake
等文件,并且生成了Makefile
.
現在不需要理會這些文件的作用,以后你也可以不去理會。最關鍵的是,它自動生成了Makefile.
然后進行工程的實際構建,在這個目錄輸入make
命令,大概會得到如下的彩色輸出:
Scanning dependencies of target hello
[100%] Building C object CMakeFiles/hello.dir/main.o Linking C executable hello
[100%] Built target hello
如果你需要看到make
構建的詳細過程,可以使用make VERBOSE=1
或者VERBOSE=1 make
命令來進行構建。
這時候,我們需要的目標文件hello 已經構建完成,
位于當前目錄,嘗試運行一下:
./hello
得到輸出:
Hello World from Main
恭喜您,到這里為止您已經完全掌握了 cmake 的使用方法。
然后就是怎么清理工程
make clean
即可對構建結果進行清理。
2.3 簡單的解釋
我們來重新看一下CMakeLists.txt
,這個文件是cmake
的構建定義文件,文件名是大小寫相關的,如果工程存在多個目錄,需要確保每個要管理的目錄都存在一個CMakeLists.txt
。(關于多目錄構建,后面我們會提到,這里不作過多解釋)。
上面例子中的CMakeLists.txt
文件內容如下:
PROJECT (HELLO)
SET(SRC_LIST main.c)
MESSAGE(STATUS "This is BINARY dir " ${HELLO_BINARY_DIR})
MESSAGE(STATUS "This is SOURCE dir "${HELLO_SOURCE_DIR})
ADD_EXECUTABLE(hello ${SRC_LIST})
2.3.1 PROJECT
PROJECT 指令的語法是:
PROJECT(projectname [CXX] [C] [Java])
你可以用這個指令定義工程名稱,并可指定工程支持的語言,支持的語言列表是可以忽略的,默認情況表示支持所有語言。這個指令隱式的定義了兩個cmake
變量: <projectname>_BINARY_DIR
以及<projectname>_SOURCE_DIR
,這里就是HELLO_BINARY_DIR
和HELLO_SOURCE_DIR
(所以CMakeLists.txt
中兩個MESSAGE
指令可以直接使用了這兩個變量),因為采用的是內部編譯,兩個變量目前指的都是工程所在路cmake/t1
,后面我們會講到外部編譯,兩者所指代的內容會有所不同。
同時cmake
系統也幫助我們預定義了PROJECT_BINARY_DIR
和PROJECT_SOURCE_DIR
變量,他們的值分別跟HELLO_BINARY_DIR
與HELLO_SOURCE_DIR
一致。
為了統一起見,建議以后直接使用PROJECT_BINARY_DIR
,PROJECT_SOURCE_DIR
,即使修改了工程名稱,也不會影響這兩個變量。如果使用了
<projectname>_SOURCE_DIR
,修改工程名稱后,需要同時修改這些變量。
2.3.2 SET
SET
指令的語法是:
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
現階段,你只需要了解SET
指令可以用來顯式的定義變量即可。
比如我們用到的是SET(SRC_LIST main.c)
,如果有多個源文件,也可以定義成:
SET(SRC_LIST main.c t1.c t2.c)
。
2.3.3 MESSAGE
MESSAGE
指令的語法是:
MESSAGE([SEND_ERROR | STATUS | FATAL_ERROR] "message to display" ...)
這個指令用于向終端輸出用戶定義的信息,包含了三種類型:
● SEND_ERROR
,產生錯誤,生成過程被跳過。
● SATUS
,輸出前綴為—的信息。
● FATAL_ERROR
,立即終止所有cmake
過程.
我們在這里使用的是STATUS
信息輸出,演示了由PROJECT
指令定義的兩個隱式變量HELLO_BINARY_DIR
和HELLO_SOURCE_DIR
。
2.3.4 ADD_EXECUTABLE
ADD_EXECUTABLE(hello ${SRC_LIST})
定義了這個工程會生成一個文件名為hello
的可執行文件,相關的源文件是SRC_LIST
中定義的源文件列表, 本例中你也可以直接寫成ADD_EXECUTABLE(hello main.c)
。
在本例我們使用了${}
來引用變量,這是cmake
的變量應用方式,但是,有一些例外,比如在IF
控制語句,變量是直接使用變量名引用,而不需要${}
。如果使用了${}
去應用變量,其實IF
會去判斷名為${}
所代表的值的變量,那當然是不存在的了。
將本例改寫成一個最簡化的CMakeLists.txt:
PROJECT(HELLO)
ADD_EXECUTABLE(hello main.c)
2.4 基本語法規則
前面提到過,cmake
其實仍然要使用”cmake 語言和語法”去構建,上面的內容就是所謂的 ”cmake 語言和語法”,最簡單的語法規則是:
-
變量使用
${}
方式取值,但是在IF
控制語句中是直接使用變量名 , -
指令
(參數1 參數 2...)
a. 參數使用括弧括起,參數之間使用空格或分號分開。
以上面的ADD_EXECUTABLE
指令為例,如果存在另外一個func.c
源文件,就要寫成:
ADD_EXECUTABLE(hello main.c func.c)
或者ADD_EXECUTABLE(hello main.c;func.c)
-
指令是大小寫無關的,參數和變量是大小寫相關的。但,推薦你全部使用大寫指令。
上面的MESSAGE
指令我們已經用到了這條規則:
MESSAGE(STATUS “This is BINARY dir” ${HELLO_BINARY_DIR})
也可以寫成:
MESSAGE(STATUS “This is BINARY dir ${HELLO_BINARY_DIR}”)
這里需要特別解釋的是作為工程名的HELLO
和生成的可執行文件hello
是沒有任何關系的。 -
工程名和執行文件
hello
定義了可執行文件的文件名,你完全可以寫成:ADD_EXECUTABLE(t1 main.c)
編譯后會生成一個t1
可執行文件。 -
關于語法的疑惑
cmake
的語法還是比較靈活而且考慮到各種情況,比如
SET(SRC_LIST main.c)
也可以寫成SET(SRC_LIST “main.c”)
是沒有區別的,但是假設一個源文件的文件名是fu nc.c
(文件名中間包含了空格)。
這時候就必須使用雙引號,如果寫成了SET(SRC_LIST fu nc.c)
,就會出現錯誤,提示你找不到fu
文件和nc.c
文件。這種情況,就必須寫成:
SET(SRC_LIST "fu nc.c")
此外,你可以可以忽略掉source 列表中的源文件后綴,比如可以寫成
ADD_EXECUTABLE(t1 main)
,cmake
會自動的在本目錄查找main.c
或者main.cpp
等,當然,最好不要偷這個懶,以免這個目錄確實存在一個main.c
一個main.
同時參數也可以使用分號來進行分割。
下面的例子也是合法的:
ADD_EXECUTABLE(t1 main.c t1.c)
可以寫成ADD_EXECUTABLE(t1 main.c;t1.c)
.
我們只需要在編寫CMakeLists.txt
時注意形成統一的風格即可。 -
清理工程:
跟經典的autotools 系列工具一樣,運行:
make clean
即可對構建結果進行清理。 -
問題?問題!
“我嘗試運行了 make distclean,這個指令一般用來清理構建過程中產生的中間文件的,如果要發布代碼,必然要清理掉所有的中間文件,但是為什么在 cmake 工程中這個命令是無效的?”
是的,cmake 并不支持 make distclean,關于這一點,官方是有明確解釋的:
因為CMakeLists.txt 可以執行腳本并通過腳本生成一些臨時文件,但是卻沒有辦法來跟蹤這些臨時文件到底是哪些。因此,沒有辦法提供一個可靠的 make distclean 方案。
Some build trees created with GNU autotools have a “make distclean” target that cleans the build and also removes Makefiles and other parts of the generated build system. CMake does not generate a “make distclean” target because CMakeLists.txt files can run scripts and arbitrary commands; CMake has no way of tracking exactly which files are generated as part of running
CMake. Providing a distclean target would give users the false impression that it would work as expected. (CMake does generate a “make clean” target to remove files generated by the compiler and linker.)
A “make distclean” target is only necessary if the user performs an in-source build. CMake supports in-source builds, but we strongly encourage users to adopt the notion of an out-of-source build. Using a build tree that is separate from the source tree will prevent CMake from generating any files in the source tree. Because CMake does not change the source tree, there is no need for a distclean target. One can start a fresh build by deleting the build tree or creating a separate build tree.
同時,還有另外一個非常重要的提示,就是:我們剛才進行的是內部構建(in-source build),而 cmake 強烈推薦的是外部構建(out-of-source build)。 -
內部構建與外部構建:
上面的例子展示的是“內部構建”,相信看到生成的臨時文件比您的代碼文件還要多的時候,估計這輩子你都不希望再使用內部構建。
對于cmake
,內部編譯上面已經演示過了,它生成了一些無法自動刪除的中間文件,所以,引出了我們對外部編譯的探討,外部編譯的過程如下:
1.首先,請清除t1
目錄中除main.c CmakeLists.txt
之外的所有中間文件,最關鍵的CMakeCache.txt
。
2.在t1
目錄中建立build
目錄,當然你也可以在任何地方建立build
目錄,不一定必須在工程目錄中。
3.進入build
目錄,運行cmake ..
(注意,…代表父目錄,因為父目錄存在我們需要的CMakeLists.txt
,如果你在其他地方建立了build
目錄,需要運行cmake <工程的全路徑>
,查看一下build 目錄,就會發現了生成了編譯需要的Makefile
以及其他的中間文件.
4.運行make
構建工程,就會在當前目錄(build 目錄)中獲得目標文件hello
。
上述過程就是所謂的out-of-source
外部編譯,一個最大的好處是,對于原有的工程沒有任何影響,所有動作全部發生在編譯目錄。通過這一點,也足以說服我們全部采用外部編譯方式構建工程。
這里需要特別注意的是:
通過外部編譯進行工程構建,HELLO_SOURCE_DIR
仍然指代工程路徑,即cmake/t1
而HELLO_BINARY_DIR
則指代編譯路徑,即cmake/t1/build
-
小結:
本小節描述了使用cmake
構建Hello World
程序的全部過程,并介紹了三個簡單的指令:
PROJECT/MESSAGE/ADD_EXECUTABLE
以及變量調用的方法,同時提及了兩個隱式變量<projectname>_SOURCE_DIR
及<projectname>_BINARY_DIR
,演示了變量調用的方法,從這個過程來看,有些開發者可能會想,這實在比我直接寫 Makefile 要復雜多了,甚至我都可以不編寫Makefile,直接使用gcc main.c
即可生成需要的目標文件。是的,正如第一節提到的,如果工程只有幾個文件,還是直接編寫Makefile
最簡單。但是,kdelibs
壓縮包達到了50 多M,您認為使用什么方案會更容易一點呢?
下一節,我們的任務是讓HelloWorld 看起來更像一個工程。
剩下的看這個鏈接
2.5 靜態庫與動態庫構建
從本節開始,我們不再折騰Hello World 了,我們來折騰Hello World 的共享庫。
本節的任務:
- 建立一個靜態庫和動態庫,提供HelloFunc函數供其他程序編程使用,HelloFunc向終端輸出Hello World 字符串。
- 安裝頭文件與共享庫。
2.5.1 準備工作
在cmake 目錄建立 t3 目錄,用于存放本節涉及到的工程
2.5.2 建立共享庫
cd make/t3
mkdir lib
在t3
目錄下建立CMakeLists.txt
,內容如下:
PROJECT(HELLOLIB)
ADD_SUBDIRECTORY(lib)
在lib
目錄下建立兩個源文件hello.c
與hello.h
hello.c 內容如下:
#include "hello.h"
void hello_func(void) {printf("Hello World!\n");return;
}
hello.h 內容如下:
#ifndef HELLO_H_
#define HELLO_H_ (1)
#include <stdio.h>
void hello_func(void);
#endif
在 lib 目錄下建立CMakeLists.txt,內容如下:
SET(LIBHELLO_SRC hello.c)
ADD_LIBRARY(hello SHARED ${LIBHELLO_SRC}
2.5.3 編譯共享庫
仍然采用out-of-source 編譯的方式,按照習慣,我們建立一個build
目錄,在 build
目錄中
cmake ..
make
這時,你就可以在lib 目錄得到一個libhello.so,這就是我們期望的共享庫。
如果你要指定libhello.so
生成的位置,可以通過在主工程文件CMakeLists.txt
中修
改ADD_SUBDIRECTORY(lib)
指令來指定一個編譯輸出位置或者在lib/CMakeLists.txt
中添加 SET(LIBRARY_OUTPUT_PATH <路徑>)
來指定一個新的位置。
這兩者的區別我們上一節已經提到了,所以,這里不再贅述,下面,我們解釋一下一個新的指令ADD_LIBRARY
ADD_LIBRARY(libname [SHARED|STATIC|MODULE] [EXCLUDE_FROM_ALL]
source1 source2 ... sourceN)
你不需要寫全libhello.so
,只需要填寫hello
即可,cmake
系統會自動為你生成libhello.X
類型有三種:
- SHARED,動態庫
- STATIC,靜態庫
- MODULE,在使用 dyld 的系統有效,如果不支持dyld,則被當作SHARED 對待。
EXCLUDE_FROM_ALL 參數的意思是這個庫不會被默認構建,除非有其他的組件依賴或者手工構建。
2.5.4 添加靜態庫
同樣使用上面的指令,我們在支持動態庫的基礎上再為工程添加一個靜態庫,按照一般的習慣,靜態庫名字跟動態庫名字應該是一致的,只不過后綴是.a
罷了。
下面我們用這個指令再來添加靜態庫:
ADD_LIBRARY(hello STATIC ${LIBHELLO_SRC})
然后再在build 目錄進行外部編譯,我們會發現,靜態庫根本沒有被構建,仍然只生成了一個動態庫。因為hello 作為一個target 是不能重名的,所以,靜態庫構建指令無效。
如果我們把上面的hello
修改為hello_static
:
ADD_LIBRARY(hello_static STATIC ${LIBHELLO_SRC})
就可以構建一個libhello_static.a
的靜態庫了。
這種結果顯示不是我們想要的,我們需要的是名字相同的靜態庫和動態庫,因為 target 名稱是唯一的,所以,我們肯定不能通過 ADD_LIBRARY 指令來實現了。這時候我們需要用到另外一個指令:
SET_TARGET_PROPERTIES,其基本語法是:
SET_TARGET_PROPERTIES(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)
這條指令可以用來設置輸出的名稱,對于動態庫,還可以用來指定動態庫版本和 API 版本。
在本例中,我們需要作的是向lib/CMakeLists.txt
中添加一條:
SET_TARGET_PROPERTIES(hello_static PROPERTIES OUTPUT_NAME "hello")
這樣,我們就可以同時得到libhello.so/libhello.a 兩個庫了。
與他對應的指令是:
GET_TARGET_PROPERTY(VAR target property)
具體用法如下例,我們向lib/CMakeListst.txt
中添加:
GET_TARGET_PROPERTY(OUTPUT_VALUE hello_static OUTPUT_NAME)
MESSAGE(STATUS “This is the hello_static OUTPUT_NAME:”${OUTPUT_VALUE})
如果沒有這個屬性定義,則返回NOTFOUND
.
讓我們來檢查一下最終的構建結果,我們發現,libhello.a
已經構建完成,位于 build/lib
目錄中,但是libhello.so
去消失了。這個問題的原因是:cmake
在構建一個新的target 時,會嘗試清理掉其他使用這個名字的庫,因為,在構建libhello.a
時,就會清理掉libhello.so
.
為了回避這個問題,比如再次使用SET_TARGET_PROPERTIES
定義CLEAN_DIRECT_OUTPUT
屬性。
向 lib/CMakeLists.txt 中添加:
SET_TARGET_PROPERTIES(hello PROPERTIES CLEAN_DIRECT_OUTPUT 1)
SET_TARGET_PROPERTIES(hello_static PROPERTIES CLEAN_DIRECT_OUTPUT 1)
這時候,我們再次進行構建,會發現build/lib 目錄中同時生成了libhello.so 和 libhello.a
2.5.5 動態庫版本號
按照規則,動態庫是應該包含一個版本號的,我們可以看一下系統的動態庫,一般情況是
libhello.so.1.2
libhello.so ->libhello.so.1
libhello.so.1->libhello.so.1.2
為了實現動態庫版本號,我們仍然需要使用 SET_TARGET_PROPERTIES 指令。
具體使用方法如下:
SET_TARGET_PROPERTIES(hello PROPERTIES VERSION 1.2 SOVERSION 1) VERSION 指代動態庫版本,
SOVERSION 指代 API 版本。
將上述指令加入lib/CMakeLists.txt 中,重新構建看看結果。
在 build/lib 目錄會生成: libhello.so.1.2 libhello.so.1->libhello.so.1.2 libhello.so ->libhello.so.1
2.5.6 安裝共享庫和頭文件
以上面的例子,我們需要將libhello.a,
libhello.so.x
以及hello.h
安裝到系統目錄,才能真正讓其他人開發使用,在本例中我們將hello
的共享庫安裝到<prefix>/lib
目錄,將hello.h
安裝到<prefix>/include/hello
目錄。
利用上一節了解到的INSTALL指令,我們向lib/CMakeLists.txt
中添加如下指令:
INSTALL(TARGETS hello hello_static
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
INSTALL(FILES hello.h DESTINATION include/hello)
注意,靜態庫要使用ARCHIVE 關鍵字
通過:
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make
make install
我們就可以將頭文件和共享庫安裝到系統目錄/usr/lib
和/usr/include/hello
中了。
如果報錯
CMake Error: cmake_symlink_library: System Error: Operation not supported
CMake Error: cmake_symlink_library: System Error: Operation not supported
make[2]: *** [lib/CMakeFiles/hello_dynamic.dir/build.make:85: lib/libhello.so.1.2] Error 1
make[2]: *** Deleting file ‘lib/libhello.so.1.2’
make[1]: *** [CMakeFiles/Makefile2:130: lib/CMakeFiles/hello_dynamic.dir/all] Error 2
make: *** [Makefile:130: all] Error 2
則說明你你可能是通過hgfs共享的Windows目錄下進行make,生成so時需要在純linux環境運行,比如把t3拷貝到~/0voice/makefile/t3, 并且先把對應的build目錄里面的內容清空,重新cmake … 再make
2.5.7 小結
本小節,我們談到了:
如何通過ADD_LIBRARY
指令構建動態庫和靜態庫。
如何通過SET_TARGET_PROPERTIES
同時構建同名的動態庫和靜態庫。如何通過SET_TARGET_PROPERTIES
控制動態庫版本
最終使用上一節談到的INSTALL
指令來安裝頭文件和動態、靜態庫。
在下一節,我們需要編寫另一個高級一點的 Hello World 來演示怎么使用我們已經構建的構建的共享庫libhello 和外部頭文件。
2.6 如何使用外部共享庫和頭文件
抱歉,本節仍然繼續折騰Hello World.
上一節我們已經完成了libhello 動態庫的構建以及安裝,本節我們的任務很簡單:
編寫一個程序使用我們上一節構建的共享庫。
2.6.1 準備工作
請在cmake 目錄建立 t4 目錄,本節所有資源將存儲在t4 目錄。
2.6.2 構建工程
重復以前的步驟,建立src 目錄,編寫源文件main.c,內容如下:
#include "hello.h"
int main(void) {hello_func();return 0;
}
編寫工程主文件CMakeLists.txt
PROJECT(NEWHELLO)
ADD_SUBDIRECTORY(src)
編寫src/CMakeLists.txt
ADD_EXECUTABLE(main main.c)
上述工作已經嚴格按照我們前面幾節提到的內容完成了。
2.6.3 外部構建
按照習慣,仍然建立build
目錄,使用cmake ..
方式構建。
過程:
cmake ..
make
構建失敗,如果需要查看細節,可以使用第一節提到的方法
make VERBOSE=1
來構建
錯誤輸出為是:
cmake/t4/src/main.c:1:19: error: hello.h: 沒有那個文件或目錄
2.6.4 引入頭文件搜索路徑
hello.h 位于/usr/include/hello
目錄中,并沒有位于系統標準的頭文件路徑,
(有人會說了,白癡啊,你就不會include <hello/hello.h>
,同志,要這么干,我這一節就沒什么可寫了,只能選擇一個glib
或者libX11
來寫了,這些代碼寫出來很多同志是看不懂的)
為了讓我們的工程能夠找到hello.h
頭文件,我們需要引入一個新的指令
INCLUDE_DIRECTORIES
,其完整語法為:
INCLUDE_DIRECTORIES([AFTER|BEFORE] [SYSTEM] dir1 dir2 ...)
這條指令可以用來向工程添加多個特定的頭文件搜索路徑,路徑之間用空格分割,如果路徑中包含了空格,可以使用雙引號將它括起來,默認的行為是追加到當前的頭文件搜索路徑的后面,你可以通過兩種方式來進行控制搜索路徑添加的方式:
CMAKE_INCLUDE_DIRECTORIES_BEFORE
,通過SET
這個cmake
變量為on
,可以將添加的頭文件搜索路徑放在已有路徑的前面。- 通過
AFTER
或者BEFORE
參數,也可以控制是追加還是置前。
現在我們在src/CMakeLists.txt
中添加一個頭文件搜索路徑,方式很簡單,加入:
INCLUDE_DIRECTORIES(/usr/include/hello)
進入build 目錄,重新進行構建,這是找不到hello.h
的錯誤已經消失,但是出現了一個新的錯誤:
main.c:(.text+0x12): undefined reference to "HelloFunc'
因為我們并沒有link
到共享庫libhello
上。
2.6.5 為 target 添加共享庫
我們現在需要完成的任務是將目標文件鏈接到 libhello,這里我們需要引入兩個新的指令
LINK_DIRECTORIES
和TARGET_LINK_LIBRARIES
LINK_DIRECTORIES
的全部語法是:
LINK_DIRECTORIES(directory1 directory2 ...)
這個指令非常簡單,添加非標準的共享庫搜索路徑,比如,在工程內部同時存在共享庫和可執行二進制,在編譯時就需要指定一下這些共享庫的路徑。這個例子中我們沒有用到這個指令。
TARGET_LINK_LIBRARIES 的全部語法是:
TARGET_LINK_LIBRARIES(target library1
<debug | optimized> library2
...)
這個指令可以用來為target 添加需要鏈接的共享庫,本例中是一個可執行文件,但是同樣可以用于為自己編寫的共享庫添加共享庫鏈接。
為了解決我們前面遇到的HelloFunc
未定義錯誤,我們需要作的是向 src/CMakeLists.txt 中添加如下指令:
TARGET_LINK_LIBRARIES(main hello)
也可以寫成
TARGET_LINK_LIBRARIES(main libhello.so)
這里的hello
指的是我們上一節構建的共享庫libhello
.
進入build 目錄重新進行構建。
cmake ..
make
這是我們就得到了一個連接到libhello
的可執行程序main
,位于build/bin
目錄,運
行main
的結果是輸出:
Hello World
讓我們來檢查一下main 的鏈接情況:
ldd main
linux-vdso.so.1 (0x00007ffd43bf7000)
libhello.so.1 => /lib/libhello.so.1 (0x00007f8e13140000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8e12f4e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8e1315b000)
可以清楚的看到main
確實鏈接了共享庫libhello
,而且鏈接的是動態庫libhello.so.1
那如何鏈接到靜態庫呢?
方法很簡單:
將 TARGET_LINK_LIBRRARIES 指令修改為: TARGET_LINK_LIBRARIES(main libhello.a)
重新構建后再來看一下main 的鏈接情況
ldd main
linux-vdso.so.1 (0x00007ffc42575000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcaade3e000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcaae046000)
說明,main 確實鏈接到了靜態庫libhello.a
2.6.6 特殊的環境變量CMAKE_INCLUDE_PATH
和CMAKE_LIBRARY_PATH
務必注意,這兩個是環境變量而不是cmake
變量。
使用方法是要在bash
中用export
或者在csh
中使用set
命令設置或者 CMAKE_INCLUDE_PATH=/home/include cmake ..
等方式。
這兩個變量主要是用來解決以前autotools
工程中
--extra-include-dir
等參數的支持的。
也就是,如果頭文件沒有存放在常規路徑(/usr/include, /usr/local/include
等),則可以通過這些變量就行彌補。
我們以本例中的hello.h
為例,它存放在/usr/include/hello
目錄,所以直接查找肯定是找不到的。
前面我們直接使用了絕對路徑INCLUDE_DIRECTORIES(/usr/include/hello)
告訴工程這個頭文件目錄。
為了將程序更智能一點,我們可以使用CMAKE_INCLUDE_PATH
來進行,使用bash
的方法如下:
export CMAKE_INCLUDE_PATH=/usr/include/hello
然后在頭文件中將INCLUDE_DIRECTORIES(/usr/include/hello)
替換為:
FIND_PATH(myHeader hello.h)
IF(myHeader)
INCLUDE_DIRECTORIES(${myHeader})
ENDIF(myHeader)
上述的一些指令我們在后面會介紹。
這里簡單說明一下,FIND_PATH
用來在指定路徑中搜索文件名,比如:
FIND_PATH(myHeader NAMES hello.h PATHS /usr/include /usr/include/hello)
這里我們沒有指定路徑,但是,cmake 仍然可以幫我們找到hello.h 存放的路徑,就是因為我們設置了環境變量CMAKE_INCLUDE_PATH
。
如果你不使用FIND_PATH
,CMAKE_INCLUDE_PATH
變量的設置是沒有作用的,你不能指望它會直接為編譯器命令添加參數-I<CMAKE_INCLUDE_PATH>
。
以此為例,CMAKE_LIBRARY_PATH
可以用在FIND_LIBRARY
中。
同樣,因為這些變量直接為FIND_
指令所使用,所以所有使用FIND_
指令的cmake
模塊都會受益。
2.6.7 小節
本節我們探討了:
如何通過INCLUDE_DIRECTORIES
指令加入非標準的頭文件搜索路徑。
如何通過LINK_DIRECTORIES
指令加入非標準的庫文件搜索路徑。
如果通過TARGET_LINK_LIBRARIES
為庫或可執行二進制加入庫鏈接。
并解釋了如果鏈接到靜態庫。
到這里為止,您應該基本可以使用cmake 工作了,但是還有很多高級的話題沒有探討,比如編譯條件檢查、編譯器定義、平臺判斷、如何跟 pkgconfig 配合使用等等。
到這里,或許你可以理解前面講到的“cmake 的使用過程其實就是學習cmake 語言并編寫 cmake 程序的過程”,既然是“cmake 語言”,自然涉及到變量、語法等.
下一節,我們將拋開程序的話題,看看常用的 CMAKE 變量以及一些基本的控制語法規則。
2.7 CMake 常用指令
前面我們講到了cmake 常用的變量,相信“cmake 即編程”的感覺會越來越明顯,無論如何,我們仍然可以看到cmake 比autotools 要簡單很多。接下來我們就要集中的看一看 cmake 所提供的常用指令。在前面的章節我們已經討論了很多指令的用法,如
PROJECT,ADD_EXECUTABLE,INSTALL,ADD_SUBDIRECTORY,SUBDIRS,INCLUDE _DIRECTORIES,LINK_DIRECTORIES,TARGET_LINK_LIBRARIES,SET 等。
本節會引入更多的cmake 指令,為了編寫的方便,我們將按照 cmake man page 的順序來介紹各種指令,不再推薦使用的指令將不再介紹,INSTALL 系列指令在安裝部分已經做了非常詳細的說明,本節也不在提及。(你可以將本章理解成選擇性翻譯,但是會加入更多的個人理解)
2.7.1 基本指令
-
ADD_DEFINITIONS
向C/C++編譯器添加-D 定義,比如:
ADD_DEFINITIONS(-DENABLE_DEBUG -DABC)
,參數之間用空格分割。
如果你的代碼中定義了#ifdef ENABLE_DEBUG #endif
,這個代碼塊就會生效。
如果要添加其他的編譯器開關,可以通過CMAKE_C_FLAGS
變量和CMAKE_CXX_FLAGS
變量設置。 -
ADD_DEPENDENCIES
定義target 依賴的其他target,確保在編譯本target 之前,其他的target 已經被構建。
ADD_DEPENDENCIES(target-name depend-target1 depend-target2 ...)
-
ADD_EXECUTABLE
、ADD_LIBRARY
、ADD_SUBDIRECTORY
前面已經介紹過了,這里不再羅唆。 -
ADD_TEST
與ENABLE_TESTING
指令。
ENABLE_TESTING
指令用來控制Makefile
是否構建test目標,涉及工程所有目錄。語
法很簡單,沒有任何參數,ENABLE_TESTING()
,一般情況這個指令放在工程的主CMakeLists.txt
中.
ADD_TEST
指令的語法是:
ADD_TEST(testname Exename arg1 arg2 ...)
testname
是自定義的test
名稱,Exename 可以是構建的目標文件也可以是外部腳本等等。后面連接傳遞給可執行文件的參數。如果沒有在同一個 CMakeLists.txt 中打開
ENABLE_TESTING()
指令,任何ADD_TEST
都是無效的。
比如我們前面的Helloworld
例子,可以在工程主CMakeLists.txt
中添加ADD_TEST(mytest ${PROJECT_BINARY_DIR}/bin/main) ENABLE_TESTING()
生成Makefile
后,就可以運行make test
來執行測試了。 -
AUX_SOURCE_DIRECTORY
基本語法是:
AUX_SOURCE_DIRECTORY(dir VARIABLE)
作用是發現一個目錄下所有的源代碼文件并將列表存儲在一個變量中,這個指令臨時被用來自動構建源文件列表。因為目前cmake
還不能自動發現新添加的源文件。
比如AUX_SOURCE_DIRECTORY(. SRC_LIST) ADD_EXECUTABLE(main ${SRC_LIST})
你也可以通過后面提到的
FOREACH
指令來處理這個LIST
-
CMAKE_MINIMUM_REQUIRED
其語法為CMAKE_MINIMUM_REQUIRED(VERSION versionNumber [FATAL_ERROR])
比如CMAKE_MINIMUM_REQUIRED(VERSION 2.5 FATAL_ERROR)
如果cmake
版本小與2.5,則出現嚴重錯誤,整個過程中止。 -
EXEC_PROGRAM
在CMakeLists.txt
處理過程中執行命令,并不會在生成的 Makefile 中執行。具體語法為:
EXEC_PROGRAM(Executable [directory in which to run]
[ARGS <arguments to executable>]
[OUTPUT_VARIABLE <var>]
[RETURN_VALUE <var>])
用于在指定的目錄運行某個程序,通過ARGS
添加參數,如果要獲取輸出和返回值,可通過 OUTPUT_VARIABLE
和RETURN_VALUE
分別定義兩個變量.
這個指令可以幫助你在CMakeLists.txt
處理過程中支持任何命令,比如根據系統情況去修改代碼文件等等。
舉個簡單的例子,我們要在src
目錄執行ls
命令,并把結果和返回值存下來。
可以直接在src/CMakeLists.txt
中添加:
EXEC_PROGRAM(ls ARGS "*.c" OUTPUT_VARIABLE LS_OUTPUT RETURN_VALUE LS_RVALUE)
IF(not LS_RVALUE)
MESSAGE(STATUS "ls result: " ${LS_OUTPUT})
ENDIF(not LS_RVALUE)
在 cmake 生成 Makefile 的過程中,就會執行ls 命令,如果返回0,則說明成功執行,那么就輸出ls *.c 的結果。關于IF 語句,后面的控制指令會提到。
FILE
指令
文件操作指令,基本語法為:
FILE(WRITE filename "message to write"... )FILE(APPEND filename "message to write"... )FILE(READ filename variable)FILE(GLOB variable [RELATIVE path] [globbing expressions]...)FILE(GLOB_RECURSE variable [RELATIVE path] [globbing expressions]...)FILE(REMOVE [directory]...)FILE(REMOVE_RECURSE [directory]...)FILE(MAKE_DIRECTORY [directory]...)FILE(RELATIVE_PATH variable directory file)FILE(TO_CMAKE_PATH path result)FILE(TO_NATIVE_PATH path result)
這里的語法都比較簡單,不在展開介紹了。
INCLUDE
指令,用來載入CMakeLists.txt
文件,也用于載入預定義的cmake
模塊.INCLUDE(file1 [OPTIONAL])
INCLUDE(module [OPTIONAL])
OPTIONAL
參數的作用是文件不存在也不會產生錯誤。
你可以指定載入一個文件,如果定義的是一個模塊,那么將在CMAKE_MODULE_PATH
中搜索這個模塊并載入。
載入的內容將在處理到INCLUDE
語句是直接執行。
2.8.2 INSTALL 指令
INSTALL 系列指令已經在前面的章節有非常詳細的說明,這里不在贅述,可參考前面的安裝部分。
2.8.2 FIND_指令
FIND_系列指令主要包含一下指令:
FIND_FILE(<VAR> name1 path1 path2 ...)
VAR 變量代表找到的文件全路徑,包含文件名
FIND_LIBRARY(<VAR> name1 path1 path2 ...)
VAR 變量表示找到的庫全路徑,包含庫文件名
FIND_PATH(<VAR> name1 path1 path2 ...)
VAR 變量代表包含這個文件的路徑。
FIND_PROGRAM(<VAR> name1 path1 path2 ...)
VAR 變量代表包含這個程序的全路徑。
FIND_PACKAGE(<name> [major.minor] [QUIET] [NO_MODULE] [[REQUIRED|COMPONENTS] [componets...]])
用來調用預定義在CMAKE_MODULE_PATH 下的Find.cmake 模塊,你也可以自己定義Find模塊,通過SET(CMAKE_MODULE_PATH dir)將其放入工程的某個目錄中供工程使用,我們在后面的章節會詳細介紹 FIND_PACKAGE 的使用方法和 Find 模塊的
編寫。
FIND_LIBRARY 示例:
FIND_LIBRARY(libX X11 /usr/lib)
IF(NOT libX)
MESSAGE(FATAL_ERROR “libX not found”)
ENDIF(NOT libX)
2.8.3 控制指令
- IF 指令,基本語法為:
IF(expression)
# THEN section. COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
...
ELSE(expression)
# ELSE section. COMMAND1(ARGS ...) COMMAND2(ARGS ...)
...
ENDIF(expression)
另外一個指令是ELSEIF
,總體把握一個原則,凡是出現IF 的地方一定要有對應的ENDIF
.出現ELSEIF
的地方,ENDIF
是可選的。
表達式的使用方法如下:
IF(var),如果變量不是:空,0,N, NO, OFF, FALSE, NOTFOUND 或 <var>_NOTFOUND 時,表達式為真。IF(NOT var ),與上述條件相反。IF(var1 AND var2),當兩個變量都為真是為真。IF(var1 OR var2),當兩個變量其中一個為真時為真。IF(COMMAND cmd),當給定的 cmd 確實是命令并可以調用是為真。IF(EXISTS dir)或者IF(EXISTS file),當目錄名或者文件名存在時為真。IF(file1 IS_NEWER_THAN file2),當file1 比file2 新,或者file1/file2 其中有一個不存在時為真,文件名請使用完整路徑。IF(IS_DIRECTORY dirname),當 dirname 是目錄時,為真。IF(variable MATCHES regex)IF(string MATCHES regex)
當給定的變量或者字符串能夠匹配正則表達式 regex 時為真。比如:
IF("hello" MATCHES "ell")MESSAGE("true")ENDIF("hello" MATCHES "ell")IF(variable LESS number)IF(string LESS number)IF(variable GREATER number)IF(string GREATER number)IF(variable EQUAL number)IF(string EQUAL number)
數字比較表達式
IF(variable STRLESS string)IF(string STRLESS string)IF(variable STRGREATER string)IF(string STRGREATER string)IF(variable STREQUAL string)IF(string STREQUAL string)
按照字母序的排列進行比較.
IF(DEFINED variable),如果變量被定義,為真。
一個小例子,用來判斷平臺差異:
IF(WIN32)
MESSAGE(STATUS “This is windows.”)
#作一些Windows 相關的操作
ELSE(WIN32)
MESSAGE(STATUS “This is not windows”)
#作一些非Windows 相關的操作
ENDIF(WIN32)
上述代碼用來控制在不同的平臺進行不同的控制,但是,閱讀起來卻并不是那么舒服,ELSE(WIN32)之類的語句很容易引起歧義。
這就用到了我們在“常用變量”一節提到的CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS
開關。
可以SET(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON)
這時候就可以寫成:
IF(WIN32)
ELSE()
ENDIF()
如果配合ELSEIF
使用,可能的寫法是這樣:
IF(WIN32)
#do something related to WIN32
ELSEIF(UNIX)
#do something related to UNIX
ELSEIF(APPLE)
#do something related to APPLE
ENDIF(WIN32)
- WHILE
WHILE 指令的語法是:
WHILE(condition)
COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
...
ENDWHILE(condition)
其真假判斷條件可以參考IF 指令。
- FOREACH
FOREACH
指令的使用方法有三種形式:
1.列表
像我們前面使用的FOREACH(loop_var arg1 arg2 ...) COMMAND1(ARGS ...) COMMAND2(ARGS ...) ... ENDFOREACH(loop_var)
AUX_SOURCE_DIRECTORY
的例子AUX_SOURCE_DIRECTORY(. SRC_LIST)
2.范圍FOREACH(F ${SRC_LIST}) MESSAGE(${F}) ENDFOREACH(F)
從 0 到 total 以1為步進FOREACH(loop_var RANGE total) ENDFOREACH(loop_var)
舉例如下:
最終得到的輸出是:FOREACH(VAR RANGE 10) MESSAGE(${VAR}) ENDFOREACH(VAR)
0
1
2
3
4
5
6
7
8
9
10
3.范圍和步進
FOREACH(loop_var RANGE start stop [step])
ENDFOREACH(loop_var)
從 start 開始到stop 結束,以step 為步進,
舉例如下
FOREACH(A RANGE 5 15 3)
MESSAGE(${A})
ENDFOREACH(A)
最終得到的結果是:
5
8
11
14
這個指令需要注意的是,知道遇到ENDFOREACH 指令,整個語句塊才會得到真正的執行。
2.7 單個文件目錄實現
2.7.1 單個文件目錄實現1
2.7.1 單個文件目錄實現2
這個是需要執行子目錄src中的CMakeLists.txt
的。
我們打印一下:
2.8 多個目錄實現
2.8.1 子目錄編譯成庫文件
語法:INCLUDE_DIRECTORIES 找頭文件
INCLUDE_DIRECTORIES("${CMAKE_CURRENT_SOURCE_DIR}/dir1")語法:ADD_SUBDIRECTORY 添加子目錄
ADD_SUBDIRECTORY("${CMAKE_CURRENT_SOURCE_DIR}/dir1")語法:ADD_LIBRARY 生成庫文件
ADD_LIBRARY( hello_shared SHARED libHelloSLAM.cpp ) 生成動態庫
ADD_LIBRARY( hello_static STATIC libHelloSLAM.cpp ) 生成靜態庫語法:TARGET_LINK_LIBRARIES鏈接庫到執行文件上,生成最后目標
TARGET_LINK_LIBRARIES(darren dir1 dir2)