在基于make的編譯環(huán)境中,正確列出makefile文件中所有的依賴(lài)項,是一個(gè)特別重要,卻又時(shí)常令人沮喪的任務(wù)。
本文檔將給出一種能讓make自動(dòng)生成并維護依賴(lài)的有效方法。
這個(gè)方法的發(fā)明人是Tom Tromey <tromey@cygnus.com> ,我僅在這里提一次。方法的所有權歸他;解釋不妥之處都由我(Paul D.Smith)負責。
為了確保在必須的時(shí)候一定會(huì )編譯(且僅在必須的時(shí)候才進(jìn)行編譯),所有的make程序都必須精確地知曉目標文件的依賴(lài)。
手動(dòng)更新這個(gè)列表不僅繁瑣,而且很容易出錯。任何規模的系統,都傾向于提供自動(dòng)提取信息的工具??赡茏畛S玫墓ぞ呔褪莔akedepend程序,它能讀取C源代碼并生成格式化的目標項依賴(lài)列表,可以插入或被包含進(jìn)makefile文件中。
另一種流行的方案,是使用合適的編譯器或預處理器(譬如GCC)來(lái)生成依賴(lài)信息。
本文的主要目的不是要討論如何生成依賴(lài)信息,雖然我會(huì )在最后一節中提及一些方法。
這里主要想介紹如何把這些工具的調用和輸出整合進(jìn)GNU make中,使依賴(lài)信息保持準確和實(shí)時(shí),并盡可能做到無(wú)縫和高效。
如上所述,這些方法只適用于GNU make。適當的修改后應該也可以用于任何包含了include功能的其它版本make程序;這可以當作留給讀者的練習。不過(guò)在做練習之前,請先閱讀Paul的Makefile第一法則:)。
原始的make depend方法
一個(gè)歷史悠久的方法是在makefile文件中加入特殊目標項,通常叫作depend,用來(lái)創(chuàng )建依賴(lài)信息。這個(gè)規則的命令是啟動(dòng)某個(gè)依賴(lài)跟蹤工具來(lái)更新目錄中的相關(guān)文件。
對于功能較弱的make程序,通常還需要借助shell腳本的幫助將生成的依賴(lài)追加至makefile自身。當然在GNU make中,我們可以用include指令完成。
這個(gè)方法雖然簡(jiǎn)單,卻常帶來(lái)嚴重問(wèn)題。首先,只有在用戶(hù)顯式指明的時(shí)候依賴(lài)才會(huì )重新生成;如果用戶(hù)不定期運行make depend,很快會(huì )因為依賴(lài)過(guò)期而不能正確生成目標?;诖?,我們不能認為這個(gè)方法是無(wú)縫和精確的。
另一個(gè)問(wèn)題是,這種方法的第二次以及以后每次運行都是相對低效的。因為它修改makefile文件,你就必須添加一個(gè)獨立的編譯步驟,這就意味著(zhù)在每個(gè)子目錄都產(chǎn)生了調用開(kāi)銷(xiāo),還得要加上依賴(lài)生成工具本身的開(kāi)銷(xiāo)。同時(shí),即使文件沒(méi)有改變,它也會(huì )檢查每一個(gè)文件。
那么,我們來(lái)瞧瞧如何做得更好。
使用GNU make的include
下文涉及的方法依賴(lài)于GNU make的include預處理語(yǔ)句。正如它的名字,include語(yǔ)句使得makefile文件可以包含其他makefile文件,效果就如同文件是在那兒輸入的一樣。
我們馬上就能找到它的用處,即用來(lái)避免用前面提到的方法追加依賴(lài)信息。并且GNU make在處理include時(shí)有一個(gè)有趣的特性:如同生成普通文件,GNU make會(huì )嘗試生成被包含的makefile文件。如果被包含的makefile被重建,make將重啟,讀取新版本的makefile文件。
我們可以利用這個(gè)自動(dòng)重建的特性來(lái)避免獨立的“make depend”步驟,而是在正常的生成應用之前生成依賴(lài)。例如,如果你定義依賴(lài)輸出文件依賴(lài)于所有的源文件,那么它將在每一次有代碼改變時(shí)重建。因此依賴(lài)信息將永遠保持最新,而不需要用戶(hù)顯式指明來(lái)生成依賴(lài)文件。當然,不幸的是,任何文件有任何變化都會(huì )導致依賴(lài)文件的重建。
關(guān)于GNU make自動(dòng)重建特性的詳情,請參閱GNU make用戶(hù)手冊,“How Makefiles Are Remade“一節。
簡(jiǎn)單地自動(dòng)生成依賴(lài)
GNU make用戶(hù)手冊中介紹了一種處理自動(dòng)生成依賴(lài)的方法,參見(jiàn)“Generating Dependencies Automatically“一節。
在此方法中,對每個(gè)源文件創(chuàng )建一個(gè)“依賴(lài)“文件(在我們的例子中使用后綴P來(lái)標識)。依賴(lài)文件中包含的是一個(gè)源文件的依賴(lài)信息聲明。
隨后makefile程序include了所有的依賴(lài)文件并從中獲取依賴(lài)信息。一個(gè)隱含的規則用來(lái)描述依賴(lài)文件是如何生成的。類(lèi)似于這樣的形式:
| 1 2 3 4 5 6 | SRCS = foo.c bar.c ... %.P : %.c $(MAKEDEPEND) @sed 's/\($*\)\.o[ :]*/\1.o $@ : /g' < $*.d > $@; \ rm -f $*.d; [ -s $@ ] || rm -f $@ include $(SRCS:.c=.P) |
這些例子中我將簡(jiǎn)單使用$(MAKEDEPEND)來(lái)代表你選擇的生成依賴(lài)的任意方式。幾種可能的實(shí)現會(huì )在稍后介紹。
在這里,輸出先被寫(xiě)入一個(gè)臨時(shí)文件,接著(zhù)被后續處理改變了正常的格式:
foo.o: foo.c foo.h bar.h baz.h將也包含.P文件自身,類(lèi)似這樣:
foo.o foo.P: foo.c foo.h bar.h baz.h每當GNU make讀取makefile后,在執行任何操作之前,它會(huì )檢查并重建每個(gè)包含的makefile文件,在這里就是.P文件。我們有創(chuàng )建他們的規則,也有它們的依賴(lài)項(在本例中與.o文件相同)。如果有任何可能導致.o文件需要重建的修改,都會(huì )導致.P文件重建。
也就是說(shuō),當源文件或其包含的文件變化后,make會(huì )重建.P文件,重啟自身,讀取新版makefile,再用常規方法生成目標,這時(shí)讀到的就是更新過(guò)的準確的依賴(lài)列表。
這里我們解決了舊方法的兩個(gè)問(wèn)題。第一,用戶(hù)不必使用特殊命令來(lái)確保依賴(lài)列表的準確性。第二,只有真正變化的依賴(lài)才會(huì )被更新,而不是更新目錄中的所有文件。
但是,這種方法帶來(lái)了三個(gè)新問(wèn)題。首先仍然是效率問(wèn)題。雖然我們只重新檢查了發(fā)生變化的文件,但是任何文件修改都會(huì )導致make重啟,在大型的編譯系統中可能會(huì )很慢。
第二個(gè)問(wèn)題只是一個(gè)小煩惱。當你添加一個(gè)新文件,或是第一次編譯時(shí),.P文件不存在。當make試圖包含它卻發(fā)現它不在,會(huì )產(chǎn)生一個(gè)警告。這不是致命的,因為make會(huì )接著(zhù)重建.P文件并自行重啟;只是有些難看而已。
第三個(gè)問(wèn)題就相對嚴重了:如果你刪除或是重命名了被依賴(lài)文件(比如C的.h文件),make將停止并報怨找不到目標:
make: *** No rule to make target `bar.h', needed by `foo.P'. Stop.這是因為.P文件依賴(lài)于一個(gè)無(wú)法找到的文件。make無(wú)法重建.P文件,除非找到它依賴(lài)的所有文件,但是在重建.P文件之前,make無(wú)法知道正確的依賴(lài)。這是鐵律。
唯一的解決辦法是手動(dòng)刪除與丟失文件相關(guān)的.P文件——簡(jiǎn)單的做法是直接全部都刪掉而不必去查找相關(guān)文件。你甚至可以創(chuàng )建一個(gè)clean-deps目標來(lái)讓它自動(dòng)化(需要根據MAKECMDGOALS的具體情況來(lái)實(shí)現以避免重建.P文件)。毫無(wú)疑問(wèn)這是令人煩惱的,但鑒于在典型環(huán)境中不會(huì )經(jīng)常有文件改名或刪除的操作,這個(gè)問(wèn)題也許不那么嚴重。
進(jìn)階自動(dòng)生成依賴(lài)
這里介紹的方法由Tom Tromey發(fā)明,同時(shí)也是FSF的automake工具所使用的標準方法。我認為它極為巧妙。
避免重新執行make
讓我們再來(lái)審視上面提及的第一個(gè)問(wèn)題:重新執行make。如果你認為重新調用真的很沒(méi)有必要。因為目標項的依賴(lài)被更改這點(diǎn)我們是已經(jīng)知道的,實(shí)際上我們在此次生成時(shí)不需要最新的依賴(lài)列表。我們已經(jīng)知道目標需要重新生成了,而最新的依賴(lài)列表對這點(diǎn)毫無(wú)影響。我們真正需要確保的是在下次執行make,判斷目標是否需要重新生成時(shí),依賴(lài)列表是已更新的。
因為我們在本次生成時(shí)不需要最新的依賴(lài)列表,避免重新執行make就是完全可行的:我們可以在生成目標的同時(shí)生成依賴(lài)列表。換句話(huà)說(shuō),我們可以修改目標的生成規則,在其命令中加入生成依賴(lài)列表。此外,在這種情況下,我們必須小心不要再提供自動(dòng)生成依賴(lài)的規則了:如果那樣,make會(huì )重新生成它們并重啟:這不是我們所希望的。
現在我們不再關(guān)心依賴(lài)文件的存在與否,解決第二個(gè)問(wèn)題(畫(huà)蛇添足的警告)就很簡(jiǎn)單了:我們可以使用GNU make的-include指令來(lái)包含它們,這樣它們不存在時(shí)就不會(huì )有任何提示了。
讓我們來(lái)看看到目前為止的一個(gè)例子:
| 1 2 3 4 5 | SRCS = foo.c bar.c ... %.o : %.c @$(MAKEDEPEND) $(COMPILE.c) -o $@ $< -include $(SRCS:.c=.P) |
避免“No rule to make target…“錯誤
這個(gè)問(wèn)題有些棘手。事實(shí)上,我們可以通過(guò)顯式在目標中指明文件來(lái)說(shuō)服make不要報錯退出。如果存在目標項,卻不包含命令(無(wú)論是顯式或隱式)或任何依賴(lài)項,則make簡(jiǎn)單地認為目標項是最新的。這是合情合理的,并且也正是我們所期待的。
對于上述發(fā)生錯誤的情況,目標項不存在。根據GNU make用戶(hù)手冊,“沒(méi)有命令或依賴(lài)項的規則“:
如果規則不包含任何依賴(lài)項或命令,而且目標文件不存在,那么make會(huì )認為目標項總是已更改的。這意味著(zhù)其他依賴(lài)于此目標項的命令一定會(huì )被執行。
完美。這條規則保證了make在處理不存在的文件時(shí)不會(huì )拋出異常,而且保證了任何依賴(lài)于目標項的文件都會(huì )被重新生成,這正是我們想要的。
因此,我們要做的就是在生成完原來(lái)的依賴(lài)文件后,將所有的依賴(lài)項放到目標項中,不給它添加命令或依賴(lài)項。類(lèi)似于這樣的[1]:
| 1 2 3 4 5 6 7 8 9 | SRCS = foo.c bar.c ... %.o : %.c @$(MAKEDEPEND); \ cp $*.d $*.P; \ sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \ -e '/^$$/ d' -e 's/$$/ :/' < $*.d > $*.P; \ rm -f $*.d $(COMPILE.c) -o $@ $< -include $(SRCS:.c=.P) |
簡(jiǎn)單解釋一下,這里首先創(chuàng )建原始的依賴(lài)列表,然后對依賴(lài)文件中的每一行作如下處理后追加至依賴(lài)列表:去掉原來(lái)的目標頂和所有的行繼續符(\),在末尾追加依賴(lài)分隔符(:)。這個(gè)方法在下文的幾種MAKEDEPEND實(shí)現時(shí)工作正常;如果你用了其他依賴(lài)生成工具,或許需要作些修改。
放置輸出文件
也許你不喜歡讓.P文件塞滿(mǎn)你的源碼目錄。你可以很容易讓makefile將它們放到別的地方。這里有一個(gè)針對進(jìn)階方法的例子;你可以依理應用到其他方法:
| 1 2 3 4 5 6 7 8 9 10 11 12 | DEPDIR = .deps df = $(DEPDIR)/$( *F) SRCS = foo.c bar.c ... %.o : %.c @$(MAKEDEPEND); \ cp $(df).d $(df).P; \ sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \ -e '/^$$/ d' -e 's/$$/ :/' < $(df).d >> $(df).P; \ rm -f $(df).d $(COMPILE.c) -o $@ $< -include $(SRCS:%.c=$(DEPDIR)/%.P) #注意你需要把所有MAKEDEPEND腳本中的所有$*.d都替換成$(df).d。 |
實(shí)現MAKEDEPEND
我在上文中無(wú)所顧忌地使用了MAKEDEPEND這個(gè)變量,下面將討論幾種可能的實(shí)現。
1. MAKEDEPEND = /usr/lib/cpp
生成依賴(lài)最簡(jiǎn)單的方法是使用C預處理器本身。這需要對你的預處理器的輸出格式有一定了解——幸運的是對我們的目的而言,大部分UNIX預處理器都有類(lèi)似的輸出。為了維護在輸出錯誤或調試信息時(shí)所要的行號信息,預處理器必須在每次進(jìn)入或跳出#include文件時(shí)提供行號及文件名信息。這些信息可以被用作分析哪些文件被包含了。
大多數UNIX系統會(huì )輸出這種格式的特殊行:
#lineno "filename" extra我們只關(guān)心文件名。如果你的預處理器產(chǎn)生上面的輸出,像這樣定義MAKEDEPEND應該是可行的:
| 1 | MAKEDEPEND = $(CPP) $(CPPFLAGS) $< \ | sed -n 's/^\# *[0-9][0-9]* *"\([^"]*\)".*/$*.o: \1/p' \ | sort | uniq > $*.d |
如果你使用的是進(jìn)階方法,你可以在sed腳本中將$*.o替換成$@。如果你使用了現代版本的sort,你也可以把sort | uniq用sort -u替換。
當然了,如果你走這條路,你也可以把你要添加的后期處理加入腳本中。
2. MAKEDEPEND = makedepend
X window系統的源代碼樹(shù)提供了一個(gè)makedepend程序。它檢查C源文件及頭文件生成依賴(lài)列表。它默認設計是將依賴(lài)列表追加至makefile文件的尾部,因此想用我們自己的方式來(lái)使用它需要使用一點(diǎn)小伎倆。例如某些版本會(huì )在輸出文件不存在時(shí)報錯。
這樣做應該是可行的:
| 1 | MAKEDEPEND = touch $*.d && makedepend $(CPPFLAGS) -f $*.d $< |
3. MAKEDEPEND = gcc -M
GCC包含了一個(gè)可生成依賴(lài)文件的預處理器。這樣做應該是可行的:
| 1 | MAKEDEPEND = gcc -M $(CPPFLAGS) -o $*.d $< |
將編譯和依賴(lài)合在一起
如果你使用GCC,你可以在編譯時(shí)同時(shí)生成依賴(lài),從而節省大量的時(shí)間。如果你有一個(gè)GCC的最新版本,你可以使用-MD選項使之生成依賴(lài)信息。這個(gè)選項始終把依賴(lài)信息輸出到.d文件中。因此,你可以在進(jìn)階方法的基礎上稍作修改,得到一個(gè)快一些的版本:
| 1 2 3 4 5 6 | %.o : %.c $(COMPILE.c) -MD -o $@ $< @cp $*.d $*.P; \ sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \ -e '/^$$/ d' -e 's/$$/ :/' < $*.d >> $*.P; \ rm -f $*.d |
在一些舊版的GCC上使用環(huán)境變量也能做到。你還可以向GCC傳遞一個(gè)選項序列,類(lèi)似于-Wp,-MD,$*.xx,來(lái)用指定的文件名替換GCC的默認輸出。這在你想輸出依賴(lài)文件到不同的目錄時(shí)特別有用。查閱你的編譯器/預處理器以得到更多信息。
非C文件的依賴(lài)生成
一般來(lái)說(shuō),你需要用某種方式生成依賴(lài)文件,以使用這些方法。如果你的工作不是基于C文件的,你需要找到或寫(xiě)自己的方法。只要能生成依賴(lài)文件就行。這通常不會(huì )太難。
Han-Wen Nienhuys提出了一個(gè)有趣的方案,并有一個(gè)“用于驗證”的實(shí)現,盡管它目前只在Linux上工作。他提出使用LD_PRELOAD環(huán)境變量來(lái)插入特殊的共享庫替換open(2)系統調用。新版本的open會(huì )輸出命令執行時(shí)讀過(guò)的所有文件。于是不用任何特殊擴展工具就能得到可信賴(lài)的依賴(lài)信息。在他用于驗證的實(shí)現在,你可以控制輸出文件來(lái)排除一些類(lèi)型文件(也許是共享庫)。
[1]注意我修改了Tom在automake中使用的預處理腳本,使之可適應不同風(fēng)格的MAKEDEPEND輸出。
原文作者:Paul D.Smith
原文地址:Advanced Auto-Dependency Generation
聯(lián)系客服