徹底掌握Makefile(一)

2022-09-22 12:00:24

徹底掌握Makefile(一)

介紹

makefile就是一個可以被make命令解析的檔案,他定義了一系列編譯的規則,幫助我們更加方便、簡潔的去完成編譯的過程。在一個大工程當中我們會有各種各樣的檔案,我們可能會分模組去存放各種檔案,可能有些檔案還依賴其他的檔案,因此我們在編譯的時候需要先將被依賴的檔案先編譯,其他檔案後編譯,而我們使用makefile就可以更好的去完成這件事兒。

Makefile基礎

在很多情況下我們在C/C++的專案當中使用makefile,其實在其他語言的專案也可以使用make和makefile,它不侷限於語言的,可以是C也可以是Java。現在寫一個基本的例子:

demo: demo.c
	gcc demo.c -o demo

clean:
	rm demo

上面是一個makefile的例子,我們簡要說明一下makefile的書寫規則:

編譯目標:依賴檔案
	編譯命令

然後我們使用make命令去解釋執行makefile,我們可以使用make 編譯目標去執行特定的語句,比如在上面的例子當中我們執行make demo的話就會執行gcc demo.c -o demo命令。

其實上面我們也可以直接使用make命令,不需要指定編譯目標,因為make會自己尋找第一個目標作為被執行的目標。

在上面的程式碼當中我們當我們執行make的時候尋找到makefile檔案的第一個目標demo,但是因為我沒有改動demo.c這個檔案,而且這個檔案已經編譯過了,因此我們沒有必要再去編譯這個檔案,這也是make給我們提供的一個非常好的特性,我們不需要重新編譯已經編譯好的檔案,這在一個大型專案當中是非常有用的,當我們的專案當中有成千上萬的檔案的時候,如果我們重新編譯每一個檔案的話,那麼編譯的時間消耗是非常大的。因此我們在執行make執行執行clean編譯目標先刪除demo這個編譯結果,然後在執行make這次它再找到demo目標,而此時demo已經被刪除了,因此會重新編譯。

Make命令的工作流程

當我們在命令列當中輸入make的時候他的執行流程如下:

  • make命令首先會在當前目錄下面尋找makefile或者Makefile檔案。
  • 尋找到makefile檔案之後,他會在檔案當中尋找到一個編譯目標,比如在上面的makefile檔案當中他會找到demo這個編譯目標,而不是clean這個目標,因為clean是第二個編譯目標。
  • 然後make會解析編譯目標的依賴,如果這個依賴是其他的編譯目標A的話,那麼make會先完成它依賴的編譯目標A的命令,如果它依賴的編譯目標A也存在依賴B的話,make就會去執行依賴的B的編譯命令,如此的遞迴下去,知道有所得依賴目標都存在了,才會完成第一個編譯目標的編譯,這個也很好理解,只有依賴檔案都存在了我們才能夠完成正確的編譯過程。

make編譯的過程為尋找編譯目標,依賴關係,如果依賴的檔案還存在依賴那麼make會一層一層的尋找下去,只要所有的依賴都被成功解析了,才會最終執行第一個編譯目標的編譯命令。但是在makefile當中不被第一個編譯目標的目標的編譯命令是不會被執行的,比如上面我們執行make的時候會執行demo編譯目標,但是不會執行clean編譯目標。

下面我們寫一個例子瞭解make解析依賴的過程:

main: demo.o myprint.o
	gcc demo.o myprint.o -o out
	echo make 解析編譯完成

demo.o: demo.c 
	gcc -c demo.c -o demo.o

myprint.o: myprint.c 
	gcc -c myprint.c -o myprint.o

clean:
	rm myprint.o demo.o out

執行make之後的結果如下圖所示:

因為我們沒有指定編譯的目標,因此make會尋找一個編譯目標,也就是main,但是在main當中依賴兩個檔案demo.omyprint.o,因此make會再去執行demo.omyprint.o兩個編譯目標,當這兩個編譯目標被完成之後才能執行main的編譯,根據make的輸出結果我們可以得到這一個結果的印證。

Makefilex小技巧

Makefile當中的變數

在makefile當中我們可以定義一些我們的變數這樣就可以避免反覆輸入了。比如我們經常會變編譯檔案的時候增加一些編譯選項,比如像下面這樣:

cflags=-c
main: demo.o myprint.o
	gcc demo.o myprint.o -o out

demo.o: demo.c 
	gcc $(cflags) demo.c -o demo.o

myprint.o: myprint.c 
	gcc $(cflags) myprint.c -o myprint.o

clean:
	rm myprint.o demo.o out

在上面的makefile當中我們定義了一個變數cflags並且在編譯命令當中使用,我們定義變數的方法其實和shell差不多,我們直接使用=可以定義變數,然後使用$(變數名)可以使用變數,因為上面的例子當中cflag=-c比較短,比較簡單,但是如果當我們的編譯引數很多很長的時候使用變數就非常有效了,而且如果在一個專案當中如果有成千上萬個檔案我們像統一改變編譯時候的引數的話,我們一個一個改是很麻煩的,但是如果我們使用變數就可以做到一改全改。

Makefile當中的include命令

在makefile當中我們也可以使用include命令去包含其他的makefile檔案,比如我們將上面的makefile檔案分成兩個部分makefilesubmakefile:

makefile:

include submakefile

demo.o: demo.c 
	gcc $(cflags) demo.c -o demo.o

myprint.o: myprint.c 
	gcc $(cflags) myprint.c -o myprint.o

clean:
	rm myprint.o demo.o out

submakefile:

cflags=-c
main: demo.o myprint.o
	gcc demo.o myprint.o -o out

然後在目錄下執行make命令, 得到的效果和前文當中提到的makefile結果是一樣的,這就相當於將submakefile的內容放到include語句的位置。

Makefile中的PHONY

在上面談到的makefile當中有一個clean的編譯目標用於清除我們編譯的結果檔案,現在我們在當前的目錄下面增加一個檔案clean在執行make命令,我們的makefile檔案如下:

cflags=-c
main: demo.o myprint.o
	gcc demo.o myprint.o -o main

demo.o: demo.c 
	gcc $(cflags) demo.c -o demo.o

myprint.o: myprint.c 
	gcc $(cflags) myprint.c -o myprint.o

clean:
	rm myprint.o demo.o main

然後執行下面的命令(touch命令是新增一個檔案,touch clean 就是往目錄中增加一個名字為clean的檔案):

我們可以看到當目錄下面增加一個clean檔案之後,我們使用make clean命令的時候,make給我們的提示資訊為clean檔案是最新的了。這是因為當執行make 編譯目標,make首先會檢查當前目錄下面是否存在clean檔案如果不存在則執行編譯目標clean的命令。如果存在clean檔案的話,make會檢查編譯目標clean的依賴檔案是否發生更改,如果發生更改了那麼就會執行clean對應的命令,但是在上面的makefile當中clean沒有依賴檔案,因此相當於他的依賴檔案沒有發生改變,make不會執行編譯目標clean對應的命令了。

但是我們的需求是仍然希望執行clean之後的命令,這個時候我們就可以使用PHONY了,他可以保證即使存在clean檔案的時候,make命令依然會執行編譯目標clean對應的命令。

cflags=-c
main: demo.o myprint.o
	gcc demo.o myprint.o -o main

demo.o: demo.c 
	gcc $(cflags) demo.c -o demo.o

myprint.o: myprint.c 
	gcc $(cflags) myprint.c -o myprint.o

clean:
	rm myprint.o demo.o main
.PHONY: clean # 增加這一行

執行結果如下圖所示:


我們現在來測試一下當我們命令生成的檔案和編譯目標不同名的時候,make是如何解釋執行makefile的,makefile的內容如下:

cflags=-c
main: demo.o myprint.o
	gcc demo.o myprint.o -o out # 這裡的輸出檔案是 out 而不是 main 輸出檔案名字和目標不同名

demo.o: demo.c 
	gcc $(cflags) demo.c -o demo.o

myprint.o: myprint.c  
	gcc $(cflags) myprint.c -o myprint.o

clean:
	rm myprint.o demo.o main
.PHONY: clean

執行結果:

從上面的結果我們可以發現,當我們編譯的結果檔案(out)和編譯目標(main)不同名的時候make每次都回去執行這個目標,因為make在進行檢測的時候沒有發現main這個檔案,因此每次執行make命令的時候都會去執行這個編譯目標。

Makefile的萬用字元

我們現在修改前面的makefile,修改的結果如下(當前目錄下面有兩個檔案demo.c和myprint.c):

cflags=-c

main: demo.o myprint.o
	gcc demo.o myprint.o -o main

%.o: %.c 
	gcc $(cflags) $<
clean:
	rm myprint.o demo.o main
.PHONY: clean

上面的makefile當中有一個萬用字元%,其中%.c表示當前目錄下面的所有的以.c結尾的檔案。上面的makefile與下面的makefile的效果是一樣的:

cflags=-c

main: demo.o myprint.o
	gcc demo.o myprint.o -o main

demo.o:demo.c
	gcc $(cflags) demo.c 

myprint.o:myprint.c
	gcc $(cflags) myprint.c 

clean:
	rm myprint.o demo.o main
.PHONY: clean

在上面的makefile當中 $< 表示第一個依賴檔案,上面的%就是一個萬用字元,%.c可以匹配任何以.c結尾的檔案,你可能會有疑問%.c匹配不是所有的.c檔案嗎?那麼等價的結果不應該是:

cflags=-c

main: demo.o myprint.o
	gcc demo.o myprint.o -o main

demo.o myprint.o:demo.c myprint.c
	gcc $(cflags) demo.c 

clean:
	rm myprint.o demo.o main
.PHONY: clean

事實上你可以認為make會將萬用字元匹配的檔案一一展開,有幾個檔案就將產生對應數目的編譯目標,而不是將他們都放在一起。

Makefile檔案自動搜尋

在一個工程專案當中我們可以會有許多的目錄以及原始檔,而make命令只會自動搜尋當前目錄下的檔案,如果當前目錄下沒有那麼make命令就會產生錯誤。因此make也給我們提供了一種檔案搜尋的功能。

在makefile當中,我們可以使用VAPTH指定make去搜尋檔案的路徑。我們先來測試一下當我們沒有使用VPATH的時候指定其他目錄下的檔案會出現什麼情況,我們的檔案目錄結構如下圖所示:

我們的makefile內容如下(先把VPATH的那一行註釋掉):

cflags=-c

# VPATH=./files

main: demo.o myprint.o a.o b.o
	gcc demo.o myprint.o a.o b.o -o main

demo.o:demo.c
	gcc $(cflags) demo.c 

myprint.o:myprint.c
	gcc $(cflags) myprint.c 
a.o: a.c
	gcc $(cflags) a.c
b.o: b.c 
	gcc $(cflags) b.c

clean:
	rm myprint.o demo.o main
.PHONY: clean

執行結果如下圖所示:

我們現在修改我們的makefile檔案如下:

cflags=-c

VPATH=./files

main: demo.o myprint.o a.o b.o
	gcc demo.o myprint.o a.o b.o -o main

demo.o:demo.c
	gcc $(cflags) demo.c 

myprint.o:myprint.c
	gcc $(cflags) myprint.c 

a.o: a.c
	gcc $(cflags) $<
b.o: b.c 
	gcc $(cflags) $<

clean:
	rm myprint.o demo.o main
.PHONY: clean

再次執行make命令,執行結果如下圖所示:

我們可以看到仍然出錯了,這是因為雖然make可以找到a.c和b.c的位置,但是在執行編譯命令的時候我們執行的依然是gcc -c a.c,這條命令是會在當前目錄下去尋找a.c的,而不會在其他路徑下去尋找a.c,在這裡我們可以使用$<去解決這個問題,$<表示第一個依賴的檔案。

修改之後的makefile檔案如下所示:

cflags=-c

VPATH=./files

main: demo.o myprint.o a.o b.o
	gcc demo.o myprint.o a.o b.o -o main

demo.o:demo.c
	gcc $(cflags) demo.c 

myprint.o:myprint.c
	gcc $(cflags) myprint.c 

a.o: a.c
	gcc $(cflags) $<
b.o: b.c 
	gcc $(cflags) $<

clean:
	rm myprint.o demo.o main a.o b.o
.PHONY: clean

執行結果如下圖所示:

如果你想設定多條目錄的話,設定的規則為VPATH=dir1:dir2:dir3:dir4

總結

在本篇文章當中主要介紹了一些makefile的基礎用法,和一些常用的小技巧,雖然不是很難,但是掌握了可以很大的跳高我們的工作效率,也可以方便我們閱讀別人寫的makefile檔案。


以上就是本篇文章的所有內容了,我是LeHung,我們下期再見!!!更多精彩內容合集可存取專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演演算法與資料結構)知識。