從簡單實例開始,學會寫Makefile(二)

五、.d文件,解決文件間的相互引用


1、自動生成依賴關係


        在前文的項目基礎上,考慮一下這種情況:如果我們在w1.h文件裏包含了頭文件w2.h以及w3.h並且用到其中定義的函數。


        第一次編譯沒有遇到問題,但是如果後續的開發過程中修改了w2.h或者w3.h文件中的內容,再執行gmake命令的時候,就遇到問題了——w1.cpp文件不會被重新編譯了!

        

        顯然,我們需要將生成目標文件w1.o的規則的依賴項加上w2.h和w3.h。可是如果手動的去檢查每一個文件的引用關係,然後修改Makefile文件,這樣做的效率就太低了。

 

        萬幸的是,編譯器可以幫助我們自動生成依賴關係,只需要在編譯命令中加上“-M”選項,就可以讓編譯器自動尋找源文件中包含的頭文件,並生成一個依賴關係,例如,你可以在shell界面下敲下如下的命令:

        

        g++-MM w1.cpp

        

        可以看到,其輸出爲w1.o:w1.cppw2.h w3.h。這裏需要特別注意的是,我們使用“-MM”而不是“-M”,因爲我們使用的是GUN的C/C++編譯器,使用“-M”參數會將標準庫的頭文件也一併包含進來,但這並不是我們想要的,而使用“-MM”則不會。

 

        現在的問題是,如何利用這個命令去寫好我們的Makefile呢?

        GUN組織建議把每一個源文件自動生成的依賴關係放到一個.d文件中,讓每一個.cpp文件都對應一個.d文件,例如之前的w1.cpp,我們可以生成一個w1.d文件,內容爲自動生成的依賴關係 w1.o:w1.cpp w2.h w3.h,然後在Makefile中包含所有的.d文件,我們只需要寫出.cpp文件和.d文件的依賴關係,讓make自動更新或生成.d文件即可。

 

2、生成.d文件


        dep/%.d:%.cpp

                @if test ! -d "dep"; then\

                        mkdir -p dep;\

                fi; \

                set -e; rm -f $@;

                g++ -MM $< > $@.$$$$; \

                sed 's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d: /g' < $@.$$$$ > $@; \

                rm -f $@.$$$$

        

        在Makefile中加上如上的代碼,就可以生成我們所需要的.d文件了。

        又是一堆莫名其妙的符號,我們還是來逐句進行分析。

 

(1)dep/%.d: %.cpp

        使所有的.d文件依賴於對應的.cpp文件,也就是說只要.cpp更新了,我們就重新生成對應的.d文件。這裏和.o文件類似的,我們也創建一個dep目錄用來存放所有的.d文件,既能保持項目文件的整潔和統一,也方便管理。

 

(2)@if test ! -d "dep"; then\

                   mkdir -p dep;\

            fi; \

        檢查當前目錄下是否存在dep目錄,如果不存在,就使用mkdir命令創建dep目錄。

 

(3)set -e; rm -f $@;

        set–e 的作用是如果命令執行出錯就直接退出。$@的含義之前已經說過,這裏rm –f $@的意思就是刪除所有的目標文件。

 

(4)g++ -MM $< > $@.$$$$; \

        $< 的含義是第一個依賴項的名稱,> 是重定向符號,將輸出結果重定向到指定文件中。$@.$$$$ 就是這個文件的文件名,其中“$$$$”表示一個隨機的編號,例如如果有目標文件是w1.d,那麼“$@.$$$$”一個可能的結果就是w1.d.12345。那麼,這句話的含義就是將g++ -MM w1.cpp的輸出結果重定向到w1.d.12345這個文件中。

 

(5)sed 's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d : /g' < $@.$$$$ > $@;\

        這裏使用了sed這個工具對文本進行替換處理,單引號中的規則是’s/old/new/g’,s表示替換,末尾的g代表全局的意思,對文本中所有符合要求的字符串進行替換,sed會將符合old模式的字符串替換爲new,具體的使用方法可以查閱一下sed這個工具的幫助文檔。


        <$@$$$$,將這個文件的內容作爲sed工具的輸入。


        > $@,將sed處理後的內容重定向輸出到這個文件中。


        經過這一步的處理後,就把自動生成的依賴關係:

 

        w1.o:w1.cpp w2.hw3.h

 

        轉成:

 

        w1.o w1.d:w1.cppw2.h w3.h

 

        這樣,我們的.d文件也會自動更新啦。

 

(6)rm -f $@.$$$$

        刪除掉這個臨時文件。

 

3、使用include包含其他文件


        在Makefile中我們也可以像在C++文件中那樣包含其他文件。


        現在在我們的Makefile中加上這樣一句:

        

        includew1.d

        

        使用這個語句就可以將之前我們生成的.d文件中的內容包含到當前的Makefile中。


        當然,也可以用這個命令來包含其他的Makefile文件。具體的用法後面再進行介紹。

 

        我們希望把所有的.d文件都包含在當前的Makefile中。


        先定義一個變量,存放所有的.d文件名:

 

        DEPS = $(addsuffix .d,$(addprefix dep/,$(BASE)))

        

        然後使用include$(DEPS) 包含所有的.d文件。

 

六、-I,引用其他目錄下的.h文件


        考慮這種情況:現在有兩個目錄,一個inc目錄用來存放.h文件,一個src目錄,用來存放.cpp文件。


        怎麼讓編譯器找到引用的.h文件在哪個目錄下呢?

        

        我們可以使用“-I”選項。  格式爲“-I目錄名”,這樣在編譯的時候,編譯器就會依次到我們指定的目錄中尋找.h文件。

 

        同樣,先定義一個變量,存放所有頭文件的目錄名:


        INCLUDEDIR = -I../inc

 

        然後將

        g++ -c -o $@$<

        這樣的編譯命令中寫成

        g++ -c -o $@$(INCLUDEDIR) $<

 

        OK,再來嘗試用gmake命令編譯一下吧,已經可以成功編譯了。

        如果需要包含多個目錄下的.h文件,可以重複使用-I選項,中間需要用空格隔開。

 

七、使用靜態庫


1、修改生成靜態庫的Makefile


        有的時候我們不需要生成一個可執行的程序,而是生成一個靜態庫文件,之後在其他的地方引用這個靜態庫文件。

        

        假設我們的項目目錄結構是這樣的,src是項目根目錄,src下面有common和app以及lib兩個目錄,common和app下面都有inc和src兩個目錄。common存放公共庫的源文件,app存放程序源文件,lib存放生成的靜態庫。

 

        修改我們在common目錄下的Makefile文件:

 

        top_srcdir         = ../..

        #生成靜態庫後所存放的位置

        libdir =$(top_srcdir)/lib

        #靜態庫文件名

        LIBNAME          = libfa_common.a

        #路徑+靜態庫文件名

        TARGET             = $(libdir)/$(LIBNAME)


        $(TARGET): $(OBJS)

                -rm -f $@

                ar cr $(TARGET) $(OBJS)


        (1)  top_srcdir是項目根目錄的路徑,使用相對路徑,方便我們在後面引用其他目錄。

        (2)  libdir是生成的靜態庫所存放的路徑。

        (3)  LIBNAME是靜態庫名稱,注意,靜態庫的命名必須以“lib”開頭,以“.a”結尾。

        (4)  TARGET是目標文件名稱,包含路徑。

        (5)  在生成靜態庫文件的規則中,使用ar這個命令。

 

2、修改引用靜態庫的Makefile


        在app/src目錄下的源文件中,編譯的時候需要引用libfa_common.a這個靜態庫,這就需要我們再修改app目錄下的Makefile文件。

        

        這裏使用了兩個新的參數,“-l”和“-L”。

 

        “-l”參數指定要引用的庫的名稱。例如我們要引用libfa_common.a這個靜態庫,那麼需要在編譯命令里加上“-lfa_common”,可以看出,-l後面的庫名稱需要去除前面的“lib”和後面的“.a”。

 

        “-L”參數指定了要引用的庫的目錄,用法和之前的“-I”一樣。這裏需要注意的是,我們需要修改一下VPATH這個變量,指明要引用的靜態庫的目錄。類似這樣:

        VPATH:= -L $(top_srcdir)/lib

 

八、完整的Makefile


        其實在每一個目錄下的Makefile中有很多部分是重複的,我們可以考慮將重複的部分提取出來,單獨放在一個公共的Makefile中,然後在其他Makefile中用include包含這個公共的Makefile即可。

        

        我寫了三套Makefile,分別是Makefile(app)、Makefile(lib)、Make.rules。

 

        其中,Make.rules是公共部分,Makefile(app)是用來生成可執行程序的,Makefile(lib)是用來生成靜態庫的,爲了以後遷移方便,考慮到Linux和Unix平臺的差異,以及各個編譯器之間的差異,可以將各種命令也定義成變量,之後使用宏定義進行條件編譯。

        

        貼一下完整的Makefile代碼。

 

1、Make.rules

        

        #公用Make規則配置

 

        #設置編譯器類型

        CXX := g++

        CC := gcc

 

        #設置編譯.d文件相關內容

        DEPFLAGS := -MM

        DEPFILE = $@.$$$$

 

        #設置所有靜態庫文件所在位置,會根據每個Makefile文件的top_srcdir設置相對位置

        LIBDIR := $(top_srcdir)/lib

 

        #設置編譯程序時需要在哪些目錄查找靜態庫文件

        LDFLAGS := -L.\

                                -L$(top_srcdir)/lib

 

        #設置VPATH,在檢查依賴關係時,如果查找-lxxxx時,在哪些目錄查找靜態庫文件

        VPATH := $(LIBDIR)

 

        #設置編譯程序時查找頭文件的目錄位置

        INCLUDEDIR := -I.\

                                      -I../inc\

 

        #聲明要生成的目標文件,具體規則在具體的Makefile中定義

        $(TARGET):

 

        #生成.o文件所依賴的.cpp和.c文件

        obj/%.o:%.cpp

        @if test ! -d "obj"; then\

                mkdir-p obj;\

        fi;

        $(CXX)-c -o $@ $(INCLUDEDIR) $<

        

        obj/%.o:%.c

                @iftest ! -d "obj"; then\

                        mkdir-p obj;\

                fi;

                $(CC)-c -o $@ $(INCLUDEDIR) $<

        

        #生成.d文件,存放.cpp文件的所有依賴規則

        dep/%.d: %.cpp

                @iftest ! -d "dep"; then\

                        mkdir-p dep;\

                fi;\

                set-e; rm -f $@;

                $(CXX)$(DEPFLAGS) $(INCLUDEDIR) $< >$(DEPFILE); \

                sed's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d : /g' < $@.$$$$ > $@;\

                rm-f $@.$$$$

 

        #生成.d文件,存放.c文件的所有依賴規則

        dep/%.d: %.c

                @iftest ! -d "dep"; then\

                        mkdir-p dep;\

                fi;\

                set-e; rm -f $@;

                $(CC)$(DEPFLAGS) $(INCLUDEDIR) $< > $(DEPFILE); \

                sed's/$*\.o[ :]*/obj\/$*\.o dep\/$*\.d : /g' < $@.$$$$ > $@; \

                rm-f $@.$$$$

 

        include $(DEPS)

 

        #檢測是否有文件被修改,只要有就全部編譯

        all: $(SRCS) $(TARGETS)

 

        #清除編譯文件

        .PHONY:clean

        clean:

                -rm-f $(TARGET)

                -rm-f obj/*.o

                -rm-f dep/*.d

                -rm-f core

 

2、Makefile(lib)


        #需要生成靜態庫的Makefile

 

        #程序根目錄

        top_srcdir         =../../..

 

        #生成靜態庫後所存放的位置

        libdir = $(top_srcdir)/lib

        #靜態庫文件名

        LIBNAME          =libfa_common.a

        #路徑+靜態庫文件名

        TARGET             =$(libdir)/$(LIBNAME)

 

        CPP_FILES = $(shell ls *.cpp)

        C_FILES = $(-shell ls *.c)

        SRCS = $(CPP_FILES) $(C_FILES)

        BASE = $(basename $(SRCS))

        OBJS = $(addsuffix .o, $(addprefixobj/,$(BASE)))

        DEPS = $(addsuffix .d, $(addprefixdep/,$(BASE)))

 

        #包含公共Make規則

        include$(top_srcdir)/makeinclude/Make.rules

 

        #設置頭文件及庫文件的位置

        INCLUDEDIR := $(INCLUDEDIR)

 

        $(TARGET): $(OBJS)

                -rm-f $@

                ar cr $(TARGET) $(OBJS)


3、Makefile(app)


        #需要生成可執行程序的Makefile

 

        #程序根目錄

        top_srcdir         =../../..

 

        #目標程序名

        TARGET = test

 

        CPP_FILES = $(shell ls *.cpp)

        C_FILES = $(-shell ls *.c)

        SRCS = $(CPP_FILES) $(C_FILES)

        BASE = $(basename $(SRCS))

        OBJS = $(addsuffix .o, $(addprefixobj/,$(BASE)))

        DEPS = $(addsuffix .d, $(addprefixdep/,$(BASE)))

 

        #包含公共Make規則

        include $(top_srcdir)/makeinclude/Make.rules

 

        #額外需要包含的頭文件的目錄位置

        INCLUDEDIR := $(INCLUDEDIR)\

                                        -I$(top_srcdir)/src/common/inc\

 

        #所有要包含的靜態庫的名稱

        LIBS := -lfa_common

 

        #設置目標程序依賴的.o文件

        $(TARGET):$(OBJS) $(LIBS)

                -rm-f $@

                $(CXX)-o $(TARGET) $(INCLUDEDIR) $(LDFLAGS) $(OBJS) $(LIBS)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章