GNU Make
參考:Make 命令教程 | 阮一峰的網路日誌
Makefile 檔案的格式
Makefile 檔案由一系列 規則
(rules)構成。每條 規則
的形式如下。
<target>: [prerequisites]
[commands]
上面第一行冒號前面的部分,叫做 目標
(target),冒號後面的部分叫做 前置條件
(prerequisites);第二行必須由一個 tab 鍵起首,後面跟著 命令
(commands)。
目標
是必需的,不可省略;前置條件
和 命令
都是可選的,但是兩者之中必須至少存在一個。
每條規則就明確兩件事:構建目標的前置條件是什麼,以及如何構建。下面就詳細講解,每條規則的這三個組成部分。
目標(target)
一個 目標
(target)就構成一條規則。目標通常是檔名,指明 make
命令所要構建的物件。目標可以是一個檔名,也可以是多個檔名,之間用空格分隔。
c : a b # 構建 c 需要 a 和 b 兩個檔案
cat a b > c # 用 cat 命令將 a 和 b 合併為 c
除了檔名,目標還可以是某個操作的名字,這稱為 偽目標
(phony target)。
clean:
rm *.o
上面程式碼的目標是 clean
,它不是檔名,而是一個操作的名字,屬於 偽目標
,作用是刪除物件檔案。
make clean
但是,如果當前目錄中,正好有一個檔案叫做 clean
,那麼這個命令不會執行。因為 make
發現 clean
檔案已經存在,就認為沒有必要重新構建了,就不會執行指定的 rm
命令。
為了避免這種情況,可以明確宣告 clean
是 偽目標
(phony target),寫法如下。
clean:
rm *.o temp
.PHONY: clean
宣告 clean
是 偽目標
之後,make
就不會去檢查是否存在一個叫做 clean
的檔案,而是每次執行都執行對應的命令。像 .PHONY
這樣的內建目標名還有不少,可以檢視GNU Make 手冊。
如果 make
命令執行時沒有指定目標,預設會執行 Makefile 檔案的第一個目標。
make # 執行 Makefile 檔案的第一個目標
前置條件(prerequisites)
前置條件
通常是一組檔名,之間用空格分隔。它指定了 目標
是否重新構建的判斷標準:只要有一個前置檔案不存在,或者有過更新(前置檔案的 last-modification
時間戳比目標的時間戳新),目標
就需要重新構建。
result: source
cp source result
上面程式碼中,構建 result
的前置條件是 source
。如果當前目錄中,source
已經存在,那麼 make result
可以正常執行,否則必須再寫一條規則,來生成 source
。
source:
echo "this is the source" > source
上面程式碼中,source
後面沒有前置條件,就意味著它跟其他檔案都無關,只要這個檔案還不存在,每次呼叫 make source
,它都會生成。
make result
make result
上面連續執行兩次 make result
命令。第一次執行會先新建 source
,然後再新建 result
。第二次執行,make
發現 source
沒有變動(時間戳早於 result
),就不會執行任何操作,result
也不會重新生成。
如果需要生成多個檔案,往往採用下面的寫法。
source: file1 file2 file3
上面程式碼中,source
是一個 偽目標
,只有三個前置檔案,沒有任何對應的命令。
make source
執行 make source
命令後,就會一次性生成 file1
,file2
,file3
三個檔案。這比下面的寫法要方便很多。
make file1
make file2
make file3
命令(commands)
命令
(commands)表示如何更新目標檔案,由一行或多行的 shell 命令組成。它是構建 目標
的具體指令,它的執行結果通常就是生成目標檔案。
每行命令之前必須有一個 tab 鍵。
需要注意的是,每行命令在一個單獨的 shell 中執行。這些 shell 之間沒有繼承關係。
var-lost:
export foo=bar
echo "foo=[$$foo]"
make var-lost
上面程式碼執行後,取不到 foo
的值。因為兩行命令在兩個不同的程序執行。一個解決辦法是將兩行命令寫在一行,中間用分號分隔。
var-kept:
export foo=bar; echo "foo=[$$foo]"
另一個解決辦法是在換行符前加反斜槓轉義。
var-kept:
export foo=bar; \
echo "foo=[$$foo]"
最後一個方法是加上 .ONESHELL:
命令。
.ONESHELL:
var-kept:
export foo=bar;
echo "foo=[$$foo]"
呼叫 shell 變數需要兩個美元符號
讀完上面你就已經掌握了 Makefile 的基本內容。
Makefile 檔案的語法
註釋
井號 #
在 Makefile 中表示註釋。
# 這是註釋
result.txt: source.txt
# 這是註釋
cp source.txt result.txt # 這也是註釋
回聲(echoing)
正常情況下,make
會列印每條命令,然後再執行,這就叫做回聲(echoing)。
test:
# 這是測試
執行上面的規則,會得到下面的結果。
$ make test
# 這是測試
在命令的前面加上 @
,就可以關閉回聲。
test:
@# 這是測試
現在再執行 make test
,就不會有任何輸出。
由於在構建過程中,需要了解當前在執行哪條命令,所以通常只在註釋和純顯示的 echo 命令前面加上 @
。
test:
@# 這是測試
@echo TODO
萬用字元
萬用字元
(wildcard)用來指定一組符合條件的檔名。Makefile 的萬用字元與 bash 一致,主要有星號 *
、問號 ?
和 [ ]
。比如,*.o
表示所有字尾名為 .o
的檔案。
clean:
rm -f *.o
模式匹配
make
命令允許對檔名進行類似正則運算的匹配,主要用到的匹配符是 %
。比如,假定當前目錄下有 f1.c
和 f2.c
兩個原始碼檔案,需要將它們編譯為對應的物件檔案。
%.o: %.c
等同於下面的寫法。
f1.o: f1.c
f2.o: f2.c
使用匹配符 %
,可以將大量同型別的檔案,只用一條規則就完成構建。
字尾規則
字尾規則在 Makefile 中用於定義如何從一種檔案型別轉換成另一種檔案型別。
下面是幾個關鍵點來幫助理解 Makefile 字尾規則(又稱為隱晦規則)的功能:
-
定義:字尾規則透過一個點(
.
)後接一系列字元的形式來標識檔案型別。這種規則說明了如何將一種字尾名的檔案轉換為另一種字尾名的檔案。例如,從.c
檔案編譯生成.o
(物件檔案)的規則。 -
格式:一個典型的字尾規則看起來像這樣:
.suffix1.suffix2: command
其中
suffix1
表明原始檔型別,suffix2
表明目標檔案型別,而command
是執行的命令列,用以從suffix1
型別檔案生成suffix2
型別檔案。 -
例子:如果有
.c
到.o
的轉換規則,它可能看起來像這樣:.c.o: $(CC) -c $(CFLAGS) $< -o $@
在這個例子中,
.c.o:
定義了從.c
原始碼檔案編譯生成.o
物件檔案的規則。$(CC)
是編譯器(通常是gcc
或clang
),$(CFLAGS)
包含編譯器標誌,$<
是規則的依賴項(這裡是.c
檔案),$@
是規則的目標(這裡是.o
檔案)。 -
優點:使用字尾規則可以簡潔地定義檔案轉換方法,減少 Makefile 的複雜度。這種規則特別適用於簡單專案或者那些檔案轉換步驟非常標準化的場景。
-
侷限性:雖然字尾規則在某些情況下很有用,但它們不具備模式規則的靈活性和強大功能。隨著專案複雜度增加,使用模式規則(使用
%
萬用字元)可能更合適,因為它們允許更為複雜和靈活的檔名處理。 -
過時警告:值得注意的是,在新版的
make
工具中,字尾規則有時被視為過時的,推薦使用更靈活強大的模式規則。儘管如此,在理解遺留專案或維護較老的構建系統時,瞭解字尾規則仍然很重要。
變數和賦值符
Makefile 允許使用等號自定義變數。
txt = Hello World
test:
@echo $(txt)
上面程式碼中,變數 txt
等於 Hello World。呼叫時,變數需要放在 $( )
之中。
呼叫 shell 變數,需要在美元符號前,再加一個美元符號($$),這是因為 make
命令會對美元符號轉義。
test:
@echo $$HOME
有時,變數的值可能指向另一個變數。
v1 = $(v2)
上面程式碼中,變數 v1
的值是另一個變數 v2
。這時會產生一個問題,v1
的值到底在定義時擴充套件(靜態擴充套件),還是在執行時擴充套件(動態擴充套件)?如果 v2
的值是動態的,這兩種擴充套件方式的結果可能會差異很大。
為了解決類似問題,Makefile 一共提供了四個賦值運算子(=
、:=
、?=
、+=
):
# 在執行時擴充套件,允許遞迴擴充套件
VARIABLE = value
# 在定義時擴充套件
VARIABLE := value
# 只有在該變數為空時才設定值
VARIABLE ?= value
# 將值追加到變數的尾端
VARIABLE += value
更加具體的區別參閱 What is the difference between the GNU Makefile variable assignments =, ?=, := and +=? | StackOverflow
內建變數(Implicit Variables)
make
命令提供一系列內建變數,比如,$(CC)
指向當前使用的編譯器(如 GCC),$(MAKE)
指向當前使用的 make 工具。這主要是為了跨平臺的相容性,詳細的內建變數清單見 GNU Make 手冊。
output:
$(CC) -o output input.c
自動變數(Automatic Variables)
make
命令還提供一些自動變數,它們的值與當前規則有關。主要有以下幾個。
$@
$@
指代當前目標,就是 make
命令當前構建的那個目標。比如,make foo
的 $@
就指代 foo
。
a b:
touch $@
等同於下面的寫法。
a:
touch a
b:
touch b
$<
$<
指代第一個前置條件。比如,規則為 t: p1 p2
,那麼 $<
就指代 p1
。
a: b c
cp $< $@
等同於下面的寫法。
a: b c
cp b a
$?
$?
指代比目標更新的所有前置條件,之間以空格分隔。比如,規則為 t: p1 p2
,其中 p2
的時間戳比 t
新,$?
就指代 p2
。
$^
$^
指代所有前置條件,之間以空格分隔。比如,規則為 t: p1 p2
,那麼 $^
就指代 p1 p2
。
$*
$*
指代匹配符 %
匹配的部分, 比如 %
匹配 f1.txt
中的 f1
,$*
就表示 f1
。
$(@D)
和$(@F)
$(@D)
和 $(@F)
分別指向 $@
的目錄名和檔名。比如,$@
是 src/input.c
,那麼 $(@D)
的值為 src
,$(@F)
的值為 input.c
。
$(<D)
和$(<F)
$(<D)
和 $(<F)
分別指向 $<
的目錄名和檔名。
所有的自動變數清單,請看 GNU Make 手冊。下面是自動變數的一個例子。
dest/%.txt: src/%.txt
@[ -d dest ] || mkdir dest
cp $< $@
上面程式碼將 src
目錄下的 txt
檔案,複製到 dest
目錄下。
程式碼首先判斷 dest
目錄是否存在,如果不存在就新建,然後,$<
指代前置檔案(src/%.txt
), $@
指代目標檔案(dest/%.txt
)。
下面的內容不太常用
判斷和迴圈
Makefile 使用 bash 語法,完成判斷和迴圈。
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
上面程式碼判斷當前編譯器是否為 gcc ,然後指定不同的庫檔案。
LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done
# 等同於
all:
for i in one two three; do \
echo $i; \
done
上面程式碼的執行結果:
one
two
three
函式
Makefile 還可以使用函式,格式如下。
$(function arguments)
# 或者
${function arguments}
Makefile 提供了許多內建函式可供呼叫,下面是幾個常用的內建函式。
- shell 函式
shell
函式用來執行 shell 命令
srcfiles := $(shell echo src/{00..99}.txt)
- wildcard 函式
wildcard
函式用來在 Makefile 中替換 bash 的萬用字元。
srcfiles := $(wildcard src/*.txt)
- subst 函式
subst
函式用來文字替換,格式如下。
$(subst from,to,text)
下面的例子將字串 feet on the street
替換成 fEEt on the strEEt
。
$(subst ee,EE,feet on the street)
下面是一個稍微複雜的例子。
comma := ,
empty :=
# space 變數用兩個空變數作為識別符號,當中是一個空格
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))
# bar is now 'a,b,c'.
- patsubst 函式
patsubst
函式用於模式匹配的替換,格式如下。
$(patsubst pattern,replacement,text)
下面的例子將檔名 x.c.c bar.c
,替換成 x.c.o bar.o
。
$(patsubst %.c,%.o,x.c.c bar.c)
- 替換字尾名
替換字尾名函式的寫法是:變數名:字尾名替換規則
。它實際上是 patsubst
函式的一種簡寫形式。
min: $(OUTPUT:.js=.min.js)
上面程式碼的意思是,將變數 OUTPUT
中的字尾名 .js
全部替換成 .min.js
。
Makefile 例項
- 執行多個目標
.PHONY: cleanall cleanobj cleandiff
cleanall: cleanobj cleandiff
rm program
cleanobj:
rm *.o
cleandiff:
rm *.diff
上面程式碼可以呼叫不同目標,刪除不同字尾名的檔案,也可以呼叫一個目標(cleanall
),刪除所有指定型別的檔案。
- 編譯 C 語言專案
edit: main.o kbd.o command.o display.o
cc -o edit main.o kbd.o command.o display.o
main.o: main.c defs.h
cc -c main.c
kbd.o: kbd.c defs.h command.h
cc -c kbd.c
command.o: command.c defs.h command.h
cc -c command.c
display.o: display.c defs.h
cc -c display.c
clean:
rm edit main.o kbd.o command.o display.o
.PHONY: edit clean
如何用 Make 來構建 Node.js 專案 | 阮一峰的網路日誌