您當前的位置:首頁 > 歷史

[教程]從零開始快速編寫Makefile

作者:由 亂步的少年 發表于 歷史時間:2021-11-06

本文將講述如何從零開始快速為一個小型專案編寫Makefile,適合剛開始接觸make命令、想弄懂Makefile機制的新手。對於已經弄清楚透過Makefile編譯專案機制的小夥伴,文末附上針對C語言專案的Makefile模板,透過套用模板,可以快速為小型專案編寫一套編譯指令碼。

依賴關係

透過make命令進行程式碼工程編譯,其實際過程就是按照開發者在Makefile檔案中所描述的模組與模組、模組與原始碼檔案之間的依賴關係,將原始碼檔案編譯成obj檔案,再將obj檔案連結成庫檔案或可執行程式檔案的過程。

如果程式只有一個原始碼檔案main。c,那麼下面一條命令就可以完成程式test的編譯,其依賴關係非常簡單,就是可執行程式test依賴於原始檔main。c。

gcc main。c -o test

但軟體專案中,模組之間的依賴關係一般比較複雜,想要透過一條命令gcc命令完成專案的編譯非常困難,並且不利於擴充套件和維護。

下面給出一個簡單工程示例,如何用Makefile規則為其快速編寫一套編譯指令碼。

[教程]從零開始快速編寫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

將所有原始檔和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中的函式,使用“$( )”的形式可以呼叫函式。(這時不打算列舉Makefile中支援的函式,因為實在太多了,對於需要用到的函式,可以到網上去查詢)。Makefile與shell指令碼類似,支援萬用字元

萬用字元

作用

*

匹配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 ”引數,可以在編譯過程中生成一個文字檔案,說明在編譯這個原始碼檔案時,實際include了哪些檔案。那麼這裡在Makefile中sinclude這個檔案,增加。o檔案對這些標頭檔案的依賴關係。而當第一次編譯時,雖然檔案不存在,但是所有中間檔案都重新生成了,即被更新了,也不會存在報錯。後續修改標頭檔案時,make命令也會檢查依賴的標頭檔案時間戳是否比目標檔案新,從而更新與之相關的所有目標檔案。

make命令引數

當直接執行make命令,後面不接target引數時,預設會生成Makefile中的第一個目標。如果要生成指定目標,需要在make命令後面接target名稱。

“make VAR_A=

_

a> -j”表示同時產生個程序編譯,同時設定Makefile中變數VAR_A的值為var_a。如果-j後面不接數字引數,將會為每個目標檔案產生一個程序進行編譯,如果工程是原始檔過多,可能導致程序數量過多而使計算機沒有響應,所以直接使用-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

}

標簽: var  檔案  makefile  make  main