1. 程式人生 > >makefile(05)_自動生成依賴關系

makefile(05)_自動生成依賴關系

makefile 自動生成依賴 include

11.自動生成依賴關系_上

11.0. 實驗原料

本節實驗所需的源文件和頭文件:
原文件:func.c

#include "stdio.h"
#include "func.h"
void foo()
{
    printf("void foo() : %s\n", HELLO);
}

原文件:main.c

#include <stdio.h>
#include "func.h"
int main()
{
    foo();

    return 0;
}  

頭文件func.c

#ifndef FUNC_H
#define FUNC_H

#define HELLO "Hello D.T."

void foo();

#endif

11.1.問題和方案

問題:

  1. 目標文件.o是否只依賴於源文件.c?編譯器如何編譯源文件和頭文件?
    編譯器處理頭文件中的代碼直接插入源文件中,編譯器只通過預處理後的原文件產生目標文件,因此,規則中以源文件為依賴,命令可能無法執行。
  2. 下面Makefile有沒有問題?
    技術分享圖片
OBJS := func.o main.o

hello.out : $(OBJS)
    @gcc -o $@ $^
    @echo "Target File ==> $@"

$(OBJS) : %.o : %.c
    @gcc -o $@ -c $<

此時看似可以編譯成功,但存在潛在隱患。
存在問題:目標文件只依賴於.c文件,而沒有關註.h文件,這樣當.h文件的內容更新時,不會重新編譯.c文件。

解決方案:
我們將.h文件也作為依賴寫到Makefile中。

OBJS := func.o main.o

hello.out : $(OBJS)
    @gcc -o $@ $^
    @echo "Target File ==> $@"

$(OBJS) : %.o : %.c func.h
    @gcc -o $@ -c $<

上述解決方案問題:
頭文件作為依賴出現於每一個目標文件對應的規則中,當頭文件改動,任何源文件都會被重新編譯(編譯低效),而且當項目中頭文件數量巨大時,Makefile件很難維護。

11.2.實現自動依賴

通過命令自動生成對頭文件的依賴,將生成的依賴自動包含進入Makefile中,當頭文件改動後,自動確認需要重新編譯的文件。

預備工作:
1.Linux命令sed,sed時一個流編輯器,用於流文本的修改(增、刪、查、改),文件替換,格式為:sed ‘s/abc/xyz/g’;
Sed可以支持正則表達,sed ‘s/(.).o[ :]/objs/\1.o : /g’ 正則匹配目標((.).o[ :]),替換值(objs/\1.o : )
2.編譯器選項,生成依賴關系
gcc -MM 獲取目標的完整依賴關系
gcc -M 獲取目標的部分依賴關系
3.Makefile中目標拆分技巧,將目標的完整依賴拆分為多個部分依賴
技術分享圖片

OBJS := func.o main.o

hello.out : $(OBJS)
    @gcc -o $@ $^
    @echo "Target File ==> $@"

$(OBJS) : %.o : %.c func.h
    @gcc -o $@ -c $<

思考:如果使用上面的預備工作實現頭文件的自動依賴?

12.自動生成依賴關系_中

12.1.Include

Make中的include關鍵字,類似於C語言中的關鍵字,在處理是將所包含的文件的內容原封不動的搬到當前文件。
語法:include filename
Eg: include foo.make *.mk $(var)
Make對include關鍵字的處理方式,在當前目錄搜索或者指定目錄搜索目標文件,搜索成功:將文件內容搬入當前Makefile中;搜索失敗,以文件名作為目標查找並執行對應規則。當文件名對應的規則不存在時,產生錯誤。
下面的代碼怎麽執行,為什麽?

.PHONY : all

include test.txt

all : 
    @echo "this is all"

test.txt :
    @echo "test.txt"
    @touch test.txt

輸出結果:
技術分享圖片
解析:初次執行文件,自然搜索不到test.txt文件,然後會test.txt文件名作為目標查找並執行對應規則,當再次執行文件時,則test.txt的內容已經被寫入當前Makefile,則先執行第一個目標(test.txt中的目標)。
註意:在include關鍵字前面加上-,可以消除警告。

12.2.命令執行機制

1.Makefile中的命令執行時,每一條命令默認都是一個新的進程;(這樣當我們希望使用上一個命令的執行結果,繼續執行命令時往往得不到結果,譬如下面的代碼);

.PHONY : all

all :
    set -e;
    mkdir test;
    cd test;
    mkdir subtest

輸出結果:
技術分享圖片
很顯然,沒有達到我們與其的目的(在test文件夾中創建subtest文件夾)
2.可以通過接續符(;)將多個命令組合成為一個命令,組合的命令一次在同一個進程中被執行;
3.可以使用set -e指定發生錯誤時立即退出。

.PHONY : all

all :
        set -e;         mkdir test;         cd test;         mkdir subtest

輸出結果:
技術分享圖片

12.3.實現自動生成依賴

1.通過gcc -MM 和sed命令得到.dep文件(目標的部分依賴),並使用接續符使得命令可以連續執行;
2.通過include指令包含所有的.dep依賴文件(當.dep文件不存在時,查找與.dep文件同名的規則並執行)

.PHONY : all clean

MKDIR := mkdir
RM := rm -fr
CC := gcc

SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)

-include $(DEPS)

all :
        @echo "all"

%.dep : %.c
        @echo "Creating $@ ..."
        @set -e;         $(CC) -MM -E $^ | sed ‘s,\(.*\)\.o[ :]*,objs/\1.o : ,g‘ > $@

clean :
        $(RM) $(DEPS)

輸出結果:
技術分享圖片
我們此時已經成功的生成了依賴文件main.dep和func.dep並在文件中記錄了目標和依賴的關系。
思考:如果組織依賴文件相關的規則與源碼編譯相關的規則,進而形成功能完整的Makefile?

13.自動生成依賴關系_下

13.1.遺留問題

如何在makefile中組織.dep文件到指定目錄?
解決思路:
當include 發現.dep文件不存在時,通過規則和命令創建deps文件夾,將所有的.dep文件創建到deps文件夾,並在.dep文件中記錄目標文件的依賴關系。

$(DIR_DEPS) :
    $(MKDIR) $@

$(DIR_DEPS)/%.dep : $(DIR_DEPS)  %.c
    @echo "Creating $@ ..."
    @set -e;     $(CC) -MM -E  $^  | sed ‘s,\(.*\)\.o[ :]*,objs/\1.o : ,g‘ > $@

這樣做確實解決了上述問題,生成了deps文件夾:
技術分享圖片
但同時我們看到兩個問題:
1.因為依賴中包含deps文件夾,以deps文件夾作為 gcc -MM 的輸入時沒有意義的,會報告warning,所以使用下面的方法過濾掉deps文件夾

$(CC) -MM -E $(filter %.c, $^) | sed ‘s,\(.*\)\.o[ :]*,objs/\1.o : ,g‘ > $@

2.func.dep被重復創建了多次?
問題本質分析:
deps文件夾的時間屬性會因為依賴文件創建而發生改變,make發現deps文件夾比對於的目標更新時,會觸發相應規則的重新解釋和命令的執行。
解決方案:使用ifeq動態決定.dep目標的依賴;

ifeq ("$(wildcard $(DIR_DEPS))", "")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif

13.2.Include黑暗操作

1.使用- 不但關閉了include發出的警告,同時關閉了錯誤,當發生錯誤時,make將忽略這些錯誤。
2.如果include 觸發規則創建了文件則會發生下面的事情:

// 使用include 時的暗黑操作
if(如果目標文件不存在)
{
    //以文件名為規則查找並執行,
    if(查找到的規則中創建了文件)
    {
        //將創建成功的目標文件包含進當前makefile
    }
}
else // 如果目標文件存在
{
    // 將目標文件包含進當前makefile
    if(以目標文件名查找是否有相應的規則)
    {
        if(比較規則的依賴關系,決定是否執行規則的命令)
        {
            // (依賴文件更新,則執行)
        }
        else
        {
            // 無操作
        }
    }
    else
    {
        // 無操作
    }
}

實驗1:include包含的目標文件不存在,並且以文件名為目標的規則存在,並在規則中創建了文件

.PHONY : all

-include test.txt

all : 
    @echo "this is all"

test.txt :
    @echo "creating $@ ..."
    @echo "other : ; @echo "this is other" " > test.txt

我們期望了輸出結果因該是:this is all,因為all是第一個(默認)目標。
運行結果:
技術分享圖片
原因在於當出現上面的情況時:以文件名為規則查找並執行,同時如果查找到的規則中創建了文件,將創建成功的目標文件包含進當前makefile,此時在makefile中第一個目標變成了other
實驗2:

.PHONY : all

-include test.txt

all : 
    @echo "this is all"

test.txt : b.txt
    @echo "creating $@ ..."

當不存在b.txt時的運行結果:
技術分享圖片
當存在b.txt,但b.txt文件比test.txt文件舊時的運行結果:
技術分享圖片
當存在b.txt,但b.txt文件比test.txt文件新時的運行結果:
技術分享圖片
結論:如果目標文件存在:將目標包含進當前makefile,以目標文件名查找是否有相應的規則
如果有則比較規則的依賴關系,決定是否執行規則的命令(依賴文件更新,則執行),如果規則中的命令更新了目標文件,替換之前包含了的內容。未更新,則無操作。
以目標文件名查找是否有相應的規則,不能找到,則無操作
實驗3:

.PHONY : all

-include test.txt

all : 
    @echo "$@ : $^"

test.txt : b.txt
    @echo "creating $@ ..."
    @echo "all : c.txt" > test.txt

a.txt內容:

all : a.txt

當該文件中所需的所有文件都存在,並且test.txt的內容為最新時,make all輸出結果:
技術分享圖片
當b.txt文件最新時,make all輸出結果:
技術分享圖片

14.自動生成依賴關系_續

經過前面的技巧學習,我們現可以去完成這個自動生成依賴關系的想法了
註意:
思考:我們在13節中最終創建出來的makefile是否存在問題?
當.dep文件生成後,如果動態的改變文件間的依賴關系,那麽make可能無法檢測到這個改變,進而做出錯誤的判斷。
實例:

輸出結果:

解決方案:
將依賴文件的文件名作為目標加入自動生成的依賴關系中,通過include加載依賴文件時判斷是否執行規則,在規則執行時重新生成依賴關系文件,最後加載新的依賴文件。
舉個栗子:當我們前面編譯過之後(生成了依賴文件),又添加了新的頭文件,這時根據include的暗黑操作,要去檢查與include所包含的依賴文件同名的規則是否存在,如果存在,則檢查這個目標所對應的依賴是否被更新,如果更新,則執行相應規則。
最終方案:

.PHONY : all clean rebuild

MKDIR := mkdir
RM := rm -fr
CC := gcc

DIR_DEPS := deps
DIR_EXES := exes
DIR_OBJS := objs

DIRS := $(DIR_DEPS) $(DIR_EXES) $(DIR_OBJS)

EXE := app.out
EXE := $(addprefix $(DIR_EXES)/, $(EXE))

SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

all : $(DIR_OBJS) $(DIR_EXES) $(EXE)

ifeq ("$(MAKECMDGOALS)", "all")
include $(DEPS)
endif

ifeq ("$(MAKECMDGOALS)", "")
include $(DEPS)
endif

$(EXE) : $(OBJS)
    $(CC) -o $@ $^
    @echo "Success! Target => $@"

$(DIR_OBJS)/%.o : %.c
    $(CC) -o $@ -c $(filter %.c, $^)
#   $(CC) -o $@ -c $(filter %.c, $^)

$(DIRS) :
    $(MKDIR) $@

ifeq ("$(wildcard $(DIR_DEPS))", "")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
    @echo "Creating $@ ..."
    @set -e;     $(CC) -MM -E $(filter %.c, $^) | sed ‘s,\(.*\)\.o[ :]*,objs/\1.o $@ : ,g‘ > $@

clean :
    $(RM) $(DIRS)

rebuild :
    @$(MAKE) clean
    @$(MAKE) all

總結:
Makefile中可以將目標的依賴拆分寫到不同的地方;
include關鍵字能夠觸發相應的規則的執行;
如果規則的執行導致依賴更新,可能導致再次解釋執行相應的規則;
依賴文件可需要依賴源文件得到正確的編譯決策
自動生成文件的依賴關系能夠提高Makefile的移植性。

makefile(05)_自動生成依賴關系