目的:編寫一個實用的makefile,能自動編譯當前目錄下所有.c/.cpp源文件,支持二者混合編譯。並且當某個.c/.cpp、.h或依賴的源文件被修改后,僅重編涉及到的源文件,未涉及的不編譯。
二
要達到這個目的,用到的技術有:
1-使用wildcard函數來獲得當前目錄下所有.c/.cpp文件的列表。
2-make的多目標規則。
3-make的模式規則。
4-用gcc -MM命令得到一個.c/.cpp文件include了哪些文件。
5-用sed命令對gcc -MM命令的結果作修改。
6-用include命令包含依賴描述文件.d。
三 准備知識
(一)多目標
對makefile里下面2行,可看出多目標特征,執行make bigoutput或make littleoutput可看到結果:
- bigoutput littleoutput: defs.h pub.h
- @echo $@ $(subst output,OUTPUT,$@) $^ # $@指這個規則里所有目標的集合,$^指這個規則里所有依賴的集合。該行是把目標(bigoutput或littleoutput)里所有子串output替換成大寫的OUTPUT
(二)隱含規則
對makefile里下面4行,可看出make的隱含規則,執行foo可看到結果:
第3、4行表示由.c得到.o,第1、2行表示由.o得到可執行文件。
如果把第3、4行注釋的話,效果一樣。
即不寫.o來自.c的規則,它會自動執行gcc -c -o foo.o foo.c這條命令,由.c編譯出.o(其中-c表示只編譯不鏈接),然后自動執行gcc -o foo foo.o鏈接為可執行文件。
- foo:foo.o
- gcc -o foo foo.o; ./foo
- foo.o:foo.c #注釋該行看效果
- gcc -c foo.c -o foo.o #注釋該行看效果
(三)定義模式規則
下面定義了一個模式規則,即如何由.c文件生成.d文件的規則。
- foobar: foo.d bar.d
- @echo complete generate foo.d and bar.d
- %.d: %.c #make會對當前目錄下每個.c文件,依次做一次里面的命令,從而由每個.c文件生成對應.d文件。
- @echo from $< to $@
- g++ -MM $< > $@
假定當前目錄下有2個.c文件:foo.c和bar.c(文件內容隨意)。
驗證方法有2種,都可:
1-運行make foo.d(或make bar.d),表示想要生成foo.d這個目標。
根據規則%.d: %.c,這時%匹配foo,這樣%.c等於foo.c,即foo.d這個目標依賴於foo.c。
此時會自動執行該規則里的命令gcc -MM foo.c > foo.d,來生成foo.d這個目標。
2-運行make foobar,因為foobar依賴於foo.d和bar.d這2個文件,即會一次性生成這2個文件。
四
下面詳述如何自動生成依賴性,從而實現本例的makefile。
(一)
本例使用了makefile的模式規則,目的是對當前目錄下每個.c文件,生成其對應的.d文件,例如由main.c生成的.d文件內容為:
- main.o : main.c command.h
這里指示了main.o目標依賴於哪幾個源文件,我們只要把這一行的內容,通過make的include指令包含到makefile文件里,即可在其任意一個依賴文件被修改后,重新編譯目標main.o。
下面詳解如何生成這個.d文件。
(二)
gcc/g++編譯器有一個-MM選項,可以對某個.c/.cpp文件,分析其依賴的源文件,例如假定main.c的內容為:
- #include <stdio.h>//標准頭文件(以<>方式包含的),被-MM選項忽略,被-M選項收集
- #include "stdlib.h"//標准頭文件(以""方式包含的),被-MM選項忽略,被-M選項收集
- #include "command.h"
- int main()
- {
- printf("##### Hello Makefile #####\n");
- return 0;
- }
則執行gcc -MM main.c后,屏幕輸出:
- main.o: main.c command.h
執行gcc -M main.c后,屏幕輸出:
- main.o: main.c /usr/include/stdio.h /usr/include/features.h \
- /usr/include/bits/predefs.h /usr/include/sys/cdefs.h \
- /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \
- /usr/include/gnu/stubs-64.h \
- /usr/lib/gcc/x86_64-linux-gnu/4.4.3/include/stddef.h \
- /usr/include/bits/types.h /usr/include/bits/typesizes.h \
- /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
- /usr/lib/gcc/x86_64-linux-gnu/4.4.3/include/stdarg.h \
- /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h \
- /usr/include/stdlib.h /usr/include/sys/types.h /usr/include/time.h \
- /usr/include/endian.h /usr/include/bits/endian.h \
- /usr/include/bits/byteswap.h /usr/include/sys/select.h \
- /usr/include/bits/select.h /usr/include/bits/sigset.h \
- /usr/include/bits/time.h /usr/include/sys/sysmacros.h \
- /usr/include/bits/pthreadtypes.h /usr/include/alloca.h command.h
(三)
可見,只要把這些行挪到makefile里,就能自動定義main.c的依賴是哪些文件了,做法是把命令的輸出重定向到.d文件里:gcc -MM main.c > main.d,再把這個.d文件include到makefile里。
如何include當前目錄每個.c生成的.d文件:
- sources:=$(wildcard *.c) #使用$(wildcard *.cpp)來獲取工作目錄下的所有.c文件的列表。
- dependence=$(sources:.c=.d) #這里,dependence是所有.d文件的列表.即把串sources串里的.c換成.d。
- include $(dependence) #include后面可以跟若干個文件名,用空格分開,支持通配符,例如include foo.make *.mk。這里是把所有.d文件一次性全部include進來。注意該句要放在終極目標all的規則之后,否則.d文件里的規則會被誤當作終極規則了。
(四)
現在main.c command.h這幾個文件,任何一個改了都會重編main.o。但是這里還有一個問題,如果修改了command.h,在command.h中加入#include "pub.h",這時:
1-再make,由於command.h改了,這時會重編main.o,並且會使用新加的pub.h,看起來是正常的。
2-這時打開main.d查看,發現main.d中未加入pub.h,因為根據模式規則%.d: %.c中的定義,只有依賴的.c文件變了,才會重新生成.d,而剛才改的是command.h,不會重新生成main.d、及在main.d中加入對pub.h的依賴關系,這會導致問題。
3-修改新加的pub.h的內容,再make,果然問題出現了,make報告up to date,沒有像期望那樣重編譯main.o。
現在問題在於,main.d里的某個.h文件改了,沒有重新生成main.d。進一步說,main.d里給出的每個依賴文件,任何一個改了,都要重新生成這個main.d。
所以main.d也要作為一個目標來生成,它的依賴應該是main.d里的每個依賴文件,也就是說make里要有這樣的定義:
- main.d: main.c command.h
這時我們發現,main.d與main.o的依賴是完全相同的,可以利用make的多目標規則,把main.d與main.o這兩個目標的定義合並為一句:
- main.o main.d: main.c command.h
現在,main.o: main.c command.h這一句我們已經有了,如何進一步得到main.o main.d: main.c command.h呢?
(五)
解決方法是行內字符串替換,對main.o,取出其中的子串main,加上.d后綴得到main.d,再插入到main.o后面。能實現這種替換功能的命令是sed。
實現的時候,先用gcc -MM命令生成臨時文件main.d.temp,再用sed命令從該臨時文件中讀出內容(用<重定向輸入)。做替換后,再用>輸出到最終文件main.d。
命令可以這么寫:
- g++ -MM main.c > main.d.temp
- sed 's,main\.o[ :]*,\1.o main.d : ,g' < main.d.temp > main.d
其中:
sed 's,main\.o[ :]*,\1.o main.d : ,g',是sed命令。
< main.d.temp,指示sed命令從臨時文件main.d.temp讀取輸入,作為命令的來源字符串。
> main.d,把行內替換結果輸出到最終文件main.d。
(六)
這條sed命令的結構是s/match/replace/g。有時為了清晰,可以把每個/寫成逗號,即這里的格式s,match,replace,g。
該命令表示把源串內的match都替換成replace,s指示match可以是正則表達式。
g表示把每行內所有match都替換,如果去掉g,則只有每行的第1處match被替換(實際上不需要g,因為一個.d文件中,只會在開頭有一個main.o:)。
這里match是正則式main\.o[ :]*,它分成3段:
第1段是main,在sed命令里把main用和括起來,使接下來的replace中可以用\1引用main。
第2段是\.o,表示匹配main.o,(這里\不知何意,去掉也是可以的)。
第3段是正則式[ :]*,表示若干個空格或冒號,(其實一個.d里只會有一個冒號,如果這里寫成[ ]*:,即匹配若干個空格后跟一個冒號,也是可以的)。
總體來說match用來匹配'main.o :'這樣的串。
這里的replace是\1.o main.d :,其中\1會被替換為前面第1個和括起的內容,即main,這樣replace值為main.o main.d :
這樣該sed命令就實現了把main.o :替換為main.o main.d :的目的。
這兩行實現了把臨時文件main.d.temp的內容main.o : main.c command.h改為main.o main.d : main.c command.h,並存入main.d文件的功能。
(七)
進一步修改,采用自動化變量。使得當前目錄下有多個.c文件時,make會依次對每個.c文件執行這段規則,生成對應的.d:
- gcc -MM $< > $@.temp;
- sed 's,$∗\.o[ :]*,\1.o $@ : ,g' < $@.temp > $@;
(八)
現在來看上面2行的執行流程:
第一次make,假定這時從來沒有make過,所有.d文件不存在,這時鍵入make:
1-include所有.d文件的命令無效果。
2-首次編譯所有.c文件。每個.c文件中若#include了其它頭文件,會由編譯器自動讀取。由於這次是完整編譯,不存在什么依賴文件改了不會重編的問題。
3-對每個.c文件,會根據依賴規則%.d: %.c,生成其對應的.d文件,例如main.c生成的main.d文件為:
- main.o main.d: main.c command.h
第二次make,假定改了command.h、在command.h中加入#include "pub.h",這時再make:
1-include所有.d文件,例如include了main.d后,得到依賴規則:
- main.o main.d: main.c command.h
注意所有include命令是首先執行的,make會先把所有include進來,再生成依賴規則關系。
2-此時,根據依賴規則,由於command.h的文件戳改了,要重新生成main.o和main.d文件。
3-先調用gcc -c main.c -o main.o生成main.o,
再調用gcc -MM main.c > main.d重新生成main.d。
此時main.d的依賴文件里增加了pub.h:
- main.o main.d: main.c command.h pub.h
4-對其它依賴文件沒改的.c(由其.d文件得到),不會重新編譯.o和生成其.d。
5-最后會執行gcc $(objects) -o main生成最終可執行文件。
第三次make,假定改了pub.h,再make。由於第二遍中,已把pub.h加入了main.d的依賴,此時會重編main.c,重新生成main.o和main.d。
這樣便實現了當前目錄下任一源文件改了,自動編譯涉及它的.c。
(九)
進一步修改,得到目前大家普遍使用的版本:
- set -e; rm -f $@; \
- $(CC) -MM $(CPPFLAGS) $< > $@.
- sed 's,$∗\.o[ :]*,\1.o $@ : ,g' < $@.
- rm -f $@.
第一行,set -e表示,如果某個命令的返回參數非0,那么整個程序立刻退出。
rm -f用來刪除上一次make時生成的.d文件,因為現在要重新生成這個.d,老的可以刪除了(不刪也可以)。
第二行:前面臨時文件是用固定的.d.temp作為后綴,為了防止重名覆蓋掉有用的文件,這里把temp換成一個隨機數,該數可用
的值是當前進程號。
由於$是makefile特殊符號,一個$要用
。
每個shell命令的進程號通常是不同的,為了每次調用
便是同一個文件了。
你可以在makefile里用下面命令來比較:
- echo
- echo
第四行:當make完后,每個臨時文件.d.$$,已經不需要了,刪除之。
但每個.d文件要在下一次make時被include進來,要保留。
(十)
綜合前面的分析,得到我們的makefile文件:
- #使用$(wildcard *.c)來獲取工作目錄下的所有.c文件的列表
- sources:=$(wildcard *.c)
- objects:=$(sources:.c=.o)
- #這里,dependence是所有.d文件的列表.即把串sources串里的.c換成.d
- dependence:=$(sources:.c=.d)
- #所用的編譯工具
- CC=gcc
- #當$(objects)列表里所有文件都生成后,便可調用這里的 $(CC) $^ -o $@ 命令生成最終目標all了
- #把all定義成第1個規則,使得可以把make all命令簡寫成make
- all: $(objects)
- $(CC) $^ -o $@
- #這段是make的模式規則,指示如何由.c文件生成.o,即對每個.c文件,調用gcc -c XX.c -o XX.o命令生成對應的.o文件。
- #如果不寫這段也可以,因為make的隱含規則可以起到同樣的效果
- %.o: %.c
- $(CC) -c $< -o $@
- include $(dependence) #注意該句要放在終極目標all的規則之后,否則.d文件里的規則會被誤當作終極規則了
- %.d: %.c
- set -e; rm -f $@; \
- $(CC) -MM $(CPPFLAGS) $< > $@.
- sed 's,$∗\.o[ :]*,\1.o $@ : ,g' < $@.
- rm -f $@.
- .PHONY: clean #之所以把clean定義成偽目標,是因為這個目標並不對應實際的文件
- clean:
- rm -f all $(objects) $(dependence) #清除所有臨時文件:所有.o和.d。.$$已在每次使用后立即刪除。-f參數表示被刪文件不存在時不報錯
(十一)
上面這個makefile已經能正常工作了(編譯C程序),但如果要用它編譯C++,變量CC值要改成g++,每個.c都要改成.cpp,有點繁瑣。
現在我們繼續完善它,使其同時支持C和C++,並支持二者的混合編譯。
- #一個實用的makefile,能自動編譯當前目錄下所有.c/.cpp源文件,支持二者混合編譯
- #並且當某個.c/.cpp、.h或依賴的源文件被修改后,僅重編涉及到的源文件,未涉及的不編譯
- #詳解文檔:http://blog.csdn.net/huyansoft/article/details/8924624
- #author:胡彥 2013-5-21
- #----------------------------------------------------------
- #編譯工具用g++,以同時支持C和C++程序,以及二者的混合編譯
- CC=g++
- #使用$(winldcard *.c)來獲取工作目錄下的所有.c文件的列表
- #sources:=main.cpp command.c
- #變量sources得到當前目錄下待編譯的.c/.cpp文件的列表,兩次調用winldcard、結果連在一起即可
- sources:=$(wildcard *.c) $(wildcard *.cpp)
- #變量objects得到待生成的.o文件的列表,把sources中每個文件的擴展名換成.o即可。這里兩次調用patsubst函數,第1次把sources中所有.cpp換成.o,第2次把第1次結果里所有.c換成.o
- objects:=$(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(sources)))
- #變量dependence得到待生成的.d文件的列表,把objects中每個擴展名.o換成.d即可。也可寫成$(patsubst %.o,%.d,$(objects))
- dependence:=$(objects:.o=.d)
- #----------------------------------------------------------
- #當$(objects)列表里所有文件都生成后,便可調用這里的 $(CC) $^ -o $@ 命令生成最終目標all了
- #把all定義成第1個規則,使得可以把make all命令簡寫成make
- all: $(objects)
- $(CC) $(CPPFLAGS) $^ -o $@
- @./$@ #編譯后立即執行
- #這段使用make的模式規則,指示如何由.c文件生成.o,即對每個.c文件,調用gcc -c XX.c -o XX.o命令生成對應的.o文件
- #如果不寫這段也可以,因為make的隱含規則可以起到同樣的效果
- %.o: %.c
- $(CC) $(CPPFLAGS) -c $< -o $@
- #同上,指示如何由.cpp生成.o,可省略
- %.o: %.cpp
- $(CC) $(CPPFLAGS) -c $< -o $@
- #----------------------------------------------------------
- include $(dependence) #注意該句要放在終極目標all的規則之后,否則.d文件里的規則會被誤當作終極規則了
- #因為這4行命令要多次凋用,定義成命令包以簡化書寫
- define gen_dep
- set -e; rm -f $@; \
- $(CC) -MM $(CPPFLAGS) $< > $@.
- sed 's,$∗\.o[ :]*,\1.o $@ : ,g' < $@.
- rm -f $@.
- endef
- #指示如何由.c生成其依賴規則文件.d
- #這段使用make的模式規則,指示對每個.c文件,如何生成其依賴規則文件.d,調用上面的命令包即可
- %.d: %.c
- $(gen_dep)
- #同上,指示對每個.cpp,如何生成其依賴規則文件.d
- %.d: %.cpp
- $(gen_dep)
- #----------------------------------------------------------
- #清除所有臨時文件(所有.o和.d)。之所以把clean定義成偽目標,是因為這個目標並不對應實際的文件
- .PHONY: clean
- clean: #.$$已在每次使用后立即刪除。-f參數表示被刪文件不存在時不報錯
- rm -f all $(objects) $(dependence)
- echo: #調試時顯示一些變量的值
- @echo sources=$(sources)
- @echo objects=$(objects)
- @echo dependence=$(dependence)
- @echo CPPFLAGS=$(CPPFLAGS)
- #提醒:當混合編譯.c/.cpp時,為了能夠在C++程序里調用C函數,必須把每一個要調用的C函數,其聲明都包括在extern "C"{}塊里面,這樣C++鏈接時才能成功鏈接它們。
五
makefile學習體會:
剛學過C語言的讀者,可能會覺得makefile有點難,因為makefile不像C語言那樣,一招一式都那么清晰明了。
在makefile里到處是“潛規則”,都是一些隱晦的東西,要弄明白只有搞清楚這些“潛規則”。
基本的規則無非是“一個依賴改了,去更新哪些目標”。
正因為隱晦動作較多,寫成一個makefile才不需要那么多篇幅,畢竟項目代碼才是主體。只要知道makefile的框架,往它的套路里填就行了。
較好的學習資料是《跟我一起寫Makefile.pdf》這篇文檔(下載包里已經附帶了),比較詳細,適合初學者。
我們學習的目的是,能夠編寫一個像本文這樣的makefile,以滿足簡單項目的基本需求,這要求理解前面makefile幾個關鍵點:
1-多目標
2-隱含規則
3-定義模式規則
4-自動生成依賴性
可惜的是,這篇文檔雖然比較全面,卻沒有以一個完整的例子為引導,對幾處要點沒有突出指明,尤其是“定義模式規則”在最后不顯眼的位置(第十一部分第五點),導致看了“自動生成依賴性”一節后還比較模糊。
所以,看了《跟我一起寫Makefile.pdf》后,再結合本文針對性的講解,會有更實際的收獲。
另一個學習資料是《GNU make v3.80中文手冊v1.5.pdf》,這個手冊更詳細,但較枯燥,不適合完整學習,通常是遇到問題再去查閱。