Makefile 自動生成標頭檔案的依賴關係

工程師WWW發表於2014-01-26

最近在看一本書《Windows遊戲程式設計大師技巧》 (Tricks of Windows Game Programming Gurus). 第一章給出了一個打磚塊小遊戲的示例程式. 包括三個檔案: blackbox.h, blackbox.cpp和freakout.cpp (600行程式碼, 對於Windows C++程式來說還好, 沒有讓我freak out…). blackbox.cpp封裝了部分DirectDraw, 提供了一些更傻瓜化的初始化DirectDraw, 畫點, 畫方框的工具函式. blackbox.h包括了這些函式的宣告. freakout.cpp引用了blackbox.h檔案, 包括WinMain主函式和主要的遊戲邏輯.

How to build?

和我一樣還在使用N年前的垃圾電腦的看官們, 多半也不願意用Visual Studio來挑戰自己的耐心. 但是所以讓我們選擇小米加步槍: GCC (MinGW) + Makefile. 寫個簡單的makefile例如:

all:    freakout.exe
 
freakout.exe: freakout.o blackbox.o
    g++ freakout.o blackbox.o -o stupid.exe -L D:/gamedev/dx81sdk/DXF/DXSDKlib -lddraw -mwindows
 
freakout.o: freakout.cpp blackbox.h
    g++ -c freakout.cpp -o freakout.o -I D:/gamedev/dx81sdk/DXF/DXSDK/include
 
blackbox.o: blackbox.cpp blackbox.h
    g++ -c blackbox.cpp -o blackbox.o -I D:/gamedev/dx81sdk/DXF/DXSDK/include

Problem?

上面的程式碼當然有很多問題, 比如”-L <DX lib path>”, “-I <DX inc path>”可以被定義在類似$(LIB), $(INC)的變數裡面, 比如我沒有按照makefile的習慣寫一個clean目標清理生成的東東…不過我的重點是在紅色高亮的blackbox.h: 兩個.o檔案都需要依賴於blackbox.h.

假設我們修改freakout.cpp引用更多的標頭檔案, 每加一條#include “somefile.h”指令, 就需要相應地在makefile裡為freakout.o增加一個依賴項somefile.h.

假設我們修改stupid.h檔案, 讓stupid.h也引用一個標頭檔案someotherfile.h, 那麼我們也需要相應地在makefile裡為所有依賴stupid.h的.o檔案 (在這個例子中是freakout.o和blackbox.o) 增加依賴項someotherfile.h.

所以問題是: 在.cpp和.h被修改的情況下, 怎麼維護.o目標檔案和.h標頭檔案的依賴關係.

Solution – 自動生成依賴關係

Google一下”makefile 標頭檔案 依賴”會發現大多數編譯器都提供了一個選項生成.o目標檔案所依賴的檔案列表. 比如GCC的”-MM”選項. 執行GCC –MM freakout.cpp blackbox.cpp <庫檔案和標頭檔案選項>得到輸出:

freakout.o: freakout.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h
blackbox.o: blackbox.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h

所以一個簡單的處理依賴關係的辦法是把這些編譯器生成的依賴關係寫入一個檔案裡, 然後在makefile中用include指令包含這個檔案:

CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:/gamedev/dx81sdk/DXF/DXSDK/lib -mwindows -l ddraw
INC = -I D:/gamedev/dx81sdk/DXF/DXSDK/include
 
all: freakout.exe
 
freakout.exe: ${OBJ}
    ${CPP} ${OBJ} -o freakout.exe ${LIB}
 
include depend
 
# note the symbols: $< and '$@
%.o: %.cpp
    ${CPP} -c $< -o $@ ${INC}
 
# generate depend file
depend:
    ${CPP} -MM ${OBJ:.o=.cpp} ${INC} > depend

注意紅色的部分, depend是GCC生成的包含了依賴關係的檔案 (也注意上面的makefile中有”$<”, “$@”這種perl風格的bt匹配字元…) . 有了這樣的makefile, 我們就能用兩個步驟來build專案:

1) 在需要更新依賴關係的時候 (比如在某個檔案裡多加了一條#include指令)執行make depend.

2)執行make.

Makefile Auto Dependency – 只執行一次make

懶惰是程式設計師的一大美德, 所以GNU make的手冊裡提供了一個執行一次make就能更新所有依賴關係並且按照依賴關係build的辦法 (http://www.gnu.org/software/make/manual/make.html#Automatic-Prerequisites): 與整體引入一個depend目標不同, 我們為每一個.o/.cpp檔案引入一個.d依賴關係檔案. 依賴關係檔案由GCC的”-MM”選項生成, 並且在GCC輸出的基礎上把自己本身加入到目標列表中. 比如:

# freakout.d
freakout.o freakout.d: freakout.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h
 
# blackbox.d
blackbox.o blackbox.d: blackbox.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h

注意紅色的部份: .d檔案被加入到了目標列表, 依賴相關的.h和.cpp檔案. 那麼怎樣自動生成這些.d檔案呢? 類似的, makefile:

CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:/gamedev/dx81sdk/DXF/DXSDK/lib -mwindows -l ddraw
INC = -I D:/gamedev/dx81sdk/DXF/DXSDK/include
 
all: freakout.exe
 
freakout.exe: ${OBJ}
    ${CPP} ${OBJ} -o freakout.exe ${LIB}
 
include ${OBJ:.o=.d}
 
%.o: %.cpp
    ${CPP} -c $< -o $@ ${INC}
 
%.d: %.cpp
    rm -f $@ & 
    ${CPP} -MM $< ${INC} > $@.$$ & 
    insertdfile.exe $< $@.$$ > $@ & 
    rm -f $@.$$

這個makefile和上一節給出的makefile大致差不多, 不同的是兩個紅色的部分: 第一部分我們用include指令include相關的所有.d檔案; 第二部分定義了生成.d檔案的規則 (又是$@, $<符號…): 第一行首先刪除原有的.d檔案; 第二行執行g++ -MM生成依賴關係寫入一個臨時檔案($@.$$)裡; 第三行把.d檔案加入到第二行生成的依賴關係中並把最終結果寫到.d檔案中; 第四行刪除臨時檔案.

P.S. insertdfile.exe是我自己寫的一個小程式, 用來把.d檔案加入到第二行生成的依賴關係中, 例如把”blackbox.o: blackbox.cpp blackbox.h”轉換為”blackbox.o blackbox.d: blackbox.cpp blackbox.h”. 我為什麼要自己寫一個字串替換的程式? 因為我沒有裝sed工具…如果有, 當然用官方推薦的辦法, 把第三行換成神奇的符咒: sed ’s,($*).o[ :]*,1.o $@ : ,g’ < $@.$$ > $@ .

好了, 執行make:

1) make嘗試去包含blackbox.d和freakout.d檔案, 沒有發現, 所以檢查有沒有規則可以生成這些.d檔案, 發現我們%.d: %.cpp的這條規則, 於是執行這條規則生成所有的.d檔案並且包含進來.
2) 按照正常規則生成.o檔案, 然後生成.exe.

然後我們做一些修改, 比如增加一個stupid.h檔案, 然後修改blackbox.h檔案以包含stupid.h. 執行make:

1) make把第一個目標all加入到需要生成的目標列表中.
2) make包含blackbox.d和freakout.d檔案. 注意在這個過程中這兩個.d檔案也會被加入到make的需要生成的目標列表中 – 因為可能有規則能更新這兩個.d檔案.
3) 根據blackbox.d和freakout.d包含的規則: .d檔案 (已經被入到了make的需要生成目標列表中) 需要被更新, 於是相應地執行%.d: %.cpp規則更新.d檔案: 新加入的 stupid.h檔案被加入到兩個.d檔案的依賴項中.
4) make發現包含的兩個.d檔案在第2)步中都被更新, 於是重新包含這兩個.d檔案.
5) make根據在第2)步生成在第3)步被包含進來的規則, 發現stupid.h的日期比兩個.o檔案的日期都要新, 於是重新生成.o檔案.
6) 根據規則生成.exe.


相關文章