GNU Make (Makefile)是經常被用於程式原始碼專案中,幫助使用者編譯原始碼的建置工具。在Makefile中,可以分別替不同的原始碼檔案定義其編譯的方式(編譯所使用的指令),GNU Make在編譯原始碼專案時,就會依照原始碼是否有在產生目的檔案後又被修改(依賴文件的修改日期比目的檔案的修改日期來得晚),來自動判斷是否真的需要再重新編譯原始碼,以省下重複編譯的時間。



GNU Make通常會搭配C/C++語言的程式專案來使用,不過就算不是C/C++語言的程式專案也還是可以使用GNU Make。這篇文章將會介紹Makefile的基本用法。

安裝GNU Make

Linux發行版通常會內建GNU Make。如果沒有的話,Debian或是其衍生的Linux發行版可以用以下指令來安裝:

sudo apt install make

紅帽系的Linux發行版可以用以下指令來安裝:

sudo dnf install make

Makefile

Makefile是GNU Make工具的腳本檔案,裡面描述了程式原始碼專案中的原始碼檔案應該要如何被編譯。在終端機執行make指令時,GNU Make就會依照以下順序來在目前的工作目錄中尋找可用的Makefile檔案來執行:

  • GNUmakefile
  • makefile
  • Makefile

例如GNU Make找到了GNUmakefile檔案,那麼即便makefileMakefile檔案也有存在,GNU Make也不會再去管那兩個檔案究竟寫了什麼。若沒有特殊需求的話,會比較常用Makefile這個名稱。

標準的Makefile結構如下:

all:
	# Command 1
	# Command 2
	# ...

clean:
	# Command 1
	# Command 2
	# ...

install:
	# Command 1
	# Command 2
	# ...

# uninstall:
	# Command 1
	# Command 2
	# ...

一行中井字號#後的字會被視為註解。以上,allcleaninstalluninstall是具有特殊意義的目標(target),意義如下:

  • all:意思為「全部編譯」,通常會是Makefile的第一個規則所用的目標。
  • clean:用來清除編譯時產生的所有檔案。
  • install:用來安裝全部已經被編譯好的程式。
  • uninstall:這個目標相對少見一點,用來移除已經被安裝好的程式。

一個目標所形成的結構稱為一個規則(rule),規則的格式如下:

目標文件: [依賴文件]
	指令1
	指令2
	...

目標文件要填寫檔案名稱或路徑,也就是編譯程式之後會產生的檔案,如果有多個的話,要以空白字元 隔開。依賴文件要填寫檔案名稱或路徑,也就是要編譯出目標文件所需要的原始碼檔案,可不填寫,如果有多個的話,要以空白字元 隔開。如果檔案名稱或路徑中有空白字元的話,要以反斜線\來跳脫。如果依賴文件所填寫的名稱或路徑正好是其它規則的目標文件的話,GNU make就會先去執行其它的規則。

同一規則中,指令和指令執行的順序是從上到下,如果前一個指令執行不成功(回傳的Exit Status不為0的話),就不會再繼續執行之後的指令。這邊還有個特別要注意的地方是,Makefile規則中的指令縮排是必要的,而且只能夠使用TAB字元 作為行首。

舉個例子,使用javac指令來編譯Hello.java這個檔案,編譯成功後會產生Hello.class。所以Makefile的規則應該要這樣寫:

Hello.class: Hello.java
	javac Hello.java

與剛才提到的「具有特殊意義的目標」搭配使用的話,完整的Makefile就變成:

all: Hello.class

Hello.class: Hello.java
	javac Hello.java

clean:
	rm -f *.class

install:
	# Command 1
	# Command 2
	# ...

# uninstall:
	# Command 1
	# Command 2
	# ...

也可以擴展一下,加入測試、安裝和解除安裝的腳本:

all: Hello.jar

Hello.jar: Hello.class manifest.mf
	jar cfm Hello.jar manifest.mf Hello.class

Hello.class: Hello.java
	javac Hello.java

manifest.mf:
	echo "Main-Class: Hello" > manifest.mf

clean:
	rm -f *.class *.jar manifest.mf

test:
	java -jar Hello.jar

install: test
	cp Hello.jar ~/Hello.jar

uninstall:
	rm -f ~/Hello.jar

make指令

指定目標

make指令可以用來執行Makefile,在什麼參數都不加的時候,make只會去執行Makefile中的第一個規則。如果要指定要執行哪個規則的話,可在make指令的第一個參數加上已經定義在Makefile中的目標文件名稱,例如make allmake install或是make Hello.class。這也是為什麼all這個目標通常會被放在第一個規則的原因,因為這樣執行make就等於是執行make all

下圖是用make指令執行上面Makefile範例的演示:

makefile

平行編譯

由於Makefile把原始碼的相依性和編譯方式都分別定義好了,因此還可以簡單實現出「平行編譯」,運用更多的硬體資源來加快編譯速度。若要啟用GNU make的平行編譯功能,可以在執行make指令時加上-j參數,-j後須接一個數值,表示要同時進行的最大工作數量。至於-j的值究竟要多大才會是最快的並沒有一定,通常會把這個值的範圍控制在處理器(邏輯處理核心)數量的一倍到三倍之間。可以搭配nproc指令取得的處理器(邏輯處理核心)數量來串接出make指令。

例如:

make -j$(nproc)

稍微進階一點的Makefile寫法

隱藏指令

預設寫在Makefile的指令在使用make指令來執行的時候都會被顯示出來,如果要隱藏的話,可以在該指令前加上小老鼠@

例如:

Hello.class: Hello.java
	@javac Hello.java

略過執行失敗的指令

先前有提到,在相同規則下,只要有某個指令執行失敗,其接下來的指令就不會被執行。如果這個指令本身是允許失敗的話,想要在其失敗後繼續執行之後的指令,可以在該指令前加上減號-

例如:

Hello.class: Hello.java
	-rm Hello.class
	javac Hello.java

相依文件太多,難以一一列舉

撰寫Makefile時常會遇到相依檔案太多,不太能夠一一列舉在規則內的情況,此時我們可以借助fileseq這兩個外部指令的力量,來作為依賴文件的值。

例如:

all: class manifest.mf $(shell find . -maxdepth 1 -type f -iname '*.class' | sed 's/ /\\ /g')
	jar cfm Hello.jar manifest.mf *.class

class: $(shell find . -maxdepth 1 -type f -iname '*.java' | sed 's/ /\\ /g')
	javac *.java

以上-maxdepth 1參數可以限制find指令不去搜尋子目錄。如果要同時判斷很多檔名,可以使用-o參數來連接多個-iname或是-name參數,前者不會判斷英文大小寫,後者才會判斷。

萬用字元%

在目標和依賴文件的路徑中,可以使用%來表示任意數量的任意字元。由於%也可以包含空格,如果有多個文件,且%位於最後一個依賴文件前的文件的路徑結尾,%後要加上一個冒號:來作分隔。

例如:

install: target/%: data/%
	...

萬用字元%

變數與環境變數

在Makefile中的變數宣告及指派方式有三種。

= (recursively expanded variable)

變數名稱 = 值

此為基本的變數指派方式,例如值為$(CC) -std=c90,其中的$(CC)會在該變數名稱被使用到時,才會被取代為當下CC變數所儲存的值。如果被用到很多次,就要重複進行解開變數的動作。

:= (simply expanded variable)

變數名稱 := 值

如果值裡面有用到其它變數的話,如果值裡面有用到其它變數的話,例如值為$(CC) -std=c90,其中的$(CC)會先被取代為實際CC變數所儲存的值後,再給指定的變數名稱來儲存。對比=這種指派方式,:=是比較直覺且安全的作法,效能也會好一點點。

?= (conditional variable)

變數名稱 ?= 值

如果該變數名稱尚未被定義,新值才會被指派。

在讀取Makefile所定義的變數或是外部的環境變數時,都可以使用以下的語法:

$(變數名稱)

Makefile中的目標文件依賴文件欄位,或是指令中都可以直接使用以上的語法來讀取變數。

但如果想要在指令中使用環境變數,卻不想要透過GNU make來事先讀取的話,可以使用以下的語法:

$${環境變數名稱}

其實兩個錢字號$$在Makefile中所代表的意義就是跳脫成一個錢字號$啦!

規則內的特殊變數

以下面這個規則來舉例:

a.o: library.cpp main.cpp
	...
  • $@:等同於a.o
  • $<:等同於library.cpp
  • $^:等同於library.cpp main.cpp

特殊意義的目標名稱與文件撞名的處理方式──.PHONY

由於Makefile的規則主要是以判斷文件的修改日期來決定是否要執行指令的關係,當我們將目標文件欄位直接填寫具有特殊意義的目標名稱(如cleaninstall)時,就並不是期望這個規則的指令在執行之後可以產生出與該目標名稱相同的檔案。

因此,原則上我們會需要確保與該目標名稱相同的檔案不存在,才能讓這些特殊的規則可以順利地被GNU make來執行。否則,如果我們在使用make clean時,卻因為有「clean」這個檔案的存在而導致make clean只會提示make: 'clean' is up to date.,而沒有任何的清除功能,那豈不是很可笑?

然而,若是撞名的情況真的無法避免,還是有個解決方案,那就是利用.PHONY這個目標文件,將要忽略檢查檔案是否修改的目標文件列舉出來。實際上,.PHONY本身並不是一個檔案,它也是一個特殊意義的目標名稱,其規則寫法如下:

.PHONY: 要忽略檢查的目標文件

例如:

.PHONY: clean install

有了以上設定,即便「.PHONY」、「clean」和「install」檔案存在於工作目錄中,make cleanmake install指令也還是可以正常使用。