[教程]從零開始快速編寫Makefile
本文將講述如何從零開始快速為一個小型專案編寫Makefile,適合剛開始接觸make命令、想弄懂Makefile機制的新手。對於已經弄清楚透過Makefile編譯專案機制的小夥伴,文末附上針對C語言專案的Makefile模板,透過套用模板,可以快速為小型專案編寫一套編譯指令碼。
依賴關係
透過make命令進行程式碼工程編譯,其實際過程就是按照開發者在Makefile檔案中所描述的模組與模組、模組與原始碼檔案之間的依賴關係,將原始碼檔案編譯成obj檔案,再將obj檔案連結成庫檔案或可執行程式檔案的過程。
如果程式只有一個原始碼檔案main。c,那麼下面一條命令就可以完成程式test的編譯,其依賴關係非常簡單,就是可執行程式test依賴於原始檔main。c。
gcc main。c -o test
但軟體專案中,模組之間的依賴關係一般比較複雜,想要透過一條命令gcc命令完成專案的編譯非常困難,並且不利於擴充套件和維護。
下面給出一個簡單工程示例,如何用Makefile規則為其快速編寫一套編譯指令碼。
簡單的工程中目標檔案與原始檔依賴關係
# test依賴於libtest。a main。o
test: libtest。a main。o
gcc main。o -ltest -L。 -o test
# main。o依賴於main。c
main。o:main。c
gcc main。c -c -o main。o
# libtest。a依賴於test1。o test2。o
libtest。a: test1。o test2。o
ar -r libtest。a test1。o test2。o
# test1。o依賴於test1。c
test1。o:test1。c
gcc test1。c -c -o test1。o
# test2。o依賴於test2。c
test2。o:test2。c
gcc test2。c -c -o test2。o
將所有原始檔和Makefile放在一個目錄下,執行make命令,可以看到直接生成了可執行程式test。這裡沒有使用Makefile裡面任何技巧,僅僅使用Makefile的規則描述了各個檔案之間的依賴關係。
target。。。 : prerequisites 。。。
command
……
target
可以是一個object file(目標檔案),也可以是一個執行檔案,還可以是一個標籤(label)。對於標籤這種特性,後面會介紹。
prerequisites
生成該target所依賴的檔案,可以有多個依賴
command
該target要執行的命令(任意的shell命令)
當要生成目標test時,make工具會從test開始依次尋找依賴關係,由原始檔逐步生成目標test。test依賴於libtest。a和main。o,或者說libtest。a和main。o是生成test的前置條件( prerequisites),如果libtest。a和main。o存在,那麼將直接使用命令“gcc main。o -ltest -L。 -o test”生成test;如果libtest。a和main。o不存在,再向下遍歷,尋找libtest。a和main。o的前置條件,去生成libtest。a和main。o,就這樣一直尋找到可以滿足的前置條件,逐步生成目標test。如果找到最底層,依然無法滿足生成條件,那麼就會報錯“make:*** 沒有明確目標並且找不到 makefile。停止”。
在檢查依賴關係時,同時會檢查目標與原始檔的時間戳,當原始檔時間戳更新時,make會更新依賴它的鏈路上所有目錄。例如,當test1。c更新時,再次執行make命令,依次會重新生成test1。o、libtest。a、test。
變數與函式
上面的Makefile中,所有的依賴關係中直接使用檔名稱,當新增加檔案或者模組時,需要手動去新增新的依賴關係,那麼這就比較麻煩,不利於擴充套件,因此可以使用變數,自動完成新依賴關係新增。
觀察上面的依賴關係,可以發現所有的。o中間檔案都依賴於一個。c原始碼檔案。那麼這裡就可以用變量表達。o中間檔案和。c原始碼檔案,並指定它們之間的依賴關係。Makefile中的變數型別基本上就可以直接理解為字串型別。
SRC
=
test1。c test2。c main。c
OBJ
=
test1。o test2。o main。o
${OBJ}
:
${
SRC
}
gcc -c
${
SRC
}
與shell指令碼中的變數類似,定義變數時,直接使用等號賦值(Makefile中的等號有多種,後面會解釋),使用變數時用“${VAR}”表示即可。上面SRC和OBJ變數分別表示。c與。o檔案,但是展開寫太麻煩了,這裡可以用更為方便的辦法。
SRC = $(wildcard *。c)
OBJ = $(patsubst %。c,%。o,${SRC})
${OBJ}:${SRC}
gcc -c ${SRC}
這裡使用了Makefile中的函式,使用“$(
萬用字元
作用
*
匹配0個或者是任意個字元
?
匹配任意一個字元
[]
指定匹配的字元放在 “[]” 中
“$(wildcard *。c)”表示將萬用字元*。c展開,即“test1。c test2。c main。c”。patsubst是模式替換函式,“$(patsubst %。c,%。o,${SRC})”表示將變數SRC中符合“%。c”形式的字串,修改為“%。o”形式,例如字串“main。c”就符合“%。c”形式,%匹配“main”,“main。c”替換之後就變成了“main。o”。因此OBJ變成了“test1。o test2。o main。o”。
上面“${OBJ}:${SRC}”的依賴關係描述其實不是很合適,因為它表示“test1。o test2。o main。o”依賴於“test1。c test2。c main。c”,沒有指明哪個。o檔案依賴於哪個。c檔案,test1。o檔案只依賴於test1。c檔案,而不是依賴於三個。c檔案。那麼這裡可以用模式匹配的形式來描述依賴關係。
SRC
=
$(
wildcard *。c
)
OBJ
=
$(
patsubst %。c,%。o,
${
SRC
}
)
${OBJ}
:
%。
o
:%。
c
gcc -c $< -o
$@
“${OBJ}:%。o:%。c”表示OBJ變數中符合“%。o”模式的檔案都依賴於“%。c”檔案,例如OBJ中的test1。o依賴於test1。c,%在這時就匹配“test1”。另外,這裡的命令使用了自動化變數。
自動化變數
作用
$@
規則的目標檔名(依賴關係中冒號:左邊的檔案,如果a: b c,那麼$@指a)
$%
當目標檔案是一個靜態庫檔案時,代表靜態庫的一個成員名。
$<
被依賴檔案的第一項(如果a: b c,那麼$<指b)
$?
所有比目標檔案更新的依賴檔案列表,空格分隔
$^
所有依賴檔案列表,使用空格分隔(如果a: b c c,那麼$^指b c),不包含重複檔案
$+
所有依賴檔案列表,使用空格分隔(如果a: b c c,那麼$+指b c c),包含重複檔案
$*
在模式規則和靜態模式規則中的“%”所匹配的內容
偽目標
。PHONY : clean
clean:
rm *。o *。a test
這裡定義了一個clean目標,它沒有依賴。當執行make clean命令時,會刪除所有的。o、。a檔案和test檔案,這樣就可以利用Makefile來清除生成的檔案。這裡make clean並不是為了生成一個名稱為clean的檔案,為了防止檔案同名,可以用。PHONY來宣告偽目標。
也可以利用偽目標來一次生成多個目標檔案。
。PHONY : all clean
all: ${Target1} ${Target2} ${Target3} 。。。
${Target1}: 。。。。
……
${Target2}: 。。。。
……
${Target3}: 。。。。
……
……
clean:
rm ${Target1} ${Target2} ${Target3} 。。。
如果按照上面的形式定義目標,執行make all時,會一次生成多個目標檔案。
四種等號
Makefile中的等號有4種,“=”,“:=”,“?=”,“+=”。先解釋兩種比較容易理解的“?=”與“+=”。
“?=”表示,如果左邊的變數沒有被賦值,那麼將等號右邊的值賦給左邊的變數。下面的例子中,如果VAR_A被賦過值了,VAR_A中的值將保持原來的值不變,否則其值變為123。
VAR_A ?= 123
“+=”表示將等號右邊的值追加到左邊變數中,類似於C語言中的strcat函式。下面的例子中,VAR_B的最終值為123 456(中間有空格)。
VAR_B = 123
VAR_B += 456
“=”與“:=”是比較不好區分的兩個等號,可以將“=”理解為“址傳遞”或引用,“:=”理解為“值傳遞”。使用“=”賦值時,會將整個Makefile展開後再解釋被賦值的變數內容,“VAR_A = ${VAR_B}”,當後面VAR_B的值發生改變時VAR_A的值會跟著進行變化;使用“:=”賦值時,被賦值的變數的值為此時等號右側語句表示的值,“VAR_A := ${VAR_B}”,如果此時VAR_B的值是123,那麼VAR_A的值也為123,後面VAR_B的值被修改了,VAR_A的值依舊為123。
var_a
=
1
2
3
var_b
=
$(
var_a
)
var_a
+=
4
var_a
:
echo
${
var_a
}
echo
${
var_b
}
————————————————————————————————————————
# 執行make var_a的結果:
echo
1
2
3
4
1
2
3
4
echo
1
2
3
4
1
2
3
4
var_a
=
1
2
3
var_b
:=
$(
var_a
)
var_a
+=
4
var_a
:
echo
${
var_a
}
echo
${
var_b
}
————————————————————————————————————————-
# 執行make var_a的結果:
echo
1
2
3
4
1
2
3
4
echo
1
2
3
1
2
3
在Makefile中是不允許將變數自己的值賦給自己的,Makefile不允許出現迴圈引用。
var_a := ${var_a} 1 2 3 # 允許
var_a = ${var_a} 1 2 3 # 不允許
環境變數
Makefile的執行是受shell環境變數影響的,shell環境變數會直接傳遞到Makefile的執行過程中。
例如,針對語句“VAR_A ?= yes”,如果在shell中設定過環境變數“export VAR_A=no”,那麼在執行make命令時VAR_A的值會是no,而不是yes。另外可以在執行make命令時為傳遞變數的值,如果執行“make VAR_A=maybe”命令,那麼執行過程中VAR_A是maybe。
利用這個特性,可以在Shell中設定環境變數來影響Makefile的執行過程。同樣可以在Makefile中透過修改PATH等變數的值,來解決找不命令的問題。
變數的巢狀使用
Makefile允許變數的巢狀使用,下面的例子中${var_a}會解釋為b,var_${var_a}變成var_b,${var_${var_a}}的值就變成了123。
var_a = b
var_b = 123
var_c = ${var_${var_a}}
var_c:
echo ${var_c}
# 執行make var_c結果
echo 123
123
條件判斷
下面給出了一個條件判斷的示例,當DEBUG_BUILD的值是yes時,CFLAGS中將包含“-ggdb -ggdb3 -gdwarf-2 -D_DEBUG_=1 -g”,否則將包含“-O3 -DNDEBUG”,透過這段語句,可以在環境變數中設定DEBUG_BUILD,是否生成除錯版本的程式。
ifeq (${DEBUG_BUILD},“yes”)
CFLAGS += -ggdb -ggdb3 -gdwarf-2 -D_DEBUG_=1 -g
else
CFLAGS += -O3 -DNDEBUG
endif
關於@與-
在執行make命令時,會列印Makefile裡面執行的command,有時候Command過長,不容易檢視編譯過程中出現的錯誤與警告,可以透過在command前加上@來取消列印command。
var_a = 123
var_a:
echo ${var_a}
——————————————-
# 執行make var_a輸出
echo 123
123
var_a = 123
var_a:
@echo ${var_a}
————————————————
# 執行make var_a輸出
123
生成target的過程中,可能需要執行多條命令,執行過程中也可能出現錯誤, 一般出現錯誤後,make命令會立即退出,停止編譯。如果想要忽略執行過程中的錯誤,可以在command前加上-來忽略這條命令的執行錯誤。
var_a = 123
var_a:
@ls dir1
@echo ${var_a}
————————————————————————
# 執行make var_a輸出
ls: 無法訪問 ‘dir1’: 沒有那個檔案或目錄
make: [Makefile:19:var_a] 錯誤 2
var_a = 123
var_a:
-@ls dir1
@echo ${var_a}
————————————————————————
# 執行make var_a輸出
ls: 無法訪問 ‘dir1’: 沒有那個檔案或目錄
make: [Makefile:19:var_a] 錯誤 2 (已忽略)
123
關於依賴中的標頭檔案
Makefile與C/C++一樣,支援include另外一檔案,這個機制允許Makefile可以根據不同的環境或者平臺設定不同編譯過程。當此檔案找不到時,Makefile會報錯。
但是這裡更想提及的是另外一條語法sinclude的妙用,sinclude在找不到檔案時,並不會報錯,會直接跳過。利用這個機制,可以更新目標檔案的依賴關係。
在上面舉過的例子中,所有。o檔案僅依賴於一個。c檔案,而這個。c檔案其實是包含了不少標頭檔案的,所以更加正確的依賴關係應該是下面這樣的。
main。o: main。c include_file1。h include_file2。h include_file3。h ……
gcc @< -o $@
或者
main。o: include_file1。h include_file2。h include_file3。h ……
main。o: main。c
gcc @< -o $@
只有這樣,當頭檔案被修改時,make命令才會重新編譯目標檔案,否則make命令無法知道原始碼其實被更新了。但是一個原始碼檔案一般會include多個頭檔案,而標頭檔案往往又會include其它的標頭檔案,如果要手寫整個依賴關係,其過程會十分繁瑣。
這裡會用到編譯器的一個功能,透過在編譯引數中增加“-MM -MF
make命令引數
當直接執行make命令,後面不接target引數時,預設會生成Makefile中的第一個目標。如果要生成指定目標,需要在make命令後面接target名稱。
“make _ a> -j “make -C /build/path -f make1。mak”表示在開始編譯前,先將當前目錄切換到/build/path路徑下,再執行編譯,相當於“cd /build/path && make -f make1。mak”。-f引數用於指定要使用的Makefile檔案,如果不使用-f引數,則預設使用當前目錄下的名稱為“Makefile”的檔案。注意,這裡的make1。mak檔案是存放在/build/path目錄下的。 簡單Makefile模板 這裡給出了一個較為簡單的Makefile模板,其最終生成目標為可執行程式,如果最終生成目標為庫檔案,需要進行簡單調整。如果你能看懂,那麼上面所講到的要點基本上都理解了,可以直接利用這個模板為小型工程快速搭建一套編譯指令碼了。利用這套模板生成目標檔案,所有中間檔案都存放在單獨的tmp目錄下,防止與原始碼檔案混在一起,並且可以透過。dep檔案自動更新依賴關係。 # 編譯工具鏈設定 PATH := ${ PATH } :/your/tool_chain/path TOOL_CHAIN = CC = ${ TOOL_CHAIN } gcc AR = ${ TOOL_CHAIN } ar DEBUG_BUILD ?= yes # SHOW_COMMAND=yes,顯示編譯命令 ifeq (${SHOW_COMMAND}, yes) QUIET := else QUIET := @ endif # 目錄設定 # 工程根路徑 PROJ_ROOT = $( abspath 。。/。。 ) # 中間檔案快取資料夾 TMP_PATH = $( abspath 。 ) /tmp # 當前路徑 PWD_PATH = $( abspath 。 ) # 原始檔。c SRC := ${ PROJ_ROOT } /module1/*。c SRC += ${ PROJ_ROOT } /module2/*。c # 展開*匹配,獲取所有原始檔完整路徑 SRC := $( wildcard ${ SRC } ) # 標頭檔案路徑設定 INCLUDE_PATH += /include/path1 INCLUDE_PATH += /include/path2 INCLUDE_PATH += ${ PROJ_ROOT } /include/path1 INCLUDE_PATH += ${ PROJ_ROOT } /include/path2 # 編譯宏設定 DEFINE_SETTINGS := LINUX DEFINE_SETTINGS += A72 = “A72” DEFINE_SETTINGS += TARGET_NUM_CORES = 1 DEFINE_SETTINGS += TARGET_ARCH = 64 DEFINE_SETTINGS += ARCH_64 DEFINE_SETTINGS += ARM # 庫路徑設定 # 靜態庫。a資料夾路徑 STATIC_LIB_PATH := ${ PROJ_ROOT } /moduleXXX1/lib STATIC_LIB_PATH += ${ PROJ_ROOT } /moduleXXX2/lib # 動態庫。so資料夾路徑 DYNAMIC_LIB_PATH := ${ PROJ_ROOT } /moduleXXX3/lib # 庫設定(靜態庫) STATIC_LIB += static_lib1 STATIC_LIB += static_lib2 STATIC_LIB += static_lib3 STATIC_LIB += static_lib4 # 庫設定(動態庫) DYNAMIC_LIB := stdc++ DYNAMIC_LIB += m DYNAMIC_LIB += rt DYNAMIC_LIB += pthread # 編譯選項 CFLAGS := -fPIC -Wall -fms-extensions -Wno-write-strings -Wno-format-security CFLAGS += -fno-short-enums -Werror CFLAGS += -mlittle-endian -Wno-format-truncation ifeq ( “${DEBUG_BUILD}” , “yes” ) CFLAGS += -ggdb -ggdb3 -gdwarf-2 -D_DEBUG_ = 1 -g else CFLAGS += -O3 -DNDEBUG endif # 生成的中間檔案。o OBJ := $( patsubst ${ PROJ_ROOT } /%。c, ${ TMP_PATH } /%。o, ${ SRC } ) # 標頭檔案存放路徑設定 INC := $( foreach path, ${ INCLUDE_PATH } ,-I ${ path } ) # 編譯宏設定 DEF := $( foreach macro, ${ DEFINE_SETTINGS } ,-D ${ macro } ) # 庫設定 LIB := -rdynamic -Wl,——cref LIB += $( foreach path, ${ DYNAMIC_LIB_PATH } , “-Wl,-rpath-link= ${ path } ” ) LIB += $( foreach path, ${ STATIC_LIB_PATH } ,-L ${ path } ) LIB += -Wl,-Bstatic -Wl,——start-group LIB += $( foreach lib, ${ STATIC_LIB } ,-l ${ lib } ) LIB += -Wl,——end-group LIB += -Wl,-Bdynamic LIB += $( foreach lib, ${ DYNAMIC_LIB } ,-l ${ lib } ) # 生成目標 TARGET := ${ PWD_PATH } /demo/demo # 生成目標中的詳細符號資訊檔案 DEP_FILE := -Wl,-Map = ${ TMP_PATH } / $( notdir ${ TARGET } ) 。dep 。PHONY : all clean all : ${ TARGET } ${TARGET} : ${ OBJ } @echo “[Linking $@ ]” ${ QUIET }${ CC } ${ OBJ } ${ CFLAGS } ${ LIB } -o $@ ${ DEP_FILE } >/dev/null ${TMP_PATH}/%。o : ${ PROJ_ROOT }/%。 c @echo “[Compiling $@ ]” @mkdir $( dir $@ ) -p ${ QUIET }${ CC } -c $< -o $@ ${ CFLAGS } ${ DEF } ${ INC } -MMD -MF $( patsubst %。o,%。dep, $@ ) -MT ‘$@’ clean : @echo “[cleaning ${ TARGET } ]” ${ QUIET } rm -rf ${ TARGET } ${ QUIET } rm -rf ${ TMP_PATH }