Makefile文件
GNUmake是一個用來控制軟件構建過程的自動工具,程序員通過定義構建規則來控制代碼的創建過程。這些規則通常定義在一個名為Makefile的文件中。Makefile被用來告訴make編譯哪些文件、怎樣編譯和何時編譯。Makefile中的每條規則事實上都包含如下一些內容:
◆ 目標(target)是make最終需要創建的對象;
◆ 依賴(dependency)通常是一個列表,指明編譯目標時需要用到的其它文件;
◆ 命令(command)也是一個列表,指明從依賴文件創建出目標對象所需要執行的命令。
雖然Makefile中的目標通常都是可執行程序,但事實上可以是諸如文本文件和HTML頁面等任何內容,甚至能夠用來測試或設置環境變量。Makefile中的命令則不僅可以是編譯命令,還可以是任何Shell命令。
先來看一個例子。假設整個軟件項目是由control.c、io.c和main.c三個源文件所構成的,編寫的Makefile文件內容如下:
program : control.o ui.o main.o
gcc -o program control.o ui.o main.o
control.o : control.c
gcc -Wall -c -o control.o control.c
ui.o : ui.c
gcc -Wall -c -o ui.o ui.c
main.o : main.c
gcc -Wall -c -o main.o main.c
clean :
rm -f program *.o
在將上述Makefile文件與源文件保存到同一目錄之后,就可以在命令行中輸入“make”命令來編譯整個項目了。make在執行過程中,首先會查找到Makefile文件第一條規則中的目標,即上述文件中的all。根據設定好的規則,該目標需要依賴于program。由于all并不是一個已經存在的文件,所以每次在make被調用的時候,顯然都需要先檢查program。繼續往下不難發現,program目標是依賴于control.o、ui.o和main.o的。這就意味著如果其中任何一個比生成的可執行文件要新,那么就需要重新構建可執行文件program,否則就沒有必要執行這一步了。
在Makefile文件的其余部分,為每一個中間生成的目標文件都專門定義了一條規則,用來指明創建過程中它們與C源文件的依賴性。也就是說,如果一個特定的C源文件被更新了,那么與之對應的目標文件也必須重新生成。下面是make在構建項目過程中的輸出結果:
gcc -Wall -c -o control.o control.c
gcc -Wall -c -o ui.o ui.c
gcc -Wall -c -o main.o main.c
gcc -o program control.o ui.o main.o
不難看出,首先是C源文件被編譯成目標文件,然后才是目標文件被連接成最終的可執行文件。由于相互間依賴關系的制約,這些步驟會被有條不紊地依次執行。最終可執行文件要求目標文件都被更新過,而每個目標文件則要求C源文件被更新過。如果此時重新執行“make”命令,會出現下面的結果。原因是程序已經被編譯過了,并且沒有做過任何改動,所以就沒有再編譯的必要了:
make: Nothing to be done for 'all'.
如果只是改變了其中的部分文件,那么make會自動檢測出需要對哪些源文件重新進行編譯,并連接成最后的可執行文件。用戶可以參考下面的過程:
# make
gcc -Wall -c -o main.o main.c
gcc -o program control.o ui.o main.o
當make檢測到main.o目標時,發現main.c文件已經被更新,于是main.o文件必須被重新編譯,相應地program需要被重新連接。make的魅力就在于能夠自動進行條件檢測,并采取適當的行動。它永遠也不會去編譯那些沒有改動過的源文件,因此大大節省了在開發大型軟件項目時所浪費在編譯上的時間。
變量
為了簡化Makefile的編寫,make引入了變量。變量實際上是為文本串在Makefile中定義一個便于記憶的名稱。變量的定義和應用與Linux的環境變量一樣,變量名大寫,變量一旦定義之后,就可以通過將變量名用圓括號包起來,并在前面加上“$”符號來進行引用。
變量一般都在Makefile的頭部定義。如果變量的值發生了改變,很顯然只需在一個地方進行修改就可以了,從而大大簡化了Makefile的維護。下面是將前面用到的Makefile利用變量進行改寫后的結果:
CC = GCC
CFLAGS = -Wall
all : program
program : $(OBJS)
$(CC) $(OBJS) -o program
control.o : control.c
$(CC) $(CFLAGS) -c -o control.o control.c
ui.o : ui.c
$(CC) $(CFLAGS) -c -o ui.o ui.c
main.o : main.c
$(CC) $(CFLAGS) -c -o main.o main.c
clean :
rm -f program $(OBJS)
make將其使用的變量細分為兩類:遞歸展開變量和簡單展開變量。遞歸展開變量在被引用時會逐層展開,即如果在展開式中包含了對其它變量的引用,則這些變量也會被展開,直到沒有需要被展開的變量為止。假設變量TOPDIR和SUBDIR的定義如下:
SUBDIR = $(TOPDIR)/project
此時變量SUBDIR的值在解析時會被正確地展開為/home/xiaowp/project,但對于下面的定義:
SUBDIR = $(TOPDIR)/project
SUBDIR = $(SUBDIR)/src
很清楚,希望得到的結果是/home/xiaowp/project/src,但實際并非如此。SUBDIR在引用時會被遞歸展開,從而陷入一個無限循環當中,make能夠檢測到這個問題并報告如下錯誤:
*** Recursive variable 'SUBDIR' references itself (eventually). Stop
為了避免這個問題,可以使用簡單展開變量。與遞歸展開變量在引用時展開不同,簡單展開變量是在定義處展開的,并且只展開一次,從而消除了變量的嵌套引用。在定義時,其語法與遞歸展開變量有細微的不同:
SUBDIR := $(TOPDIR)/project
SUBDIR += /src
SUBDIR在第一次定義時使用“:=”將其值設置為“/home/xiaowp/project”,而在第二次定義時則使用“+=”在已有的基礎上添加“/src”,這樣就使得SUBDIR的最終值變為“/home/xiaowp/project/src”。許多程序員在Makefile中只使用簡單展開變量,以避免可能出現的錯誤。
除了用戶自定義變量之外,在Makefile中還可以使用環境變量、自動變量和預定義變量。使用環境變量的方法相對來講比較簡單,make在啟動時會自動讀取系統當前已經定義了的環境變量,并且會創建與之具有相同名稱和數值的變量。需要注意的是,如果用戶在Makefile中定義了相同名稱的變量,那么用戶自定義變量將會覆蓋同名的環境變量。javascript:window.open(this.src);" style="CURSOR: pointer" onload="return imgzoom(this,550)">
此外,make還提供了一些預定義變量和自動變量,但它們看起來都不如自定義變量那么直觀。之所以稱為自動變量是因為make會自動用特定的、熟知的值來替換它們,表1給出了常用的部分自動變量。
利用make的自動變量和預定義變量,可以簡化前面給出的那個Makefile文件:
CC = GCC
CFLAGS = -Wall
all : program
program : $(OBJS)
$(CC) $(OBJS) -o $@
control.o : control.c
$(CC) $(CFLAGS) -c -o $@ $<
ui.o : ui.c
$(CC) $(CFLAGS) -c -o $@ $<
main.o : main.c
$(CC) $(CFLAGS) -c -o $@ $<
clean :
$(RM) program $(OBJS)
偽目標
在Makefile中,并不是所有的目標都對應于磁盤上的文件。有的目標存在只是為了形成一條規則,從而完成特定的工作,并不生成新的目標文件,這樣的目標稱為偽目標。它并不是真正意義上的目標文件,只是為了滿足Makefile的語法規則而存在的。
在已經給出的Makefile文件中,最后一個目標clean就是偽目標。它規定了make應該執行的命令。當make處理到目標clean時,會先查看其對應的依賴對象。由于clean沒有任何依賴對象,所以make會認為該目標是最新的而不會執行任何操作。為了編譯這個目標體,必須手工執行如下命令:# make clean
作為慣例,clean目標一般用于刪除最終生成的可執行文件和在編譯過程中產生的所有目標文件。問題是,如果恰巧有一個名為clean的文件存在時該怎么辦呢?此時因為在這個規則里沒有任何依賴對象,所以目標文件肯定是最新的,規則中的命令無論如何也不會被執行,即使用命令“makeclean”也無濟于事。解決這一問題的方法是標明該規則中的目標是偽目標,并不對應于任何文件。這可以通過.PHONY目標實現。它告訴make不檢查規則的目標文件是否存在于磁盤上,也不查找任何隱含規則,而直接假設指定的目標需要被更新就行了。在使用了.PHONY之后,前面的給出的Makefile文件就將變為如下的內容:
CC = GCC
CFLAGS = -Wall
all : program
program : $(OBJS)
$(CC) $(OBJS) -o $@
control.o : control.c
$(CC) $(CFLAGS) -c -o $@ $<
ui.o : ui.c
$(CC) $(CFLAGS) -c -o $@ $<
main.o : main.c
$(CC) $(CFLAGS) -c -o $@ $<
.PHONY : clean
clean :
$(RM) program $(OBJS)
其它規則
除了可以在Makefile中明確指定規則(顯示規則)之外,make還維護了一整套隱式規則。隱式規則可以在用戶沒有完整地給出某些命令的時候,自動執行恰當的操作。隱式規則最大的好處是可以簡化Makefile的編寫和維護,例如前面給出的Makefile運用隱式規則后可以簡化為如下內容:
program : $(OBJS)
$(CC) $(OBJS) -o $@
.PHONY : clean
clean :
$(RM) program $(OBJS)
默認目標program依賴于control.o、ui.o和main.o三個目標文件,但Makefile中并沒有給出怎樣編譯生成這些目標的規則。此時make就會使用隱式規則,對每一個名為foo.o的目標文件,找到與之對應的源代碼foo.c,然后使用“gcc -cfoo.c -o foo.o”命令來生成對應的目標文件。
除了系統預定義的隱式規則外,在Makefile中還可以定義自己的隱式規則,這種規則也被稱為模式規則。模式規則類似于普通規則,但它的目標必須含有“%”這一通配符,以便能與任何非空字符相匹配,與目標對應的依賴文件中也必須使用通配符,例如下面的規則:
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
上面的規則將告訴make所有形為foo.o的目標文件,都應該根據指定的命令從源文件foo.c編譯而來。
小結
在構建大型的軟件項目時,make是一個優秀的持續集成工具。它對于軟件開發過程來講非常重要。本文介紹了基本的make命令,以及如何編寫簡單實用的Makefile文件,相信用戶已經能夠使用make來管理軟件項目的創建和維護過程了。(T111)