天天看點

自動生成依賴關系(十)

        我們在之前的 makefile 學習中,其目标檔案(.o)隻依賴于源檔案(.c)。那麼如果在源檔案中還包含有頭檔案,此時編譯器如何編譯源檔案和頭檔案呢?我們來看看編譯行為帶來的缺陷:1、預處理器将頭檔案中的代碼直接插入源檔案;2、編譯器隻通過預處理後的源檔案産生目标檔案;3、規則中以源檔案為依賴,指令就可能無法執行。

        我們來看看下面的 makefile 有沒有問題

makefile 源碼

OBJS := func.o main.o

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

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

func.h 源碼

#ifndef _FUNC_H_
#define _FUNC_H_

#define HELLO "Hello D.T."

void foo();

#endif      

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.h 源檔案中想要改掉這個字元串為 Software 呢?試試看能不能修改成功

自動生成依賴關系(十)

        我們看到在重新編譯的時候,它并沒有因為頭檔案的改變而改變,我們在 makefile 中又沒有進行頭檔案的相關添加,改掉頭檔案中的内容肯定是不動的。下來我們在模式規則中加上頭檔案,在 %.c 後加上 func.h,再來看看編譯結果

自動生成依賴關系(十)

        我們看到直接添加之後,編譯出錯了。因為 -c 後面的目标中含有頭檔案,是以不能直接進行編譯。我們可以隻編譯 %.o 後面的第一依賴 %.c,這樣就不會去編譯 func.h 頭檔案了,将下面的 $^ 改為 $< ,我們來看看效果

自動生成依賴關系(十)

        我們看到已經正确改過來了。經過上面的實驗,我們看到:頭檔案作為依賴條件出現于每個目标對應的規則中,當頭檔案改動時,任何源檔案都将會被重新編譯(編譯低效);當項目中頭檔案巨大時,makefile 将很難維護。那麼我們的頭腦中不禁會冒出這麼個想法:通過指令對自動生成對頭檔案的依賴;将生成的依賴自動包含進 makefile 中;當頭檔案改動後,自動确認需要重新編譯的檔案。那麼此時我們還需要知道一個指令,Linux 中的 sed 指令。sed 是一個流編輯器,用于流文本的修改(增、删、查、改);它可用于流文本中的字元串替換,其字元串替換方式為:sed 's:src:des:g',具體格式如下

自動生成依賴關系(十)

        sed 同樣也支援正規表達式,在 sed 中可以用正規表達式比對替換目标,并且可以使用比對的目标生成替換結果。格式如下

自動生成依賴關系(十)

        下來我們以代碼為例來看看 sed 指令是如何使用的

自動生成依賴關系(十)

        再來看看 gcc 關鍵編譯選項,擷取目标的完整依賴關系:gcc -M test.c;擷取目标的部分依賴關系:gcc -MM test.c。makefile 如下

.PHONY : test

test :
    gcc -M main.c      

        編譯結果如下

自動生成依賴關系(十)

        我們看到 -M 是擷取了它的所有依賴關系,再來試試 -MM 呢

自動生成依賴關系(十)

        我們看到 -MM 後,它隻依賴與 main.c func.h。我們可以拆分目标的依賴,即将目标的完整依賴差分為多個部分依賴。格式如下

自動生成依賴關系(十)

        我們來做個實驗

.PHONY : a b c

test : a b

test : b c

test :
    @echo "$^"      

        我們來列印看看目标 test 的依賴都有哪些,編譯結果如下

自動生成依賴關系(十)

        那麼我們思考下:如何将 sed 和 gcc -MM 用于 makefile,并自動生成依賴關系呢?

        我們再來看看 makefile 中的 include 關鍵字,它類似于 C 語言中的 include,是将其它檔案的内容原封不動的搬入目前檔案。make 對 include 關鍵字的處理方式是在目前目錄下搜尋或指定搜尋目标檔案。如果搜尋一成功,便将檔案内容搬入目前 makefile 中;如果搜尋失敗,将會産生警告,以檔案名作為目标查找并執行對應規則,當檔案名對應的規則不存在時,最終産生錯誤。格式如下

自動生成依賴關系(十)

        下來還是以代碼為例來進行說明

.PHONY : test

include test.txt

all :
    @echo "this is $@"

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

        我們在第 3 行包含 test.txt,可是目前目錄下并沒有 test.txt,然後觸發 test.txt 的規則。因而會列印出 test.txt,然後再建立 test.txt,我們來看看編譯結果

自動生成依賴關系(十)

        我們看到确實是建立了一個 test.txt 檔案。那麼在 makefile 中指令的執行是:1、規則中的每個指令預設是在一個新的程序中執行(Shell);2、可以通過接續符(;)将多個指令組合成一個指令;3、組合的指令依次在同一個程序中被執行;4、set -e 指定發生錯誤後立即退出執行。那麼我們看看下面的代碼會實作想要的功能嗎?

.POHONY : all

all :
    mkdir test
    cd test
    mkdir subtest      
自動生成依賴關系(十)

        我們看到在目前目錄下建立了目錄,但是 subtest 目錄卻不是在 test 目錄下建立的,這是怎麼回事呢?在第一條指令執行時建立了目錄 test,此時這個程序已經關閉了;在第二條指令執行時,執行的是另一個程序,雖然它已經進入到目錄 test 中,但是随着這個程序的關閉,又回到了目前目錄;第三個程序是重新建立了目錄 subtest。那麼如何解決這個問題呢?直接利用 set -e 和 接續符來解決

.PHONY : test

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

        看看編譯結果

自動生成依賴關系(十)

        那麼我們之前思考問題的初步思路是:1、通過 gcc -MM 和 sed 得到 .dep 依賴檔案(目标的部分依賴),技術點是規則中指令的連續執行;2、通過 include 指令包含所有的 .dep 依賴檔案。技術點是當 .dep 依賴檔案不存在時,使用規則自動生成。下面我們來看看解決方案是怎樣的

ONY : 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)      
自動生成依賴關系(十)

        我們先來分析下,在執行 make all 前,它先通過 include 包含 $(DEPS),通過 $(DEPS) 觸發模式規則,進而建立檔案夾。我們看到在前面出現兩個沒有檔案夾的資訊,其實這條資訊是可以隐藏的。我們在 include 前面加上 - 就 OK,來看看效果

自動生成依賴關系(十)

        我們看到并沒列印出前面的兩條資訊了。那麼我們再來思考下:如何組織依賴檔案相關的規則與源碼編譯相關的規則,進而形成功能完整的 makefile  程式呢?我們如何在 makefile 中組織 .dep 檔案到指定目錄呢?初步想法是當 include 發現 .dep 檔案不存在時:1、通過規則和指令建立 deps 檔案;2、将所有 .dep 檔案建立到 deps 檔案夾;3、.dep 檔案中記錄目标檔案的依賴關系。

        我們下來看看初步的代碼設計是怎樣的

.PHONY : all clean

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

DIR_DEPS := deps

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

include $(DEPS)

all :
    @echo "all"

$(DIR_DEPS) :
    $(MKDIR) $@

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

clean :
    $(RM) $(DIR_DEPS)      

         我們來看看編譯結果,是不是都将所有的 .dep 檔案放入一個 deps 檔案中

自動生成依賴關系(十)

        我們看到已經實作效果了。我們仔細看看 make 有一個警告,說 main.dep 被修改了,也就是說 main.dep 被重新建立了。那麼我們來分析下,為什麼一些 .dep 依賴檔案會被重複建立多次呢?deps 檔案夾的時間屬性會因為依賴檔案建立而發生改變,make 發現 deps 檔案夾比對應的目标更新,于是乎就觸發相應的規則重新解析和執行指令。那麼我們知道了原因,此時這個方案該如何優化呢?我們可以使用 ifeq 動态決定 .dep 目标的依賴,具體 makefile 如下

.PHONY : all clean

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

DIR_DEPS := deps

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


all : 
    @echo "all"

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

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

$(DIR_DEPS) :
    $(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) $(DIR_DEPS)      

        我們再次編譯看看

自動生成依賴關系(十)

        我們看到它還是報了這樣的錯誤,有可能是編譯器的優化造成的。思路是正确的。下來我們來看看 include 的一些鮮為人知的秘密。

        A、 使用減号(-)不但關閉了 include 發出的警告,同時将關閉了錯誤;當錯誤發生時 make 将忽略這些錯誤! 以代碼為例來進行分析說明

.PHONY : all

include test.txt

all :
    @echo "this is all"

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

        我們來編譯看看

自動生成依賴關系(十)

        我們看到不但發出警告,而且報錯了。下來我們來在 include 前面加上 - 試試

自動生成依賴關系(十)

        這樣它也不報錯了,直接就通過了,我們還以為 makefile 寫的對着呢。這便是第一個暗黑操作。下來看看第二個暗黑操作

        B、如果 include 觸發規則建立了檔案,之後還會發生什麼?以代碼為例來進行分析說明

.PHONY : all

include test.txt

all :
    @echo "this is all"

test.txt :
    @echo "creating $@ ..."
    @echo "other : ; @echo "this is other" " > test.txt      
自動生成依賴關系(十)

        我們進行直接 make 的時候,發現它輸出的 this is other,并不是我們所期望的 this is all。這是為什麼呢?因為在 include 的時候,直接将 test.txt 鋪開在這,此時會觸發規則。makefile 就變成了下面這樣

.PHONY : all

other : 
    @echo "creating $@ ..."
    @echo "this is other"

all :
    @echo "this is all"      

        我們在直接 make 的時候,它預設執行的是第一個目标,是以便會輸出 this is other,隻有當我們 make all 的時候才會輸出 this is all。這便是 include 的第二個暗黑操作了,下面繼續看看第三個

        C、如果 include 包含的檔案存在,之後會發生什麼呢?以代碼為例來進行分析說明

.PHONY : all

-include test.txt

all :
    @echo "this is all"

test.txt : b.txt
    @echo "this is $@"      

        在目前目錄下建立一個 b.txt 檔案,看看編譯結果

自動生成依賴關系(十)

        我們看到同樣也執行了 test.txt 的相應的規則。看看下面這個 makefile 将會輸出什麼

.PHONY : all

-include test.txt

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

        看看結果

自動生成依賴關系(十)

        我們看到它最後輸出的 all 的依賴是 c.txt,不應該覺得奇怪嗎?我們明明在 all 後面沒有依賴啊。再來看看生成的 test.txt 檔案,它的内容是 all : c.txt,是以輸出的結果是我們意想不到的。那麼我們關于 include 便有了這幾條總結:1、當目标檔案不存在時,以檔案名查找規則并執行;2、當目标檔案不存在時且查找到的規則中建立了目标檔案,将建立成功的目标檔案包含進目前的 makefile 中;3、當目标檔案存在,将目标檔案包含進目前 makefile,以目标檔案名查找是否有相應規則,YES 的話則比較規則的依賴關系來決定是否執行規則的指令,NO 的話則 NULL(無操作)。4、當目标檔案存在且目标名對應的規則被執行,規則中的指令更新了目标檔案,make 重新包含目标檔案,替換之前包含的内容。目标檔案未被更新,便是 NULL(無操作)。

        經過了這麼多的知識點的探索,此時已經具備實作之前的想法的能力了。想要實作的具體格式如下

自動生成依賴關系(十)

        下面我們就根據這個來編寫相關的 makefile。

#ifndef FUNC_H
#define FUNC_H

#define HELLO "hello Makefile"

#endif      
#include <stdio.h>
#include "func.h"

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

int main()
{
    foo();
    
    return 0;
}      
.PHONY : all clean

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

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

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 $^

$(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)      
自動生成依賴關系(十)

        我們看到已經自動生成了,并且最後的結果也是我們想要的,那麼我們如果在 func.h 中改變字元串,看看結果是否也會改變

自動生成依賴關系(十)

        我們看到在編譯的時候報錯了,原因是隻能編譯 .c 檔案,.h 頭檔案不參與編譯,這時我們便要用到預定義函數 filter 了。是以我們需要在 makefile 第37 行将它改為 $(CC) -o $@ -c $(filter %.c, $^);再來看看效果

自動生成依賴關系(十)

        我們看到也成功的替換掉了。這時我們基本上已經完成我們之前的想法了,那麼在實際開發中,肯定需要時不時的添加頭檔案,我們再來在 func.h 中包含一個頭檔案 define.h,在 define.h 檔案中定義字元串 hello-makefile,看看結果是否會跟着改變

自動生成依賴關系(十)

        我們看到字元串并沒有發生改變,再來看看 func.dep 和 main.dep 中是否包含了 define.h

自動生成依賴關系(十)

        也沒有包含,按理說不應該,因為我們在 func.h 中包含了 define.h,那麼在 func.c 和 main.c 中肯定也就包含了 define.h。下來我們來分析下這個,當 .dep 檔案生成後,如果動态的改變頭檔案間的依賴關系,那麼 make 可能無法檢測到這個改變,進而做出錯誤的編譯決策。解決方案便是:1、将依賴檔案名作為目标加入自動生成的依賴關系中;2、通過 include 加載依賴檔案時判斷是否執行規則;3、在規則執行時重新生成依賴關系檔案;4、最後加載新的依賴檔案。解決方法是在 sed 指令後加上 $@,看看編譯效果,順便我們再來加上 rebuild。

自動生成依賴關系(十)

        我們看到已經正确實作了,我們來看看在 deps 檔案下的 .dep 檔案是否包含 define.h 呢?

自動生成依賴關系(十)

        确實是包含了 define.h,我們再來加上 new.h,看看是否還會有效

自動生成依賴關系(十)

繼續閱讀