======================= **GNU 下 MAKEFILE 基本規則** =======================
前言:
對於系統來講,make 其實也是一個腳本,有着自身的一些規則和要求。而這個腳本主要做的任務就是幫助程序員減少源文件到可執行文件中間的一系列的(預處理,編譯,匯編,鏈接)操作,提高效率。
環境(GNU Make 4.2.1 / gcc version 9.3.0 (Ubuntu 9.3.0-10ubuntu2)), 學習過程中涉及的文件github link;
學習主要參考鏈接: 跟我一起寫Makefile / MAKE 官方文檔
Makefile里主要包含了五個東西:顯式規則、隱晦規則、變量定義、文件指示和注釋。
- 顯式規則。顯式規則說明了如何生成一個或多個目標文件。這是由Makefile的書寫者明顯指出要生成的文件、文件的依賴文件和生成的命令。
- 隱晦規則。由於我們的make有自動推導的功能,所以隱晦的規則可以讓我們比較簡略地書寫 Makefile,這是由make所支持的。
- 變量的定義。在Makefile中我們要定義一系列的變量,變量一般都是字符串,這個有點像你C語言中的宏,當Makefile被執行時,其中的變量都會被擴展到相應的引用位置上。
- 文件指示。其包括了三個部分,一個是在一個Makefile中引用另一個Makefile,就像C語言中的include一樣;另一個是指根據某些情況指定Makefile中的有效部分,就像C語言中的預編譯#if一樣;還有就是定義一個多行的命令。有關這一部分的內容,我會在后續的部分中講述。
- 注釋。Makefile中只有行注釋,和UNIX的Shell腳本一樣,其注釋是用 # 字符,這個就像C/C++中的 // 一樣。如果你要在你的Makefile中使用 # 字符,可以用反斜杠進行轉義,如: \# 。
======================= **基礎知識** =======================
基本語法篇:
在Makefile中的命令,必須要以 Tab
鍵開始。
#####變量篇 : 變量類似 C的宏定義,在使用中直接展開
- 變量的命名字可以包含字符、數字,下划線(可以是數字開頭),但不應該含有
:
、#
、=
或是空字符(空格、回車等)。 - 變量是大小寫敏感的,“foo”、“Foo”和“FOO”是三個不同的變量名。
- 變量在聲明時需要給予初值;
傳統的Makefile的變量名是全大寫的命名方式, 另外其中有寫特殊字符變量$<, $^, 這些參考后面的cheat sheet;
使用變量時候:一般最好使用\${ARGs}/\$(ARGS) 這種括號表示對應的ARGS 變量,前面的變量可以調用后面定義的變量);
objects = program.o foo.o utils.o program : $(objects) cc -o program $(objects) #可以相互調用,然后展開 foo = $(bar) #調用后面定義的變量; bar = $(ugh) ugh = Huh all: echo $(foo) #最終的結果就是Hug
因為變量就是展開,當出現遞歸時候,就會進入死循環(雖然make 會報錯),但在實際使用中還是要盡量避免;
此時可以使用下面三種方式來避免一些可能bug的出現;
:=(如果調用未定義變量,為空) ;
?=(如果該變量未定義,則為后面定義的值,如果已經定義過,則不變);
+= 給變量追加值, 如果沒有定義過該變量,就相當於=
y := $(x) bar #這里展開結果是 y = bar x := foo #利用這個表達符,還可以有效定義空格: nullstring := space := $(nullstring) #使用#表示變量定義終止; FOO ?= bar #如果FOO沒有被定義過,那么變量FOO的值就是“bar”,如果FOO先前被定義過,那么這條語將什么也不做 objects = main.o foo.o bar.o utils.o objects += another.o # $(objects) 值變成 main.o foo.o bar.o utils.o another.o
高級用法:
- 替換字符
foo := a.o b.o c.o bar := $(foo:.o=.c) #替換foo 中所有.o 后綴為.c bar := $(foo:%.o=%.c) #替換foo 中所有.o 后綴為.c,靜態模式?
- 變量值在當成變量
first_second = Hello a = first b = second all = $($a_$b) #加強版: a_objects := a.o b.o c.o 1_objects := 1.o 2.o 3.o sources := $($(a1)_objects:.o=.c) #這樣a1 = a(l),就可以分別表示不同結果
- 目標變量 : 為某個目標設置局部變量,它可以 和“全局變量”同名,因為它的作用范圍只在這條規則以及連帶規則中,所以其值也只在作用范圍內有效。
#語法示例: <target ...> : <variable-assignment>; #具體例子: prog : CFLAGS = -g prog : prog.o foo.o bar.o $(CC) $(CFLAGS) prog.o foo.o bar.o prog.o : prog.c $(CC) $(CFLAGS) prog.c foo.o : foo.c $(CC) $(CFLAGS) foo.c bar.o : bar.c $(CC) $(CFLAGS) bar.c #不管全局的 $(CFLAGS) 的值是什么,這段依賴命令 $CFLAGS 都是 -g
- 模式變量 : 給定一種“模式”,可以把變量定義在符合這種模式的所有目標上;類似上面目標變量,這里是對應一種模式的變量,目標變量則是指定了目標;
%.o : CC = XXXXG #給所有以 .o 結尾的目標定義變量CC 為 XXXXG
#####通配符: 很多與正則(linux 中特殊變量) 類似,也有一些是特有的;
最常見的通配符: * , 代表一個或多個字符;另外在模式規則中 % 代表非空字符串匹配;
tips: 模式規則:對應的是生成規則,
rm *.o #普通通配符 #模式規則中 通配符; %.o : %.cpp ${CC} -c ${CFLAGS} %< -o $@
但是在變量定義中使用通配符有一個坑要注意:當定義變量為*.o 時候: 如果通配符表達式匹配不到任何合適的對象,通配符語句本身就會被賦值給變量;
所以下面看到的兩次執行時候,過程與結果是不一樣的;
EXAMPLE: makefile 中內容、執行結果 如下;
CC=g++ OBJ=*.o .PHONY:test test : ${OBJ} echo ${OBJ} ${CC} -c *.cpp .PHONY:clean clean: -rm *.o
echo "*.o" #第一次make 時候,echo 實際執行; echo *.o #第二次make 時候,echo 實際執行;
因為存在上面的問題,所以一般都是用通配符函數(wildcard)來解決上面的問題;
CC=g++ OBJ=${wildcard *.o} .PHONY:test1 test1 : ${OBJ} ${CC} -c *.cpp echo ${OBJ} .PHONY:clean clean: -rm *.o
#第一次make test1, ${wildcard *.o}, 沒有任何匹配中,所以為空;因為匹配在g++ -c *.cpp 之前,所以沒有匹配; #第二次make test2, ${wildcard *.o} 匹配中了,所以有對應值
上面舉例通配符中要要注意的地方,正常來寫可以用下面方法:
CC=g++ SRC=${wildcard *.cpp} OBJ=${SRC:%.cpp=%.o} ###模式規則中常用的語法,將SRC中所有.cpp 文件換成.o 文件 .PHONY:test test : ${OBJ} @echo "do the compile " %.o : %.cpp #定義模式規則 ${CC} -c ${CFLAGS} $< .PHONY:clean clean: -rm *.o
#####條件判斷篇 :關鍵字有 ifeq/ifneq/ifdef/ifndef , else, endif;
ifeq (condition) #condition可以是變量是否為空${VAR},可以直接比較值(${CC} gcc) ,也可以調用函數 #operation1 else #operation2 endif
對於if/else 來講,可以對於不同的PHONY 給不同的定義/debug 選項,從而實現不同的編譯條件 和 使用場景(debug/release/release with debug information...);
#####函數篇 : 常見函數link
#函數調用規則 $(<function> <argument0>,<argument1>) #函數名與參數之間使用空格分隔, 參數與參數之間用 , 分隔;
MAKEFILE執行邏輯:
-
GNU 中 make會在當前目錄下依次尋找名字叫“GNUmakefile”、“makefile”和“Makefile" 的文件;
-
如果找到,它會找文件中的第一個目標文件(target),並把這個target文件作為最終的目標文件。
-
如果target文件不存在,或是target所依賴的后面的 .o 文件的文件修改時間要比 target這個文件新,那么,他就會執行后面所定義的命令來生成 target。
-
如果 target所依賴的 .o 文件也不存在,那么make會在當前文件中找目標為 .o 文件的依賴性,如果找到則再根據那一個規則生成 .o 文件。(類似棧操作);
-
當所有的前置文件都存在 且 是最新時候,於是make會生成最終執行文件;
tips: 出現錯誤時候,直接退出並報錯;
make常見相關操作:
返回值: 0 :表示成功執行。 1: 如果make運行時出現任何錯誤,其返回1。 2: 如果你使用了make的“-q”選項,並且make使得一些目標不需要更新,那么返回2。# 指定目標: make -f(/--file/--makefile) FILENAME.mk #指定執行make 文件 make clean #指定特定的目標,例如:clean #檢查規則 -n/--just-print/--dry-run/--recon : 不執行參數,只是打印命令,不管目標是否更新,把規則和連帶規則下的命令打印出來;用來debug; -t/--touch : 只更新目標文件時間戳,假裝編譯了,其實沒有變化 -q/--question : 尋找目標,存在的話,就什么都不輸出,否則會輸出錯誤; -W <file> : file 一般是源文件,make 會根據規則運行依賴這個文件的命令; -w/--print-directory : 輸出運行makefile 之前和之后的信息,對於嵌套調用makefile 很有用; -k/--keep-going : 出錯也不停止 -i : 執行時候忽略所有錯誤; -I : 指定被包含在makefile 的搜索目標,可以通過多個-I 指定多個目錄;
======================= **代碼演示** =======================
文件的依賴規則: 也就是說生成target 需要 prerequisites 中的文件,所以如果prerequisites 中有文件更新,那么就要更新target 文件;
target : 可以是一個obj/執行文件、標簽, 可以是一個, 也可以是多個,中間用空格分開;
如果是多目標的話,可以用$@ 來代表這個多目標的集合;
prerequisite : 生成target 所依賴的文件
commnad : 該target 要執行的文件, 如果不是於prerequisites 同一行的話,那么要用tap 開頭;
注意MAKEFILE 中第一個目標會被作為默認的目標,
target ... : prerequisites ... command ... ...
EXAMPLE:
下面是最簡單的例子:其中 \ 為換行符,對於太長行可以用來換行;
all : main.o module1.o \ module2.o # \換行符號 g++ -o result.out main.o module1.o module2.o main.o : main.cpp module1.h module2.h g++ -c main.cpp module1.o : module1.cpp module1.h g++ -c module1.cpp module2.o : module2.cpp module2.h g++ -c module2.cpp clean: rm *out *.o
其中有很多優化地方,優化后結果如下:
1. 宏定義;變量定義
2. 自動推導:
3. 偽目標文件:只是一個標簽,指明這個目標就是為目標文件,不管有沒有clean 這個文件,make clean 就是進行下面操作;
而且偽目標不會生成文件;
偽目標一樣可以有依賴,實際執行時候,就是先執行依賴,再執行自身;

我們可以根據這種性質來讓我們的makefile根據指定的不同的目標來完成不同的事。在Unix世界中,軟件發布時,特別是GNU這種開源軟件的發布時,其makefile都包含了編譯、安裝、打包等功能。我們可以參照這種規則來書寫我們的makefile中的目標。 all:這個偽目標是所有目標的目標,其功能一般是編譯所有的目標。 clean:這個偽目標功能是刪除所有被make創建的文件。 install:這個偽目標功能是安裝已編譯好的程序,其實就是把目標執行文件拷貝到指定的目標中去。 print:這個偽目標的功能是例出改變過的源文件。 tar:這個偽目標功能是把源程序打包備份。也就是一個tar文件。 dist:這個偽目標功能是創建一個壓縮文件,一般是把tar文件壓成Z文件。或是gz文件。 TAGS:這個偽目標功能是更新所有的目標,以備完整地重編譯使用。 check和test:這兩個偽目標一般用來測試makefile的流程。
########優化1:使用類似宏定義方式將一些參數集中,方便維護 OBJ=main.o module1.o module2.o RESULT = result.out #all : main.o module1.o \ # module2.o # \換行符號 all : ${OBJ} g++ -o $(RESULT) $(OBJ) #main.o : main.cpp module1.h module2.h # g++ -c main.cpp #module1.o : module1.cpp module1.h # g++ -c module1.cpp #module2.o : module2.cpp module2.h # g++ -c module2.cpp #####優化2: GNU make 會自動推導同名的.cpp 文件,並且推導出來要調用g++ -c main.o : module1.h module2.h module1.o : module1.h module2.o : module2.h .PHONY : clean #####優化3:這里表明clean 是個偽目標文件,防止該名字 與 某個文件名字重合 clean: rm ${RESULT} ${OBJ}
再優化:
靜態模式規則:多目標規則,語法如下
<targets ...> : <target-pattern> : <prereq-patterns ...> <commands> ...
targets: 定義了一系列的目標文件,可以有通配符。是目標的一個集合。如果其中有多種后綴,可以使用 $(filter %.o, ${OBJ)) 進行過濾;
target-pattern: 是指明了targets的模式,也就是的目標集模式。 下面例子中就是說,%.o都是.o 結尾的
prereq-patterns : 是目標的依賴模式,它對target-pattern形成的模式再進行一次依賴目標的定義; 下面例子中就是說,%.cpp都是對target-pattern 所形成的目標進行二次定義,就是將%,o集合中所有目標,后綴改成.cpp 后新的集合;
命令中的 $<
和 $@
則是自動化變量, $<
表示第一個依賴文件, $@
表示目標集
OBJ1 = module1.o module2.o OBJ2 = main.o x.txt OBJ= ${OBJ1} ${OBJ2} RESULT = result.out cc = g++ all : ${OBJ} @echo "complie the final output result.out file" #這行命令執行時候,不會輸出具體命令過程,但是會正常執行; ${cc} -o $(RESULT) $(OBJ) #####優化1: 靜態模式 ${OBJ1} : %.o : %.cpp %.h ${cc} -c $< -o $@ ## $<:表示第一個依賴文件,$@:表示目標集 #${OBJ2} : %.o : %.cpp ${filter %.o, ${OBJ2}} : %.o : %.cpp ##使用filter 進行過濾 ${cc} -c $< -o $@ .PHONY : clean clean: rm -f ${RESULT} ${OBJ}
Tips: 8af1961a73da6c7(hashcode in github)
1. 使用filter 篩選文件; 2. @ 使用,避免輸出執行的命令過程;要是打算全面禁止輸出可以使用 -s/--silent/--quiet 選項;
================在大工程中會涉及到的操作=================
嵌套操作:
在一些大工程中,需要將不同模塊放在不同目錄中,這樣可以在每個目錄中都書寫一個該目錄的makefile, 然后在最外層可以寫一個總控makefile, 通過這個總控makefile 來實現對每個目錄中文件控制;
基本命令格式:
cd <subdir> && make #與下行命令等效 make -C <subdir>
這里還有變量傳遞到下級makefile 的操作,基本語法如下;
不過SHELL/MAKEFLAGS 這兩個變量總是會傳遞到下層的,這里涉及到一些關鍵字 和 參數選項;參考后面的cheatsheet;
export #要傳遞所有的變量,只要export 即可 export <variable ...> #傳遞變量到下級Makefile中 unexport <variable ...> #不想讓某些變量傳遞到下級Makefile中
文件尋找:
在大工程中,有大量源文件,通過會將文件放在不同目錄中,所以在make 做文件依賴關系時候,可以通過在文件前加上路徑,但是最好是將路徑告訴make,讓make 自己尋找;
在MAKEFILE 中定義特殊變量VPATH, 可以定義多個目錄文件,有冒號分隔;
VPATH = src:.../headers #這里定義了2個目錄, src , ../headers, make會按照這個順序去搜索f
將上面的嵌套操作 與 文件尋找 結合在一起,可以用來編譯不同目錄下文件;
OBJ1 = module1.o module2.o OBJ2 = main.o x.txt SUB_OBJ = sub_module.o OBJ= ${OBJ1} ${OBJ2} ${SUB_OBJ} RESULT = result.out cc = g++ VPATH = ./ : subdir ## 增加文件鏈接范圍,多個范圍之間使用:分隔,按照定義的順序尋找,直到找到為止; all : ${OBJ} @echo "complie the final output result.out file" #這行命令執行時候,不會輸出具體命令過程,但是會正常執行;可以使用-s 實現全面禁止執行命令輸出 ${cc} -o $(RESULT) $(OBJ) #####優化1: 靜態模式 ${OBJ1} : %.o : %.cpp %.h ${cc} -c $< -o $@ ## $<:表示第一個依賴文件,$@:表示目標集 #${OBJ2} : %.o : %.cpp ${filter %.o, ${OBJ2}} : %.o : %.cpp ##使用filter 進行過濾 touch x.txt ${cc} -c $< -o $@ ${SUB_OBJ} : subsys .PHONY : execmd execmd: # 展示; 作用 cd subdir pwd cd subdir; pwd #展示忽略錯誤操作 @echo "測試 - 作用" -no_cmd # 也可以通過 -i(--ignore-errors) 參數忽略所有的錯誤 @echo "contiue next cmd" .PHONY : subsys subsys : #cd subdir && make #與下行命令等效 make -C subdir .PHONY : clean clean: -rm -f ${RESULT} ${OBJ} make clean -C subdir
tip: 953708b3079a109 (hash code in github)
1.使用嵌套make 操作;
2. 指定文件尋找范圍;
定義命令包:(類似自定義函數)
基本格式: define 開始,endef 結尾;
define <FUNC_NAME> <operation> endefj
簡單示例:
define create_file touch x.txt endef ${filter %.o, ${OBJ2}} : %.o : %.cpp ##使用filter 進行過濾 ${create_file} ${cc} -c $< -o $@
tip: aa8ef50dd0b (hash code in github)
1. makefile 中定義命令包;
make 命令使用是傳遞參數:
在實際使用make 命令時候,有時候需要傳遞一些參數進去,可以通過下面的方式來實現;
CFLAGS=${CFLAG} CFLAGS+=-g -Wall all: gcc ${CFLAGS} a.o b.o -o a.out
使用: make CFLAG=-DDEBUG, 就可以將-DDEBUG 參數傳遞進去;
隱含規則:
在makefile 中,有一些使用頻率很高的規則,這些就被作為隱含規則來使用;
對於個人來講,我們也可以通過上面的 “模式規則” 來寫下自己的隱含規則;
下面是對於C/C++ 的隱含規則,但是在makefile 中還支持很多其他的語言
######C x.o 的目標依賴自動推導為 x.c, 其生成命令是 ${CC} -c ${CPPFLAGS} ${CFLAGS} ######C++ x.o 的目標依賴自動推導為x.cc 或者x.c , 生成命令是 ${CXX} -c ${CPPFLAGS} ${CXXFLAGS}
同時在隱含規則中,makefile有很多預先設置的變量,可以通過在makefile 中改變這些變量
#####命令變量 CC : C語言編譯程序。默認命令是 cc CXX : C++語言編譯程序。默認命令是 g++ ####命令參數的變量 CFLAGS : C語言編譯器參數。 CXXFLAGS : C++語言編譯器參數 LDFLAGS : 鏈接器參數。(如: ld )
所以上面示例的makefile, 可以簡化成下面的形式;
OBJ = module1.o module2.o main.o sub_module.o RESULT = result.out VPATH = ./ : subdir ${RESULT} : ${OBJ} ${CXX} -o ${RESULT} ${OBJ} .PHONY : clean clean: -rm -f ${RESULT} ${OBJ}
后記:隨着cmake一些更有效的工具出現,越來越多的項目都是用了cmake 來管理編譯過程;該文章只是對自己學習過的知識的一次記錄;
同時對於make 工具,個人理解最重要的是對於文件的依賴關系,以及為了makefile 文件的書寫、閱讀、維護的便利,利用變量定義/隱式規則/模式規則 /遞歸條用來簡化文件依賴關系的編寫;另外在linux 系統中,也可以利用makefile 來實現打包、備份、擴展等一些固定操作;
CheatSheet:
make中定義的自動化變量: % 的意思是表示一個或多個任意字符 $@ : 表示規則中的目標文件集。在模式規則中,如果有多個目標,那么, $@ 就是匹配於 目標中模式定義的集合。 $< : 依賴目標中的第一個目標名字。如果依賴目標是以模式(即 % )定義的,那么 $< 將是符合模式的一系列的文件集。注意,其是一個一個取出來的。 $% : 僅當目標是函數庫文件中,表示規則中的目標成員名。例如,如果一個目標是 foo.a(bar.o) , 那么, $% 就是 bar.o , $@ 就是 foo.a 。如果目標不是函數庫文件 (Unix下是 .a ,Windows下是 .lib ),那么,其值為空。 $? : 所有比目標新的依賴目標的集合。以空格分隔。 $^ : 所有的依賴目標的集合。以空格分隔。如果在依賴目標中有多個重 復的,那個這個變量會去除 重復的依賴目標,只保留一份。 $+ : 這個變量很像 $^ ,也是所有依賴目標的集合。只是它不去除重復的依賴目標 $* : 這個變量表示目標模式中 % 及其之前的部分。如果目標是 dir/a.foo.b ,並且 目標的模式是 a.%.b ,那么, $* 的值就是 dir/a.foo 函數部分: wildcard : %在變量定義和函數引用時無效,比如$SRC=$(wildcard *.c)不能寫作$SRC=%.c notdir : 去除路徑 patsubst :替換通配符 make 命令參數: -c 只編譯並生成目標文件。 -g 生成調試信息。GNU 調試器可利用該信息, 增加調試信息,利用gdb進行調試, 使用方法 gdb ./a.out -Wall 打開大部分警告信息 -O0 不進行優化處理。 -O 或 -O1 優化生成代碼。 -O2 進一步優化。 -O3 比 -O2 更進一步優化,包括 inline 函數。 :-M 自動尋找依賴關系,(GNU gcc/g++ 需要使用-MM, 否者會把標准庫文件加進來) @ 字符在命令行前,這個命令不會被顯示出來, 但是仍然會執行; -n(--just-print) 只顯示命令,不執行命令,可以用來查看命令執行的樣子與順序, 用來DEBUG -s(--silent/--quiet) 全面禁止命令執行中的輸出; ; 來分割同一行多個命令,后面命令都是基於前面命令 - 在命令前面,不管命令出不出錯,都認為成功 -i(--ignore-errors),Makefile 中所有命令都忽略錯誤 -k(--keep-going),某個規則出錯,忽略該規則,繼續執行其他規則,不至於中斷其他命令的執行; CFLAGS 環境變量,定義以后就會使用該環境變量; 當make嵌套調用時,CFLAGS會傳遞下去; -e make命令行帶入時候,會覆蓋上面CFLAGS定義的環境變量; -I ./include //將當前目錄下include 文件夾增加進系統目錄中