1. 程式人生 > >跟我一起寫 Makefile - 2. Makefile 介紹

跟我一起寫 Makefile - 2. Makefile 介紹

跟我一起寫 Makefile - 2. Makefile 介紹

http://wiki.ubuntu.org.cn

2. Makefile 介紹

make 命令執行時,需要一個 Makefile 檔案,以告訴 make 命令如何去編譯和連結程式。

以下示例來源於 GNU 的 make 使用手冊,在這個示例中,我們的工程有 8 個 c 檔案和 3 個頭檔案,我們要寫一個 Makefile 來告訴 make 命令如何編譯和連結這幾個檔案。我們的規則是:
(1) 如果這個工程沒有編譯過,那麼我們的所有 c 檔案都要編譯並被連結。
(2) 如果這個工程的某幾個 c 檔案被修改,那麼我們只編譯被修改的 c 檔案,並連結目標程式。
(3) 如果這個工程的標頭檔案被改變了,那麼我們需要編譯引用了這幾個標頭檔案的 c 檔案,並連結目標程式。

只要我們的 Makefile 寫得夠好,所有的這一切,我們只用一個 make 命令就可以完成,make 命令會自動智慧地根據當前的檔案修改的情況來確定哪些檔案需要重編譯,從而自己編譯所需要的檔案和連結目標程式。

2.1 Makefile 的規則

先來粗略地看一看 Makefile 的規則。

target ... : prerequisites ...
	command
	...
	...

target 可以是一個 Object File (目標檔案),也可以是一個執行檔案,還可以是一個標籤 (label)。對於標籤這種特性,在後續的偽目標章節中會有敘述。

prerequisites 就是要生成那個 target 所需要的檔案或是目標。

command 就是 make 需要執行的命令。(任意的 shell 命令)

這是一個檔案的依賴關係,target 這一個或多個的目標檔案依賴於 prerequisites 中的檔案,其生成規則定義在 command 中。prerequisites 中如果有一個以上的檔案比 target 檔案要新的話,command 所定義的命令就會被執行。這就是 Makefile 的規則,也是 Makefile 的主線和核心。

2.2 一個示例

如果一個工程有 3 個頭檔案和 8 個 c 檔案,我們為了完成前面所述的那三個規則,我們的 Makefile 應該是下面這個樣子的。

edit : main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o      
/*註釋:如果後面這些 .o 檔案比 edit 可執行檔案新,那麼才會去執行下面這句命令*/
	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 就不會自動去找它的依賴性,也就不會自動執行其後所定義的命令。要執行其後的命令 (不僅用於 clean,其他 lable 同樣適用),就要在 make 命令後明顯得指出這個 lable 的名字。這樣的方法非常有用,我們可以在一個 Makefile 中定義不用的編譯或是和編譯無關的命令,比如程式的打包,程式的備份等等。

2.3 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 會被重新連結。

2.4 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 即可。

2.5 讓 make 自動推導

GNU 的 make 很強大,它可以自動推導檔案以及檔案依賴關係後面的命令,於是我們就沒必要去在每一個 .o 檔案後都寫上類似的命令,我們的 make 會自動識別,並自己推導命令。

只要 make 看到一個 .o 檔案,它就會自動的把 .c 檔案加在依賴關係中,如果 make 找到一個 whatever.o,那麼 whatever.c,就會是 whatever.o 的依賴檔案。並且 cc -c whatever.c 也會被推匯出來。於是我們的 Makefile 再也不用寫得這麼複雜。

objects = main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o
 cc = gcc

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 是個偽目標檔案。

2.6 另類風格的 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 檔案,那就理不清楚了。

2.7 清空目標檔案的規則

每個 Makefile 中都應該寫一個清空目標檔案 (.o 和執行檔案) 的規則,這不僅便於重新編譯,也很利於保持檔案的清潔 《程式設計修養》。一般的風格都是:

clean:
	rm edit $(objects)

更為穩健的做法是:

.PHONY : clean
clean :
	-rm edit $(objects)

前面說過,.PHONY 意思表示 clean 是一個偽目標。而在 rm 命令前面加了一個小減號的意思就是,也許某些檔案出現問題,但不要管,繼續做後面的事。當然,clean 的規則不要放在檔案的開頭,不然就會變成 make 的預設目標,相信誰也不願意這樣。不成文的規矩是 clean 從來都是放在檔案的最後。

2.8 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 鍵開始。

2.9 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。

2.10 引用其它的 Makefile

在 Makefile 使用 include 關鍵字可以把別的 Makefile 包含進來,這很像 C 語言的 #include,被包含的檔案會原模原樣的放在當前檔案的包含位置。include 的語法是:

include <filename>;

filename 可以是當前作業系統 shell 的檔案模式 (可以包含路徑和萬用字元)。

在 include 前面可以有一些空字元,但是絕不能是 Tab 鍵開始。include 和 <filename>; 可以用一個或多個空格隔開。舉個例子,你有這樣幾個 Makefile:a.mkb.mkc.mk,還有一個檔案叫 foo.make,以及一個變數 $(bar),其包含了 e.mkf.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,其作用和這一個是一樣的。

2.11 環境變數 MAKEFILES

如果你的當前環境中定義了環境變數 MAKEFILES,那麼 make 會把這個變數中的值做一個類似於 include 的動作。這個變數中的值是其它的 Makefile,用空格分隔。只是它和 include 不同的是,從這個環境變數中引入的 Makefile 的目標不會起作用,如果環境變數中定義的檔案發現錯誤,make 也會不理。

但是在這裡我還是建議不要使用這個環境變數,因為只要這個變數一被定義,那麼當你使用 make 時,所有的 Makefile 都會受到它的影響,這絕不是你想看到的。在這裡提這個事,只是為了告訴大家,也許有時候你的 Makefile 出現了怪事,那麼你可以看看當前環境中有沒有定義這個變數。

2.12 make 的工作方式

GNU 的 make 工作時的執行步驟如下:
(1) 讀入所有的 Makefile。
(2) 讀入被 include 的其它 Makefile。
(3) 初始化檔案中的變數。
(4) 推導隱晦規則,並分析所有規則。
(5) 為所有的目標檔案建立依賴關係鏈。
(6) 根據依賴關係,決定哪些目標要重新生成。
(7) 執行生成命令。

1-5 步為第一個階段,6-7 為第二個階段。第一個階段中,如果定義的變數被使用了,那麼 make 會把其展開在使用的位置。但 make 並不會完全馬上展開,make 使用的是拖延戰術,如果變量出現在依賴關係的規則中,那麼僅當這條依賴被決定要使用了,變數才會在其內部展開。

Wordbook

GNU Compiler Collection,GCC:GNU 編譯器套件
GNU C Compiler,GCC:GNU C 編譯器
Microsoft Visual C++,MSVC
Unix / UNIX /ˈjuːnɪks/