【轉載】跟我一起寫 Makefile
跟我一起寫 Makefile
陳皓 (CSDN)
概述
——
什麼是makefile?或許很多Winodws 的程序員都不知道這個東西,因為那
些Windows 的IDE 都為你做了這個工作,但我覺得要作一個好的和
professional 的程序員,makefile 還是要懂。這就好像現在有這麼多的HTML
的編輯器,但如果你想成為一個專業人士,你還是要了解HTML 的標識的含
義。特別在Unix 下的軟件編譯,你就不能不自己寫makefile 了,會不會寫
makefile,從一個側麵說明了一個人是否具備完成大型工程的能力。
因為,makefile 關係到了整個工程的編譯規則。一個工程中的源文件不計
數,其按類型、功能、模塊分別放在若幹個目錄中,makefile 定義了一係
列的規則來指定,哪些文件需要先編譯,哪些文件需要後編譯,哪些文件需
要重新編譯,甚至於進行更複雜的功能操作,因為makefile 就像一個Shell
腳本一樣,其中也可以執行操作係統的命令。
makefile 帶來的好處就是——“自動化編譯”,一旦寫好,隻需要一個make
命令,整個工程完全自動編譯,極大的提高了軟件開發的效率。make 是一
個命令工具,是一個解釋makefile 中指令的命令工具,一般來說,大多數
的IDE 都有這個命令,比如:Delphi 的make,Visual C++的nmake,Linux
下GNU 的make。可見,makefile 都成為了一種在工程方麵的編譯方法。
現在講述如何寫makefile 的文章比較少,這是我想寫這篇文章的原因。當
然,不同產商的make 各不相同,也有不同的語法,但其本質都是在“文件
依賴性”上做文章,這裏,我僅對GNU 的make 進行講述,我的環境是RedHat
Linux 8.0,make 的版本是3.80。必竟,這個make 是應用最為廣泛的,也
是用得最多的。而且其還是最遵循於IEEE 1003.2-1992 標準的(POSIX.2)。
在這篇文檔中,將以C/C++的源碼作為我們基礎,所以必然涉及一些關於
C/C++的編譯的知識,相關於這方麵的內容,還請各位查看相關的編譯器的
文檔。這裏所默認的編譯器是UNIX 下的GCC 和CC。
關於程序的編譯和鏈接
——————————
在此,我想多說關於程序編譯的一些規範和方法,一般來說,無論是C、C++、
還是pas,首先要把源文件編譯成中間代碼文件,在Windows 下也就是 .obj
文件,UNIX 下是 .o 文件,即 Object File,這個動作叫做編譯(compile)。
然後再把大量的Object File 合成執行文件,這個動作叫作鏈接(link)。
編譯時,編譯器需要的是語法的正確,函數與變量的聲明的正確。對於後者,
通常是你需要告訴編譯器頭文件的所在位置(頭文件中應該隻是聲明,而定
義應該放在C/C++文件中),隻要所有的語法正確,編譯器就可以編譯出中
間目標文件。一般來說,每個源文件都應該對應於一個中間目標文件(O 文
件或是OBJ 文件)。
鏈接時,主要是鏈接函數和全局變量,所以,我們可以使用這些中間目標文
件(O 文件或是OBJ 文件)來鏈接我們的應用程序。鏈接器並不管函數所在
的源文件,隻管函數的中間目標文件(Object File),在大多數時候,由於
源文件太多,編譯生成的中間目標文件太多,而在鏈接時需要明顯地指出中
間目標文件名,這對於編譯很不方便,所以,我們要給中間目標文件打個包,
在Windows 下這種包叫“庫文件”(Library File),也就是 .lib 文件,在
UNIX 下,是Archive File,也就是 .a 文件。
總結一下,源文件首先會生成中間目標文件,再由中間目標文件生成執行文
件。在編譯時,編譯器隻檢測程序語法,和函數、變量是否被聲明。如果函
數未被聲明,編譯器會給出一個警告,但可以生成Object File。而在鏈接
程序時,鏈接器會在所有的Object File 中找尋函數的實現,如果找不到,
那到就會報鏈接錯誤碼(Linker Error),在VC 下,這種錯誤一般是:Link
2001 錯誤,意思說是說,鏈接器未能找到函數的實現。你需要指定函數的
Object File.
好,言歸正傳,GNU 的make 有許多的內容,閑言少敘,還是讓我們開始吧。
Makefile 介紹
———————
make 命令執行時,需要一個 Makefile 文件,以告訴make 命令需要怎麼樣
的去編譯和鏈接程序。
首先,我們用一個示例來說明Makefile 的書寫規則。以便給大家一個感興
認識。這個示例來源於GNU 的make 使用手冊,在這個示例中,我們的工程
有8 個C 文件,和3 個頭文件,我們要寫一個Makefile 來告訴make 命令如
何編譯和鏈接這幾個文件。我們的規則是:
1)如果這個工程沒有編譯過,那麼我們的所有C 文件都要編譯並被鏈接。
2)如果這個工程的某幾個C 文件被修改,那麼我們隻編譯被修改的C 文件,
並鏈接目標程序。
3)如果這個工程的頭文件被改變了,那麼我們需要編譯引用了這幾個頭文
件的C 文件,並鏈接目標程序。
隻要我們的Makefile 寫得夠好,所有的這一切,我們隻用一個make 命令就
可以完成,make 命令會自動智能地根據當前的文件修改的情況來確定哪些
文件需要重編譯,從而自己編譯所需要的文件和鏈接目標程序。
一、Makefile 的規則
在講述這個Makefile 之前,還是讓我們先來粗略地看一看Makefile 的規則。
target ... : prerequisites ...
command
...
...
target 也就是一個目標文件,可以是Object File,也可以是執行文件。還
可以是一個標簽(Label),對於標簽這種特性,在後續的“偽目標”章節中
會有敘述。
prerequisites 就是,要生成那個target 所需要的文件或是目標。
command 也就是make 需要執行的命令。(任意的Shell 命令)
這是一個文件的依賴關係,也就是說,target 這一個或多個的目標文件依
賴於prerequisites 中的文件,其生成規則定義在command 中。說白一點就
是說,prerequisites 中如果有一個以上的文件比target 文件要新的話,
command 所定義的命令就會被執行。這就是Makefile 的規則。也就是
Makefile 中最核心的內容。
說到底,Makefile 的東西就是這樣一點,好像我的這篇文檔也該結束了。
嗬嗬。還不盡然,這是Makefile 的主線和核心,但要寫好一個Makefile 還
不夠,我會以後麵一點一點地結合我的工作經驗給你慢慢到來。內容還多著
呢。:)
二、一個示例
正如前麵所說的,如果一個工程有3 個頭文件,和8 個C 文件,我們為了完
成前麵所述的那三個規則,我們的Makefile 應該是下麵的這個樣子的。
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
反斜杠(\)是換行符的意思。這樣比較便於Makefile 的易讀。我們可以把
這個內容保存在文件為“Makefile”或“makefile”的文件中,然後在該目
錄下直接輸入命令“make”就可以生成執行文件edit。如果要刪除執行文
件和所有的中間目標文件,那麼,隻要簡單地執行一下“make clean”就可
以了。
在這個makefile 中,目標文件(target)包含:執行文件edit 和中間目標
文件(*.o),依賴文件(prerequisites)就是冒號後麵的那些 .c 文件和 .h
文件。每一個 .o 文件都有一組依賴文件,而這些 .o 文件又是執行文件
edit 的依賴文件。依賴關係的實質上就是說明了目標文件是由哪些文件生
成的,換言之,目標文件是哪些文件更新的。
在定義好依賴關係後,後續的那一行定義了如何生成目標文件的操作係統命
令,一定要以一個Tab 鍵作為開頭。記住,make 並不管命令是怎麼工作的,
他隻管執行所定義的命令。make 會比較targets 文件和prerequisites 文
件的修改日期,如果prerequisites 文件的日期要比targets 文件的日期要
新,或者target 不存在的話,那麼,make 就會執行後續定義的命令。
這裏要說明一點的是,clean 不是一個文件,它隻不過是一個動作名字,有
點像C 語言中的lable 一樣,其冒號後什麼也沒有,那麼,make 就不會自
動去找文件的依賴性,也就不會自動執行其後所定義的命令。要執行其後的
命令,就要在make 命令後明顯得指出這個lable 的名字。這樣的方法非常
有用,我們可以在一個makefile 中定義不用的編譯或是和編譯無關的命令,
比如程序的打包,程序的備份,等等。
三、make 是如何工作的
在默認的方式下,也就是我們隻輸入make 命令。那麼,
1、make 會在當前目錄下找名字叫“Makefile”或“makefile”的文件。
2、如果找到,它會找文件中的第一個目標文件(target),在上麵的例子中,
他會找到“edit”這個文件,並把這個文件作為最終的目標文件。
3、如果edit 文件不存在,或是edit 所依賴的後麵的 .o 文件的文件修改
時間要比edit 這個文件新,那麼,他就會執行後麵所定義的命令來生成edit
這個文件。
4、如果edit 所依賴的.o 文件也存在,那麼make 會在當前文件中找目標為.o
文件的依賴性,如果找到則再根據那一個規則生成.o 文件。(這有點像一個
堆棧的過程)
5、當然,你的C 文件和H 文件是存在的啦,於是make 會生成 .o 文件,然
後再用 .o 文件生命make 的終極任務,也就是執行文件edit 了。
這就是整個make 的依賴性,make 會一層又一層地去找文件的依賴關係,直
到最終編譯出第一個目標文件。在找尋的過程中,如果出現錯誤,比如最後
被依賴的文件找不到,那麼make 就會直接退出,並報錯,而對於所定義的
命令的錯誤,或是編譯不成功,make 根本不理。make 隻管文件的依賴性,
即,如果在我找了依賴關係之後,冒號後麵的文件還是不在,那麼對不起,
我就不工作啦。
通過上述分析,我們知道,像clean 這種,沒有被第一個目標文件直接或間
接關聯,那麼它後麵所定義的命令將不會被自動執行,不過,我們可以顯示
要make 執行。即命令——“make clean”,以此來清除所有的目標文件,以
便重編譯。
於是在我們編程中,如果這個工程已被編譯過了,當我們修改了其中一個源
文件,比如file.c,那麼根據我們的依賴性,我們的目標file.o 會被重編
譯(也就是在這個依性關係後麵所定義的命令),於是file.o 的文件也是最
新的啦,於是file.o 的文件修改時間要比edit 要新,所以edit 也會被重
新鏈接了(詳見edit 目標文件後定義的命令)。
而如果我們改變了“command.h”,那麼,kdb.o、command.o 和files.o 都
會被重編譯,並且,edit 會被重鏈接。
四、makefile 中使用變量
在上麵的例子中,先讓我們看看edit 的規則:
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
我們可以看到[.o]文件的字符串被重複了兩次,如果我們的工程需要加入一
個新的[.o]文件,那麼我們需要在兩個地方加(應該是三個地方,還有一個
地方在clean 中)。當然,我們的makefile 並不複雜,所以在兩個地方加也
不累,但如果makefile 變得複雜,那麼我們就有可能會忘掉一個需要加入
的地方,而導致編譯失敗。所以,為了makefile 的易維護,在makefile 中
我們可以使用變量。makefile 的變量也就是一個字符串,理解成C 語言中
的宏可能會更好。
比如,我們聲明一個變量,叫objects, OBJECTS, objs, OBJS, obj, 或是
OBJ,反正不管什麼啦,隻要能夠表示obj 文件就行了。我們在makefile 一
開始就這樣定義:
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
於是,我們就可以很方便地在我們的makefile 中以“$(objects)”的方式
來使用這個變量了,於是我們的改良版makefile 就變成下麵這個樣子:
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects)
於是如果有新的 .o 文件加入,我們隻需簡單地修改一下 objects 變量就
可以了。
關於變量更多的話題,我會在後續給你一一道來。
五、讓make 自動推導
GNU 的make 很強大,它可以自動推導文件以及文件依賴關係後麵的命令,
於是我們就沒必要去在每一個[.o]文件後都寫上類似的命令,因為,我們的
make 會自動識別,並自己推導命令。
隻要make 看到一個[.o]文件,它就會自動的把[.c]文件加在依賴關係中,
如果make 找到一個whatever.o,那麼whatever.c,就會是whatever.o 的
依賴文件。並且 cc -c whatever.c 也會被推導出來,於是,我們的makefile
再也不用寫得這麼複雜。我們的是新的makefile 又出爐了。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)
這種方法,也就是make 的“隱晦規則”。上麵文件內容中,“.PHONY”表示,
clean 是個偽目標文件。
關於更為詳細的“隱晦規則”和“偽目標文件”,我會在後續給你一一道來。
六、另類風格的makefile
即然我們的make 可以自動推導命令,那麼我看到那堆[.o]和[.h]的依賴就
有點不爽,那麼多的重複的[.h],能不能把其收攏起來,好吧,沒有問題,
這個對於make 來說很容易,誰叫它提供了自動推導命令和文件的功能呢?
來看看最新風格的makefile 吧。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h
.PHONY : clean
clean :
rm edit $(objects)
這種風格,讓我們的makefile 變得很簡單,但我們的文件依賴關係就顯得
有點淩亂了。魚和熊掌不可兼得。還看你的喜好了。我是不喜歡這種風格的,
一是文件的依賴關係看不清楚,二是如果文件一多,要加入幾個新的.o 文
件,那就理不清楚了。
七、清空目標文件的規則
每個Makefile 中都應該寫一個清空目標文件(.o 和執行文件)的規則,這
不僅便於重編譯,也很利於保持文件的清潔。這是一個“修養”(嗬嗬,還
記得我的《編程修養》嗎)。一般的風格都是:
clean:
rm edit $(objects)
更為穩健的做法是:
.PHONY : clean
clean :
-rm edit $(objects)
前麵說過,.PHONY 意思表示clean 是一個“偽目標”,。而在rm 命令前麵加
了一個小減號的意思就是,也許某些文件出現問題,但不要管,繼續做後麵
的事。當然,clean 的規則不要放在文件的開頭,不然,這就會變成make
的默認目標,相信誰也不願意這樣。不成文的規矩是——“clean 從來都是
放在文件的最後”。
上麵就是一個makefile 的概貌,也是makefile 的基礎,下麵還有很多
makefile 的相關細節,準備好了嗎?準備好了就來。
Makefile 總述
———————
一、Makefile 裏有什麼?
Makefile 裏主要包含了五個東西:顯式規則、隱晦規則、變量定義、文件
指示和注釋。
1、顯式規則。顯式規則說明了,如何生成一個或多的的目標文件。這是由
Makefile 的書寫者明顯指出,要生成的文件,文件的依賴文件,生成的命
令。
2、隱晦規則。由於我們的make 有自動推導的功能,所以隱晦的規則可以讓
我們比較粗糙地簡略地書寫Makefile,這是由make 所支持的。
3、變量的定義。在Makefile 中我們要定義一係列的變量,變量一般都是字
符串,這個有點你C 語言中的宏,當Makefile 被執行時,其中的變量都會
被擴展到相應的引用位置上。
4、文件指示。其包括了三個部分,一個是在一個Makefile 中引用另一個
Makefile,就像C 語言中的include 一樣;另一個是指根據某些情況指定
Makefile 中的有效部分,就像C 語言中的預編譯#if 一樣;還有就是定義一
個多行的命令。有關這一部分的內容,我會在後續的部分中講述。
5、注釋。Makefile 中隻有行注釋,和UNIX 的Shell 腳本一樣,其注釋是
用“#”字符,這個就像C/C++中的“//”一樣。如果你要在你的Makefile
中使用“#”字符,可以用反斜框進行轉義,如:“\#”。
最後,還值得一提的是,在Makefile 中的命令,必須要以[Tab]鍵開始。
二、Makefile 的文件名
默認的情況下, make 命令會在當前目錄下按順序找尋文件名為
“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解釋這個文件。
在這三個文件名中,最好使用“Makefile”這個文件名,因為,這個文件名
第一個字符為大寫,這樣有一種顯目的感覺。最好不要用“GNUmakefile”,
這個文件是GNU 的make 識別的。有另外一些make 隻對全小寫的“makefile”
文件名敏感,但是基本上來說,大多數的make 都支持“makefile”和
“Makefile”這兩種默認文件名。
當然,你可以使用別的文件名來書寫Makefile,比如:“Make.Linux”,
“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以
使用make 的“-f”和“--file”參數,如:make -f Make.Linux 或make --file
Make.AIX。
三、引用其它的Makefile
在Makefile 使用include 關鍵字可以把別的Makefile 包含進來,這很像C
語言的#include,被包含的文件會原模原樣的放在當前文件的包含位置。
include 的語法是:
include <filename>
filename 可以是當前操作係統Shell 的文件模式(可以保含路徑和通配符)
在include 前麵可以有一些空字符,但是絕不能是[Tab]鍵開始。include
和<filename>可以用一個或多個空格隔開。舉個例子,你有這樣幾個
Makefile:a.mk、b.mk、c.mk,還有一個文件叫foo.make,以及一個變量
$(bar),其包含了e.mk 和f.mk,那麼,下麵的語句:
include foo.make *.mk $(bar)
等價於:
include foo.make a.mk b.mk c.mk e.mk f.mk
make 命令開始時,會把找尋include 所指出的其它Makefile,並把其內容
安置在當前的位置。就好像C/C++的#include 指令一樣。如果文件都沒有指
定絕對路徑或是相對路徑的話,make 會在當前目錄下首先尋找,如果當前
目錄下沒有找到,那麼,make 還會在下麵的幾個目錄下找:
1、如果make 執行時,有“-I”或“--include-dir”參數,那麼make 就會
在這個參數所指定的目錄下去尋找。
2、如果目錄<prefix>/include(一般是:/usr/local/bin 或/usr/include)
存在的話,make 也會去找。
如果有文件沒有找到的話,make 會生成一條警告信息,但不會馬上出現致
命錯誤。它會繼續載入其它的文件,一旦完成makefile 的讀取,make 會再
重試這些沒有找到,或是不能讀取的文件,如果還是不行,make 才會出現
一條致命信息。如果你想讓make 不理那些無法讀取的文件,而繼續執行,
你可以在include 前加一個減號“-”。如:
-include <filename>
其表示,無論include 過程中出現什麼錯誤,都不要報錯繼續執行。和其它
版本make 兼容的相關命令是sinclude,其作用和這一個是一樣的。
四、環境變量 MAKEFILES
如果你的當前環境中定義了環境變量MAKEFILES,那麼,make 會把這個變量
中的值做一個類似於include 的動作。這個變量中的值是其它的Makefile,
用空格分隔。隻是,它和include 不同的是,從這個環境變中引入的Makefile
的“目標”不會起作用,如果環境變量中定義的文件發現錯誤,make 也會
不理。
但是在這裏我還是建議不要使用這個環境變量,因為隻要這個變量一被定
義,那麼當你使用make 時,所有的Makefile 都會受到它的影響,這絕不是
你想看到的。在這裏提這個事,隻是為了告訴大家,也許有時候你的Makefile
出現了怪事,那麼你可以看看當前環境中有沒有定義這個變量。
五、make 的工作方式
GNU 的make 工作時的執行步驟入下:(想來其它的make 也是類似)
1、讀入所有的Makefile。
2、讀入被include 的其它Makefile。
3、初始化文件中的變量。
4、推導隱晦規則,並分析所有規則。
5、為所有的目標文件創建依賴關係鏈。
6、根據依賴關係,決定哪些目標要重新生成。
7、執行生成命令。
1-5 步為第一個階段,6-7 為第二個階段。第一個階段中,如果定義的變量
被使用了,那麼,make 會把其展開在使用的位置。但make 並不會完全馬上
展開,make 使用的是拖延戰術,如果變量出現在依賴關係的規則中,那麼
僅當這條依賴被決定要使用了,變量才會在其內部展開。
當然,這個工作方式你不一定要清楚,但是知道這個方式你也會對make 更
為熟悉。有了這個基礎,後續部分也就容易看懂了。
書寫規則
————
規則包含兩個部分,一個是依賴關係,一個是生成目標的方法。
在Makefile 中,規則的順序是很重要的,因為,Makefile 中隻應該有一個
最終目標,其它的目標都是被這個目標所連帶出來的,所以一定要讓make
知道你的最終目標是什麼。一般來說,定義在Makefile 中的目標可能會有
很多,但是第一條規則中的目標將被確立為最終的目標。如果第一條規則中
的目標有很多個,那麼,第一個目標會成為最終的目標。make 所完成的也
就是這個目標。
好了,還是讓我們來看一看如何書寫規則。
一、規則舉例
foo.o : foo.c defs.h # foo 模塊
cc -c -g foo.c
看到這個例子,各位應該不是很陌生了,前麵也已說過,foo.o 是我們的目
標,foo.c 和defs.h 是目標所依賴的源文件,而隻有一個命令“cc -c -g
foo.c”(以Tab 鍵開頭)。這個規則告訴我們兩件事:
1、文件的依賴關係,foo.o 依賴於foo.c 和defs.h 的文件,如果foo.c 和
defs.h 的文件日期要比foo.o 文件日期要新,或是foo.o 不存在,那麼依
賴關係發生。
2、如果生成(或更新)foo.o 文件。也就是那個cc 命令,其說明了,如何
生成foo.o 這個文件。(當然foo.c 文件include 了defs.h 文件)
二、規則的語法
targets : prerequisites
command
...
或是這樣:
targets : prerequisites ; command
command
...
targets 是文件名,以空格分開,可以使用通配符。一般來說,我們的目標
基本上是一個文件,但也有可能是多個文件。
command 是命令行,如果其不與“targetrerequisites”在一行,那麼,必
須以[Tab 鍵]開頭,如果和prerequisites 在一行,那麼可以用分號做為分
隔。(見上)
prerequisites 也就是目標所依賴的文件(或依賴目標)。如果其中的某個
文件要比目標文件要新,那麼,目標就被認為是“過時的”,被認為是需要
重生成的。這個在前麵已經講過了。
如果命令太長,你可以使用反斜框(‘\’)作為換行符。make 對一行上有多
少個字符沒有限製。規則告訴make 兩件事,文件的依賴關係和如何成成目
標文件。
一般來說,make 會以UNIX 的標準Shell,也就是/bin/sh 來執行命令。
三、在規則中使用通配符
如果我們想定義一係列比較類似的文件,我們很自然地就想起使用通配符。
make 支持三各通配符:“*”,“?”和“[...]”。這是和Unix 的B-Shell 是
相同的。
波浪號(“~”)字符在文件名中也有比較特殊的用途。如果是“~/test”,這
就表示當前用戶的$HOME 目錄下的test 目錄。而“~hchen/test”則表示用
戶hchen 的宿主目錄下的test 目錄。(這些都是Unix 下的小知識了,make
也支持)而在Windows 或是MS-DOS 下,用戶沒有宿主目錄,那麼波浪號所
指的目錄則根據環境變量“HOME”而定。
通配符代替了你一係列的文件,如“*.c”表示所以後綴為c 的文件。一個
需要我們注意的是,如果我們的文件名中有通配符,如:“*”,那麼可以用
轉義字符“\”,如“\*”來表示真實的“*”字符,而不是任意長度的字符
串。
好吧,還是先來看幾個例子吧:
clean:
rm -f *.o
上麵這個例子我不不多說了,這是操作係統Shell 所支持的通配符。這是在
命令中的通配符。
print: *.c
lpr -p $?
touch print
上麵這個例子說明了通配符也可以在我們的規則中,目標print 依賴於所有
的[.c]文件。其中的“$?”是一個自動化變量,我會在後麵給你講述。
objects = *.o
上麵這個例子,表示了,通符同樣可以用在變量中。並不是說[*.o]會展開,
不!objects 的值就是“*.o”。Makefile 中的變量其實就是C/C++中的宏。
如果你要讓通配符在變量中展開,也就是讓objects 的值是所有[.o]的文件
名的集合,那麼,你可以這樣:
objects := $(wildcard *.o)
這種用法由關鍵字“wildcard”指出,關於Makefile 的關鍵字,我們將在
後麵討論。
四、文件搜尋
在一些大的工程中,有大量的源文件,我們通常的做法是把這許多的源文件
分類,並存放在不同的目錄中。所以,當make 需要去找尋文件的依賴關係
時,你可以在文件前加上路徑,但最好的方法是把一個路徑告訴make,讓
make 在自動去找。
Makefile 文件中的特殊變量“VPATH”就是完成這個功能的,如果沒有指明
這個變量,make 隻會在當前的目錄中去找尋依賴文件和目標文件。如果定
義了這個變量,那麼,make 就會在當當前目錄找不到的情況下,到所指定
的目錄中去找尋文件了。
VPATH = src:../headers
上麵的的定義指定兩個目錄,“src”和“../headers”,make 會按照這個順
序進行搜索。目錄由“冒號”分隔。(當然,當前目錄永遠是最高優先搜索
的地方)
另一個設置文件搜索路徑的方法是使用make 的“vpath”關鍵字(注意,它
是全小寫的),這不是變量,這是一個make 的關鍵字,這和上麵提到的那個
VPATH 變量很類似,但是它更為靈活。它可以指定不同的文件在不同的搜索
目錄中。這是一個很靈活的功能。它的使用方法有三種:
1、vpath <pattern> <directories>
為符合模式<pattern>的文件指定搜索目錄<directories>。
2、vpath <pattern>
清除符合模式<pattern>的文件的搜索目錄。
3、vpath
清除所有已被設置好了的文件搜索目錄。
vapth 使用方法中的<pattern>需要包含“%”字符。“%”的意思是匹配零或
若幹字符,例如,“%.h”表示所有以“.h”結尾的文件。<pattern>指定了
要搜索的文件集,而<directories>則指定了<pattern>的文件集的搜索的目
錄。例如:
vpath %.h ../headers
該語句表示,要求make 在“../headers”目錄下搜索所有以“.h”結尾的
文件。(如果某文件在當前目錄沒有找到的話)
我們可以連續地使用vpath 語句,以指定不同搜索策略。如果連續的vpath
語句中出現了相同的<pattern>,或是被重複了的<pattern>,那麼,make
會按照vpath 語句的先後順序來執行搜索。如:
vpath %.c foo
vpath % blish
vpath %.c bar
其表示“.c”結尾的文件,先在“foo”目錄,然後是“blish”,最後是“bar”
目錄。
vpath %.c foo:bar
vpath % blish
而上麵的語句則表示“.c”結尾的文件,先在“foo”目錄,然後是“bar”
目錄,最後才是“blish”目錄。
五、偽目標
最早先的一個例子中,我們提到過一個“clean”的目標,這是一個“偽目
標”,
clean:
rm *.o temp
正像我們前麵例子中的“clean”一樣,即然我們生成了許多文件編譯文件,
我們也應該提供一個清除它們的“目標”以備完整地重編譯而用。 (以“make
clean”來使用該目標)
因為,我們並不生成“clean”這個文件。“偽目標”並不是一個文件,隻是
一個標簽,由於“偽目標”不是文件,所以make 無法生成它的依賴關係和
決定它是否要執行。我們隻有通過顯示地指明這個“目標”才能讓其生效。
當然,“偽目標”的取名不能和文件名重名,不然其就失去了“偽目標”的
意義了。
當然,為了避免和文件重名的這種情況,我們可以使用一個特殊的標記
“.PHONY”來顯示地指明一個目標是“偽目標”,向make 說明,不管是否有
這個文件,這個目標就是“偽目標”。
.PHONY : clean
隻要有這個聲明,不管是否有“clean”文件,要運行“clean”這個目標,
隻有“make clean”這樣。於是整個過程可以這樣寫:
.PHONY: clean
clean:
rm *.o temp
偽目標一般沒有依賴的文件。但是,我們也可以為偽目標指定所依賴的文件。
偽目標同樣可以作為“默認目標”,隻要將其放在第一個。一個示例就是,
如果你的Makefile 需要一口氣生成若幹個可執行文件,但你隻想簡單地敲
一個make 完事,並且,所有的目標文件都寫在一個Makefile 中,那麼你可
以使用“偽目標”這個特性:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
我們知道,Makefile 中的第一個目標會被作為其默認目標。我們聲明了一
個“all”的偽目標,其依賴於其它三個目標。由於偽目標的特性是,總是
被執行的,所以其依賴的那三個目標就總是不如“all”這個目標新。所以,
其它三個目標的規則總是會被決議。也就達到了我們一口氣生成多個目標的
目的。“.PHONY : all”聲明了“all”這個目標為“偽目標”。
隨便提一句,從上麵的例子我們可以看出,目標也可以成為依賴。所以,偽
目標同樣也可成為依賴。看下麵的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
“make clean”將清除所有要被清除的文件。“cleanobj”和“cleandiff”
這兩個偽目標有點像“子程序”的意思。我們可以輸入“make cleanall”
和“make cleanobj”和“make cleandiff”命令來達到清除不同種類文件
的目的。
六、多目標
Makefile 的規則中的目標可以不止一個,其支持多目標,有可能我們的多
個目標同時依賴於一個文件,並且其生成的命令大體類似。於是我們就能把
其合並起來。當然,多個目標的生成規則的執行命令是同一個,這可能會可
我們帶來麻煩,不過好在我們的可以使用一個自動化變量“$@”(關於自動
化變量,將在後麵講述),這個變量表示著目前規則中所有的目標的集合,
這樣說可能很抽象,還是看一個例子吧。
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述規則等價於:
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
其中,-$(subst output,,$@)中的“$”表示執行一個Makefile 的函數,函
數名為subst,後麵的為參數。關於函數,將在後麵講述。這裏的這個函數
是截取字符串的意思,“$@”表示目標的集合,就像一個數組,“$@”依次取
出目標,並執於命令。
七、靜態模式
靜態模式可以更加容易地定義多目標的規則,可以讓我們的規則變得更加的
有彈性和靈活。我們還是先來看一下語法:
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
...
targets 定義了一係列的目標文件,可以有通配符。是目標的一個集合。
target-parrtern 是指明了targets 的模式,也就是的目標集模式。
prereq-parrterns 是目標的依賴模式,它對target-parrtern 形成的模式
再進行一次依賴目標的定義。
這樣描述這三個東西,可能還是沒有說清楚,還是舉個例子來說明一下吧。
如果我們的<target-parrtern>定義成“%.o”,意思是我們的<target>集合
中都是以“.o”結尾的,而如果我們的<prereq-parrterns>定義成“%.c”,
意思是對<target-parrtern>所形成的目標集進行二次定義,其計算方法是,
取<target-parrtern>模式中的“%”(也就是去掉了[.o]這個結尾),並為其
加上[.c]這個結尾,形成的新集合。
所以,我們的“目標模式”或是“依賴模式”中都應該有“%”這個字符,
如果你的文件名中有“%”那麼你可以使用反斜杠“\”進行轉義,來標明真
實的“%”字符。
看一個例子:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
上麵的例子中,指明了我們的目標從$object 中獲取,“%.o”表明要所有以
“.o”結尾的目標,也就是“foo.o bar.o”,也就是變量$object 集合的模
式,而依賴模式“%.c”則取模式“%.o”的“%”,也就是“foo bar”,並為
其加下“.c”的後綴,於是,我們的依賴目標就是“foo.c bar.c”。而命令
中的“$<”和“$@”則是自動化變量,“$<”表示所有的依賴目標集(也就
是“foo.c bar.c”),“$@”表示目標集(也就是“foo.o bar.o”)。於是,
上麵的規則展開後等價於下麵的規則:
foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
試想,如果我們的“%.o”有幾百個,那種我們隻要用這種很簡單的“靜態
模式規則”就可以寫完一堆規則,實在是太有效率了。“靜態模式規則”的
用法很靈活,如果用得好,那會一個很強大的功能。再看一個例子:
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
$(filter %.o,$(files))表示調用Makefile 的filter 函數,過濾“$filter”
集,隻要其中模式為“%.o”的內容。其的它內容,我就不用多說了吧。這
個例字展示了Makefile 中更大的彈性。
八、自動生成依賴性
在Makefile 中,我們的依賴關係可能會需要包含一係列的頭文件,比如,
如果我們的main.c 中有一句“#include "defs.h"”,那麼我們的依賴關係
應該是:
main.o : main.c defs.h
但是,如果是一個比較大型的工程,你必需清楚哪些C 文件包含了哪些頭文
件,並且,你在加入或刪除頭文件時,也需要小心地修改Makefile,這是
一個很沒有維護性的工作。為了避免這種繁重而又容易出錯的事情,我們可
以使用C/C++編譯的一個功能。大多數的C/C++編譯器都支持一個“-M”的
選項,即自動找尋源文件中包含的頭文件,並生成一個依賴關係。例如,如
果我們執行下麵的命令:
cc -M main.c
其輸出是:
main.o : main.c defs.h
於是由編譯器自動生成的依賴關係,這樣一來,你就不必再手動書寫若幹文
件的依賴關係,而由編譯器自動生成了。需要提醒一句的是,如果你使用
GNU 的C/C++編譯器,你得用“-MM”參數,不然,“-M”參數會把一些標準
庫的頭文件也包含進來。
gcc -M main.c 的輸出是:
main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h
\
/usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \
/usr/include/bits/types.h /usr/include/bits/pthreadtypes.h \
/usr/include/bits/sched.h /usr/include/libio.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/include/bits/wchar.h /usr/include/gconv.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \
/usr/include/bits/stdio_lim.h
gcc -MM main.c 的輸出則是:
main.o: main.c defs.h
那麼,編譯器的這個功能如何與我們的Makefile 聯係在一起呢。因為這樣
一來,我們的Makefile 也要根據這些源文件重新生成,讓Makefile 自已依
賴於源文件?這個功能並不現實,不過我們可以有其它手段來迂回地實現這
一功能。GNU 組織建議把編譯器為每一個源文件的自動生成的依賴關係放到
一個文件中,為每一個“name.c”的文件都生成一個“name.d”的Makefile
文件,[.d]文件中就存放對應[.c]文件的依賴關係。
於是,我們可以寫出[.c]文件和[.d]文件的依賴關係,並讓make 自動更新
或自成[.d]文件,並把其包含在我們的主Makefile 中,這樣,我們就可以
自動化地生成每個文件的依賴關係了。
這裏,我們給出了一個模式規則來產生[.d]文件:
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
這個規則的意思是,所有的[.d]文件依賴於[.c]文件,“rm -f $@”的意思
是刪除所有的目標,也就是[.d]文件,第二行的意思是,為每個依賴文件
“$<”,也就是[.c]文件生成依賴文件,“$@”表示模式“%.d”文件,如果
有一個C 文件是name.c,那麼“%”就是“name”,“$$$$”意為一個隨機編
號,第二行生成的文件有可能是“name.d.12345”,第三行使用sed 命令做
了一個替換,關於sed 命令的用法請參看相關的使用文檔。第四行就是刪除
臨時文件。
總而言之,這個模式要做的事就是在編譯器生成的依賴關係中加入[.d]文件
的依賴,即把依賴關係:
main.o : main.c defs.h
轉成:
main.o main.d : main.c defs.h
於是,我們的[.d]文件也會自動更新了,並會自動生成了,當然,你還可以
在這個[.d]文件中加入的不隻是依賴關係,包括生成的命令也可一並加入,
讓每個[.d]文件都包含一個完賴的規則。一旦我們完成這個工作,接下來,
我們就要把這些自動生成的規則放進我們的主Makefile 中。我們可以使用
Makefile 的“include”命令,來引入別的Makefile 文件(前麵講過),例
如:
sources = foo.c bar.c
include $(sources:.c=.d)
上述語句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做一個替換,
把變量$(sources)所有[.c]的字串都替換成[.d],關於這個“替換”的內容,
在後麵我會有更為詳細的講述。當然,你得注意次序,因為include 是按次
來載入文件,最先載入的[.d]文件中的目標會成為默認目標。
書寫命令
————
每條規則中的命令和操作係統Shell 的命令行是一致的。make 會一按順序
一條一條的執行命令,每條命令的開頭必須以[Tab]鍵開頭,除非,命令是
緊跟在依賴規則後麵的分號後的。在命令行之間中的空格或是空行會被忽
略,但是如果該空格或空行是以Tab 鍵開頭的,那麼make 會認為其是一個
空命令。
我們在UNIX 下可能會使用不同的Shell,但是make 的命令默認是被
“/bin/sh”——UNIX 的標準Shell 解釋執行的。除非你特別指定一個其它
的Shell。Makefile 中,“#”是注釋符,很像C/C++中的“//”,其後的本
行字符都被注釋。
一、顯示命令
通常,make 會把其要執行的命令行在命令執行前輸出到屏幕上。當我們用
“@”字符在命令行前,那麼,這個命令將不被make 顯示出來,最具代表性
的例子是,我們用這個功能來像屏幕顯示一些信息。如:
@echo 正在編譯XXX 模塊......
當make 執行時,會輸出“正在編譯XXX 模塊......”字串,但不會輸出命
令,如果沒有“@”,那麼,make 將輸出:
echo 正在編譯XXX 模塊......
正在編譯XXX 模塊......
如果make 執行時,帶入make 參數“-n”或“--just-print”,那麼其隻是
顯示命令,但不會執行命令,這個功能很有利於我們調試我們的Makefile,
看看我們書寫的命令是執行起來是什麼樣子的或是什麼順序的。
而make 參數“-s”或“--slient”則是全麵禁止命令的顯示。
二、命令執行
當依賴目標新於目標時,也就是當規則的目標需要被更新時,make 會一條
一條的執行其後的命令。需要注意的是,如果你要讓上一條命令的結果應用
在下一條命令時,你應該使用分號分隔這兩條命令。比如你的第一條命令是
cd 命令,你希望第二條命令得在cd 之後的基礎上運行,那麼你就不能把這
兩條命令寫在兩行上,而應該把這兩條命令寫在一行上,用分號分隔。如:
示例一:
exec:
cd /home/hchen
pwd
示例二:
exec:
cd /home/hchen; pwd
當我們執行“make exec”時,第一個例子中的cd 沒有作用,pwd 會打印出
當前的Makefile 目錄,而第二個例子中,cd 就起作用了,pwd 會打印出
“/home/hchen”。
make 一般是使用環境變量SHELL 中所定義的係統Shell 來執行命令,默認
情況下使用UNIX 的標準Shell——/bin/sh 來執行命令。但在MS-DOS 下有
點特殊,因為MS-DOS 下沒有SHELL 環境變量,當然你也可以指定。如果你
指定了UNIX 風格的目錄形式,首先,make 會在SHELL 所指定的路徑中找尋
命令解釋器,如果找不到,其會在當前盤符中的當前目錄中尋找,如果再找
不到,其會在PATH 環境變量中所定義的所有路徑中尋找。MS-DOS 中,如果
你定義的命令解釋器沒有找到,其會給你的命令解釋器加上諸如“.exe”、
“.com”、“.bat”、“.sh”等後綴。
三、命令出錯
每當命令運行完後,make 會檢測每個命令的返回碼,如果命令返回成功,
那麼make 會執行下一條命令,當規則中所有的命令成功返回後,這個規則
就算是成功完成了。如果一個規則中的某個命令出錯了(命令退出碼非零),
那麼make 就會終止執行當前規則,這將有可能終止所有規則的執行。
有些時候,命令的出錯並不表示就是錯誤的。例如mkdir 命令,我們一定需
要建立一個目錄,如果目錄不存在,那麼mkdir 就成功執行,萬事大吉,如
果目錄存在,那麼就出錯了。我們之所以使用mkdir 的意思就是一定要有這
樣的一個目錄,於是我們就不希望mkdir 出錯而終止規則的運行。
為了做到這一點,忽略命令的出錯,我們可以在Makefile 的命令行前加一
個減號“-”(在Tab 鍵之後),標記為不管命令出不出錯都認為是成功的。
如:
clean:
-rm -f *.o
還有一個全局的辦法是,給make 加上“-i”或是“--ignore-errors”參數,
那麼,Makefile 中所有命令都會忽略錯誤。而如果一個規則是以“.IGNORE”
作為目標的,那麼這個規則中的所有命令將會忽略錯誤。這些是不同級別的
防止命令出錯的方法,你可以根據你的不同喜歡設置。
還有一個要提一下的make 的參數的是“-k”或是“--keep-going”,這個參
數的意思是,如果某規則中的命令出錯了,那麼就終目該規則的執行,但繼
續執行其它規則。
四、嵌套執行make
在一些大的工程中,我們會把我們不同模塊或是不同功能的源文件放在不同
的目錄中,我們可以在每個目錄中都書寫一個該目錄的Makefile,這有利
於讓我們的Makefile 變得更加地簡潔,而不至於把所有的東西全部寫在一
個Makefile 中,這樣會很難維護我們的Makefile,這個技術對於我們模塊
編譯和分段編譯有著非常大的好處。
例如,我們有一個子目錄叫subdir,這個目錄下有個Makefile 文件,來指
明了這個目錄下文件的編譯規則。那麼我們總控的Makefile 可以這樣書寫:
subsystem:
cd subdir && $(MAKE)
其等價於:
subsystem:
$(MAKE) -C subdir
定義$(MAKE)宏變量的意思是,也許我們的make 需要一些參數,所以定義成
一個變量比較利於維護。這兩個例子的意思都是先進入“subdir”目錄,然
後執行make 命令。
我們把這個Makefile 叫做“總控Makefile”,總控Makefile 的變量可以傳
遞到下級的Makefile 中(如果你顯示的聲明),但是不會覆蓋下層的
Makefile 中所定義的變量,除非指定了“-e”參數。
如果你要傳遞變量到下級Makefile 中,那麼你可以使用這樣的聲明:
export <variable ...>
如果你不想讓某些變量傳遞到下級Makefile 中,那麼你可以這樣聲明:
unexport <variable ...>
如:
示例一:
export variable = value
其等價於:
variable = value
export variable
其等價於:
export variable := value
其等價於:
variable := value
export variable
示例二:
export variable += value
其等價於:
variable += value
export variable
如果你要傳遞所有的變量,那麼,隻要一個export 就行了。後麵什麼也不
用跟,表示傳遞所有的變量。
需要注意的是,有兩個變量,一個是SHELL,一個是MAKEFLAGS,這兩個變
量不管你是否export,其總是要傳遞到下層Makefile 中,特別是MAKEFILES
變量,其中包含了make 的參數信息,如果我們執行“總控Makefile”時有
make 參數或是在上層Makefile 中定義了這個變量,那麼MAKEFILES 變量將
會是這些參數,並會傳遞到下層Makefile 中,這是一個係統級的環境變量。
但是make 命令中的有幾個參數並不往下傳遞,它們是“-C”,“-f”,“-h”
“-o”和“-W”(有關Makefile 參數的細節將在後麵說明),如果你不想往
下層傳遞參數,那麼,你可以這樣來:
subsystem:
cd subdir && $(MAKE) MAKEFLAGS=
如果你定義了環境變量MAKEFLAGS,那麼你得確信其中的選項是大家都會用
到的,如果其中有“-t”,“-n”,和“-q”參數,那麼將會有讓你意想不到
的結果,或許會讓你異常地恐慌。
還有一個在“嵌套執行”中比較有用的參數,“-w”或是“--print-directory”
會在make 的過程中輸出一些信息,讓你看到目前的工作目錄。比如,如果
我們的下級make 目錄是“/home/hchen/gnu/make”,如果我們使用“make -w”
來執行,那麼當進入該目錄時,我們會看到:
make: Entering directory `/home/hchen/gnu/make'.
而在完成下層make 後離開目錄時,我們會看到:
make: Leaving directory `/home/hchen/gnu/make'
當你使用“-C”參數來指定make 下層Makefile 時,“-w”會被自動打開的。
如果參數中有“-s”(“--slient”)或是“--no-print-directory”,那麼,
“-w”總是失效的。
五、定義命令包
如果Makefile 中出現一些相同命令序列,那麼我們可以為這些相同的命令
序列定義一個變量。定義這種命令序列的語法以“define”開始,以“endef”
結束,如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
這裏,“run-yacc”是這個命令包的名字,其不要和Makefile 中的變量重名。
在“define”和“endef”中的兩行就是命令序列。這個命令包中的第一個
命令是運行Yacc 程序,因為Yacc 程序總是生成“y.tab.c”的文件,所以
第二行的命令就是把這個文件改改名字。還是把這個命令包放到一個示例中
來看看吧。
foo.c : foo.y
$(run-yacc)
我們可以看見,要使用這個命令包,我們就好像使用變量一樣。在這個命令
包的使用中,命令包“run-yacc”中的“$^”就是“foo.y”,“$@”就是“foo.c”
(有關這種以“$”開頭的特殊變量,我們會在後麵介紹),make 在執行命
令包時,命令包中的每個命令會被依次獨立執行。
使用變量
————
在Makefile 中的定義的變量,就像是C/C++語言中的宏一樣,他代表了一
個文本字串,在Makefile 中執行的時候其會自動原模原樣地展開在所使用
的地方。其與C/C++所不同的是,你可以在Makefile 中改變其值。在Makefile
中,變量可以使用在“目標”,“依賴目標”,“命令”或是Makefile 的其它
部分中。
變量的命名字可以包含字符、數字,下劃線(可以是數字開頭),但不應該
含有“:”、“#”、“=”或是空字符(空格、回車等)。變量是大小寫敏感的,
“foo”、“Foo”和“FOO”是三個不同的變量名。傳統的Makefile 的變量名
是全大寫的命名方式,但我推薦使用大小寫搭配的變量名,如:MakeFlags。
這樣可以避免和係統的變量衝突,而發生意外的事情。
有一些變量是很奇怪字串,如“$<”、“$@”等,這些是自動化變量,我會在
後麵介紹。
一、變量的基礎
變量在聲明時需要給予初值,而在使用時,需要給在變量名前加上“$”符
號,但最好用小括號“()”或是大括號“{}”把變量給包括起來。如果你要
使用真實的“$”字符,那麼你需要用“$$”來表示。
變量可以使用在許多地方,如規則中的“目標”、“依賴”、“命令”以及新的
變量中。先看一個例子:
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)
$(objects) : defs.h
變量會在使用它的地方精確地展開,就像C/C++中的宏一樣,例如:
foo = c
prog.o : prog.$(foo)
$(foo)$(foo) -$(foo) prog.$(foo)
展開後得到:
prog.o : prog.c
cc -c prog.c
當然,千萬不要在你的Makefile 中這樣幹,這裏隻是舉個例子來表明
Makefile 中的變量在使用處展開的真實樣子。可見其就是一個“替代”的
原理。
另外,給變量加上括號完全是為了更加安全地使用這個變量,在上麵的例子
中,如果你不想給變量加上括號,那也可以,但我還是強烈建議你給變量加
上括號。
二、變量中的變量
在定義變量的值時,我們可以使用其它變量來構造變量的值,在Makefile
中有兩種方式來在用變量定義變量的值。
先看第一種方式,也就是簡單的使用“=”號,在“=”左側是變量,右側是
變量的值,右側變量的值可以定義在文件的任何一處,也就是說,右側中的
變量不一定非要是已定義好的值,其也可以使用後麵定義的值。如:
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
我們執行“make all”將會打出變量$(foo)的值是“Huh?”( $(foo)的值是
$(bar),$(bar)的值是$(ugh),$(ugh)的值是“Huh?”)可見,變量是可以
使用後麵的變量來定義的。
這個功能有好的地方,也有不好的地方,好的地方是,我們可以把變量的真
實值推到後麵來定義,如:
CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar
當“CFLAGS”在命令中被展開時,會是“-Ifoo -Ibar -O”。但這種形式也
有不好的地方,那就是遞歸定義,如:
CFLAGS = $(CFLAGS) -O
或:
A = $(B)
B = $(A)
這會讓make 陷入無限的變量展開過程中去,當然,我們的make 是有能力檢
測這樣的定義,並會報錯。還有就是如果在變量中使用函數,那麼,這種方
式會讓我們的make 運行時非常慢,更糟糕的是,他會使用得兩個make 的函
數“wildcard”和“shell”發生不可預知的錯誤。因為你不會知道這兩個
函數會被調用多少次。
為了避免上麵的這種方法,我們可以使用make 中的另一種用變量來定義變
量的方法。這種方法使用的是“:=”操作符,如:
x := foo
y := $(x) bar
x := later
其等價於:
y := foo bar
x := later
值得一提的是,這種方法,前麵的變量不能使用後麵的變量,隻能使用前麵
已定義好了的變量。如果是這樣:
y := $(x) bar
x := foo
那麼,y 的值是“bar”,而不是“foo bar”。
上麵都是一些比較簡單的變量使用了,讓我們來看一個複雜的例子,其中包
括了make 的函數、條件表達式和一個係統變量“MAKELEVEL”的使用:
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami)
host-type := $(shell arch)
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif
關於條件表達式和函數,我們在後麵再說,對於係統變量“MAKELEVEL”,其
意思是,如果我們的make 有一個嵌套執行的動作(參見前麵的“嵌套使用
make”),那麼,這個變量會記錄了我們的當前Makefile 的調用層數。
下麵再介紹兩個定義變量時我們需要知道的,請先看一個例子,如果我們要
定義一個變量,其值是一個空格,那麼我們可以這樣來:
nullstring :=
space := $(nullstring) # end of the line
nullstring 是一個Empty 變量,其中什麼也沒有,而我們的space 的值是
一個空格。因為在操作符的右邊是很難描述一個空格的,這裏采用的技術很
管用,先用一個Empty 變量來標明變量的值開始了,而後麵采用“#”注釋
符來表示變量定義的終止,這樣,我們可以定義出其值是一個空格的變量。
請注意這裏關於“#”的使用,注釋符“#”的這種特性值得我們注意,如果
我們這樣定義一個變量:
dir := /foo/bar # directory to put the frobs in
dir 這個變量的值是“/foo/bar”,後麵還跟了4 個空格,如果我們這樣使
用這樣變量來指定別的目錄——“$(dir)/file”那麼就完蛋了。
還有一個比較有用的操作符是“?=”,先看示例:
FOO ?= bar
其含義是,如果FOO 沒有被定義過,那麼變量FOO 的值就是“bar”,如果
FOO 先前被定義過,那麼這條語將什麼也不做,其等價於:
ifeq ($(origin FOO), undefined)
FOO = bar
endif
三、變量高級用法
這裏介紹兩種變量的高級使用方法,第一種是變量值的替換。
我們可以替換變量中的共有的部分, 其格式是“ $(var:a=b) ” 或是
“${var:a=b}”,其意思是,把變量“var”中所有以“a”字串“結尾”的
“a”替換成“b”字串。這裏的“結尾”意思是“空格”或是“結束符”。
還是看一個示例吧:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
這個示例中,我們先定義了一個“$(foo)”變量,而第二行的意思是把
“$(foo)”中所有以“.o”字串“結尾”全部替換成“.c”,所以我們的“$(bar)”
的值就是“a.c b.c c.c”。
另外一種變量替換的技術是以“靜態模式”(參見前麵章節)定義的,如:
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
這依賴於被替換字串中的有相同的模式,模式中必須包含一個“%”字符,
這個例子同樣讓$(bar)變量的值為“a.c b.c c.c”。
第二種高級用法是——“把變量的值再當成變量”。先看一個例子:
x = y
y = z
a := $($(x))
在這個例子中,$(x)的值是“y”,所以$($(x))就是$(y),於是$(a)的值就
是“z”。(注意,是“x=y”,而不是“x=$(y)”)
我們還可以使用更多的層次:
x = y
y = z
z = u
a := $($($(x)))
這裏的$(a)的值是“u”,相關的推導留給讀者自己去做吧。
讓我們再複雜一點,使用上“在變量定義中使用變量”的第一個方式,來看
一個例子:
x = $(y)
y = z
z = Hello
a := $($(x))
這裏的$($(x))被替換成了$($(y)),因為$(y)值是“z”,所以,最終結果是:
a:=$(z),也就是“Hello”。
再複雜一點,我們再加上函數:
x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))
這個例子中,“$($($(z)))”擴展為“$($(y))”,而其再次被擴展為“$($(subst
1,2,$(x)))”。$(x)的值是“variable1”,subst 函數把“variable1”中的
所有“1”字串替換成“2”字串,於是,“variable1”變成“variable2”,
再取其值,所以,最終,$(a)的值就是$(variable2)的值——“Hello”。(喔,
好不容易)
在這種方式中,或要可以使用多個變量來組成一個變量的名字,然後再取其
值:
first_second = Hello
a = first
b = second
all = $($a_$b)
這裏的“$a_$b”組成了“first_second”,於是,$(all)的值就是“Hello”。
再來看看結合第一種技術的例子:
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)
這個例子中,如果$(a1)的值是“a”的話,那麼,$(sources)的值就是“a.c
b.c c.c”;如果$(a1)的值是“1”,那麼$(sources)的值是“1.c 2.c 3.c”。
再來看一個這種技術和“函數”與“條件語句”一同使用的例子:
ifdef do_sort
func := sort
else
func := strip
endif
bar := a d b g q c
foo := $($(func) $(bar))
這個示例中,如果定義了“do_sort”,那麼:foo := $(sort a d b g q c),
於是$(foo)的值就是“a b c d g q”,而如果沒有定義“do_sort”,那麼:
foo := $(sort a d b g q c),調用的就是strip 函數。
當然,“把變量的值再當成變量”這種技術,同樣可以用在操作符的左邊:
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print
lpr $($(dir)_sources)
endef
這個例子中定義了三個變量:“dir”,“foo_sources”和“foo_print”。
四、追加變量值
我們可以使用“+=”操作符給變量追加值,如:
objects = main.o foo.o bar.o utils.o
objects += another.o
於是,我們的$(objects)值變成:“main.o foo.o bar.o utils.o another.o”
(another.o 被追加進去了)
使用“+=”操作符,可以模擬為下麵的這種例子:
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o
所不同的是,用“+=”更為簡潔。
如果變量之前沒有定義過,那麼,“+=”會自動變成“=”,如果前麵有變量
定義,那麼“+=”會繼承於前次操作的賦值符。如果前一次的是“:=”,那
麼“+=”會以“:=”作為其賦值符,如:
variable := value
variable += more
等價於:
variable := value
variable := $(variable) more
但如果是這種情況:
variable = value
variable += more
由於前次的賦值符是“=”,所以“+=”也會以“=”來做為賦值,那麼豈不
會發生變量的遞補歸定義,這是很不好的,所以make 會自動為我們解決這
個問題,我們不必擔心這個問題。
五、override 指示符
如果有變量是通常make 的命令行參數設置的,那麼Makefile 中對這個變量
的賦值會被忽略。如果你想在Makefile 中設置這類參數的值,那麼,你可
以使用“override”指示符。其語法是:
override <variable> = <value>
override <variable> := <value>
當然,你還可以追加:
override <variable> += <more text>
對於多行的變量定義,我們用define 指示符,在define 指示符前,也同樣
可以使用ovveride 指示符,如:
ove 上一篇:
下一篇:
陳皓 (CSDN)
概述
——
什麼是makefile?或許很多Winodws 的程序員都不知道這個東西,因為那
些Windows 的IDE 都為你做了這個工作,但我覺得要作一個好的和
professional 的程序員,makefile 還是要懂。這就好像現在有這麼多的HTML
的編輯器,但如果你想成為一個專業人士,你還是要了解HTML 的標識的含
義。特別在Unix 下的軟件編譯,你就不能不自己寫makefile 了,會不會寫
makefile,從一個側麵說明了一個人是否具備完成大型工程的能力。
因為,makefile 關係到了整個工程的編譯規則。一個工程中的源文件不計
數,其按類型、功能、模塊分別放在若幹個目錄中,makefile 定義了一係
列的規則來指定,哪些文件需要先編譯,哪些文件需要後編譯,哪些文件需
要重新編譯,甚至於進行更複雜的功能操作,因為makefile 就像一個Shell
腳本一樣,其中也可以執行操作係統的命令。
makefile 帶來的好處就是——“自動化編譯”,一旦寫好,隻需要一個make
命令,整個工程完全自動編譯,極大的提高了軟件開發的效率。make 是一
個命令工具,是一個解釋makefile 中指令的命令工具,一般來說,大多數
的IDE 都有這個命令,比如:Delphi 的make,Visual C++的nmake,Linux
下GNU 的make。可見,makefile 都成為了一種在工程方麵的編譯方法。
現在講述如何寫makefile 的文章比較少,這是我想寫這篇文章的原因。當
然,不同產商的make 各不相同,也有不同的語法,但其本質都是在“文件
依賴性”上做文章,這裏,我僅對GNU 的make 進行講述,我的環境是RedHat
Linux 8.0,make 的版本是3.80。必竟,這個make 是應用最為廣泛的,也
是用得最多的。而且其還是最遵循於IEEE 1003.2-1992 標準的(POSIX.2)。
在這篇文檔中,將以C/C++的源碼作為我們基礎,所以必然涉及一些關於
C/C++的編譯的知識,相關於這方麵的內容,還請各位查看相關的編譯器的
文檔。這裏所默認的編譯器是UNIX 下的GCC 和CC。
關於程序的編譯和鏈接
——————————
在此,我想多說關於程序編譯的一些規範和方法,一般來說,無論是C、C++、
還是pas,首先要把源文件編譯成中間代碼文件,在Windows 下也就是 .obj
文件,UNIX 下是 .o 文件,即 Object File,這個動作叫做編譯(compile)。
然後再把大量的Object File 合成執行文件,這個動作叫作鏈接(link)。
編譯時,編譯器需要的是語法的正確,函數與變量的聲明的正確。對於後者,
通常是你需要告訴編譯器頭文件的所在位置(頭文件中應該隻是聲明,而定
義應該放在C/C++文件中),隻要所有的語法正確,編譯器就可以編譯出中
間目標文件。一般來說,每個源文件都應該對應於一個中間目標文件(O 文
件或是OBJ 文件)。
鏈接時,主要是鏈接函數和全局變量,所以,我們可以使用這些中間目標文
件(O 文件或是OBJ 文件)來鏈接我們的應用程序。鏈接器並不管函數所在
的源文件,隻管函數的中間目標文件(Object File),在大多數時候,由於
源文件太多,編譯生成的中間目標文件太多,而在鏈接時需要明顯地指出中
間目標文件名,這對於編譯很不方便,所以,我們要給中間目標文件打個包,
在Windows 下這種包叫“庫文件”(Library File),也就是 .lib 文件,在
UNIX 下,是Archive File,也就是 .a 文件。
總結一下,源文件首先會生成中間目標文件,再由中間目標文件生成執行文
件。在編譯時,編譯器隻檢測程序語法,和函數、變量是否被聲明。如果函
數未被聲明,編譯器會給出一個警告,但可以生成Object File。而在鏈接
程序時,鏈接器會在所有的Object File 中找尋函數的實現,如果找不到,
那到就會報鏈接錯誤碼(Linker Error),在VC 下,這種錯誤一般是:Link
2001 錯誤,意思說是說,鏈接器未能找到函數的實現。你需要指定函數的
Object File.
好,言歸正傳,GNU 的make 有許多的內容,閑言少敘,還是讓我們開始吧。
Makefile 介紹
———————
make 命令執行時,需要一個 Makefile 文件,以告訴make 命令需要怎麼樣
的去編譯和鏈接程序。
首先,我們用一個示例來說明Makefile 的書寫規則。以便給大家一個感興
認識。這個示例來源於GNU 的make 使用手冊,在這個示例中,我們的工程
有8 個C 文件,和3 個頭文件,我們要寫一個Makefile 來告訴make 命令如
何編譯和鏈接這幾個文件。我們的規則是:
1)如果這個工程沒有編譯過,那麼我們的所有C 文件都要編譯並被鏈接。
2)如果這個工程的某幾個C 文件被修改,那麼我們隻編譯被修改的C 文件,
並鏈接目標程序。
3)如果這個工程的頭文件被改變了,那麼我們需要編譯引用了這幾個頭文
件的C 文件,並鏈接目標程序。
隻要我們的Makefile 寫得夠好,所有的這一切,我們隻用一個make 命令就
可以完成,make 命令會自動智能地根據當前的文件修改的情況來確定哪些
文件需要重編譯,從而自己編譯所需要的文件和鏈接目標程序。
一、Makefile 的規則
在講述這個Makefile 之前,還是讓我們先來粗略地看一看Makefile 的規則。
target ... : prerequisites ...
command
...
...
target 也就是一個目標文件,可以是Object File,也可以是執行文件。還
可以是一個標簽(Label),對於標簽這種特性,在後續的“偽目標”章節中
會有敘述。
prerequisites 就是,要生成那個target 所需要的文件或是目標。
command 也就是make 需要執行的命令。(任意的Shell 命令)
這是一個文件的依賴關係,也就是說,target 這一個或多個的目標文件依
賴於prerequisites 中的文件,其生成規則定義在command 中。說白一點就
是說,prerequisites 中如果有一個以上的文件比target 文件要新的話,
command 所定義的命令就會被執行。這就是Makefile 的規則。也就是
Makefile 中最核心的內容。
說到底,Makefile 的東西就是這樣一點,好像我的這篇文檔也該結束了。
嗬嗬。還不盡然,這是Makefile 的主線和核心,但要寫好一個Makefile 還
不夠,我會以後麵一點一點地結合我的工作經驗給你慢慢到來。內容還多著
呢。:)
二、一個示例
正如前麵所說的,如果一個工程有3 個頭文件,和8 個C 文件,我們為了完
成前麵所述的那三個規則,我們的Makefile 應該是下麵的這個樣子的。
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
反斜杠(\)是換行符的意思。這樣比較便於Makefile 的易讀。我們可以把
這個內容保存在文件為“Makefile”或“makefile”的文件中,然後在該目
錄下直接輸入命令“make”就可以生成執行文件edit。如果要刪除執行文
件和所有的中間目標文件,那麼,隻要簡單地執行一下“make clean”就可
以了。
在這個makefile 中,目標文件(target)包含:執行文件edit 和中間目標
文件(*.o),依賴文件(prerequisites)就是冒號後麵的那些 .c 文件和 .h
文件。每一個 .o 文件都有一組依賴文件,而這些 .o 文件又是執行文件
edit 的依賴文件。依賴關係的實質上就是說明了目標文件是由哪些文件生
成的,換言之,目標文件是哪些文件更新的。
在定義好依賴關係後,後續的那一行定義了如何生成目標文件的操作係統命
令,一定要以一個Tab 鍵作為開頭。記住,make 並不管命令是怎麼工作的,
他隻管執行所定義的命令。make 會比較targets 文件和prerequisites 文
件的修改日期,如果prerequisites 文件的日期要比targets 文件的日期要
新,或者target 不存在的話,那麼,make 就會執行後續定義的命令。
這裏要說明一點的是,clean 不是一個文件,它隻不過是一個動作名字,有
點像C 語言中的lable 一樣,其冒號後什麼也沒有,那麼,make 就不會自
動去找文件的依賴性,也就不會自動執行其後所定義的命令。要執行其後的
命令,就要在make 命令後明顯得指出這個lable 的名字。這樣的方法非常
有用,我們可以在一個makefile 中定義不用的編譯或是和編譯無關的命令,
比如程序的打包,程序的備份,等等。
三、make 是如何工作的
在默認的方式下,也就是我們隻輸入make 命令。那麼,
1、make 會在當前目錄下找名字叫“Makefile”或“makefile”的文件。
2、如果找到,它會找文件中的第一個目標文件(target),在上麵的例子中,
他會找到“edit”這個文件,並把這個文件作為最終的目標文件。
3、如果edit 文件不存在,或是edit 所依賴的後麵的 .o 文件的文件修改
時間要比edit 這個文件新,那麼,他就會執行後麵所定義的命令來生成edit
這個文件。
4、如果edit 所依賴的.o 文件也存在,那麼make 會在當前文件中找目標為.o
文件的依賴性,如果找到則再根據那一個規則生成.o 文件。(這有點像一個
堆棧的過程)
5、當然,你的C 文件和H 文件是存在的啦,於是make 會生成 .o 文件,然
後再用 .o 文件生命make 的終極任務,也就是執行文件edit 了。
這就是整個make 的依賴性,make 會一層又一層地去找文件的依賴關係,直
到最終編譯出第一個目標文件。在找尋的過程中,如果出現錯誤,比如最後
被依賴的文件找不到,那麼make 就會直接退出,並報錯,而對於所定義的
命令的錯誤,或是編譯不成功,make 根本不理。make 隻管文件的依賴性,
即,如果在我找了依賴關係之後,冒號後麵的文件還是不在,那麼對不起,
我就不工作啦。
通過上述分析,我們知道,像clean 這種,沒有被第一個目標文件直接或間
接關聯,那麼它後麵所定義的命令將不會被自動執行,不過,我們可以顯示
要make 執行。即命令——“make clean”,以此來清除所有的目標文件,以
便重編譯。
於是在我們編程中,如果這個工程已被編譯過了,當我們修改了其中一個源
文件,比如file.c,那麼根據我們的依賴性,我們的目標file.o 會被重編
譯(也就是在這個依性關係後麵所定義的命令),於是file.o 的文件也是最
新的啦,於是file.o 的文件修改時間要比edit 要新,所以edit 也會被重
新鏈接了(詳見edit 目標文件後定義的命令)。
而如果我們改變了“command.h”,那麼,kdb.o、command.o 和files.o 都
會被重編譯,並且,edit 會被重鏈接。
四、makefile 中使用變量
在上麵的例子中,先讓我們看看edit 的規則:
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
我們可以看到[.o]文件的字符串被重複了兩次,如果我們的工程需要加入一
個新的[.o]文件,那麼我們需要在兩個地方加(應該是三個地方,還有一個
地方在clean 中)。當然,我們的makefile 並不複雜,所以在兩個地方加也
不累,但如果makefile 變得複雜,那麼我們就有可能會忘掉一個需要加入
的地方,而導致編譯失敗。所以,為了makefile 的易維護,在makefile 中
我們可以使用變量。makefile 的變量也就是一個字符串,理解成C 語言中
的宏可能會更好。
比如,我們聲明一個變量,叫objects, OBJECTS, objs, OBJS, obj, 或是
OBJ,反正不管什麼啦,隻要能夠表示obj 文件就行了。我們在makefile 一
開始就這樣定義:
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
於是,我們就可以很方便地在我們的makefile 中以“$(objects)”的方式
來使用這個變量了,於是我們的改良版makefile 就變成下麵這個樣子:
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects)
於是如果有新的 .o 文件加入,我們隻需簡單地修改一下 objects 變量就
可以了。
關於變量更多的話題,我會在後續給你一一道來。
五、讓make 自動推導
GNU 的make 很強大,它可以自動推導文件以及文件依賴關係後麵的命令,
於是我們就沒必要去在每一個[.o]文件後都寫上類似的命令,因為,我們的
make 會自動識別,並自己推導命令。
隻要make 看到一個[.o]文件,它就會自動的把[.c]文件加在依賴關係中,
如果make 找到一個whatever.o,那麼whatever.c,就會是whatever.o 的
依賴文件。並且 cc -c whatever.c 也會被推導出來,於是,我們的makefile
再也不用寫得這麼複雜。我們的是新的makefile 又出爐了。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)
這種方法,也就是make 的“隱晦規則”。上麵文件內容中,“.PHONY”表示,
clean 是個偽目標文件。
關於更為詳細的“隱晦規則”和“偽目標文件”,我會在後續給你一一道來。
六、另類風格的makefile
即然我們的make 可以自動推導命令,那麼我看到那堆[.o]和[.h]的依賴就
有點不爽,那麼多的重複的[.h],能不能把其收攏起來,好吧,沒有問題,
這個對於make 來說很容易,誰叫它提供了自動推導命令和文件的功能呢?
來看看最新風格的makefile 吧。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h
.PHONY : clean
clean :
rm edit $(objects)
這種風格,讓我們的makefile 變得很簡單,但我們的文件依賴關係就顯得
有點淩亂了。魚和熊掌不可兼得。還看你的喜好了。我是不喜歡這種風格的,
一是文件的依賴關係看不清楚,二是如果文件一多,要加入幾個新的.o 文
件,那就理不清楚了。
七、清空目標文件的規則
每個Makefile 中都應該寫一個清空目標文件(.o 和執行文件)的規則,這
不僅便於重編譯,也很利於保持文件的清潔。這是一個“修養”(嗬嗬,還
記得我的《編程修養》嗎)。一般的風格都是:
clean:
rm edit $(objects)
更為穩健的做法是:
.PHONY : clean
clean :
-rm edit $(objects)
前麵說過,.PHONY 意思表示clean 是一個“偽目標”,。而在rm 命令前麵加
了一個小減號的意思就是,也許某些文件出現問題,但不要管,繼續做後麵
的事。當然,clean 的規則不要放在文件的開頭,不然,這就會變成make
的默認目標,相信誰也不願意這樣。不成文的規矩是——“clean 從來都是
放在文件的最後”。
上麵就是一個makefile 的概貌,也是makefile 的基礎,下麵還有很多
makefile 的相關細節,準備好了嗎?準備好了就來。
Makefile 總述
———————
一、Makefile 裏有什麼?
Makefile 裏主要包含了五個東西:顯式規則、隱晦規則、變量定義、文件
指示和注釋。
1、顯式規則。顯式規則說明了,如何生成一個或多的的目標文件。這是由
Makefile 的書寫者明顯指出,要生成的文件,文件的依賴文件,生成的命
令。
2、隱晦規則。由於我們的make 有自動推導的功能,所以隱晦的規則可以讓
我們比較粗糙地簡略地書寫Makefile,這是由make 所支持的。
3、變量的定義。在Makefile 中我們要定義一係列的變量,變量一般都是字
符串,這個有點你C 語言中的宏,當Makefile 被執行時,其中的變量都會
被擴展到相應的引用位置上。
4、文件指示。其包括了三個部分,一個是在一個Makefile 中引用另一個
Makefile,就像C 語言中的include 一樣;另一個是指根據某些情況指定
Makefile 中的有效部分,就像C 語言中的預編譯#if 一樣;還有就是定義一
個多行的命令。有關這一部分的內容,我會在後續的部分中講述。
5、注釋。Makefile 中隻有行注釋,和UNIX 的Shell 腳本一樣,其注釋是
用“#”字符,這個就像C/C++中的“//”一樣。如果你要在你的Makefile
中使用“#”字符,可以用反斜框進行轉義,如:“\#”。
最後,還值得一提的是,在Makefile 中的命令,必須要以[Tab]鍵開始。
二、Makefile 的文件名
默認的情況下, make 命令會在當前目錄下按順序找尋文件名為
“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解釋這個文件。
在這三個文件名中,最好使用“Makefile”這個文件名,因為,這個文件名
第一個字符為大寫,這樣有一種顯目的感覺。最好不要用“GNUmakefile”,
這個文件是GNU 的make 識別的。有另外一些make 隻對全小寫的“makefile”
文件名敏感,但是基本上來說,大多數的make 都支持“makefile”和
“Makefile”這兩種默認文件名。
當然,你可以使用別的文件名來書寫Makefile,比如:“Make.Linux”,
“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以
使用make 的“-f”和“--file”參數,如:make -f Make.Linux 或make --file
Make.AIX。
三、引用其它的Makefile
在Makefile 使用include 關鍵字可以把別的Makefile 包含進來,這很像C
語言的#include,被包含的文件會原模原樣的放在當前文件的包含位置。
include 的語法是:
include <filename>
filename 可以是當前操作係統Shell 的文件模式(可以保含路徑和通配符)
在include 前麵可以有一些空字符,但是絕不能是[Tab]鍵開始。include
和<filename>可以用一個或多個空格隔開。舉個例子,你有這樣幾個
Makefile:a.mk、b.mk、c.mk,還有一個文件叫foo.make,以及一個變量
$(bar),其包含了e.mk 和f.mk,那麼,下麵的語句:
include foo.make *.mk $(bar)
等價於:
include foo.make a.mk b.mk c.mk e.mk f.mk
make 命令開始時,會把找尋include 所指出的其它Makefile,並把其內容
安置在當前的位置。就好像C/C++的#include 指令一樣。如果文件都沒有指
定絕對路徑或是相對路徑的話,make 會在當前目錄下首先尋找,如果當前
目錄下沒有找到,那麼,make 還會在下麵的幾個目錄下找:
1、如果make 執行時,有“-I”或“--include-dir”參數,那麼make 就會
在這個參數所指定的目錄下去尋找。
2、如果目錄<prefix>/include(一般是:/usr/local/bin 或/usr/include)
存在的話,make 也會去找。
如果有文件沒有找到的話,make 會生成一條警告信息,但不會馬上出現致
命錯誤。它會繼續載入其它的文件,一旦完成makefile 的讀取,make 會再
重試這些沒有找到,或是不能讀取的文件,如果還是不行,make 才會出現
一條致命信息。如果你想讓make 不理那些無法讀取的文件,而繼續執行,
你可以在include 前加一個減號“-”。如:
-include <filename>
其表示,無論include 過程中出現什麼錯誤,都不要報錯繼續執行。和其它
版本make 兼容的相關命令是sinclude,其作用和這一個是一樣的。
四、環境變量 MAKEFILES
如果你的當前環境中定義了環境變量MAKEFILES,那麼,make 會把這個變量
中的值做一個類似於include 的動作。這個變量中的值是其它的Makefile,
用空格分隔。隻是,它和include 不同的是,從這個環境變中引入的Makefile
的“目標”不會起作用,如果環境變量中定義的文件發現錯誤,make 也會
不理。
但是在這裏我還是建議不要使用這個環境變量,因為隻要這個變量一被定
義,那麼當你使用make 時,所有的Makefile 都會受到它的影響,這絕不是
你想看到的。在這裏提這個事,隻是為了告訴大家,也許有時候你的Makefile
出現了怪事,那麼你可以看看當前環境中有沒有定義這個變量。
五、make 的工作方式
GNU 的make 工作時的執行步驟入下:(想來其它的make 也是類似)
1、讀入所有的Makefile。
2、讀入被include 的其它Makefile。
3、初始化文件中的變量。
4、推導隱晦規則,並分析所有規則。
5、為所有的目標文件創建依賴關係鏈。
6、根據依賴關係,決定哪些目標要重新生成。
7、執行生成命令。
1-5 步為第一個階段,6-7 為第二個階段。第一個階段中,如果定義的變量
被使用了,那麼,make 會把其展開在使用的位置。但make 並不會完全馬上
展開,make 使用的是拖延戰術,如果變量出現在依賴關係的規則中,那麼
僅當這條依賴被決定要使用了,變量才會在其內部展開。
當然,這個工作方式你不一定要清楚,但是知道這個方式你也會對make 更
為熟悉。有了這個基礎,後續部分也就容易看懂了。
書寫規則
————
規則包含兩個部分,一個是依賴關係,一個是生成目標的方法。
在Makefile 中,規則的順序是很重要的,因為,Makefile 中隻應該有一個
最終目標,其它的目標都是被這個目標所連帶出來的,所以一定要讓make
知道你的最終目標是什麼。一般來說,定義在Makefile 中的目標可能會有
很多,但是第一條規則中的目標將被確立為最終的目標。如果第一條規則中
的目標有很多個,那麼,第一個目標會成為最終的目標。make 所完成的也
就是這個目標。
好了,還是讓我們來看一看如何書寫規則。
一、規則舉例
foo.o : foo.c defs.h # foo 模塊
cc -c -g foo.c
看到這個例子,各位應該不是很陌生了,前麵也已說過,foo.o 是我們的目
標,foo.c 和defs.h 是目標所依賴的源文件,而隻有一個命令“cc -c -g
foo.c”(以Tab 鍵開頭)。這個規則告訴我們兩件事:
1、文件的依賴關係,foo.o 依賴於foo.c 和defs.h 的文件,如果foo.c 和
defs.h 的文件日期要比foo.o 文件日期要新,或是foo.o 不存在,那麼依
賴關係發生。
2、如果生成(或更新)foo.o 文件。也就是那個cc 命令,其說明了,如何
生成foo.o 這個文件。(當然foo.c 文件include 了defs.h 文件)
二、規則的語法
targets : prerequisites
command
...
或是這樣:
targets : prerequisites ; command
command
...
targets 是文件名,以空格分開,可以使用通配符。一般來說,我們的目標
基本上是一個文件,但也有可能是多個文件。
command 是命令行,如果其不與“targetrerequisites”在一行,那麼,必
須以[Tab 鍵]開頭,如果和prerequisites 在一行,那麼可以用分號做為分
隔。(見上)
prerequisites 也就是目標所依賴的文件(或依賴目標)。如果其中的某個
文件要比目標文件要新,那麼,目標就被認為是“過時的”,被認為是需要
重生成的。這個在前麵已經講過了。
如果命令太長,你可以使用反斜框(‘\’)作為換行符。make 對一行上有多
少個字符沒有限製。規則告訴make 兩件事,文件的依賴關係和如何成成目
標文件。
一般來說,make 會以UNIX 的標準Shell,也就是/bin/sh 來執行命令。
三、在規則中使用通配符
如果我們想定義一係列比較類似的文件,我們很自然地就想起使用通配符。
make 支持三各通配符:“*”,“?”和“[...]”。這是和Unix 的B-Shell 是
相同的。
波浪號(“~”)字符在文件名中也有比較特殊的用途。如果是“~/test”,這
就表示當前用戶的$HOME 目錄下的test 目錄。而“~hchen/test”則表示用
戶hchen 的宿主目錄下的test 目錄。(這些都是Unix 下的小知識了,make
也支持)而在Windows 或是MS-DOS 下,用戶沒有宿主目錄,那麼波浪號所
指的目錄則根據環境變量“HOME”而定。
通配符代替了你一係列的文件,如“*.c”表示所以後綴為c 的文件。一個
需要我們注意的是,如果我們的文件名中有通配符,如:“*”,那麼可以用
轉義字符“\”,如“\*”來表示真實的“*”字符,而不是任意長度的字符
串。
好吧,還是先來看幾個例子吧:
clean:
rm -f *.o
上麵這個例子我不不多說了,這是操作係統Shell 所支持的通配符。這是在
命令中的通配符。
print: *.c
lpr -p $?
touch print
上麵這個例子說明了通配符也可以在我們的規則中,目標print 依賴於所有
的[.c]文件。其中的“$?”是一個自動化變量,我會在後麵給你講述。
objects = *.o
上麵這個例子,表示了,通符同樣可以用在變量中。並不是說[*.o]會展開,
不!objects 的值就是“*.o”。Makefile 中的變量其實就是C/C++中的宏。
如果你要讓通配符在變量中展開,也就是讓objects 的值是所有[.o]的文件
名的集合,那麼,你可以這樣:
objects := $(wildcard *.o)
這種用法由關鍵字“wildcard”指出,關於Makefile 的關鍵字,我們將在
後麵討論。
四、文件搜尋
在一些大的工程中,有大量的源文件,我們通常的做法是把這許多的源文件
分類,並存放在不同的目錄中。所以,當make 需要去找尋文件的依賴關係
時,你可以在文件前加上路徑,但最好的方法是把一個路徑告訴make,讓
make 在自動去找。
Makefile 文件中的特殊變量“VPATH”就是完成這個功能的,如果沒有指明
這個變量,make 隻會在當前的目錄中去找尋依賴文件和目標文件。如果定
義了這個變量,那麼,make 就會在當當前目錄找不到的情況下,到所指定
的目錄中去找尋文件了。
VPATH = src:../headers
上麵的的定義指定兩個目錄,“src”和“../headers”,make 會按照這個順
序進行搜索。目錄由“冒號”分隔。(當然,當前目錄永遠是最高優先搜索
的地方)
另一個設置文件搜索路徑的方法是使用make 的“vpath”關鍵字(注意,它
是全小寫的),這不是變量,這是一個make 的關鍵字,這和上麵提到的那個
VPATH 變量很類似,但是它更為靈活。它可以指定不同的文件在不同的搜索
目錄中。這是一個很靈活的功能。它的使用方法有三種:
1、vpath <pattern> <directories>
為符合模式<pattern>的文件指定搜索目錄<directories>。
2、vpath <pattern>
清除符合模式<pattern>的文件的搜索目錄。
3、vpath
清除所有已被設置好了的文件搜索目錄。
vapth 使用方法中的<pattern>需要包含“%”字符。“%”的意思是匹配零或
若幹字符,例如,“%.h”表示所有以“.h”結尾的文件。<pattern>指定了
要搜索的文件集,而<directories>則指定了<pattern>的文件集的搜索的目
錄。例如:
vpath %.h ../headers
該語句表示,要求make 在“../headers”目錄下搜索所有以“.h”結尾的
文件。(如果某文件在當前目錄沒有找到的話)
我們可以連續地使用vpath 語句,以指定不同搜索策略。如果連續的vpath
語句中出現了相同的<pattern>,或是被重複了的<pattern>,那麼,make
會按照vpath 語句的先後順序來執行搜索。如:
vpath %.c foo
vpath % blish
vpath %.c bar
其表示“.c”結尾的文件,先在“foo”目錄,然後是“blish”,最後是“bar”
目錄。
vpath %.c foo:bar
vpath % blish
而上麵的語句則表示“.c”結尾的文件,先在“foo”目錄,然後是“bar”
目錄,最後才是“blish”目錄。
五、偽目標
最早先的一個例子中,我們提到過一個“clean”的目標,這是一個“偽目
標”,
clean:
rm *.o temp
正像我們前麵例子中的“clean”一樣,即然我們生成了許多文件編譯文件,
我們也應該提供一個清除它們的“目標”以備完整地重編譯而用。 (以“make
clean”來使用該目標)
因為,我們並不生成“clean”這個文件。“偽目標”並不是一個文件,隻是
一個標簽,由於“偽目標”不是文件,所以make 無法生成它的依賴關係和
決定它是否要執行。我們隻有通過顯示地指明這個“目標”才能讓其生效。
當然,“偽目標”的取名不能和文件名重名,不然其就失去了“偽目標”的
意義了。
當然,為了避免和文件重名的這種情況,我們可以使用一個特殊的標記
“.PHONY”來顯示地指明一個目標是“偽目標”,向make 說明,不管是否有
這個文件,這個目標就是“偽目標”。
.PHONY : clean
隻要有這個聲明,不管是否有“clean”文件,要運行“clean”這個目標,
隻有“make clean”這樣。於是整個過程可以這樣寫:
.PHONY: clean
clean:
rm *.o temp
偽目標一般沒有依賴的文件。但是,我們也可以為偽目標指定所依賴的文件。
偽目標同樣可以作為“默認目標”,隻要將其放在第一個。一個示例就是,
如果你的Makefile 需要一口氣生成若幹個可執行文件,但你隻想簡單地敲
一個make 完事,並且,所有的目標文件都寫在一個Makefile 中,那麼你可
以使用“偽目標”這個特性:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
我們知道,Makefile 中的第一個目標會被作為其默認目標。我們聲明了一
個“all”的偽目標,其依賴於其它三個目標。由於偽目標的特性是,總是
被執行的,所以其依賴的那三個目標就總是不如“all”這個目標新。所以,
其它三個目標的規則總是會被決議。也就達到了我們一口氣生成多個目標的
目的。“.PHONY : all”聲明了“all”這個目標為“偽目標”。
隨便提一句,從上麵的例子我們可以看出,目標也可以成為依賴。所以,偽
目標同樣也可成為依賴。看下麵的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
“make clean”將清除所有要被清除的文件。“cleanobj”和“cleandiff”
這兩個偽目標有點像“子程序”的意思。我們可以輸入“make cleanall”
和“make cleanobj”和“make cleandiff”命令來達到清除不同種類文件
的目的。
六、多目標
Makefile 的規則中的目標可以不止一個,其支持多目標,有可能我們的多
個目標同時依賴於一個文件,並且其生成的命令大體類似。於是我們就能把
其合並起來。當然,多個目標的生成規則的執行命令是同一個,這可能會可
我們帶來麻煩,不過好在我們的可以使用一個自動化變量“$@”(關於自動
化變量,將在後麵講述),這個變量表示著目前規則中所有的目標的集合,
這樣說可能很抽象,還是看一個例子吧。
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述規則等價於:
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
其中,-$(subst output,,$@)中的“$”表示執行一個Makefile 的函數,函
數名為subst,後麵的為參數。關於函數,將在後麵講述。這裏的這個函數
是截取字符串的意思,“$@”表示目標的集合,就像一個數組,“$@”依次取
出目標,並執於命令。
七、靜態模式
靜態模式可以更加容易地定義多目標的規則,可以讓我們的規則變得更加的
有彈性和靈活。我們還是先來看一下語法:
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
...
targets 定義了一係列的目標文件,可以有通配符。是目標的一個集合。
target-parrtern 是指明了targets 的模式,也就是的目標集模式。
prereq-parrterns 是目標的依賴模式,它對target-parrtern 形成的模式
再進行一次依賴目標的定義。
這樣描述這三個東西,可能還是沒有說清楚,還是舉個例子來說明一下吧。
如果我們的<target-parrtern>定義成“%.o”,意思是我們的<target>集合
中都是以“.o”結尾的,而如果我們的<prereq-parrterns>定義成“%.c”,
意思是對<target-parrtern>所形成的目標集進行二次定義,其計算方法是,
取<target-parrtern>模式中的“%”(也就是去掉了[.o]這個結尾),並為其
加上[.c]這個結尾,形成的新集合。
所以,我們的“目標模式”或是“依賴模式”中都應該有“%”這個字符,
如果你的文件名中有“%”那麼你可以使用反斜杠“\”進行轉義,來標明真
實的“%”字符。
看一個例子:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
上麵的例子中,指明了我們的目標從$object 中獲取,“%.o”表明要所有以
“.o”結尾的目標,也就是“foo.o bar.o”,也就是變量$object 集合的模
式,而依賴模式“%.c”則取模式“%.o”的“%”,也就是“foo bar”,並為
其加下“.c”的後綴,於是,我們的依賴目標就是“foo.c bar.c”。而命令
中的“$<”和“$@”則是自動化變量,“$<”表示所有的依賴目標集(也就
是“foo.c bar.c”),“$@”表示目標集(也就是“foo.o bar.o”)。於是,
上麵的規則展開後等價於下麵的規則:
foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
試想,如果我們的“%.o”有幾百個,那種我們隻要用這種很簡單的“靜態
模式規則”就可以寫完一堆規則,實在是太有效率了。“靜態模式規則”的
用法很靈活,如果用得好,那會一個很強大的功能。再看一個例子:
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
$(filter %.o,$(files))表示調用Makefile 的filter 函數,過濾“$filter”
集,隻要其中模式為“%.o”的內容。其的它內容,我就不用多說了吧。這
個例字展示了Makefile 中更大的彈性。
八、自動生成依賴性
在Makefile 中,我們的依賴關係可能會需要包含一係列的頭文件,比如,
如果我們的main.c 中有一句“#include "defs.h"”,那麼我們的依賴關係
應該是:
main.o : main.c defs.h
但是,如果是一個比較大型的工程,你必需清楚哪些C 文件包含了哪些頭文
件,並且,你在加入或刪除頭文件時,也需要小心地修改Makefile,這是
一個很沒有維護性的工作。為了避免這種繁重而又容易出錯的事情,我們可
以使用C/C++編譯的一個功能。大多數的C/C++編譯器都支持一個“-M”的
選項,即自動找尋源文件中包含的頭文件,並生成一個依賴關係。例如,如
果我們執行下麵的命令:
cc -M main.c
其輸出是:
main.o : main.c defs.h
於是由編譯器自動生成的依賴關係,這樣一來,你就不必再手動書寫若幹文
件的依賴關係,而由編譯器自動生成了。需要提醒一句的是,如果你使用
GNU 的C/C++編譯器,你得用“-MM”參數,不然,“-M”參數會把一些標準
庫的頭文件也包含進來。
gcc -M main.c 的輸出是:
main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h
\
/usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \
/usr/include/bits/types.h /usr/include/bits/pthreadtypes.h \
/usr/include/bits/sched.h /usr/include/libio.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/include/bits/wchar.h /usr/include/gconv.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \
/usr/include/bits/stdio_lim.h
gcc -MM main.c 的輸出則是:
main.o: main.c defs.h
那麼,編譯器的這個功能如何與我們的Makefile 聯係在一起呢。因為這樣
一來,我們的Makefile 也要根據這些源文件重新生成,讓Makefile 自已依
賴於源文件?這個功能並不現實,不過我們可以有其它手段來迂回地實現這
一功能。GNU 組織建議把編譯器為每一個源文件的自動生成的依賴關係放到
一個文件中,為每一個“name.c”的文件都生成一個“name.d”的Makefile
文件,[.d]文件中就存放對應[.c]文件的依賴關係。
於是,我們可以寫出[.c]文件和[.d]文件的依賴關係,並讓make 自動更新
或自成[.d]文件,並把其包含在我們的主Makefile 中,這樣,我們就可以
自動化地生成每個文件的依賴關係了。
這裏,我們給出了一個模式規則來產生[.d]文件:
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
這個規則的意思是,所有的[.d]文件依賴於[.c]文件,“rm -f $@”的意思
是刪除所有的目標,也就是[.d]文件,第二行的意思是,為每個依賴文件
“$<”,也就是[.c]文件生成依賴文件,“$@”表示模式“%.d”文件,如果
有一個C 文件是name.c,那麼“%”就是“name”,“$$$$”意為一個隨機編
號,第二行生成的文件有可能是“name.d.12345”,第三行使用sed 命令做
了一個替換,關於sed 命令的用法請參看相關的使用文檔。第四行就是刪除
臨時文件。
總而言之,這個模式要做的事就是在編譯器生成的依賴關係中加入[.d]文件
的依賴,即把依賴關係:
main.o : main.c defs.h
轉成:
main.o main.d : main.c defs.h
於是,我們的[.d]文件也會自動更新了,並會自動生成了,當然,你還可以
在這個[.d]文件中加入的不隻是依賴關係,包括生成的命令也可一並加入,
讓每個[.d]文件都包含一個完賴的規則。一旦我們完成這個工作,接下來,
我們就要把這些自動生成的規則放進我們的主Makefile 中。我們可以使用
Makefile 的“include”命令,來引入別的Makefile 文件(前麵講過),例
如:
sources = foo.c bar.c
include $(sources:.c=.d)
上述語句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做一個替換,
把變量$(sources)所有[.c]的字串都替換成[.d],關於這個“替換”的內容,
在後麵我會有更為詳細的講述。當然,你得注意次序,因為include 是按次
來載入文件,最先載入的[.d]文件中的目標會成為默認目標。
書寫命令
————
每條規則中的命令和操作係統Shell 的命令行是一致的。make 會一按順序
一條一條的執行命令,每條命令的開頭必須以[Tab]鍵開頭,除非,命令是
緊跟在依賴規則後麵的分號後的。在命令行之間中的空格或是空行會被忽
略,但是如果該空格或空行是以Tab 鍵開頭的,那麼make 會認為其是一個
空命令。
我們在UNIX 下可能會使用不同的Shell,但是make 的命令默認是被
“/bin/sh”——UNIX 的標準Shell 解釋執行的。除非你特別指定一個其它
的Shell。Makefile 中,“#”是注釋符,很像C/C++中的“//”,其後的本
行字符都被注釋。
一、顯示命令
通常,make 會把其要執行的命令行在命令執行前輸出到屏幕上。當我們用
“@”字符在命令行前,那麼,這個命令將不被make 顯示出來,最具代表性
的例子是,我們用這個功能來像屏幕顯示一些信息。如:
@echo 正在編譯XXX 模塊......
當make 執行時,會輸出“正在編譯XXX 模塊......”字串,但不會輸出命
令,如果沒有“@”,那麼,make 將輸出:
echo 正在編譯XXX 模塊......
正在編譯XXX 模塊......
如果make 執行時,帶入make 參數“-n”或“--just-print”,那麼其隻是
顯示命令,但不會執行命令,這個功能很有利於我們調試我們的Makefile,
看看我們書寫的命令是執行起來是什麼樣子的或是什麼順序的。
而make 參數“-s”或“--slient”則是全麵禁止命令的顯示。
二、命令執行
當依賴目標新於目標時,也就是當規則的目標需要被更新時,make 會一條
一條的執行其後的命令。需要注意的是,如果你要讓上一條命令的結果應用
在下一條命令時,你應該使用分號分隔這兩條命令。比如你的第一條命令是
cd 命令,你希望第二條命令得在cd 之後的基礎上運行,那麼你就不能把這
兩條命令寫在兩行上,而應該把這兩條命令寫在一行上,用分號分隔。如:
示例一:
exec:
cd /home/hchen
pwd
示例二:
exec:
cd /home/hchen; pwd
當我們執行“make exec”時,第一個例子中的cd 沒有作用,pwd 會打印出
當前的Makefile 目錄,而第二個例子中,cd 就起作用了,pwd 會打印出
“/home/hchen”。
make 一般是使用環境變量SHELL 中所定義的係統Shell 來執行命令,默認
情況下使用UNIX 的標準Shell——/bin/sh 來執行命令。但在MS-DOS 下有
點特殊,因為MS-DOS 下沒有SHELL 環境變量,當然你也可以指定。如果你
指定了UNIX 風格的目錄形式,首先,make 會在SHELL 所指定的路徑中找尋
命令解釋器,如果找不到,其會在當前盤符中的當前目錄中尋找,如果再找
不到,其會在PATH 環境變量中所定義的所有路徑中尋找。MS-DOS 中,如果
你定義的命令解釋器沒有找到,其會給你的命令解釋器加上諸如“.exe”、
“.com”、“.bat”、“.sh”等後綴。
三、命令出錯
每當命令運行完後,make 會檢測每個命令的返回碼,如果命令返回成功,
那麼make 會執行下一條命令,當規則中所有的命令成功返回後,這個規則
就算是成功完成了。如果一個規則中的某個命令出錯了(命令退出碼非零),
那麼make 就會終止執行當前規則,這將有可能終止所有規則的執行。
有些時候,命令的出錯並不表示就是錯誤的。例如mkdir 命令,我們一定需
要建立一個目錄,如果目錄不存在,那麼mkdir 就成功執行,萬事大吉,如
果目錄存在,那麼就出錯了。我們之所以使用mkdir 的意思就是一定要有這
樣的一個目錄,於是我們就不希望mkdir 出錯而終止規則的運行。
為了做到這一點,忽略命令的出錯,我們可以在Makefile 的命令行前加一
個減號“-”(在Tab 鍵之後),標記為不管命令出不出錯都認為是成功的。
如:
clean:
-rm -f *.o
還有一個全局的辦法是,給make 加上“-i”或是“--ignore-errors”參數,
那麼,Makefile 中所有命令都會忽略錯誤。而如果一個規則是以“.IGNORE”
作為目標的,那麼這個規則中的所有命令將會忽略錯誤。這些是不同級別的
防止命令出錯的方法,你可以根據你的不同喜歡設置。
還有一個要提一下的make 的參數的是“-k”或是“--keep-going”,這個參
數的意思是,如果某規則中的命令出錯了,那麼就終目該規則的執行,但繼
續執行其它規則。
四、嵌套執行make
在一些大的工程中,我們會把我們不同模塊或是不同功能的源文件放在不同
的目錄中,我們可以在每個目錄中都書寫一個該目錄的Makefile,這有利
於讓我們的Makefile 變得更加地簡潔,而不至於把所有的東西全部寫在一
個Makefile 中,這樣會很難維護我們的Makefile,這個技術對於我們模塊
編譯和分段編譯有著非常大的好處。
例如,我們有一個子目錄叫subdir,這個目錄下有個Makefile 文件,來指
明了這個目錄下文件的編譯規則。那麼我們總控的Makefile 可以這樣書寫:
subsystem:
cd subdir && $(MAKE)
其等價於:
subsystem:
$(MAKE) -C subdir
定義$(MAKE)宏變量的意思是,也許我們的make 需要一些參數,所以定義成
一個變量比較利於維護。這兩個例子的意思都是先進入“subdir”目錄,然
後執行make 命令。
我們把這個Makefile 叫做“總控Makefile”,總控Makefile 的變量可以傳
遞到下級的Makefile 中(如果你顯示的聲明),但是不會覆蓋下層的
Makefile 中所定義的變量,除非指定了“-e”參數。
如果你要傳遞變量到下級Makefile 中,那麼你可以使用這樣的聲明:
export <variable ...>
如果你不想讓某些變量傳遞到下級Makefile 中,那麼你可以這樣聲明:
unexport <variable ...>
如:
示例一:
export variable = value
其等價於:
variable = value
export variable
其等價於:
export variable := value
其等價於:
variable := value
export variable
示例二:
export variable += value
其等價於:
variable += value
export variable
如果你要傳遞所有的變量,那麼,隻要一個export 就行了。後麵什麼也不
用跟,表示傳遞所有的變量。
需要注意的是,有兩個變量,一個是SHELL,一個是MAKEFLAGS,這兩個變
量不管你是否export,其總是要傳遞到下層Makefile 中,特別是MAKEFILES
變量,其中包含了make 的參數信息,如果我們執行“總控Makefile”時有
make 參數或是在上層Makefile 中定義了這個變量,那麼MAKEFILES 變量將
會是這些參數,並會傳遞到下層Makefile 中,這是一個係統級的環境變量。
但是make 命令中的有幾個參數並不往下傳遞,它們是“-C”,“-f”,“-h”
“-o”和“-W”(有關Makefile 參數的細節將在後麵說明),如果你不想往
下層傳遞參數,那麼,你可以這樣來:
subsystem:
cd subdir && $(MAKE) MAKEFLAGS=
如果你定義了環境變量MAKEFLAGS,那麼你得確信其中的選項是大家都會用
到的,如果其中有“-t”,“-n”,和“-q”參數,那麼將會有讓你意想不到
的結果,或許會讓你異常地恐慌。
還有一個在“嵌套執行”中比較有用的參數,“-w”或是“--print-directory”
會在make 的過程中輸出一些信息,讓你看到目前的工作目錄。比如,如果
我們的下級make 目錄是“/home/hchen/gnu/make”,如果我們使用“make -w”
來執行,那麼當進入該目錄時,我們會看到:
make: Entering directory `/home/hchen/gnu/make'.
而在完成下層make 後離開目錄時,我們會看到:
make: Leaving directory `/home/hchen/gnu/make'
當你使用“-C”參數來指定make 下層Makefile 時,“-w”會被自動打開的。
如果參數中有“-s”(“--slient”)或是“--no-print-directory”,那麼,
“-w”總是失效的。
五、定義命令包
如果Makefile 中出現一些相同命令序列,那麼我們可以為這些相同的命令
序列定義一個變量。定義這種命令序列的語法以“define”開始,以“endef”
結束,如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
這裏,“run-yacc”是這個命令包的名字,其不要和Makefile 中的變量重名。
在“define”和“endef”中的兩行就是命令序列。這個命令包中的第一個
命令是運行Yacc 程序,因為Yacc 程序總是生成“y.tab.c”的文件,所以
第二行的命令就是把這個文件改改名字。還是把這個命令包放到一個示例中
來看看吧。
foo.c : foo.y
$(run-yacc)
我們可以看見,要使用這個命令包,我們就好像使用變量一樣。在這個命令
包的使用中,命令包“run-yacc”中的“$^”就是“foo.y”,“$@”就是“foo.c”
(有關這種以“$”開頭的特殊變量,我們會在後麵介紹),make 在執行命
令包時,命令包中的每個命令會被依次獨立執行。
使用變量
————
在Makefile 中的定義的變量,就像是C/C++語言中的宏一樣,他代表了一
個文本字串,在Makefile 中執行的時候其會自動原模原樣地展開在所使用
的地方。其與C/C++所不同的是,你可以在Makefile 中改變其值。在Makefile
中,變量可以使用在“目標”,“依賴目標”,“命令”或是Makefile 的其它
部分中。
變量的命名字可以包含字符、數字,下劃線(可以是數字開頭),但不應該
含有“:”、“#”、“=”或是空字符(空格、回車等)。變量是大小寫敏感的,
“foo”、“Foo”和“FOO”是三個不同的變量名。傳統的Makefile 的變量名
是全大寫的命名方式,但我推薦使用大小寫搭配的變量名,如:MakeFlags。
這樣可以避免和係統的變量衝突,而發生意外的事情。
有一些變量是很奇怪字串,如“$<”、“$@”等,這些是自動化變量,我會在
後麵介紹。
一、變量的基礎
變量在聲明時需要給予初值,而在使用時,需要給在變量名前加上“$”符
號,但最好用小括號“()”或是大括號“{}”把變量給包括起來。如果你要
使用真實的“$”字符,那麼你需要用“$$”來表示。
變量可以使用在許多地方,如規則中的“目標”、“依賴”、“命令”以及新的
變量中。先看一個例子:
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)
$(objects) : defs.h
變量會在使用它的地方精確地展開,就像C/C++中的宏一樣,例如:
foo = c
prog.o : prog.$(foo)
$(foo)$(foo) -$(foo) prog.$(foo)
展開後得到:
prog.o : prog.c
cc -c prog.c
當然,千萬不要在你的Makefile 中這樣幹,這裏隻是舉個例子來表明
Makefile 中的變量在使用處展開的真實樣子。可見其就是一個“替代”的
原理。
另外,給變量加上括號完全是為了更加安全地使用這個變量,在上麵的例子
中,如果你不想給變量加上括號,那也可以,但我還是強烈建議你給變量加
上括號。
二、變量中的變量
在定義變量的值時,我們可以使用其它變量來構造變量的值,在Makefile
中有兩種方式來在用變量定義變量的值。
先看第一種方式,也就是簡單的使用“=”號,在“=”左側是變量,右側是
變量的值,右側變量的值可以定義在文件的任何一處,也就是說,右側中的
變量不一定非要是已定義好的值,其也可以使用後麵定義的值。如:
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
我們執行“make all”將會打出變量$(foo)的值是“Huh?”( $(foo)的值是
$(bar),$(bar)的值是$(ugh),$(ugh)的值是“Huh?”)可見,變量是可以
使用後麵的變量來定義的。
這個功能有好的地方,也有不好的地方,好的地方是,我們可以把變量的真
實值推到後麵來定義,如:
CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar
當“CFLAGS”在命令中被展開時,會是“-Ifoo -Ibar -O”。但這種形式也
有不好的地方,那就是遞歸定義,如:
CFLAGS = $(CFLAGS) -O
或:
A = $(B)
B = $(A)
這會讓make 陷入無限的變量展開過程中去,當然,我們的make 是有能力檢
測這樣的定義,並會報錯。還有就是如果在變量中使用函數,那麼,這種方
式會讓我們的make 運行時非常慢,更糟糕的是,他會使用得兩個make 的函
數“wildcard”和“shell”發生不可預知的錯誤。因為你不會知道這兩個
函數會被調用多少次。
為了避免上麵的這種方法,我們可以使用make 中的另一種用變量來定義變
量的方法。這種方法使用的是“:=”操作符,如:
x := foo
y := $(x) bar
x := later
其等價於:
y := foo bar
x := later
值得一提的是,這種方法,前麵的變量不能使用後麵的變量,隻能使用前麵
已定義好了的變量。如果是這樣:
y := $(x) bar
x := foo
那麼,y 的值是“bar”,而不是“foo bar”。
上麵都是一些比較簡單的變量使用了,讓我們來看一個複雜的例子,其中包
括了make 的函數、條件表達式和一個係統變量“MAKELEVEL”的使用:
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami)
host-type := $(shell arch)
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif
關於條件表達式和函數,我們在後麵再說,對於係統變量“MAKELEVEL”,其
意思是,如果我們的make 有一個嵌套執行的動作(參見前麵的“嵌套使用
make”),那麼,這個變量會記錄了我們的當前Makefile 的調用層數。
下麵再介紹兩個定義變量時我們需要知道的,請先看一個例子,如果我們要
定義一個變量,其值是一個空格,那麼我們可以這樣來:
nullstring :=
space := $(nullstring) # end of the line
nullstring 是一個Empty 變量,其中什麼也沒有,而我們的space 的值是
一個空格。因為在操作符的右邊是很難描述一個空格的,這裏采用的技術很
管用,先用一個Empty 變量來標明變量的值開始了,而後麵采用“#”注釋
符來表示變量定義的終止,這樣,我們可以定義出其值是一個空格的變量。
請注意這裏關於“#”的使用,注釋符“#”的這種特性值得我們注意,如果
我們這樣定義一個變量:
dir := /foo/bar # directory to put the frobs in
dir 這個變量的值是“/foo/bar”,後麵還跟了4 個空格,如果我們這樣使
用這樣變量來指定別的目錄——“$(dir)/file”那麼就完蛋了。
還有一個比較有用的操作符是“?=”,先看示例:
FOO ?= bar
其含義是,如果FOO 沒有被定義過,那麼變量FOO 的值就是“bar”,如果
FOO 先前被定義過,那麼這條語將什麼也不做,其等價於:
ifeq ($(origin FOO), undefined)
FOO = bar
endif
三、變量高級用法
這裏介紹兩種變量的高級使用方法,第一種是變量值的替換。
我們可以替換變量中的共有的部分, 其格式是“ $(var:a=b) ” 或是
“${var:a=b}”,其意思是,把變量“var”中所有以“a”字串“結尾”的
“a”替換成“b”字串。這裏的“結尾”意思是“空格”或是“結束符”。
還是看一個示例吧:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
這個示例中,我們先定義了一個“$(foo)”變量,而第二行的意思是把
“$(foo)”中所有以“.o”字串“結尾”全部替換成“.c”,所以我們的“$(bar)”
的值就是“a.c b.c c.c”。
另外一種變量替換的技術是以“靜態模式”(參見前麵章節)定義的,如:
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
這依賴於被替換字串中的有相同的模式,模式中必須包含一個“%”字符,
這個例子同樣讓$(bar)變量的值為“a.c b.c c.c”。
第二種高級用法是——“把變量的值再當成變量”。先看一個例子:
x = y
y = z
a := $($(x))
在這個例子中,$(x)的值是“y”,所以$($(x))就是$(y),於是$(a)的值就
是“z”。(注意,是“x=y”,而不是“x=$(y)”)
我們還可以使用更多的層次:
x = y
y = z
z = u
a := $($($(x)))
這裏的$(a)的值是“u”,相關的推導留給讀者自己去做吧。
讓我們再複雜一點,使用上“在變量定義中使用變量”的第一個方式,來看
一個例子:
x = $(y)
y = z
z = Hello
a := $($(x))
這裏的$($(x))被替換成了$($(y)),因為$(y)值是“z”,所以,最終結果是:
a:=$(z),也就是“Hello”。
再複雜一點,我們再加上函數:
x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))
這個例子中,“$($($(z)))”擴展為“$($(y))”,而其再次被擴展為“$($(subst
1,2,$(x)))”。$(x)的值是“variable1”,subst 函數把“variable1”中的
所有“1”字串替換成“2”字串,於是,“variable1”變成“variable2”,
再取其值,所以,最終,$(a)的值就是$(variable2)的值——“Hello”。(喔,
好不容易)
在這種方式中,或要可以使用多個變量來組成一個變量的名字,然後再取其
值:
first_second = Hello
a = first
b = second
all = $($a_$b)
這裏的“$a_$b”組成了“first_second”,於是,$(all)的值就是“Hello”。
再來看看結合第一種技術的例子:
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)
這個例子中,如果$(a1)的值是“a”的話,那麼,$(sources)的值就是“a.c
b.c c.c”;如果$(a1)的值是“1”,那麼$(sources)的值是“1.c 2.c 3.c”。
再來看一個這種技術和“函數”與“條件語句”一同使用的例子:
ifdef do_sort
func := sort
else
func := strip
endif
bar := a d b g q c
foo := $($(func) $(bar))
這個示例中,如果定義了“do_sort”,那麼:foo := $(sort a d b g q c),
於是$(foo)的值就是“a b c d g q”,而如果沒有定義“do_sort”,那麼:
foo := $(sort a d b g q c),調用的就是strip 函數。
當然,“把變量的值再當成變量”這種技術,同樣可以用在操作符的左邊:
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print
lpr $($(dir)_sources)
endef
這個例子中定義了三個變量:“dir”,“foo_sources”和“foo_print”。
四、追加變量值
我們可以使用“+=”操作符給變量追加值,如:
objects = main.o foo.o bar.o utils.o
objects += another.o
於是,我們的$(objects)值變成:“main.o foo.o bar.o utils.o another.o”
(another.o 被追加進去了)
使用“+=”操作符,可以模擬為下麵的這種例子:
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o
所不同的是,用“+=”更為簡潔。
如果變量之前沒有定義過,那麼,“+=”會自動變成“=”,如果前麵有變量
定義,那麼“+=”會繼承於前次操作的賦值符。如果前一次的是“:=”,那
麼“+=”會以“:=”作為其賦值符,如:
variable := value
variable += more
等價於:
variable := value
variable := $(variable) more
但如果是這種情況:
variable = value
variable += more
由於前次的賦值符是“=”,所以“+=”也會以“=”來做為賦值,那麼豈不
會發生變量的遞補歸定義,這是很不好的,所以make 會自動為我們解決這
個問題,我們不必擔心這個問題。
五、override 指示符
如果有變量是通常make 的命令行參數設置的,那麼Makefile 中對這個變量
的賦值會被忽略。如果你想在Makefile 中設置這類參數的值,那麼,你可
以使用“override”指示符。其語法是:
override <variable> = <value>
override <variable> := <value>
當然,你還可以追加:
override <variable> += <more text>
對於多行的變量定義,我們用define 指示符,在define 指示符前,也同樣
可以使用ovveride 指示符,如:
ove
最後更新:2017-04-03 16:48:42
上一篇:
hdu 4608 I-number 模擬
下一篇:
hdu 4607 Park Visit dfs
undefined reference to libiconv_open ext/iconv/.libs/iconv.o by install phpsource
28個實用的源碼/文檔比較合並工具
Java SimpleDateFormat 線程不安全問題及解決方法
java泛型學習3之類型參數的限製
技術人員談管理之範圍管理案例論文
【最近麵試遇到的一些問題】數組沒有length()這個方法,有length的屬性。String有有length()這個方法。
【Java深入學習係列】之CPU的分支預測(Branch Prediction)模型
異步流複製模式如何保證不丟數據?
python利用字典保存配置實現動態調用模塊類方法
JRuby中調用java帶可變參數的方法