認識make/makefile

一個工程中的源文件不計數,其按類型、功能、模塊分別放在若干個目錄中,makefile 定義了一系列的規則來指定,哪些文件件需要先編譯,哪些文件件需要後編譯,哪些文件件需要重新編譯,甚至進行更復雜的功能操作。

存在多個源文件,這些源文件經過編譯器的處理之後變成 .o 文件,所有的 .o 文件再和庫鏈接起來形成可執行程序。如何將這多個文件最終合成一個可執行程序呢?如何將這多個文件最終合成多個可執行程序呢?這個時候自動化構建的工具就幫大忙了,不需要我們在命令行中反覆輸入gcc/g++ 相關命令,手動構建。

在使用 vs 編譯器編寫代碼時,main.c 和 test.c 這兩個文件,最終合成一個可執行文件,其中的自動化構建就是vs幫助我們做了。makefile帶來的好處就是 —— “自動動化編譯”,一旦寫好,只需要一個 make 命令,整個工程完全自動編譯,極大的提高了軟件開發的效率。

make/makefile的功能:自動化項目的構建(將源文件進行編譯變成二進制文件)

make 是一個命令,makefile一個文件


接下來看看 make/makefile 。在當前路徑下創建 makefile/Makefile 文件(建議首字母大寫),打開 Makefile 文件,寫入以下代碼:

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件

推薦下面的寫法:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_02

這就是一個簡單的 makefile。

倘若想要執行該文件,直接輸入 make 指令。結果如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_03

輸入 make 指令後,自動執行 Makeflie 中的內容,並且自動幫助我們生成 code.exe 文件。所以説make 是個命令,makefile 是個文件


實現 makefile

makefile 中的結構:

目標文件:依賴文件列表(依賴關係)

        依賴方法(使用TAB鍵開頭)

make 命令會解析 makefile 文件中的依賴關係和依賴方法,第一行 code.exe:code.c ,code.exe 的形成依賴 code.c,這種關係叫做依賴關係。第二行 gcc -o code.exe code.c 叫做依賴方法。Makefile 中重點包含的依賴關係和依賴方法

依賴關係和依賴方法是成對的,是合理的。想要生成可執行程序,需要依賴源文件,僅有依賴關係還不夠,還需要有將源文件翻譯成可執行程序的正確的依賴方法。

依賴關係錯誤,不能執行依賴方法。

【Linux】Linux項目自動化構建工具 —— make/makefile_#服務器_04

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_05

依賴方法不合理,不能執行依賴方法。

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_06

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_07


為了更好的理解 makefile,在 Makefile 文件中寫入以下內容

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_08

在命令行輸入 make 指令,解析 makefile 中的依賴關係和依賴方法。

【Linux】Linux項目自動化構建工具 —— make/makefile_#服務器_09

分析:

要生成 code.exe 文件,就需要依賴文件 code.o,code.o 文件在當前路徑下沒有,再往下找是否是依賴文件列表生成的目標文件,按照此道理類推,直到找到 code.i:code.c。code.c 在當前路徑下存在,所以生成 code.i 目標文件

code.s:code.i 依賴關係中,依賴關係成立,code.i 依賴文件存在,生成目標文件 code.s

code.o:code.s 依賴關係中,依賴關係成立,code.s 依賴文件存在,生成目標文件code.o

code.exe:code.o 依賴關係中,依賴關係成立,code.o 依賴文件存在,生成目標文件code.exe

make 是一條命令,它會解析 Makefile 文件中的依賴關係和依賴方法,那麼它是如何解析的?

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_10

make 會解析 Makefile 中的依賴關係和依賴方法,根據依賴關係形成推導棧,推導棧就是依賴方法的集合

如果將這四個依賴關係和依賴方法的順序打亂, 會根據入棧和出棧的順序執行依賴關係和依賴方法(下圖僅是方便分析,具體的入棧規則並不是這樣)

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_11

分析:

gcc -c code.s -o code.o 依賴文件 code.s 當前路徑下不存在,故依賴方法不執行,出棧

gcc -E code.c -o code.i 依賴文件 code.c 當前路徑下存在,生成文件 code.i,執行依賴方法,執行完畢後出棧

gcc code.o -o code.exe 依賴文件 code.o 當前路徑下不存在,故依賴方法不執行,出棧

gcc -S code.i -o code.s 依賴文件 code.i 當前路徑下存在,生成 code.s 文件,執行依賴方法,執行完畢後出棧

所以最後生成了code.i 和 code.s 文件。

make命令的執行結果如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_12


.PHONY

Makefile 不僅需要可以生成可執行文件的功能,也需要可以清理可執行文件的功能,所以需要清理,代碼如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_13


連續輸入兩次 make 指令

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_14

會顯示當前的 code.exe 已經是最新,如果想要 make 指令能夠執行,可以輸入 make clean,這樣就可以再次執行 make 指令。

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_15

由上圖可以知道,clean 確實將生成的可執行文件給刪除了,這樣就簡單的完成了 makefile 的構建和清理過程。


PHONY 是 makefile 提供的一個關鍵字。PHONY 是假的,偽造的的意思,它的作用是聲明一個符號(符號的符號名可以隨意命名,最好能直觀知道它的功能),表明該符號是一個偽目標,偽目標也是目標文件,只是使用 .PHONY 修飾,以此來告知 make,該目標文件與其他目標文件不一樣。既然偽目標也是目標,那麼它也有自己的依賴關係和依賴方法。

從這個偽目標的結構,總結出以下幾點:

• 依賴關係必須存在,但是依賴文件列表可以為空

• 依賴方法可以是任何 shell 命令

• clean 目標只是利用 make 的自動推導的能力,讓它執行了 rm 命令。在構建工程的視角,看起來就是清理項目,本質就是刪除不需要的臨時文件


之前解析 makefile 文件的依賴關係和依賴方法時,都是直接輸入 make 指令。但是解析 clean 文件的依賴關係和依賴方法時,為什麼要輸入 make clean 呢?為什麼解析 code.exe 文件的依賴關係和依賴方法時,不輸入 make code.exe 呢?其實輸入 make code.exe 指令也可以,如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_16

由此我們可以得出:make 命令後可以跟目標名,跟誰就解析誰的依賴關係和依賴方法

為什麼單獨輸入 make code.exe 指令,它只解析 code.exe 文件的依賴關係和依賴方法?輸入make clean 指令,只解析 clean 文件的依賴關係和依賴方法,而不執行 code.exe 文件的依賴關係和依賴方法呢?因為 clean 和 code.exe 這兩個推導鏈之間並沒有依賴關係,clean 和 code.exe 推導鏈是一個獨立的依賴關係和依賴方法,make默認只會推導一條完整的推導鏈

但是為什麼單獨輸入 make 指令,它只推導 code.exe 推導鏈,而不推導 clean 推導鏈呢?將clean 和 code.exe 的推導鏈交換位置。

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_17

輸入make指令,結果如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_18

由此我們可以知道:make 默認只會推導第一個依賴關係對應的推導鏈。但是在實際使用中,通常將清理工程放在後面。


在前面曾提到 .PHONY 修飾的文件是一個偽目標文件,那麼偽目標文件是什麼?偽目標文件的本質功能是總是被執行。如何理解“總是被執行”?

當連續輸入 make 命令時,命令行會顯示 code.exe 已經是最新的。

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_19

不允許 make 指令根據依賴關係推導依賴方法了。

然而 make clean 可以反覆被執行,如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_20

如果用 .PHONY 修飾 code.exe 文件,將 code.exe 聲明成偽目標,再連續輸入 make 指令,結果如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_21

code.exe 目標文件也可以反覆執行了。

為什麼 clean 目標文件需要被 .PHONY 修飾?因為清理工作需要時刻執行,如果不清理程序運行的臨時文件,可能會出現錯誤。

為什麼 code.exe 目標文件就默認不需要被 .PHONY 修飾呢?因為在執行完 make 指令後,會生成code.exe 文件,再輸入 make 指令後,會顯示 code.exe 已經是最新版本,這也就表明了 code.c沒有被修改,不需要重新生成一份新的 code.exe 文件。既然如此,如果 code.exe 文件被 .PHONY 修飾,不論 code.c 是否被修改,都會重新在再生成一份一模一樣的 code.exe 文件,這樣會造成不必要的浪費。所以 .exe 可執行程序不用 .PHONY 修飾,這樣可以加速編譯的效率。只有源文件發生更改才會重新編譯生成新的 .exe 可執行程序


既然 .PHONY 修飾的文件總是被執行的,那麼 .PHONY 是怎麼做到的呢?

我們知道一個文件的建立是有時間的,可以使用 stat 指令來查看文件的三個時間:access,modify,change

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_22

modify 表示文件新建或者內容被修改的最近時間

源文件經過編譯器後生成可執行文程序 code.exe,可執行程序也是文件,它也有自己的三個時間。

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_23

因為先有的源文件,再有的可執行程序,所以可執行程序的 modify 時間一定比源文件的 modify 時間更新。在時間軸上的表示情況如下所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_24

一旦源文件的內容被修改,隨即源文件的時間就會更新。重新編譯,編譯器識別到源文件的 modify 時間比當前可執行程序的 modify 時間更新,就會重新再次生成可執行程序,這樣可執行程序的 modify 時間又比源文件的 modify 時間更新了。如果再次編譯,編譯器識別到當前可執行程序的 modify 時間已經比源文件的 modify 時間更新了,就不會再編譯了。

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_25

總結:

源文件是否需要重新被編譯,看源文件和可執行文件哪個文件的修改時間更新。

可執行程序的 modify 時間一定比源文件的 modify 時間更新。

如果源文件的 modify 時間更新,則源文件需要重新編譯;如果可執行程序的 modify 時間更新,則源文件不需要重新編譯

證明上述的總結是對的

code.c 的時間

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_26

code.exe 的時間

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_27

從兩圖的 modify 時間比較中可知,code.exe 的時間更新,所以再次編譯 code.c 文件,編譯不通過。

【Linux】Linux項目自動化構建工具 —— make/makefile_#服務器_28

接下來打開 code.c 文件,修改文件中內容(增或刪),再次查看 code.c 的時間。

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_29

比較此時 code.c 和 code.exe 文件的 modify 時間,源文件的 modify 時間更新,允許再次編譯源文件。

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_30

再次查看 code.exe 的三個時間:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_31

現在可執行程序的 modify 時間又比源文件的 modify 時間更新了。

前面更新 code.c 文件的時間,是打開該文件修改文件中的內容,有沒有一種方法不用打開文件,就能將文件的時間更新到當前的最新時間呢?touch 已存在的文件名 指令,功能:更新已經存在的文件的三個時間。

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_32

現在源文件的 modify 時間又比可執行程序的 modify 時間更新,允許再次編譯源文件

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_33

接下來再使用 .PHONY 修飾 code.exe 文件,就可以反覆編譯源文件了。

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_34

所以 .PHONY 如何做到由它修飾的文件總是被執行的?讓編譯器或者對應的命令忽略 modify 時間(有些命令是忽略文件的時間的,如rm命令)。


ACM 時間

acm 時間就是前面説的三個時間:access,modify,change。

modify 和change都是修改的意思,如何理解change時間?我們知道文件 = 文件內容+文件屬性,modify 時間表示最近一次修改文件的內容的時間,change 時間表示最近一次修改文件的屬性的時間

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_35

更改文件的權限,即更改文件的屬性,文件的 change 時間也就更新了。

接下來打開 code.c 文件,修改文件中的內容,再次查看 code.c 的 acm 時間

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_36

modify 和 change 時間都被修改了,為什麼 change 時間也更新了?因為修改文件的內容,會影響文件的大小,文件的大小也是文件屬性,並且 modify 時間也是文件屬性,所以 change 時間更新了。

access 時間表示文件最近被訪問的時間。怎樣叫做訪問文件呢?我們之前一直輸入的 cat/stat 指令就是在訪問文件。

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_37

看上圖,起初輸入 cat  指令,查看 code.c 文件中的內容,再輸入 stat 指令查看文件的時間,發現access 時間被修改;然而再次輸入 cat 指令,查看 code.c 文件的內容,再次輸入 stat 指令查看文件的時間,會發現 access 時間沒有被修改。為什麼一開始訪問文件時 access 時間被修改了,再次訪問 code.c 文件時 access 時間卻沒有被修改呢?

一個文件的內容/屬性被更改時,是需要刷新到磁盤上的。但是 linux 系統中這麼多的文件,當我們新建文件時,修改文件的內容/屬性,查看文件的內容/屬性的次數一定會比修改文件的內容/屬性的次數更多,查文件的比重是高於該文件的比重的(修改文件的前提是查看文件,無論對文件做什麼,第一步都是查看文件)。並且 access 時間表示文件最近被訪問的時間,每次訪問文件,文件的 access 時間都會刷新到磁盤。如果我們每次訪問文件,都會馬上修改文件的時間,讀寫磁盤,如果頻繁訪問文件呢?豈不是要反覆修改文件的時間,讀寫磁盤,這樣肯定會增加訪問磁盤的次數(磁盤是一個外設,效率低下),如此會降低操作系統的效率(時間都用到刷新磁盤上了)。

所以訪問文件時,訪問特定的次數之後(次數與操作系統的內核有關),才會更新一次access時間。至於上圖中 access 時間更新了,是因為恰好訪問到了特定的次數。


makefile 的語法

往 code.c 文件中寫入以下代碼:

【Linux】Linux項目自動化構建工具 —— make/makefile_#服務器_38

輸入 make 指令,可以發現 makefile 中的內容會被顯示在終端上

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_39

如果想要禁止命令顯示在終端上,可以在 makefile 文件的依賴方法的前面加上@,如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_40

效果:

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_41


當前的 Makefile 文件編譯的是 code.c 文件,如果之後想要編譯其它.c文件呢?豈不是還要修改 Makefile 涉及 .c 文件的部分,這樣效率也太低了。可以在 Makefile 文件的開頭處定義變量,如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_42

如果想要將 code.c 文件編譯成 test.exe ,像上面這樣寫是否有問題。查看 make 指令解析的結果:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_43

為什麼 code.c 編譯的結果依舊是 code.exe ?因為依賴關係使用變量取代,但是依賴方法並沒有使用變量取代。若想要達成目的,需要用變量取代依賴方法中的某些地方,如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_44

在 makefile 中,$@ 和 $^ 中的 @ 和 ^ 是一個特殊的內置變量,@ 和 ^ 是變量,$ 表示取變量中的內容,$@表示對應依賴關係的目標文件,$^表示對應依賴關係的依賴文件列表。makefile在解析make語法時,會將$@解析成目標文件,$^解析成依賴文件列表

為了更好的理解 $@ 和 $^,將 Makefile 文件中的內容更改成下圖所示,便於看到命令的回顯:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_45

演示結果:

【Linux】Linux項目自動化構建工具 —— make/makefile_可執行程序_46


之前操作的都是一個源文件,如果有多個源文件呢?將 code.c 重命名為 main.c ,創建100個源文件,如何創建100個以 .c 結尾源文件,方法:touch xxx{1..100}.c,如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_47

如果要刪除這100個源文件,rm src{1..100}.c 即可。

【Linux】Linux項目自動化構建工具 —— make/makefile_#服務器_48

這100多個源文件應該怎麼編譯?將所有的源文件翻譯成對應的 .o 文件,再將所有的 .o 文件進行鏈接形成可執行程序。

如此一來需要修改 Makefile,問題是依賴文件列表應該怎麼寫?一個個寫,一直寫到100?src1.c,src2.c,src3.c,……,src100.c。正確寫法為:SRC=$(shell ls *.c)。$(shell ls *.c) 的功能:羅列出當前目錄下的所有以 .c 為後綴的文件

測試代碼:

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_49

測試結果:

【Linux】Linux項目自動化構建工具 —— make/makefile_#服務器_50

這個方法還不夠好,推薦使用該方法:SRC=$(wildcad *.c)。wildcad 是一個函數,功能:獲取當前目錄下所有以 .c 為後綴的文件

測試結果:

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_51

知道了依賴文件列表應該怎麼寫之後,怎麼將所有的.c文件編譯成.o文件呢?怎麼一步到位呢?我們可以再定義一個 OBJ 變量:OBJ=$(SRC:.c=.o) 。功能:將 SRC 的所有同名 .c 替換成為 .o 形成目標文件列表

測試結果:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_52

將所有的 .o 文件鏈接形成可執行程序,方法如下所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_53

這樣寫就可以了嗎?不可以。現在僅僅只是將所有的 .o 文件生成可執行程序,但是各個 .o 文件如何編譯,並沒有交待清楚。因為 .o 文件在當前目錄下並不存在,需要將所有的 .c 文件編譯成 .o 文件。寫法為:%.o:%.c。%是makefile中的一個解析符,%.c 展開當前目錄下所有的 .c;%.o: 同時展開同名 .o

之前將 .c 文件編譯成 .o 文件時,指令為:gcc -c src.c -o src.o,其實指令還可以寫成:gcc -c src.c,編譯器會自動形成同名的 .o 文件。所以 %.o:%.c 的依賴方法可以寫成:gcc -c $<。$< 表示對展開的依賴 .c 文件,一個一個的交給編譯器

為了便於測試,代碼寫成下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_54

測試結果:

【Linux】Linux項目自動化構建工具 —— make/makefile_源文件_55


增加清理工程,清理中間產生的臨時文件,也就是 .o 文件和可執行程序。代碼如下所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_#服務器_56

演示結果:

【Linux】Linux項目自動化構建工具 —— make/makefile_#linux_57


當前的 makefile 只能用於 C 語言,如果想讓它用於 C++,豈不是還要一個個將 gcc 更改為 g++?我們可以定義一個變量 CC 用於控制編譯器

最終我們實現的 makefile 如下圖所示:

【Linux】Linux項目自動化構建工具 —— make/makefile_依賴關係_58