做 Linux C++,一個穩定的工程,Makefile 是很少改動的。但是如果需要修改的時候,Makefile 的語法和用法一時半會就回憶不出來(原諒我記憶力差……)。在此把自己以前的 Makefile 學習筆記記錄一下吧,也作爲分享。本文假設讀者已經懂得了 Makefile,因此主要是作爲備忘和速查用。
全文中尖括號部分表示變量。本文地址:https://segmentfault.com/a/1190000012091117
另外,速查系列還有這一篇:正則表達式速查筆記
Make 介紹
Makefile 的基本規則就是:
target ...: prerequirements ...
command ...
...
其中 target
是目標文件,可以有多個,可以是 .o
文件或者是可執行問價,甚至可以是一個標籤。Prerequisites
是先決條件,可以是文件,也可以是另一個 target。
這就組成了一個依賴關係
:target
的先決條件定義在 prerequisites
中,而其生成規則又是由 command 決定的。如果包含多個規則的話,那麼第一條規則就是整個 Makefile 的默認規則
。
Make 的工作流程
- 在當前目錄中查找
Makefile
或者makefile
文件 - 將文件中第一條規則作爲默認規則
- 如果目標不存在,則尋找對應的
.o
文件 - 如果
.o
文件不存在,則尋找.o
的依賴關係以生成它
Makefile 有很多默認的生成規則,但是本文我們不關心,因爲絕大部分情況下,我們是需要自行寫規則的。這便於自定義、便於移植、便於交叉編譯、便於調試。
Makefile 中的變量
變量的定義和調用格式:
name = value # 注意變量的值是允許空格的
$(name)
變量值的部分可以使用換行符 "\
" 來做假換行,將兩行內容連接成一行,從而縮短 Makefile 文件的寬度。
Makefile 綜述
Makefile 裏面都有啥?
文件指示:在一個 Makefile 裏面可以制定另一個 makefile,類似於 C 的 include
Makefile 還可以做條件包含動作,類似於 #if
。Makefile 可以定一個變量爲一個多行的命令。
Makefile 裏面只有行註釋而沒有段註釋。註釋採用 #
開頭。如果要使用 #
字符,則需要轉義,寫成 “\#
”。
Makefile 規則內容裏所有的 shell 命令都要以製表符 Tab
開頭,注意,空格符是不行的。
默認的 make 文件名爲:GNUmakefile
, makefile
, Makefile
,當敲入 make
命令時,會自動搜尋這幾個文件。約定俗成使用最後一個。
引用其他 Makefile
語法:
include filename ... # 不允許 include 失敗
-include filename ... # 允許 include 失敗
可以包含路徑或者通配符,一行可以包含多個文件。
如果未指定絕對路徑或者相對路徑,那麼 make 會按照一下的順序去尋找:
- 當前目錄
- 制定 make 時,在 -I 或者 --include-dir 的參數下尋找
<prefix>/include
(一般是/usr/local/bin
或/usr/include
)
建議還是手動指定吧,自動搜尋意外可能太多了。
環境變量 MAKEFILES
這裏主要是要提醒:不要設置這個環境變量,否則會影響全局的 make include 動作。
Make 的幾個工作方法
通配符
Make 支持三個通配符:*, ?, [...]
。可以用在規則中,也可以用在變量中。
僞目標
僞目標就是 Makefile 裏面頗爲常見的 .PHONY
標識,比如:".PHONY: clean
",表示這個規則名並不代表一個真實存在的、需要生成的文件名,而只是一條純粹的規則。
- 真目標的特點是:如果目標不存在,纔會被執行
- 僞目標的特點是:無視目標是否存在,必然執行
除了 make clean
之外,僞目標還有另一種使用場景,就是一個 make 動作,實際上生成了多個目標。比如:
.PHONY: all
all: exe install # 包含了生成目標文件,以及安裝動作
多目標
規則的冒號前面可以有多個 target,表示多個 target 共用這條規則。
自動生成依賴關係
如果我們使用中規中矩的 makefile 寫法,那麼對於每個源文件都要好好寫頭文件依賴關係,從而在頭文件更新的時候,可以自動重新編譯依賴於這個頭文件的源文件。
這實在是太麻煩了。好在 gcc
裏有一個 -MM
(注意不是 “-M”) 的選項,可以分析出 .c
文件依賴的頭文件並且打印出來。因此製作 Makefile 的時候,就可以利用這一特性自動生成依賴。
實現方法有很多,這裏貼出我自己使用的例子,也可以參見我的工程代碼:
EXCLUDE_C_SRCS =#
C_SRCS = $(filter-out $(EXCLUDE_C_SRCS), $(wildcard *.c))
C_OBJS = $(C_SRCS:.c=.o)
$(C_OBJS): $(C_OBJS:.o=.c)
$(CC) -c $(CFLAGS) $*.c -o $*.o
@$(CC) -MM $(CFLAGS) $*.c > $*.d
@mv -f $*.d $*.d.tmp
@sed -e 's|.*:|$*.o:|' < $*.d.tmp > $*.d
@sed -e 's/.*://' -e 's/\\$$//' < $*.d.tmp | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $*.d
@rm -f $*.d.tmp
書寫命令
這裏的命令,指的是在 Makefile 規則裏的 “command” 部分。
命令執行
將 “@
” 放在一條命令的前面,表示實際執行的時候,不打印這條命令語句,可以節省屏幕內容,減少垃圾信息(特別是我的自動生成依賴的命令,調通了之後,那就是一堆無用信息)。如果將 “-
” 放在命令前面,則表示無視這條命令的返回值是否爲成功(0).
如果上一條命令的結果需要用於下一條命令時,需要將這些命令寫在一行中。建議用 “\
” 分開。這最典型的是 cd
命令及其之後的一連串命令。
Make 的時候加上 -n
選項或 --just-print
選項,則表示不執行 make,而只是把過程打印出來。
嵌套執行 make
在 Makefile 裏可以到另一個目錄下執行 make,執行方式類似於普通的命令調用,但特別的是,make 可以識別出這是一條嵌套 make 指令,從而在 shell 中打印出 “專項哪裏哪裏 make” 的提示語法爲:
subsystem:
$(MAKE) -C subdir
這個做法的主要好處是可以向下級 Makefile 傳遞變量或者語法:
export VARIABLE ... # 將相應變量變成當前 make 操作的全局變量
也惡意直接指定變量的值:
export VARIABLE = value
如果要傳遞所有變量(不推薦),直接寫 export
就好。
注意由兩個系統變量 SHELL
和 MAKEFLAGS
是永遠傳遞的。
此外還有一個全局變量 MAKELEVEL
用來表示當前的嵌套層數。
定義命令包
命令包類似於宏、子函數等等。使用 define
來定義,以 endif
結束,比如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endif
注意如果是命令的話,需要以製表符
開頭。調用這個命令包的方式爲:$(run-yacc)
使用變量
變量賦值
變量定義時必須賦值,至少賦一個空值(只有等號,等號右邊什麼都沒有)
使用變量的時候雖然不強制、但是爲了安全起見,應該使用括號或者打括號把變量包含起來。如果要使用字符 "$
",則使用 "$$
" 來轉義
賦值時,等號右側可以有未定義的變量,並且在其實際使用時才展開變量的內容。但這可能會導致循環引用。爲了避免這一點,可以使用 ":=
" 符號來避免使用未定義的變量
"+=
" 的作用是 “追加” 值。如果右側有變量未定義,則等價於 “:=
”
"?=
" 的作用是:如果等號左側的變量未定義,則使用等號右邊內容定義,即:
ifeq ($(some_var), undefined)
some_var = some_val
endif
另外:$@
表示當前規則的編譯目標$^
表示當前規則的所有依賴文件$$<
表示當前規則的第一個依賴。
定義一個空格變量
NULL_STR :=#
SPACE_STR := $(NULL_STR) # end of line
注意第二行的註釋與 “)” 之間是包含一個空格的。註釋的 “#” 必須有,否則不會定義一個空格出來。
變量替換
第一個方式爲:$(var: .o = .c)
,意思是將等號左邊的字符換成右邊的字符
第二個方式爲所謂的 “靜態模式”:$(var: %.o = %.c)
把變量值作爲變量
這很類似於指針,只是地址值變成了變量值。可以用變量值生成變量名,比如:a := $($(var))
或者是 $($(var)_$(idx))
之類的寫法。
override
在命令行調用 make 時,可以直接指定某個變量的全局值,使得它在整個 make 的過程中一直不變。爲了防止這個特性,可以使用這個關鍵字來處理:override <variable> = <value>
等號也可以用 :=
和 ?=
目標變量(局部變量)
如果某條約束裏面不想使用已經定義了的全局變量,可以這樣寫:
prog: CFLAGS = -g
prog: a.o b.o
$(CC) $(CFLAGS) a.o b.o
條件判斷
語法
<條件語句>
<true 執行語句>
else
<false 執行語句>
endif
其中條件語句有四種情形:
1、表示是否相等
ifeq (<arg1>, <arg2>) # 推薦
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
2、表示是否不等,上面的 ifeq 換成 ifneq
3、ifdef
4、ifndef
使用函數
Make 的所有函數都是內置函數,不能自己定義(命令包除外)。下面列出常用的函數,如果看不懂再詳細查閱。
常規函數
字符串替換
$(subst <from>, <to>, <text>)
模式字符串替換
$(patsubst <pattern>, <replacement>, <text>)
去開頭和結尾的空格
$(strip <string>)
查找字符串
$(findstring <find>, <in>)
反過濾
$(filter-out <pattern_or_string>, <text>)
排序(單詞升序)
$(sort <list>)
取單詞
$(word <n>, <text>)
取單詞串
$(wordlist <n_start>, <n_end>, <text>)
單詞個數統計
$(words <text>)
去掉每個單詞的最後文件名部分,只剩下目錄部分
$(dir <names ...>)
去掉每個單詞的目錄部分,只剩下文件名部分
$(notdir <names ...>)
讀取各文件名的後綴
$(suffix <names ...>)
加後綴
$(addsuffix <suffix>, <names ...>)
加前綴
加前後綴在動態創建局部變量很有用$(addprefix <prefix>, <names ...>)
連接字符串
$(join <list1>, <list2>)
for 循環
$(foreach <var>, <list>, <text>)
這其實是一個函數,作用是:將 list
的單詞逐一取出,放到 var
指定的變量中,然後執行 text
的表達式。返回值則是 text
的最終執行值。
Shell 函數
執行 shell 命令,並且將 stdout 作爲返回值返回,如:contents := $(shell ls -la)
控制 make 輸出
$(error <text ...>)
$(warning <text ...>)
這也同時是調試和定位 make 的好方法。
判斷文件是否存在
ifeq ($(FILE), $(wildcard $(FILE)))
...
endif