原文連接:http://www.orlion.ga/816/web
1、基本規則數據庫
對於一個擁有多個文件的c項目,編譯時多是這樣的指令:bash
gcc main.c stack.c -o main
若是編譯以後又對stack.c進行了修改,則又要從新把全部的源文件編譯一遍,即便main.c和那些頭文件都沒有修改也要跟着從新編譯,一個大型項目每每上千個源文件組成,所有編譯要幾個小時,只改一個源文件就要從新編譯顯然不合理。若是是一下的編譯方式會好一些:ui
gcc -c main.c gcc -c stack.c gcc main.o stack.o -o main
若是編譯後修改了stack.c要從新編譯只須要兩步:spa
gcc -c stack.c gcc main.o stack.o -o main
這樣又有一個問題,若是修改了三個源文件,可是有一個忘記編譯了,結果編譯完了修改沒生效。更復雜的是修改了頭文件全部包含該頭文件的源文件都須要從新編譯。命令行
要解決以上的問題就須要寫一個Makefile文件和源碼放到同一個目錄下:調試
而後在這個目錄下運行make編譯:orm
make命令會自動讀取當前目錄下的Makefile文件,完成相應的編譯步驟。Makefile由一組規則組成,每條規則的格式是:進程
target ... : prerequisites ... command1 command2 ...
例如:ci
main: main.o stack.o is_empty.o pop.o push.o gcc main.o stack.o is_empty.o pop.o push.o
main是這條規則的目標(target),main.o、stack.o、is_empty、pop.o、push.o是這條規則的條件(prerequisite)。目標和條件之間的關係是:欲更新目標,必須更新它的全部條件;全部條件中只要有一個條件被更新了,目標也必須跟着被更新。所謂"更新"就是執行一遍規則中的命令列表,命令列表中的每條命令必須以一個Tab開頭,注意不能是空格,對於Makefile中的每一個一Tab開頭的命令,make會建立一個Shell進程去執行它。
對於上邊的例子,make執行以下步驟:
嘗試更新Makefile中的第一條規則的目標main,第一條規則的目標稱之爲缺省目標,只要缺省目標更新了就算完成任務了,其餘工做都是爲這個目的而作的。因爲main文件尚未生成,顯然須要更新,但規則說必須先更新了main.o stack.o is_empty.o pop.o push.o這五個條件才能更新main。
因此make會進一步查找以這五個條件爲目標的規則,這些目標文件也沒有生成,頁須要更新,因此執行相應的命令(gcc -c main.c gcc -c stack.c …)來更新它們。
最後執行gcc main.o stack.o is_empty.o pop.o push.o -o main更新main。
若是再執行make則不會從新編譯,若是源文件修改了則會從新編譯更新對應的目標文件而後從新執行gcc main.o stack.o is_empty.o pop.o push.o -o main來更新main
一般Makefile都會有一個clean規則,用於清除編譯過程當中產生的二進制文件,保留源文件:
clean: @echo "cleanning project" -rm main *.o @echo "clean completed"
把這條規則加到Makefile文件尾,而後執行這條規則:
若是在make的命令行中指定了一個目標(例如clean),則更新這個目標,若是不指定目標則更新Makefile中第一條規則的目標(缺省目標)。
若是make執行的命令前面加了@字符則不顯示命令自己而只顯示它的結果;若是命令前面加了「-」即便這條命令出錯make也會繼續執行,(若是不加則會馬上終止,再也不執行後續的命令)一般rm命令和mkdir命令前面要加「-」,由於rm要刪除的文件可能不存在,mkdir要建立的目錄可能已經存在了,而這些錯誤是能夠忽略的,例如上邊已經執行過一遍make clean了,再執行一遍就沒有文件能夠刪了這時rm會報錯,可是make忽略了這一錯誤繼續執行後邊的echo。
若是當前目錄下存在一個文件clean會怎麼樣呢?若是存在一個clean文件,clean目標又不依賴於任何條件,make就認爲它不須要更新了,而咱們但願把clean當作一個特殊的名字使用,無論它存在不存在都要更新,能夠添加一條特殊規則,把clean聲明爲一個僞目標:
.PHONY: clean
這條規則沒有命令列表。(這條規則寫在clean:規則以後也行)相似於clean這樣的命令常見的就是install了(make install)
2、隱含規則和模式規則
Makefile有不少靈活的寫法,能夠寫得更簡潔。
一個目標所依賴的全部條件不必定非要寫在一條規則中,能夠分開來寫:
main.o: stack.h main.o: main.c gcc - c main.c
若是一個目標分來來寫則其中只有一條規則容許有命令列表,其餘規則應該沒有規則列表,不然make會報警並採用最後一條規則的命令列表。
如今咱們的Makefile能夠改寫爲:
main: main.o stack.o is_empty.o pop.o push.o gcc main.o stack.o is_empty.o pop.o push.o -o main main.o: stack.h main.o: main.c gcc -c main.c stack.o: stack.c gcc -c stack.c ... clean: -rm main *.o .PHONY: clean
能夠看到咱們把main.o分開來寫,可是這樣會顯得繁瑣了些,這時能夠簡化:
main: main.o stack.o is_empty.o pop.o push.o gcc main.o stack.o is_empty.o pop.o push.o -o main main.o: stack.h clean: -rm main *.o .PHONY: clean
可是如今main.o\stack.o\is_empty.o…這五個目標連編譯命令沒了怎麼編譯的呢?
解釋一下前五條命令怎麼來的:若是一個目標在Makefile中全部規則都沒有命令列表,make會嘗試在內建的隱含規則數據庫中查找適用的規則。make的隱含規則數據庫能夠用"make -p"命令打印,打印出來的格式也是Makefile的格式,包含不少變量和規則,其中和這個例子有關的隱含規則有:
# default OUTPUT_OPTION = -o $@ # default CC = cc # default COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c %.o: %.c # commands to execute (built-in): $(COMPILE.c) $(OUTPUT_OPTION) $<
#號在Makefile中表示單行註釋,cc是一個Makefile變量,用CC = cc定義和賦值,用$(CC)取它的值,其值應該是cc。Makefile變量像C的宏定義同樣,表明一串字符,在取值的地方展開。cc是一個符號連接,一般指向gcc。
CFLAGS這個變量沒有定義,$(CFLAGS)展開是空,CPPFLAGS和TARGET_ARCH也是如此。這樣$(COMPLIE.c)展開應該是"cc 空 空 空 -c"去掉「空」獲得"cc -c",注意中間留下4個空格,因此%.o:%.c規則的命令$(COMPLIE.c) $(OUTPUT_OPTION) $<展開以後是cc -c -o $@ $<,和上面的編譯命令已經很接近了。
$@和$<是兩個特殊的變量,$@的取值爲規則中的目標,$<的取值爲規則中的第一個條件。$.o:%.c是一種特殊的規則,稱爲模式規則。如今回顧一下整個過程,在咱們的Makefile中以main.o爲目標的規則都沒有命令列表,因此make會查找隱含規則,發現隱含規則中有這樣一條模式規則適用,main.o符合%.c的模式,如今%就表明main(稱爲main.o這個名字的Stem),再替換到%.c中就是main.c。因此這條規則就至關於:
main.o: main.c cc -c -o main.o main.c
隨後在處理stack.o目標時又用到這條模式規則,這時又至關於:
stack.o: stack.c cc -c -o stack.o stack.c
其餘三個也一樣處理。這三條規則能夠由make的隱含規則推導出來,因此沒必要寫在Makefile中。
以前咱們寫的Makefile都是以目標爲中心,如今能夠以條件爲中心:
target1 target2: prerequisite1 prerequisite2 command $< -o $@
這條規則至關於:
target1: prerequisite1 prerequisite2 command prerequisite1 -o target1 target2: prerequisite1 prerequisite2 command prerequisite1 -o target2
3、變量
foo = $(bar) bar = Huh? all: @echo $(foo)
咱們執行make將會打印出Huh?。當make讀到foo = $(bar)是,肯定foo的值是$(bar),但並不當即展開$(bar),而後讀到bar = Huh?,肯定bar的值是Huh?,而後在執行規則all:的命令列表時才須要展開$(foo),獲得$(bar),再展開$(bar),獲得Huh?。所以,雖然bar的定義寫在foo以後,$(foo)展開仍是可以取到$(bar)的值。、
這種特性有好處就是能夠把變量的定義推遲到後邊。固然可使用":="運算符make在遇到變量定義時當即展開:
x := foo y := $(x) bar all: @echo "-$(y)-"
還有一個比較有用的賦值運算符是?=,例如foo ?= $(bar)的意思是:若是foo沒有定義過,那麼?=至關於=,定義foo的值是$(bar),但不當即展開;若是先前已經定義了foo,則什麼也不作,不會給foo從新賦值。
+=運算符能夠給變量追加值,例如:
objects = main.o objects += $(foo) foo = foo.o bar.o
經常使用的特殊變量:
$@,表示規則中的目標。
$<,表示規則中的第一個條件。
$?,表示規則中全部比目標新的條件,組成一個列表,以空格分隔。
$^,表示規則中的全部條件,組成一個列表,以空格分隔。
4、自動處理頭文件的依賴關係
在寫main.o、stack.o、is_empty.o、pop.o、push.o這五個目標的規則時要查看源代碼,找出它們依賴於哪些頭文件,這很容易出錯,一是由於有的頭文件包含在另外一個頭文件中,在寫規則時很容易遺漏,二是若是之後修改源代碼改變了依賴關係,極可能忘記修改Makefile的規則。爲了解決這個問題,能夠用gcc的-M選項自動生成目標文件和源文件的依賴關係:
若是不須要輸出系統頭文件的依賴可使用"-MM"。
接下來是怎麼把這些規則包含到Makefile中,GNU make官方手冊建議這樣寫:
all: main main: main.o stack.o is_empty.o pop.o push.o gcc $^ -o $@ clean: -rm main *.o .PHONY: clean sources = main.c stack.c is_empty.c pop.c push.c include $(sources:.c=.d) %.d: %.c set -e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$
source變量包含咱們要編譯的全部.c文件,$(sources: .c=.d)是一個變量替換語法,把sources變量中每一項的.c替換成.d因此include這一句至關於:
include main.d stack.d is_empty.d pop.d push.d
這裏include表示包含五個文件。這五個文件也應該符合Makefile的語法。這時候執行make一開始找不到.d文件會報警告,可是make會把include的文件名也當作目標來嘗試更新,而這些目標適用模式規則%.d:%.c,因此執行它的命令列表。
不論是Makefile本事仍是被包含的文件,只要有一個文件在make過程當中給更新了,make就會從新讀取整個Makefile以及被它包含的全部文件。
5、經常使用的make命令行選項
-n選項只打印要執行的命令,而不會真的執行命令,這個選項有助於咱們檢查Makefile寫得是否正確,因爲Makefile不是順序執行的,用這個選項能夠先看看命令的執行順序,確認無誤了再真正執行命令。
-C選項能夠切換到另外一個目錄執行那個目錄下的Makefile,好比先退到上一級目錄再執行咱們的Makefile(假設咱們的源代碼都放在testmake目錄下):
$ cd .. $ make -C testmake
一些規模較大的項目會把不一樣的模塊或子系統的源代碼放在不一樣的子目錄中,而後在每一個子目錄下都寫一個該目錄的Makefile,而後在一個總的Makefile中用make -C命令執行每一個子目錄下的Makefile。
在make命令行也能夠用=或:=定義變量,若是此次編譯我想加調試選項-g,但我不想每次編譯都加-g選項,能夠在命令行定義CFLAGS變量,而沒必要修改Makefile編譯完了再改回來:
make CFLAGS=-g