基於make的構建環境要正確工做, 一個很重要(也很煩人)的任務是, 在makefile中正確列
舉依賴.html
這個文檔將介紹了一個很是有用的讓make自身來建立和維護這些依賴的方法.node
文章來源shell
全部的make程序都須要知道, 某個特定的target依賴的文件有哪些, 以便確認它(target)
會在必要的時候進行rebuild.函數
手動更行這個清單不只僅是讓人乏味, 並且很是容易出錯. 多數系統(不論大小)都偏向與
提供自動提取這個信息的自動化工具. 傳統的工具的是makedepend
程序, 其會讀取c源代
碼, 並以能夠include至makefile中的__目標-依賴__模式生成頭文件清單.工具
若是使用更增強大一點的編譯器或者預處理器, 更加現代話的解決方案是讓編譯器或者預
處理器來生成這個信息.post
這篇文章的意圖不是專門討論依賴信息得到的方式的(儘管有涉及到), 而是, 介紹一些有
用的將這些工具的調用,輸出和gnu make組合, 來確保依賴信息老是正確和最新的, 銜接越
緊密(且越高效)越好.ui
這些方法依賴gnu make提供的特性. 可能能夠經過修改它們來在其餘版本的make上應用.
那就等你本身嘗試啦. 可是, 在盡心那個嘗試以前請看哈paul的makefile第一原則.net
若是有誰已近不耐煩了, 這是一個完整的最佳的實踐方案. 這個方案須要你的編譯器的支
持: 默認你使用gcc做爲編譯器(或者提供了和gcc兼容的預處理選項的編譯器). 若是你的
編譯器不知足這個條件, 請看另外的方案.unix
將這個加入到你的makefile環境中,(藍色的部分是對gnu make提供的內建內容的改動). 當
然, 你能夠卻略不符合你須要的模式規則(或者添加你須要的, whatever).
(固然我這裏並無藍色...whatever)調試
depdir := .deps depflags = -mt $@ -mmd -mp -mf $(depdir)/$*.d compile.c = $(cc) $(depflags) $(cflags) $(cppflags) $(target_arch) -c %.o : %.c %.o : %.c $(depdir)/%.d | $(depdir) $(compile.c) $(output_option) $< $(depdir): ; @mkdir -p $@ depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
要注意, include
這一行須要出如今初始, 默認target以後, 不然引入的依賴會取代你的
默認target配置. 將這個加到makefile末尾是很好的(或者放在一個單獨的makefile文件裏
並include他)
還這, 這裏認爲srcs變量包含全部你想要跟蹤依賴的源文件(不是頭文件)
若是你只是先要知道這些改動的意義的話, 而且考慮一些問題和對它們的解決方案, 能夠(看原文,..)
make depend
方法一個由來已久的處理依賴生成的方式是, 在makefiles中提供一個特殊的target, 一般是
depend, 其能夠用於建立依賴信息. 這個target的命令會對xx文件調用一些依賴跟蹤工具
..生成makefile格式的依賴信息.
若是你的make版本支持include, 你能夠將它們(依賴輸出)重定向到一個文件, 而後
include這個文件. 若是不支持的話, 一般還須要利用shell來將依賴列表追加到makefile
文件末尾...
這樣雖然很簡單, 可是存在很嚴重的問題. 首先也是最重要的是, 依賴只在使用者明確要
求更新的時候才更新, 若是使用者並無常常運行make depend
, 依賴可能會嚴重果實,
make就不能正確得rebuild target.. 所以, 咱們無法說這是無縫且正確的.
第二個問題是, 運行make depend
是不高效的, 特別是第一次. 由於它會修改makefile,
一般須要做爲一個單獨的構建步驟, 也就是在每一個子目錄的每次make都須要額外調用一次
之類的, 除去依賴生成工具自身的開銷不說. 還有, 它會檢查每一個文件的依賴, 即便是沒
有改變的文件
咱們會看看到咱們如何能夠作到更好.
include
指令多數版本的make都支持某種類型的include指令(實際上, include
是最新的posix規範中
明確要求的).
你立刻就會看到爲何這個會有用, 就好比避免上面的追加依賴信息到makefile中. 而在
gnu make的include處理中有更多有趣的能力...gnu make會嘗試rebuild引入的makefile.
若是成功rebuild, gnu make會從新執行它本身類讀入新版本的makefile.
這個自動重建的特性能夠用於避免使用單獨的make depend
步驟: 若是你將全部的源文件
做爲包含依賴的文件先決條件, 而後將那個文件include到你的makefile, 則它會在每次有
源文件變更的時候重建. 這樣的結果是, 依賴信息老是最新的, 使用者不須要明確運行
make depend
固然, 這意味每次有文件變更的時候全部的文件的依賴信息都會從新計算, 很遺憾. 咱們
還能夠作得更好.
關於gnu make的自動重建特性的詳細信息, 能夠看gnu make的用戶手冊中"how makefiles are remade"一節
gnu make的用戶手冊中generating dependencies automatically
一節中介紹了一種處理自動依賴的方式.
在這個方式中, 或爲每一個源文件建立一個單獨的依賴文件(在咱們的例子中咱們會使用
basename加上.d
後綴做爲文件). 這個文件包含了從那個源文件建立的target的一條依賴
, 提供生成target的先決條件.
這些依賴文件以後都會被makefile引入. 提供了一條描述依賴文件如何建立的隱式規則.
總的來講, 差很少就是這樣:
srcs = foo.c bar.c ... %.d : %.c $(makedepend) include $(srcs:.c=.d)
在這個例子中, 我會使用變量$(makedepend)
來表明你選擇的用於建立依賴文件的方式.
這個變量的一些可能的值以後會介紹.
生成的依賴文件的格式是什麼呢? 在這個簡單的例子中, 咱們須要聲明對象文件和依賴文
件都有相同的先決條件: 源文件和全部的頭文件, 所以foo.d
文件可能會包含這個:
foo.o foo.d: foo.c foo.h bar.h baz.h
當gnu make讀取這個makefile的時候, 在進行別的事情以前, 會嘗試重建引入的makefile,
在這個例子中是後綴.d
的文件. 咱們有一條用於構建它們的規則, 而且依賴和構建.o
文件的依賴同樣. 所以, 當任何改動致使原來的target過期的時候, 也會致使.d
文件被
重建.
所以, 當任何源文件或者引入的文件變更的時候, make或重建.d
文件, 從新執行它本身
來讀入新的makefile, 而後繼續構建, 此次用的是最新的, 正確的依賴列表.
這裏咱們解決了前面的方案的兩個問題. 首先, 使用者不須要作任何工做來更新依賴列表,
make本身會完成. 第二, 只更新實際改動的文件的依賴列表, 而非目錄中的全部文件.
可是, 又有了三個新的問題. 首先是, 仍然不夠高效, 雖然咱們只從新檢查了改動的文件,
咱們仍然會在有變更的時候從新執行make, 對於大的構建系統會很慢.
第二個問題是僅僅是煩人: 當你新添加一個文件或者第一次構建, 不存在.d
文件. 當
make試圖include的時候會發現它不存在, 他會生成一個warning. 以後gnu make會繼續重
建.d
文件, 而後從新調用自身, 不致命, 可是煩人.
第三個問題更加嚴重: 若是你移除或者重命名了一個先決文件(好比c的.h
文件), make會
以至命錯誤推出, 抱怨target不存在:
make: *** no rule to make target 'bar.h', needed by 'foo.d'. stop.
這是由於.d
文件有make找不到的依賴. 沒有先決文件的話無法重建.d
文件, 而它在重
建.d
文件以前不知道它不須要這個先決條件.
惟一的解決方案是手動介入並移除任何引用了缺失的文件的.d
文件, 一般所有移除會更
簡單, 甚至能夠建立一個clean-deps
目標或者相似的來自動作這個(..).說來這個確實是
夠惱人的, 可是若是文件愛呢移除或者重命名不常發生, 可能就不是致命的了.
上面介紹的基礎的方式是由tom tromey策劃的, 他使用其做爲fsf的automake工具的標準依
賴生成方式. 我(不是我)對其進行了一些改動來讓它能夠用於一個更加通常化的構建環境
中.
先解決上面的第一個問題: make的從新調用. 若是你想想的話, 這個從新調用真的是沒
有必要的. 由於咱們知道target的一些先決條件變更了, 咱們必須重建構建target, 更新
依賴列表也不會影響這個決定. 咱們真正須要作的是確保先決條件列表在make的下次調用,
咱們再次須要決定是不是最新的時候.
由於在這個構建中不須要最新的先決條件列表, 咱們實際上能夠徹底能夠避免從新調用
make: 咱們可讓先決條件列表在target重建的時候build. 換句話說, 咱們能夠該百納
target的構建規則來加入更新依賴文件的命令.
在這個例子中, 咱們必須很是當心, 咱們沒有提供規則來自動都見依賴: 若是咱們提供了,
make仍然會嘗試從新構建它們並從新執行: 這不是咱們想要的
如今咱們不關心不存在的依賴文件, 解決第二個問題(多餘的warning)就很是簡單了: 直接
使用gnu make的wildcard
函數, 不存在的依賴文件不會致使錯誤
看一個簡單例子:
srcs = foo.c bar.c ... %.o : %.c @$(makedepend) $(compile.c) -o $@ $< include $(wildcard $(srcs:.c=.d))
這個要更加刁鑽一些. 可是, 咱們能夠經過在makefile中僅僅將文件做爲target來講服
make不要fail. 若是target存在, 可是沒有命令(隱式或者顯式)或者先決條件, 則make總
是認爲它是最新的. 這就是正常的狀況, 它會像咱們期待的那樣工做.
在出現上述錯誤的例子中, target並不存在. 而根據gnu make用戶手冊"rules without
recipes or prerequisties":
若是一個規則沒有先決條件或者recipe, 而且規則的target是不存在的文件, 那麼每次
在它的規則運行的時候, make會認爲這個target已近更新了. 這意味着全部依賴於這個
target的target老是會執行其recipe(生成這個target的命令組)
棒極了. 這確保了make不會丟出錯誤, 由於它知道如何處理那個不存在的文件, 它會確保
任何l以愛那個target的文件rebuild, 這也是咱們想要的.
(???)
所以, 咱們須要作的就是, 修改這個依賴文件輸出, 使得每一個先決條件(源文件和頭文件)
定義爲沒有命令和先決條件的target. 因此makedepend腳本的輸出因該生成一個內容像這
樣的foo.d
文件:
foo.o: foo.c foo.h bar.h baz.h foo.c foo.h bar.h baz.h:
所以.d
文件包含最開始的先決條件定義, 而後添加每一個源文件做爲一個顯式的target
這個配置還有一個問題: 若是使用者刪除了一個依賴文件, 而沒有更新任何源文件, make
不會發現任何問題, 而且不會從新建立依賴文件, 直到因爲其餘的緣由決定從新構建對應
的對象文件. 同時, make會缺失這些target的依賴信息(好比, 修改頭文件而不改動源文件
不會致使對象文件重建)
這個問題稍微有點複雜, 由於咱們不想要依賴文件被看做是"真正的"target: 若是它們是,
則咱們使用include來引入它們, make會重建它們, 而後從新執行它本身. 這並不致命, 但
是是多餘的, 咱們選擇拒絕.
automake的方式並無解決則和個問題, 之前我提供了一個"just don't do that"的方案,
加上將依賴文件放到一個單獨的目錄來使得不那麼容易碰巧刪除了它們.
可是lukas waymann提供了一個簡潔的解決方案: 將依賴文件做爲target的依賴, 而後給它
建立一個空的recipe:
srcs = foo.c bar.c ... %.o : %.c %.d @$(makedepend) $(compile.c) -o $@ $< %.d: ; include $(wildcard $(srcs:.c=.d))
這很是好地解決了問題: 當make檢查target的時候, 他會將依賴文件愛呢看做是一個先決
條件, 而後嘗試rebuild它. 若是它存在, 什麼都不會作, 由於依賴文件沒有先決條件. 如
果它不存在, 則會被標記爲過期, 由於它的recipe是空的, 這會致使object target被重建
(其重建過程當中會建立一個新的依賴文件)
當make試圖重建引入的文件的時候, 他會找到依賴的隱式規則而後使用它. 可是, 因爲規
則並無更新target文件, 沒有引入的文件會被更新, make不會從新執行自身.
上面的一個問題是, make會認爲.d
文件是中間文件, 會刪除它們. 我經過將它們定義爲
顯式的target而非使用模式規則來解決:
depfiles := $(srcs:.c=.d) $(depfiles): include $(wildcard $(depfiles))
你可能不想將全部的.d
文件放在源文件目錄下. 你很容易就可讓makefile將它們放到
別的地方. 這是一個例子. 固然, 這裏認爲你以及修改了你的makedepend只來生成輸出到
這個位置, 以及知道在寫入這個目錄以前可能會須要建立它....:
srcs = foo.c bar.c ... depdir = .deps %.o : %.c $(depdir)/%.d @$(makedepend) $(compile.c) -o $@ $< depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
這裏我會討論一些可能的定義上面使用的makedepend變量的方式.
makedepend = /usr/lib/cpp
or cc -e
最簡單的生成依賴的方式是使用c預處理其. 這須要一點對預處理其輸出格式的瞭解, 幸運
的是多數unix預處理器都有相似咱們意圖須要的輸出. 爲了編譯器錯誤消息和調試信息的
編號信息, 預處理其在每次jump到一個#include
文件以及從中返回的時候都必須提供行
號和文件名的信息(__line__
,__file__
). 這些輸出行能夠用於搞清楚引入了哪些文件.
多數unix預處理其會在輸出中插入這個格式的特殊行:
# lineno "filename" extra
咱們關心的是filename
處的值. 有了這個, 咱們就可使用這個命令以咱們想要的格式生成.d
文件..:
makedepend = $(cpp) $(cppflags) $< \ | sed -n 's,^\# *[0-9][0-9]* *"\([^"<]*\)".*,$@: \1\n\1:,p' \ | sort -u > $*.d
....
上面的一個問題是咱們須要對源文件進行兩次預處理: 一次是makedepend
命令, 一次是在編譯過程當中.
若是你在使用gcc(或者提供了等價選項的編譯器(clang)),你能夠同時生成對象文件和依賴
文件, 節省很多實踐, 由於這些編譯器能夠以編譯反作用的形式生成依賴文件. 這是一個實現示例, 從tl;dr一節中複製的:
depdir := .deps depflags = -mt $@ -mmd -mp -mf $(depdir)/$*.d compile.c = $(cc) $(depflags) $(cflags) $(cppflags) $(target_arch) -c %.o : %.c %.o : %.c $(depdir)/%.d | $(depdir) $(compile.c) $(output_option) $< $(depdir): ; @mkdir -p $@ depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
過一遍吧:
depdir = ...
: 將依賴文件放到一個叫作.deps
的子目錄depflags = ...
: gcc特定的flags, 告訴編譯器生成依賴文件
-mt $@
: 設置在生成的依賴文件中target的名稱-mmd
: 編譯之餘, 生成依賴信息. 這個版本省去系統頭文件, 若是想要系統-md
-mp
: 給每一個先決條件添加一個target, 比買在刪除文件的時候的錯誤.-mf $(depdir)/$*.d
: 將生成依賴文件$(depdir)/$*.d
%o : %.c
: 刪除內建的從.c
文件構建.o
文件的規則, 以使用咱們提供的規則... $(depdir/%.d
: 將生成的依賴文件聲明爲target的一個先決條件, 以便在它缺失的時候, rebuilt target... | $(depdir)
: 將依賴目錄聲明爲. target的一個order only的先決條件,以便在須要的時候建立它.$(depdir): ; @mkdir -p $@
: 聲明一個在依賴目錄不存在的時候建立它的規則depfiles := ...
: 生成一個可能存在的全部依賴文件的列表$(depfiles):
: 將全部依賴文件做爲target說起, 以使得make不會在文件不存在的時候failinclude ...
: 引入存在的依賴文件. 使用wildcard
來避免由於不存在的文件而失敗...:
depdir := .deps depflags = -mt $@ -mmd -mp -mf $(depdir)/$*.td postcompile = mv -f $(depdir)/$.td $(depdir)/$.d && touch $@ compile.c = $(cc) $(depflags) $(cflags) $(cppflags) $(target_arch) -c %.o : %.c %.o : %.c $(depdir)/%.d | $(depdir) $(compile.c) $(output_option) $< $(postcompile) $(depdir): ; @mkdir -p $@ depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
一般你也會想要將object文件放到一個單獨的位置, 而不只僅是依賴文件. 這裏是一個例子:
objdir := obj depdir := $(objdir)/.deps depflags = -mt $@ -mmd -mp -mf $(depdir)/$*.d compile.c = $(cc) $(depflags) $(cflags) $(cppflags) $(target_arch) -c $(objdir)/%.o : %.c $(depdir)/%.d | $(depdir) $(compile.c) $(output_option) $< $(depdir): ; @mkdir -p $@ depfiles := $(srcs:%.c=$(depdir)/%.d) $(depfiles): include $(wildcard $(depfiles))
.....