天天看點

makefile 自動推導編譯和注意事項

在學習該篇文章之前,首先你需要有一定的makefile理論基礎,如果不懂可以參考:​​Makefile教程(絕對經典,所有問題看這一篇足夠了)​​建議看2遍以上。其中關于include可以參考​​Makefile 中的 include (依賴檔案) 的執行過程​​。

在Linux C語言開發的時候我們需要自己手動編譯新添加的檔案。如果一個工程非常龐大這樣将會是程式員的噩夢。使用 ​

​-MM​

​選項擷取檔案的依賴,并且自動推導檔案依賴關系。

Demo1:以.c和.h在同一個目錄來講解

首先使用 ​​

​cc -MM​

​分析目前目錄下面所有的源檔案的依賴關系,并且把結果生成到對應的.d檔案中

#擷取目前目錄的所有.c檔案
src := $(wildcard *.c)

#生成.c對應的.d目标
d_obj := $(src:.c=.d)

#最終目标,暫時什麼都不做
main:$(d_obj)

#makefile 靜态編譯模式,$(d_obj)中所有xx.d的檔案依賴對應的xx.c檔案
$(d_obj):%.d:%.c
  cc -MM $< -o $@      

運作結果:可以看到對應的.d檔案,以及.d檔案的内容就是對應cpp的依賴項。

makefile 自動推導編譯和注意事項

此時可以看見.d的檔案内容就是我們需要的檔案依賴關系,并且可以直接通過cc編譯。是以如果我們需要自動編譯生成主程式main,我們首先分需要析一下依賴關系:

  1. main 需要依賴 *.o
  2. *.o可以通過include *.d檔案生成
  3. *.d通過 ​

    ​cc -MM​

    ​​ 生成

    是以推導的makefile檔案内容如下:

#擷取目前目錄的所有.c檔案
src := $(wildcard *.c)

o_obj:=$(src:.c=.o)

#生成main目标
main:$(o_obj)
  gcc -o $@ $^

-include $(src:.c=.d)

#靜态模式
%.d:%.c
  cc -MM $< -o $@      

這樣我們就可以每次執行make,自動更新依賴并且編譯檔案了。

是以最終的makefile如下:

ifndef GXX
GXX := g++
endif

ifndef CFLAGS
CFLAGS := -g
endif

src = $(wildcard *.c)

objs = $(src:.c=.o)

main:$(objs)
  $(GXX) -o $@ $(CFLAGS) $^

-include $(src:.c=.d)

%.d:%.c
  cc -MM $< -o $@

.PHONY:clean

clean:
  rm -rf *.o *.d main      

Demo2:.c 在src目錄,.h在include目錄,src、include、makefile同級

makefile 自動推導編譯和注意事項

Demo1和Demo2的差別就是是否需要指定 -I選項,指明頭檔案的位置。

注意事項:​

​$<,$@,$^​

​使用這兩個變量的時候擷取到的目标是帶有相對路徑的。

vpath %.h ./include
vpath %.c ./src
add.o:add.h
  @echo $<      
makefile 自動推導編譯和注意事項

是以和demo1相比我們需要處理.d的生成方式。變化主要兩點:

  1. ​.d​

    ​​的内容需要指定 ​

    ​-I​

    ​​參數(通過​

    ​sed​

    ​​指令修改 ​

    ​-MM​

    ​​ 生成的​

    ​.d​

    ​内容)
  2. ​.d​

    ​​位置不會和​

    ​.c​

    ​​同目錄,将會和makefile同目錄(為什麼不将​

    ​.d​

    ​​和​

    ​.c​

    ​​同目錄是因為在make目錄執行的時候 -I 參數為 ​

    ​./include​

    ​​,但是在src目錄 -I參數為 ​

    ​../include​

    ​,為了保持參數一緻,是以将.d和make保持同級)
%.d : %.c
  rm -f $(@F); \
  $(GXX) -MM $(INCLUDE) $< > $(@F).$$$$; \sed -e 's,^.*:,$*.o:,g' -e 's,$$,; $(GXX) -c $(CFLAGS) $(INCLUDE) $<,g'  < $(@F).$$$$ > $(@F); \
  rm -f $(@F).$$$$      

​$(@F),$(<F)​

​​:目标檔案的檔案名稱

​​

​$(@D),$(<D)​

​​:目标檔案的相對目錄位置

​​

​$*​

​​:目标模式中"%"及其之前的部分,eg:test%.d 比對的是 testmain.d那麼​

​$*​

​表示testmain

​sed -e 's,^.*:,$*.o:,g'​

​​ 将‘,’替換成’/'就可以看出是一個s/parten1/paretn2/g的替換模式

​sed -e 's/^.*:/$*.o:/g'​

​ 是以上述語義 以main.d為例,則替換之後的​

​正則表達​

​式為

​sed -e 's/^.*:/main.o:/g'​

eg:​

​$@為./src/main.o​

​​則​

​$(@F)為main.o​

​​,​

​$(@D)為./src/​

​​ 至于​

​sed​

​的用法請百度,sed用法很靈活。是以最終的makefile内容為:

vpath %.c ./src
vpath %.h ./include

ifndef GXX
GXX := g++
endif

ifndef CFLAGS
CFLAGS := -g
endif

INCLUDE := -I ./include

src = $(wildcard ./src/*.c)

c_objs = $(notdir $(src))

objs = $(c_objs:.c=.o)

main:$(objs)
  $(GXX) -o $@ $(CFLAGS) $^

-include $(objs:.o=.d)

%.d : %.c
  rm -f $(@F); \
  $(GXX) -MM $(INCLUDE) $< > $(@F).$$$$; \sed -e 's,^.*:,$*.o:,g' -e 's,$$,; $(GXX) -c $(CFLAGS) $(INCLUDE) $<,g'  < $(@F).$$$$ > $(@F); \
  rm -f $(@F).$$$$

.PHONY:clean

clean:
  rm -rf *.o *.d main      
makefile 自動推導編譯和注意事項

​編寫Makefile注意事項:​

​​

對于動态依賴的檔案(*.o)不要随便更改位置,否則容易導緻看似依賴檔案存在卻找不到的錯誤

demo:将生成的.o檔案放在bin目錄,bin和src,include同級

1 vpath %.h ./include
      2 vpath %.c ./src
      3 vpath %.o ./bin
      4 
      5 src = $(wildcard ./src/*.c)
      6 
      7 src_file_name=$(notdir $(src))
      8 
      9 o_obj = $(src_file_name:.c=.o)
     10 
     11 main:$(o_obj)
     12         gcc -o $@ $^
     13 
     14 $(o_obj):%.o:%.c
     15         gcc -c $< -o ./bin/$@ -I ./include      

運作結果:第一次沒有找到.o檔案,導緻編譯失敗。此時在執行一次,因為.o檔案實際已經存在在bin目錄,是以第二次成功了(此時大家肯定疑問,我明明指定了vpath的啊,為什麼會失敗呢)

makefile 自動推導編譯和注意事項

為了弄清楚原因,我們把執行日志列印出來,看看兩次有什麼差別

main:$(o_obj)
  @echo $^
  gcc -o $@ $^      

此時運作一下make,檢視​

​$(o_obj)​

​​的輸出(​

​注意我們上文提到$<,$@,$^是相對路徑的位置輸出​

​​),是以為什麼make失敗顯而易見,因為​

​$(o_obj)​

​​是目前目錄下的​

​*.o​

makefile 自動推導編譯和注意事項

此時我們再次運作一次make,看看​

​$(o_obj)​

​的輸出,此時為什麼make成功,顯而易見,因為​

​$(o_obj)​

​是​

​./bin/​

​目錄下的​

​*.o​

​,這個目标檔案我們在第一次make的時候已經生成了。

makefile 自動推導編譯和注意事項

此時我們可能會有疑問為什麼第一次沒有在指定的vpath裡面尋找​

​*.o​

​呢?事實上是查找了的,隻是沒找到而已,在make的過程中我們需要知道:​

​一旦檔案位置被定位了,在整個make的執行過程中都不會改變​

​。下面我們來分析一下:make關于.o查找的執行過程。

第一次 make:

—>到​

​$(o_obj)= add.o sub.o main.o​

​​指定的位置查找即目前目錄查找,沒找到

—>到vpath指定的目錄查找,沒找到

—>最終沒找到,是以​​

​$(o_obj)​

​​,最終被定位的位置為目前目錄

是以雖然後面動态生成了.o但是.o檔案位置并不在目前目錄,是以make失敗

第二次 make:

—>到​​

​$(o_obj)= add.o sub.o main.o​

​​指定的位置查找即目前目錄查找,沒找到

—>到vpath指定的目錄查找,找到OK

—>是以​​

​$(o_obj)​

​​,最終被定位的位置為./bin/

是以此時第二次make是成功的

為了驗證上述make關于.o查找的執行過程是否正确,我們把./bin目錄下的.o檔案拷貝到make同級目錄,此時.o檔案将會存在兩份,位置分别位于目前目錄和./bin。此時我們make -n檢視一下*.o的定位位置。

makefile 自動推導編譯和注意事項

結果符合預期 ​

​$(o_obj)​

​指定位置優先,之後才是vpath位置。

是以需要解決上述看似存在卻找不到的bug很簡單,隻需要将上述第15行代碼

gcc -c $< -o ./bin/$@ -I ./include      

修改為:

gcc -c $< -o $@ -I ./include      

不要随便更改目标依賴檔案的位置

///

MakeFile的遞歸調用

#include 會在目前位置展開makefile檔案内容,類似宏的展開
-include makefile 

#$(MAKE) -C dir,會進入dir目錄,然後執行make指令
#可以通過export 向下層傳遞變量
object:
  $(MAKE) -C dir      

假設目錄結構如下:

--main.c
--add
  --add.h
  --add.c
  --makefile
--makefile      

add目錄makefile内容如下:

#$(CFLAGS)由上層makefile通過export傳遞過來
add.o:add.c
  g++ -o $@ -c $^ $(CFLAGS)

.PHONY:clean
clean:
  -rm -rf *.o      

main 目錄下的makefile如下:

vpath %.h ./include ./add
vpath %.o ./add


CC = g++
#像下層makefile傳遞變量CFLAGS
export CFLAGS = -g
add_make=./add/makefile


main:main.o add.o
  $(CC) -o $@ $^

main.o:main.c
  $(CC) -o $@ -c $^ -I ./include -I ./add $(CFLAGS)
    
#-include $(add_make)

add.o:
  $(MAKE) -C add
  -cp add/$@ .


.PHONY:clean
clean:
  -rm -rf *.o main core.*
  $(MAKE) clean -C add      

此時我們可以遞歸調用make clean,執行過程如下: