“makefile”寫法詳解,一步一步寫一個實用的makefile,詳解 sed 's,$∗\.o[ :]*,\1.o $@ : ,g' < $@.


目的:編寫一個實用的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可看到結果:

[html]  view plain  copy
 
  1. bigoutput littleoutput: defs.h pub.h  
  2.  @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鏈接為可執行文件。

[html]  view plain  copy
 
  1. foo:foo.o  
  2.  gcc -o foo foo.o; ./foo  
  3. foo.o:foo.c     #注釋該行看效果  
  4.  gcc -c foo.c -o foo.o #注釋該行看效果  

(三)定義模式規則
下面定義了一個模式規則,即如何由.c文件生成.d文件的規則。

[html]  view plain  copy
 
  1. foobar: foo.d bar.d  
  2.  @echo complete generate foo.d and bar.d  
  3. %.d: %.c  #make會對當前目錄下每個.c文件,依次做一次里面的命令,從而由每個.c文件生成對應.d文件。  
  4.  @echo from $to $@  
  5.  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文件內容為:

[html]  view plain  copy
 
  1. main.o : main.c command.h  

這里指示了main.o目標依賴於哪幾個源文件,我們只要把這一行的內容,通過make的include指令包含到makefile文件里,即可在其任意一個依賴文件被修改后,重新編譯目標main.o。
下面詳解如何生成這個.d文件。

(二)
gcc/g++編譯器有一個-MM選項,可以對某個.c/.cpp文件,分析其依賴的源文件,例如假定main.c的內容為:

[cpp]  view plain  copy
 
  1. #include <stdio.h>//標准頭文件(以<>方式包含的),被-MM選項忽略,被-M選項收集  
  2. #include "stdlib.h"//標准頭文件(以""方式包含的),被-MM選項忽略,被-M選項收集  
  3. #include "command.h"  
  4. int main()  
  5. {  
  6.  printf("##### Hello Makefile #####\n");  
  7.  return 0;  
  8. }  

則執行gcc -MM main.c后,屏幕輸出:

[html]  view plain  copy
 
  1. main.o: main.c command.h  

執行gcc -M main.c后,屏幕輸出:

[html]  view plain  copy
 
  1. main.o: main.c /usr/include/stdio.h /usr/include/features.h \  
  2. /usr/include/bits/predefs.h /usr/include/sys/cdefs.h \  
  3. /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \  
  4. /usr/include/gnu/stubs-64.h \  
  5. /usr/lib/gcc/x86_64-linux-gnu/4.4.3/include/stddef.h \  
  6. /usr/include/bits/types.h /usr/include/bits/typesizes.h \  
  7. /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \  
  8. /usr/lib/gcc/x86_64-linux-gnu/4.4.3/include/stdarg.h \  
  9. /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h \  
  10. /usr/include/stdlib.h /usr/include/sys/types.h /usr/include/time.h \  
  11. /usr/include/endian.h /usr/include/bits/endian.h \  
  12. /usr/include/bits/byteswap.h /usr/include/sys/select.h \  
  13. /usr/include/bits/select.h /usr/include/bits/sigset.h \  
  14. /usr/include/bits/time.h /usr/include/sys/sysmacros.h \  
  15. /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文件:

[html]  view plain  copy
 
  1. sources:=$(wildcard *.c) #使用$(wildcard *.cpp)來獲取工作目錄下的所有.c文件的列表。  
  2. dependence=$(sources:.c=.d) #這里,dependence是所有.d文件的列表.即把串sources串里的.c換成.d。  
  3. 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里要有這樣的定義:

[html]  view plain  copy
 
  1. main.d: main.c command.h  

這時我們發現,main.d與main.o的依賴是完全相同的,可以利用make的多目標規則,把main.d與main.o這兩個目標的定義合並為一句:

[html]  view plain  copy
 
  1. 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。
命令可以這么寫:

[html]  view plain  copy
 
  1. g++ -MM main.c > main.d.temp  
  2. 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:

[html]  view plain  copy
 
  1. gcc -MM  $> $@.temp;  
  2. 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文件為:

[html]  view plain  copy
 
  1. main.o main.d: main.c command.h  

第二次make,假定改了command.h、在command.h中加入#include "pub.h",這時再make:
1-include所有.d文件,例如include了main.d后,得到依賴規則:

[html]  view plain  copy
 
  1. 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:

[html]  view plain  copy
 
  1. 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。

(九)
進一步修改,得到目前大家普遍使用的版本:

[html]  view plain  copy
 
  1. set -e; rm -f $@; \  
  2. $(CC) -MM $(CPPFLAGS) $> $@.
     
    ; \  
  3. sed 's,$\.o[ :]*,\1.o $@ : ,g' < $@.
     
     > $@; \  
  4. rm -f $@.
     
      

第一行,set -e表示,如果某個命令的返回參數非0,那么整個程序立刻退出。
rm -f用來刪除上一次make時生成的.d文件,因為現在要重新生成這個.d,老的可以刪除了(不刪也可以)。
第二行:前面臨時文件是用固定的.d.temp作為后綴,為了防止重名覆蓋掉有用的文件,這里把temp換成一個隨機數,該數可用

的值是當前進程號。
由於$是makefile特殊符號,一個$要用

2$
makefileecho
sed.


每個shell命令的進程號通常是不同的,為了每次調用

4\拆.

便是同一個文件了。
你可以在makefile里用下面命令來比較:

 

[html]  view plain  copy
 
  1. echo 
     
      
  2. echo 
     
    ; echo 
     
      

第四行:當make完后,每個臨時文件.d.$$,已經不需要了,刪除之。
但每個.d文件要在下一次make時被include進來,要保留。

(十)
綜合前面的分析,得到我們的makefile文件:

[html]  view plain  copy
 
  1. #使用$(wildcard *.c)來獲取工作目錄下的所有.c文件的列表  
  2. sources:=$(wildcard *.c)  
  3. objects:=$(sources:.c=.o)  
  4. #這里,dependence是所有.d文件的列表.即把串sources串里的.c換成.d  
  5. dependence:=$(sources:.c=.d)  
  6.   
  7. #所用的編譯工具  
  8. CC=gcc  
  9.   
  10. #當$(objects)列表里所有文件都生成后,便可調用這里的 $(CC) $^ -o $@ 命令生成最終目標all了  
  11. #把all定義成第1個規則,使得可以把make all命令簡寫成make  
  12. all: $(objects)  
  13.  $(CC) $^ -o $@  
  14.   
  15. #這段是make的模式規則,指示如何由.c文件生成.o,即對每個.c文件,調用gcc -c XX.c -o XX.o命令生成對應的.o文件。  
  16. #如果不寫這段也可以,因為make的隱含規則可以起到同樣的效果  
  17. %.o: %.c  
  18.  $(CC) -c $-o $@  
  19.   
  20. include $(dependence) #注意該句要放在終極目標all的規則之后,否則.d文件里的規則會被誤當作終極規則了  
  21. %.d: %.c  
  22.  set -e; rm -f $@; \  
  23.  $(CC) -MM $(CPPFLAGS) $> $@.
     
    ; \  
  24.  sed 's,$\.o[ :]*,\1.o $@ : ,g' < $@.
     
     > $@; \  
  25.  rm -f $@.
     
      
  26.   
  27. .PHONY: clean #之所以把clean定義成偽目標,是因為這個目標並不對應實際的文件  
  28. clean:  
  29.  rm -f all $(objects) $(dependence) #清除所有臨時文件:所有.o和.d。.$$已在每次使用后立即刪除。-f參數表示被刪文件不存在時不報錯  

(十一)
上面這個makefile已經能正常工作了(編譯C程序),但如果要用它編譯C++,變量CC值要改成g++,每個.c都要改成.cpp,有點繁瑣。
現在我們繼續完善它,使其同時支持C和C++,並支持二者的混合編譯。

[html]  view plain  copy
 
  1. #一個實用的makefile,能自動編譯當前目錄下所有.c/.cpp源文件,支持二者混合編譯  
  2. #並且當某個.c/.cpp、.h或依賴的源文件被修改后,僅重編涉及到的源文件,未涉及的不編譯  
  3. #詳解文檔:http://blog.csdn.net/huyansoft/article/details/8924624  
  4. #author:胡彥 2013-5-21  
  5.   
  6. #----------------------------------------------------------  
  7. #編譯工具用g++,以同時支持C和C++程序,以及二者的混合編譯  
  8. CC=g++  
  9.   
  10. #使用$(winldcard *.c)來獲取工作目錄下的所有.c文件的列表  
  11. #sources:=main.cpp command.c  
  12.   
  13. #變量sources得到當前目錄下待編譯的.c/.cpp文件的列表,兩次調用winldcard、結果連在一起即可  
  14. sources:=$(wildcard *.c) $(wildcard *.cpp)  
  15.   
  16. #變量objects得到待生成的.o文件的列表,把sources中每個文件的擴展名換成.o即可。這里兩次調用patsubst函數,第1次把sources中所有.cpp換成.o,第2次把第1次結果里所有.c換成.o  
  17. objects:=$(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(sources)))  
  18.   
  19. #變量dependence得到待生成的.d文件的列表,把objects中每個擴展名.o換成.d即可。也可寫成$(patsubst %.o,%.d,$(objects))  
  20. dependence:=$(objects:.o=.d)  
  21.   
  22. #----------------------------------------------------------  
  23. #當$(objects)列表里所有文件都生成后,便可調用這里的 $(CC) $^ -o $@ 命令生成最終目標all了  
  24. #把all定義成第1個規則,使得可以把make all命令簡寫成make  
  25. all: $(objects)  
  26.     $(CC) $(CPPFLAGS) $^ -o $@  
  27.     @./$@   #編譯后立即執行  
  28.   
  29. #這段使用make的模式規則,指示如何由.c文件生成.o,即對每個.c文件,調用gcc -c XX.c -o XX.o命令生成對應的.o文件  
  30. #如果不寫這段也可以,因為make的隱含規則可以起到同樣的效果  
  31. %.o: %.c  
  32.     $(CC) $(CPPFLAGS) -c $-o $@  
  33.   
  34. #同上,指示如何由.cpp生成.o,可省略  
  35. %.o: %.cpp  
  36.     $(CC) $(CPPFLAGS) -c $-o $@  
  37.   
  38. #----------------------------------------------------------  
  39. include $(dependence)   #注意該句要放在終極目標all的規則之后,否則.d文件里的規則會被誤當作終極規則了  
  40.   
  41. #因為這4行命令要多次凋用,定義成命令包以簡化書寫  
  42. define gen_dep  
  43. set -e; rm -f $@; \  
  44. $(CC) -MM $(CPPFLAGS) $> $@.
     
    ; \  
  45. sed 's,$\.o[ :]*,\1.o $@ : ,g' < $@.
     
     > $@; \  
  46. rm -f $@.
     
      
  47. endef  
  48.   
  49. #指示如何由.c生成其依賴規則文件.d  
  50. #這段使用make的模式規則,指示對每個.c文件,如何生成其依賴規則文件.d,調用上面的命令包即可  
  51. %.d: %.c  
  52.     $(gen_dep)  
  53.   
  54. #同上,指示對每個.cpp,如何生成其依賴規則文件.d  
  55. %.d: %.cpp  
  56.     $(gen_dep)  
  57.   
  58. #----------------------------------------------------------  
  59. #清除所有臨時文件(所有.o和.d)。之所以把clean定義成偽目標,是因為這個目標並不對應實際的文件  
  60. .PHONY: clean  
  61. clean:  #.$$已在每次使用后立即刪除。-f參數表示被刪文件不存在時不報錯  
  62.     rm -f all $(objects) $(dependence)  
  63.   
  64. echo:   #調試時顯示一些變量的值  
  65.     @echo sources=$(sources)  
  66.     @echo objects=$(objects)  
  67.     @echo dependence=$(dependence)  
  68.     @echo CPPFLAGS=$(CPPFLAGS)  
  69.   
  70. #提醒:當混合編譯.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》,這個手冊更詳細,但較枯燥,不適合完整學習,通常是遇到問題再去查閱。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM