1. 程式人生 > >makefile自動依賴生成

makefile自動依賴生成

自動依賴生成

基於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不會在檔案不存在的時候fail
  • include ...: 引入存在的依賴檔案. 使用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))

.....