Makefile基礎 4. 自動處理標頭檔案的依賴關係

工程師WWW發表於2014-01-26

現在我們的Makefile寫成這樣:

all: main

main: main.o stack.o maze.o
	gcc $^ -o $@

main.o: main.h stack.h maze.h
stack.o: stack.h main.h
maze.o: maze.h main.h

clean:
	-rm main *.o

.PHONY: clean

按照慣例,用all做預設目標。現在還有一點比較麻煩,在寫main.ostack.omaze.o這三個目標的規則時要檢視原始碼,找出它們依賴於哪些標頭檔案,這很容易出錯,一是因為有的標頭檔案包含在另一個標頭檔案中,在寫規則時很容易遺漏,二是如果以後修改原始碼改變了依賴關係,很可能忘記修改Makefile的規則。為了解決這個問題,可以用gcc-M選項自動生成目標檔案和原始檔的依賴關係:

$ gcc -M main.c
main.o: main.c /usr/include/stdio.h /usr/include/features.h \
  /usr/include/sys/cdefs.h /usr/include/bits/wordsize.h \
  /usr/include/gnu/stubs.h /usr/include/gnu/stubs-32.h \
  /usr/lib/gcc/i486-linux-gnu/4.3.2/include/stddef.h \
  /usr/include/bits/types.h /usr/include/bits/typesizes.h \
  /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
  /usr/lib/gcc/i486-linux-gnu/4.3.2/include/stdarg.h \
  /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h main.h \
  stack.h maze.h

-M選項把stdio.h以及它所包含的系統標頭檔案也找出來了,如果我們不需要輸出系統標頭檔案的依賴關係,可以用-MM選項

$ gcc -MM *.c
main.o: main.c main.h stack.h maze.h
maze.o: maze.c maze.h main.h
stack.o: stack.c stack.h main.h

接下來的問題是怎麼把這些規則包含到Makefile中,GNU make的官方手冊建議這樣寫:

all: main

main: main.o stack.o maze.o
	gcc $^ -o $@

clean:
	-rm main *.o

.PHONY: clean

sources = main.c stack.c maze.c

include $(sources:.c=.d)

%.d: %.c
	set -e; rm -f $@; \
	$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$

sources變數包含我們要編譯的所有.c檔案,$(sources:.c=.d)是一個變數替換語法,把sources變數中每一項的.c替換成.d,所以include這一句相當於:

include main.d stack.d maze.d

類似於C語言的#include指示,這裡的include表示包含三個檔案main.dstack.dmaze.d,這三個檔案也應該符合Makefile的語法。如果現在你的工作目錄是乾淨的,只有.c檔案、.h檔案和Makefile,執行make的結果是:

$ make
Makefile:13: main.d: No such file or directory
Makefile:13: stack.d: No such file or directory
Makefile:13: maze.d: No such file or directory
set -e; rm -f maze.d; \
	cc -MM  maze.c > maze.d.$$; \
	sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
	rm -f maze.d.$$
set -e; rm -f stack.d; \
	cc -MM  stack.c > stack.d.$$; \
	sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \
	rm -f stack.d.$$
set -e; rm -f main.d; \
	cc -MM  main.c > main.d.$$; \
	sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \
	rm -f main.d.$$
cc    -c -o main.o main.c
cc    -c -o stack.o stack.c
cc    -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main

一開始找不到.d檔案,所以make會報警告。但是make會把include的檔名也當作目標來嘗試更新,而這些目標適用模式規則%.d: %c,所以執行它的命令列表,比如生成maze.d的命令:

set -e; rm -f maze.d; \
	cc -MM  maze.c > maze.d.$$; \
	sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
	rm -f maze.d.$$

注意,雖然在Makefile中這個命令寫了四行,但其實是一條命令,make只建立一個Shell程式執行這條命令,這條命令分為5個子命令,用;號隔開,並且為了美觀,用續行符\拆成四行來寫。執行步驟為:

  1. set -e命令設定當前Shell程式為這樣的狀態:如果它執行的任何一條命令的退出狀態非零則立刻終止,不再執行後續命令。

  2. 把原來的maze.d刪掉。

  3. 重新生成maze.c的依賴關係,儲存成檔案maze.d.1234(假設當前Shell程式的id是1234)。注意,在Makefile中$有特殊含義,如果要表示它的字面意思則需要寫兩個$,所以Makefile中的四個$傳給Shell變成兩個$,兩個$在Shell中表示當前程式的id,一般用它給臨時檔案起名,以保證檔名唯一。

  4. 這個sed命令比較複雜,就不細講了,主要作用是查詢替換。maze.d.1234的內容應該是maze.o: maze.c maze.h main.h,經過sed處理之後存為maze.d,其內容是maze.o maze.d: maze.c maze.h main.h

  5. 最後把臨時檔案maze.d.1234刪掉。

不管是Makefile本身還是被它包含的檔案,只要有一個檔案在make過程中被更新了,make就會重新讀取整個Makefile以及被它包含的所有檔案,現在main.dstack.dmaze.d都生成了,就可以正常包含進來了(假如這時還沒有生成,make就要報錯而不是報警告了),相當於在Makefile中添了三條規則:

main.o main.d: main.c main.h stack.h maze.h
maze.o maze.d: maze.c maze.h main.h
stack.o stack.d: stack.c stack.h main.h

如果我在main.c中加了一行#include "foo.h",那麼:

1、main.c的修改日期變了,根據規則main.o main.d: main.c main.h stack.h maze.h要重新生成main.omain.d。生成main.o的規則有兩條:

main.o: main.c main.h stack.h maze.h
%.o: %.c
#  commands to execute (built-in):
        $(COMPILE.c) $(OUTPUT_OPTION) $<

第一條是把規則main.o main.d: main.c main.h stack.h maze.h拆開寫得到的,第二條是隱含規則,因此執行cc命令重新編譯main.o。生成main.d的規則也有兩條:

main.d: main.c main.h stack.h maze.h
%.d: %.c
	set -e; rm -f $@; \
	$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$

因此main.d的內容被更新為main.o main.d: main.c main.h stack.h maze.h foo.h

2、由於main.d被Makefile包含,main.d被更新又導致make重新讀取整個Makefile,把新的main.d包含進來,於是新的依賴關係生效了。

相關文章