自動依賴生成
基於make的構建環境要正確工作, 一個很重要(也很煩人)的任務是, 在makefile中正確列
舉依賴.
這個文檔將介紹了一個非常有用的讓make自身來創建和維護這些依賴的方法.
所有的make程序都需要知道, 某個特定的target依賴的文件有哪些, 以便確認它(target)
會在必要的時候進行rebuild.
手動更行這個清單不僅僅是讓人乏味, 而且非常容易出錯. 多數系統(不論大小)都偏向與
提供自動提取這個信息的自動化工具. 傳統的工具的是makedepend
程序, 其會讀取c源代
碼, 並以可以include至makefile中的__目標-依賴__模式生成頭文件清單.
如果使用更加強大一點的編譯器或者預處理器, 更加現代話的解決方案是讓編譯器或者預
處理器來生成這個信息.
這篇文章的意圖不是專門討論依賴信息獲得的方式的(盡管有涉及到), 而是, 介紹一些有
用的將這些工具的調用,輸出和gnu make組合, 來確保依賴信息總是正確和最新的, 銜接越
緊密(且越高效)越好.
這些方法依賴gnu make提供的特性. 可能可以通過修改它們來在其他版本的make上應用.
那就等你自己嘗試啦. 但是, 在盡心那個嘗試之前請看哈paul的makefile第一原則
gcc方案
如果有誰已近不耐煩了, 這是一個完整的最佳的實踐方案. 這個方案需要你的編譯器的支
持: 默認你使用gcc作為編譯器(或者提供了和gcc兼容的預處理選項的編譯器). 如果你的
編譯器不滿足這個條件, 請看另外的方案.
將這個加入到你的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都需要額外調用一次
之類的, 除去依賴生成工具自身的開銷不說. 還有, 它會檢查每個文件的依賴, 即使是沒
有改變的文件
我們會看看到我們如何可以做到更好.
gnu 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
先解決上面的第一個問題: 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))
避免"no rule to make target..."的錯誤
這個要更加刁鑽一些. 但是, 我們可以通過在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變量的方式.
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
來避免因為不存在的文件而失敗.
處理特殊情況
..:
- 如果構建在某個不恰當的時間被kill了, 某個依賴文件可能會損壞. 可能會導致之后的
調用由於語法錯誤而失敗. 要解決這個問題必須手動刪除文件 - 眸子額情況, gcc會不恰當地設置生成的依賴文件時間戳. 使得依賴文件比對象文件更新
. 這種情況會無限rebuild對象文件.
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文件的放置
通常你也會想要將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))
.....