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