# 假目標
PHONY := __build
__build: #空操作
# 清空需要的變數
obj-y := #需要編譯的目標檔案列表
subdir-y := #子目錄列表
EXTRA_CFLAGS := #額外編譯選項
# 包含同級目錄Makefile
include Makefile
# 獲取當前 Makefile 需要編譯的子目錄的目錄名
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)
# 生成各子目錄的目標檔案列表:要編譯的子目錄下目標檔案都打包成了dir/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
# 過濾 obj-y,得到當前目錄下需要編進程式的檔名作為,並寫為目標
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
# 使修改頭檔 .h 後,重新make後可以重新編譯(重要)
dep_files := $(foreach f,$(cur_objs),.$(f).d)
# 包含所有依賴檔案
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),) #不為空(即存在依賴檔案),則執行下一步操作
include $(dep_files) #Make 工具會讀取這些依賴檔案,從而確保 Makefile 知道源檔案與頭檔案之間的正確依賴關係,當頭檔案修改後可以重新編譯受影響的源檔案
endif
PHONY += $(subdir-y)
# 第一個目標
__build : $(subdir-y) built-in.o
# 優先編譯 子目錄的內容;-C切換到子目錄執行$(TOPDIR)/Makefile.build
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
# 把subdir的built-in.o和cur_objs連結成 總的built-in.o目標
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
dep_file = [email protected]
# 生成 cur_objs 目標
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
子目錄下 makefile,用於添加 obj-y 信息#
# 可以不添加下面兩個變數
EXTRA_CFLAGS :=
CFLAGS_view.o :=
obj-y += $(patsubst %.c,%.o,$(shell ls *.c))
# obj-y += subdir/
自動化構建工具需要實現:
1)工程從未被編譯,則所有檔案都要編譯,連結
2)工程的某幾個檔案被修改,則只編譯修改的檔案,並連結至目標程式
3)工程的頭檔案被改變,則編譯引用了這幾個頭檔案的 c 檔案,並連結目標程式
*)打包一些 shell 命令,方便工程管理,比如打包,備份,清理中間檔案
make 運行#
make 啟動#
默認腳本優先級:GNUmakefile
> makefile
> Makefile
make -f custom.mk #指定入口makefile檔案
command 打印#
make 執行 command 時會把 command 打印出來;用 @
字符在命令前,則不會打印出來;
比如echo 正在編譯XXX模組
,終端顯示:
echo 正在編譯XXX模組 正在編譯XXX模組
如果是@echo 正在編譯XXX模組
就不會輸出
make -s #靜默執行command
command 運行的 shell 環境#
make 命令默認使用環境變數 SHELL
指定的 Shell 執行,用戶沒指定SHELL
時, Unix Shell 默認是 $(SHELL)
=/bin/sh
;Windows 環境下,make
會自動尋找合適的命令解釋器,例如 cmd.exe
或者你指定的 Unix 風格 Shell(如 bash
)
-------------------讓上一條命令的結果應用在下一條命令時,用分號分隔這兩條命令。分行寫,則前一命令的效果不會保留
.RECIPEPREFIX = >
exec:
> cd /home/hchen; pwd
# 打印 /home/hchen
.RECIPEPREFIX = >
exec:
> cd /home/hchen
> pwd
# 打印 /home
make 命令失敗處理#
————————忽略錯誤,不終止 make 的執行
clean:
-rm -f *.o #command前加減號
.IGNORE: clean #特殊目標.IGNORE指定clean規則忽略錯誤,不終止 make 的執行
clean:
rm -f *.o
make -i #用 -i 或 --ignore-errors 參數選項
make --ignore-errors
————————只跳過當前失敗的規則,繼續執行其它規則,不會終止 make 執行
make -k
make --keep-going
make 查看規則#
make -p # 輸出所有的規則和變數。
make -n # 打印將要執行的規則和命令,但不執行
make --debug=verbose #
make -t # 相當於UNIX touch,把目標的修改日期變成最新的,相當於假編譯,只是把目標變成已編譯狀態
make -q # 檢查target是否存在,返回碼指示結果(0=需更新,2=錯誤)。
make -W <file> # 一般指定源檔案(或依賴檔案),Make根據規則推導運行依賴於這個檔案的命令,一般和-n一同使用查看此檔案所發生的規則命令
make 退出#
Make 退出碼:
1)0
:執行成功。
2)1
:執行中出現錯誤。
3)2
:-q
選項啟用時,目標不需要更新
make 參數#
選項 | 功能 |
---|---|
-b, -m | 忽略與其他版本兼容性相關的警告。 |
-B, --always-make | 強制重編譯所有目標。 |
-C <dir> | 切換到指定目錄運行 Makefile。 |
--debug[=<options>] | 輸出調試信息,<options> 包括 all 、basic 、verbose 、implicit 、jobs 、makefile 。 |
-d | 等同於 --debug=all |
-e, --environment-overrides | 環境變數覆蓋 Makefile 中定義的變數。 |
-f=<file> | 指定 Makefile 檔案。 |
-h | 顯示幫助信息 |
-i, --ignore-errors | 忽略所有錯誤。 |
-I <dir> | 指定一個可導入 makefile 的搜索路徑。可以使用多個 "-I" 參數來指定多個目錄 |
-j [<jobsnum>] | 指定最大並行執行任務數。 |
-k, --keep-going | 忽略失敗的目標,繼續處理其他目標。 |
-l <load> | 指定允許的最大負載值,超出則暫停新的任務。 |
-n | 打印將要執行的規則和命令,但不執行 |
-o <file> | 不重新生成的指定的<file> ,即使這個目標的依賴檔案新於它。 |
-p, --print-data-base | 輸出所有的規則和變數。 |
-q, --question | 檢查 target 是否存在,返回碼指示結果(0 = 需更新,2 = 錯誤)。 |
-r | 禁用所有內置規則。 |
-R | 禁用所有內置變數。 |
-s, --silent | 禁止命令輸出。 |
-S, --no-keep-going | 禁用 -k 參數的作用 |
-t | 相當於 UNIX touch,把目標的修改日期變成最新的,相當於假編譯,只是把目標變成已編譯狀態 |
-v, --version | 顯示版本信息。 |
-w, --print-directory | 顯示當前目錄及嵌套調用信息。 |
--no-print-directory | 禁止 "-w" 選項 |
-W <file> | 一般指定源檔案 (或依賴檔案),Make 根據規則推導運行依賴於這個檔案的命令,一般和 - n 一同使用查看此檔案所發生的規則命令 |
--warn-undefined-variables | 警告未定義的變數。 |
規則#
一條構建規則由依賴關係和命令組成:
target:要生成的目標檔案、可執行檔案或 label
prerequisites:目標檔案依賴的源檔案或中間檔案
command:調用的 shell 命令;通常是構建工具命令;一定要以一個 制表符Tab
作為開頭;或者 command 跟依賴關係在同一行,用;
隔開
執行 make
命令時,Make 會:
1)查找當前目錄下的 Makefile 或 makefile 檔案。也可指定路徑 make -f ~/makefile
2)查找 Makefile 中第一個 target;也可指定目標make target
3)檢查 target 的 prerequisites(.c/.o
)是否需更新。如 prerequisites 比 target 新,或 target 不存在,則執行 command
4)prerequisites (.o)
可能本身也作為 target 依賴其他檔案(.c)
,Make 會遞歸追蹤 .o
檔案的依賴,直到最終的源檔案 .c
被編譯。
5)編譯錯誤或檔案缺失,則 Make 報錯並終止執行;在命令前 +-
忽略檔案未找到的警告並繼續執行
如: file.c
檔案被修改, file.o
會重新編譯,且 edit
會重新連結
target: prerequisites; command; command
command
command
嵌套執行 make#
大型專案中,將程式碼劃分為多個模組或子目錄,每個子目錄維護獨立的 Makefile
是一種常見的工程管理方式。這種方法使每個模組的編譯規則更加清晰,簡化了 Makefile
的維護。
用到的 shell 變數:
MAKE
:GNU make
中的一個特殊變數,用於遞歸調用 make
自身;假設在 shell 執行make -j4
,總控 Makefile 有$(MAKE) -C subdir
;則相當於在 subdir 執行make -j4 -C subdir
MAKEFLAGS
:系統級變數,會自動傳遞到下級 Makefile
,可通過設置為空來阻止傳遞(MAKEFLAGS=
)
MAKELEVEL
: 系統變數,記錄當前 Make 執行的嵌套層數。如果你有嵌套的 make
調用,MAKELEVEL
會告訴你當前是在第幾層。
假設有一個子目錄叫 subdir,這個目錄下有個 Makefile 檔案,來指明了這個目錄下檔案的編譯規則。
總控 Makefile#
.RECIPEPREFIX = >
#如果當前 make 在最外層執行(MAKELEVEL= 0);定義一些與系統相關的變數,
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami) #當前用戶
host-type := $(shell arch) #架構
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif
.PHONY: all subsystem
all: subsystem #all依賴於subsystem
subsystem:
> $(MAKE) -C subdir MAKEFLAGS= VAR=value # -C subdir 表示在指定目錄中執行 make
#或> cd subdir && $(MAKE)
subdir/Makefile#
.PHONY: all
all:
echo "Received VAR = $(VAR)"
例子的執行結果:
-w
或 --print-directory
是調試嵌套 Makefile
的好幫手,它會輸出當前的工作目錄信息。如果使用 -C
參數,-w
會自動啟用。如果包含 -s
或 --silent
參數,則 -w
會失效。
make: Entering directory `subdir'
Received VAR = value
make: Leaving directory `subdir'
命令包#
define <命令包名>
<command1>
<command2>
...
endef
-------eg
.RECIPEPREFIX = >
define run-yacc
yacc $(firstword $^) # $(firstword $^) 會獲取 $^ 中的第一個依賴檔案。
mv y.tab.c $@ # 將生成的 y.tab.c 檔案重命名為 foo.c
endef
foo.c : foo.y
> $(run-yacc)
變數#
變數用於存儲字串,類似於 C/C++ 的宏;能提高腳本的可維護性,避免重複程式碼;eg:只需維護 objects
變數,不必修改規則;
命名規則: 可包含字母、數字、下劃線 (_
),且可以以數字開頭。大小寫敏感;系統變數通常全大寫,如 CC
、CFLAGS
。用戶自定義變數建議駝峰命名,如 MyFlags
聲明:VAR = value
引用:$(VAR)
#使命令前綴變為 >,而不是默認的制表符(tab)
.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
#第一個目標
edit: $(objects)
cc -o edit $(objects)
# 各個目標檔案的規則
main.o: main.c defs.h
cc -c main.c
kbd.o: kbd.c defs.h command.h
cc -c kbd.c
# 其他 .o 檔案的規則...
自動變數#
含義 | eg | |
---|---|---|
$@ | 依次從 target 集中取出 target,並執於命令 | |
$< | 表示依次從依賴集中取出依賴檔案,並執於命令 | |
$^ | 所有依賴檔案的集合(去重) | |
$+ | 所有依賴檔案的集合(不去重) | |
$? | 所有比target 新的依賴檔案 | target: dependency1 dependency2 command $? 如 dependency1 比 target 新,則 $? 為 dependency1 |
$* | 依次從格式規則的 target 集中取出 target,去除後綴,剩路徑部分 | target: %.c command $* 如果目標是 file.c ,則 $* 為 file 。 |
$% | 僅當目標是靜態庫時,表示依次取出庫中的成員 | libfoo.a(bar.o): bar.o ar r $@ $% 如果目標是靜態庫 libfoo.a ,則 $% 是庫中的成員 bar.o 。 |
$(@D) | 依次從 target 集中取出 target 的目錄部分,並執於命令 | target = dir/subdir/foo.o $(target): @echo $(@D) # dir/subdir |
$(@F) | 依次從 target 集中取出 target 的檔名部分,並執於命令 | target = dir/subdir/foo.o $(target): @echo $(@F) # foo.o |
$(<D) | 依次從依賴集中取出依賴檔案的目錄部分,並執於命令 | target: dir/file.c command $(<D) 如果第一個依賴檔案是 dir/file.c ,則 $(<D) 為 dir 。 |
$(<F) | 依次從依賴集中取出依賴檔案的檔名部分,並執於命令 | target: dir/file.c command $(<F) 如果第一個依賴檔案是 dir/file.c ,則 $(<F) 為 file.c 。 |
$(^D) | 所有依賴檔案的目錄部分的集合(去重) | target: dir/file1.c dir/file2.c command $(^D) 如果依賴檔案是 dir/file1.c 和 dir/file2.c ,則 $(^D) 為 dir 。 |
$(^F) | 所有依賴檔案的檔名部分的集合(去重) | target: dir/file1.c dir/file2.c command $(^F) 如依賴檔案是 dir/file1.c 和 dir/file2.c ,則 $(^F) 為 file1.c file2.c |
$(+D) | 所有依賴檔案的目錄部分的集合(不去重) | target: dir/file1.c dir/file2.c dir/file1.c command $(+D) 依賴檔案是 dir/file1.c dir/file2.c dir/file1.c ,則 $(+D) 為 dir dir dir |
$(+F) | 所有依賴檔案的檔部分的集合(不去重) | target: dir/file1.c dir/file2.c dir/file1.c command $(+F) 依賴檔案是 dir/file1.c dir/file2.c dir/file1.c ,則 $(+F) 為 file1.c file2.c file1.c |
$(?D) | 被更新過的依賴檔案的目錄部分 | target: file1.c file2.c command $(?D) 如果 file1.c 更新,則 $(?D) 為 file1 。 |
$(?F) | 被更新過的依賴檔案的檔部分 | target: file1.c file2.c command $(?F) 如果 file1.c 更新,則 $(?F) 為 file1.c 。 |
$(*D) | 依次從格式規則的 target 集中取出 target,去除後綴和檔名部分 | %.o: %.c @echo $(*D) |
$(*F) | 依次從格式規則的 target 集中取出 target,去除後綴和目錄部分 | %.o: %.c @echo $(*F) |
靜態模式規則:
1)<targets ...> 目標集,可以有通配符
2)目標集檔案格式,從 <targets ...> 中匹配出符合的新目標集
3)<prereq-patterns ...> 依賴檔案集格式,匹配新目標集 的依賴集
.RECIPEPREFIX = >
bigoutput littleoutput: text.g
> generate text.g -$(subst output,,$@) > $@ # 參數替換:$(subst output,,$@) 表示替換 $@ 中的 output 為空字串
#等價於上
.RECIPEPREFIX = >
bigoutput: text.g
> generate text.g -big > bigoutput
littleoutput: text.g
> generate text.g -little > littleoutput
-------------------------------------靜態模式規則
.RECIPEPREFIX = >
<targets ...> : <target-pattern> : <prereq-patterns ...>
> <commands>
#eg
.RECIPEPREFIX = >
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c #匹配 %.o格式 的目標檔案,由$@逐個取出;匹配目標集的 依賴集檔案格式為%.c ,由$< 逐個取出
> $(CC) -c $(CFLAGS) $< -o $@
#等同於上
foo.o: foo.c
> $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o: bar.c
> $(CC) -c $(CFLAGS) bar.c -o bar.o
命令行環境變數#
除了 **Makefile 內部定義的變數;makefile 能以 $(var) 的方式引用 1)os環境變數和 2)make 選項傳遞的命令行環境變數;
同名變數優先級:命令行傳遞的變數值 >Makefile
中定義的變數值 > os 環境變數
1)提升 ****Makefile
**** 中定義的變數值優先級:override
變數前綴
2)提升 os 環境變數優先級:make -e
**
**
eg:make CFLAGS="-O2 -Wall"
傳遞CFLAGS** 環境變數;export CC=gcc
傳遞 os 環境變數 CC
# Makefile
all:
echo $(CFLAGS) #傳遞進來了-O2 -Wall
echo $(CC)
$$
在 bash
中代表當前進程的 PID(進程 ID)
$$$$
會插入當前進程的 PID 的最後四個數字。通常用於生成唯一的臨時檔名
在 makefile 間傳遞變數#
export # 傳遞所有變數
export variable += value; # 傳遞變數variable到下級Makefile
unexport variable := value; #不想讓變數variable傳遞到下級Makefile
赋值#
變數的值可依賴於其他變數的值
-----------------= 遞歸賦值:以遞歸的方式展開依賴的變數值;這種賦值方法靈活,可推遲變數的定義,缺點是可以寫出無限遞歸賦值,而且每次依賴的變數重新賦值都要重新計算
all:
echo $(foo)
foo = $(bar)
bar = $(ugh)
ugh = Huh?
-----------------:= 立即賦值:在賦值時展開右側表達式,並將結果存儲到變數中,比遞歸賦值性能更高。更安全
x := foo
y := $(x) bar # foo bar;由於立即展開了,下面一句不影響y的值
x := later
-----------------?= 條件賦值:只有未定義變數,賦值才生效;已定義變數則什麼也不做,而非覆蓋原定義
FOO ?= bar
-----------------# 不僅能用來註釋,還可用來標示變數定義的結束位置
dir := /foo/bar # directory to put the frobs in
-----------------+= 追加變數值,原變數用 := 定義,則 +=為立即賦值;變數用 = 定義,則 +=為遞歸賦值
objects = main.o foo.o bar.o utils.o
objects += another.o #相當於objects = $(objects) another.o
-----------------指定makefile中定義的某些變數有最高優先級,不會被make選項傳入的命令行變數覆寫
override <variable> := <value>
override <variable> += <more text>
override <variable> = <value>
-----------------多行變數定義:跟命令包一樣
define <variable>
<value>
endef
變數值替換#
-----------------簡單替換
${var:a=b} #將變數 var 中以 a 结尾的部分替換為 b
foo := a.o b.o c.o
bar := $(foo:.o=.c)
-----------------模式替換
$(var:%.suffix1=%.suffix2) # 將變數var中 %.suffix1 替換為 %.suffix2
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
Target-specific Variable
#
變數只對特定目標及其依賴有效,避免影響到其他目標的設置
-----------------------------------------變數表達式只對特定目標<target>及其依賴有效
<target> : <variable-assignment> # 變數表達式只對特定目標<target>及其依賴有效
<target> : override <variable-assignment>
#eg:不論全局 CFLAGS 的值是什麼,prog 目標及其相關的目標(prog.o、foo.o、bar.o)會使用 CFLAGS = -g
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
---------------------------變數表達式只對特定格式目標有效
<pattern ...>; : <variable-assignment>;
<pattern ...>; : override <variable-assignment>;
%.o : CFLAGS = -O2 #變數表達式只對.o結尾的目標有效
動態變數名#
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c) #$(dir)_sources定義為$(dir)目錄下所有c檔案
define $(dir)_print #甚至是動態定義命令包
lpr $($(dir)_sources) #打印$(dir)_sources的值
endef
特殊值的轉義#
$$
Makefile 中,單個 $
被用於變數引用。而任何 $$
會視為$
字符的轉義
-----------------定義包含空格的變數
nullstring :=
space := $(nullstring)
檔案路徑搜索#
shell 通配符:
*(匹配任意長度字符)
? (匹配一個字符)
~ (家目錄或環境變數)
如果沒有指明特殊變數 VPATH
,make 只會在當前目錄中去尋找依賴檔案和目標檔案。如果定義了這個變數,那麼,make 就會在當前目錄找不到的情況下,到所指定的目錄中去尋找檔案
VPATH = src:../headers # 現在當前目錄下尋找。再去src,以及../headers找
vpath <pattern> <directories>
vpath %.h ../headers #當前目錄沒找到時;在../headers"目錄下搜索所有以 .h 結尾的檔案;..上級目錄
vpath %.c foo #.c 檔案,按"foo""blish""bar"目錄順序搜索
vpath % blish
vpath %.c bar
objects := $(wildcard *.o) # 展開通配符*.o通配符,列出所有 .o 檔案
$(patsubst %.c,%.o,$(wildcard *.c)) # 將%.c字串替換為%.o字串,列出所有.c檔案對應的 .o 檔案
objects := $(patsubst %.c,%.o,$(wildcard *.c)) # 編譯並連結所有 .c 和 .o 檔案
foo : $(objects)
> cc -o foo $(objects)
隱含規則#
只需編寫專案連結環節的規則,而從源檔案到目標檔案的構建規則可由make
根據 1)內置規則庫,使用者自定義的 2)模式規則和 3)後綴規則 自動推導;
隱含規則優先級:規則按內置順序匹配,優先級靠前的規則會覆蓋靠後的;make
允許重載默認的隱含規則;一般以 Pattern Rules 的形式重載。
以下是常用內置自動推導規則:
target | prerequisites | command |
---|---|---|
<n>.o | <n>.c | $(CC) -c $(CFLAGS) $(CPPFLAGS) |
<n>.o | <n>.cc / <n>.C | $(CXX) -c $(CXXFLAGS) $(CPPFLAGS) |
<n> | <n>.o | $(CC) $(LDFLAGS) <n>.o $(LDLIBS) |
<n>.c | <n>.y | $(YACC) $(YFLAGS) |
<n>.c | <n>.l | $(LEX) $(LFLAGS) |
以及默認變數:
命令變數 | 描述 | 默認值 |
---|---|---|
AR | 函數庫打包程序 | ar |
AS | 匯編語言編譯器 | as |
CC | C 语言编译器 | cc |
CXX | C++ 语言编译器 | g++ |
CO | 從 RCS 檔案中擴展檔案程序 | co |
CPP | C 程式的預處理器 | $(CC) -E |
FC | Fortran 和 Ratfor 編譯器 | f77 |
LEX | Lex 文法分析器(針對 C 或 Ratfor) | lex |
PC | Pascal 編譯器 | pc |
YACC | Yacc 文法分析器(針對 C 程式) | yacc |
MAKEINFO | 將 Texinfo 檔案轉換為 Info 格式的程序 | makeinfo |
參數變數 | 描述 | 默認值 |
---|---|---|
ARFLAGS | AR 命令的參數 | rv |
ASFLAGS | 匯編器的參數 | 空 |
CFLAGS | C 语言编译器的參數 | 空 |
CXXFLAGS | C++ 语言编译器的參數 | 空 |
COFLAGS | RCS 命令的參數 | 空 |
CPPFLAGS | C 預處理器的參數 | 空 |
FFLAGS | Fortran 編譯器的參數 | 空 |
LDFLAGS | 連結器的參數 | 空 |
LFLAGS | Lex 文法分析器的參數 | 空 |
PFLAGS | Pascal 編譯器的參數 | 空 |
RFLAGS | Ratfor 編譯器的參數 | 空 |
YFLAGS | Yacc 文法分析器的參數 | 空 |
自動推導構建規則#
GNUmake 可自動推導某些 prerequisites 到 target 的構建規則,比如 C 專案每個 x.o
檔案後必然有一個同名x.c
依賴,必然有一條cc -c x.c
;
自動推導有個問題就是規則匹配優先級:比如你顯式指定foo.o : foo.p
; 但檔案目錄下存在foo.c
;此時會自動推導出foo.c
構建foo.o
的規則;make -r
或 make --no-builtin-rules
能禁用所有內置隱含規則。添加明確規則可以覆蓋隱含規則。
.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit: $(objects)
cc -o edit $(objects)
# 自動推導規則,make 會推導出這些規則
main.o: defs.h
kbd.o: defs.h command.h
command.o: defs.h command.h
# 其他規則...
另一種風格:其實就是進一步把相同.h
檔案的目標合併表示;我不喜歡
.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit: $(objects)
cc -o edit $(objects)
# 將多個檔案的依賴合併
$(objects): defs.h
kbd.o command.o files.o: command.h
display.o insert.o search.o files.o: buffer.h
.PHONY: clean
clean:
rm edit $(objects)
Pattern Rules#
用通用的格式描述一組目標和依賴檔案的關係。在目標或依賴檔案中用 %
代表任意字符或檔名的一部分
#eg:用Pattern Rules重載內建隱含的.c構建.o規則
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
%.o : %.s #取消內建的隱含規則,只要不在後面寫命令就行
後綴規則#
保持兼容的老規則不推薦,遇到這麼寫的專案再看
引用其它的 Makefile#
將其它 Makefile 檔案的規則和變數加載到當前 Makefile ; 用 include
引入檔案時,Make 會在以下幾個位置查找檔案:
1) 當前目錄
2) 用 -I
指定的目錄
3) 默認的系統目錄(如 /usr/include
)
-include other.make *.mk $(othermake) #可以是檔名,也可用通配符
sinclude other.make *.mk $(othermake) #同上
假目標#
指明 target 不是檔案,而是 make 後跟的操作標籤。用於打包 shell 命令;假目標不能和檔案同名,為確保這一點,用.PHONY
顯式聲明,使存在同名檔案時假目標也能執行;
假目標可依賴其他假目標,形成分層結構。按深度優先序執行操作;
GNU 慣例:
1)all: 默認目標,編譯所有內容。
2)clean: 刪除所有生成的檔案,包括目標檔案和壓縮檔案。
3)install: 將編譯好的程式安裝到系統路徑中。
4)print: 列出自上次構建以來被修改過的源檔案。
5)tar: 打包源程式和相關檔案,生成.tar
檔案。
6)dist: 壓縮 tar 檔案,生成.tar.gz
檔案。
7)TAGS: 一個佔位規則,可以集成ctags
或etags
來生成代碼索引。
8)check/test: 運行測試,通常可以連結到單元測試框架或腳本。
.RECIPEPREFIX = >
.PHONY: cleanall cleanobj cleandiff
cleanall: cleanobj cleandiff
> rm -f program
cleanobj:
> rm -f *.o
cleandiff:
> rm -f *.diff
自動生成依賴關係#
make 可根據依賴關係判斷哪些.c.h 檔案修改了需要重新編譯;自動生成依賴避免大型專案的手動管理依賴苦難;
gcc -MM file.c
生成依賴關係:file.o: file.c defs.h gcc -M main.c
生成依賴關係(含標準庫頭檔案):main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \ /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
GNU 組織建議為為每個 name.c
檔案生成一個 name.d
的 Makefile, .d
存放對應 .c
的依賴關係。讓 make 自動更新或生成 .d
檔案,並把其包含在主 Makefile 中,自動化生成每個檔案的依賴關係
sed 's,pattern,replacement,g'
:字串替換命令
1)pattern
:要匹配的內容;\($*\)\.o[ :]*
;$
表示匹配零個或多個字符,*
表示前面元素出現零次或多次,捕獲組\(\)
中鍵入$*
表示字串會被捕獲到 \(\)
;\.o
表示匹配.o
;[ :]*
表示可匹配多個 :
2)replacement
:正則匹配替換成的內容;\1.o $@ :
;\1
表示第一個捕獲組;$@
**** 表示Makefile 規則中的 target;替換成main.o main.d :
;於是, .d
檔案也會自動更新和自動生成了
3)g
:表示替換所有匹配的地方。
.RECIPEPREFIX = >
sources = foo.c bar.c
%.d: %.c
> @set -e; rm -f $@; \ # @set -e 設置腳本一旦遇到錯誤(命令返回值非零)立即退出;rm -f $@刪除已存在的中間檔案
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \ # 由%.c逐個構建依賴關係,重定向到一個臨時檔案$@.$$$$,相當於%.d.$$$$
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ # < $@.$$$$表示從檔案 %.d.$$$$ 中讀取輸入;替換後的內容輸出到%.d;這一步是為子.d自動更新
rm -f $@.$$$$
include $(sources:.c=.d) # 把變數 $(sources) 所有 .c 的字串都替換成 .d;然後把依賴關係include進來
中間目標處理#
.INTERMEDIATE: mid #顯式聲明mid檔案是中間目標。make不會把它當最終目標處理,當生成mid檔案後不會被自動刪除
.SECONDARY: sec #sec 檔案是個中間目標,但 make 不會在生成最終目標後刪除它
.PRECIOUS: %.o #告訴 make 保留所有 .o 檔案,即使它們是中間目標
條件表達式#
<conditional-directive>
<text-if-true> #
else
<text-if-false>
endif
-----------------ifeq 和 ifneq
.RECIPEPREFIX = >
foo = bar
ifeq ($(foo),bar)
> echo "foo is bar"
endif
ifneq ($(foo),baz)
> echo "foo is not baz"
endif
-----------------ifdef 和 ifndef
.RECIPEPREFIX = >
foo = bar
ifdef foo
> echo "foo is defined"
else
> echo "foo is not defined"
endif
函數#
函數調用語法#
$(<function> <arguments>)
${<function> <arguments>}
#<function> 是函數名;<arguments> 是參數,用逗號分隔;函數名和參數之間用空格隔開
字串處理#
$(subst <from>,<to>,<text>) # 字串替換:把字串<text>中的<from>替換為<to>;並返回
#eg: $(subst ee,EE,feet on the street)
$(patsubst <pattern>,<replacement>,<text>) # 字串替換:把字串<text>中的<pattern>替換為<replacement>;並返回
#eg: $(patsubst %.c,%.o,x.c.c bar.c)
$(strip <string>) # 去掉字串開頭和結尾的空格
#eg: $(strip a b c )
$(findstring <sub_text>,<text>) # 在字串<text>中查找子串<sub_text>
#eg: $(findstring a,a b c) # 返回值:a
$(filter <pattern...>,<text>) # 過濾出<text>中符合格式 <pattern...>的部分
#eg: $(filter %.c %.s,bar.c baz.s ugh.h)
$(filter-out <pattern...>,<text>) # 過濾出<text>中不符合格式 <pattern...>的部分
#eg: $(filter-out main1.o main2.o,main1.o foo.o main2.o)
$(sort <list>) # 排序並去重
#eg: $(sort foo bar lose foo)
$(word <n>,<text>) # 提取<text>中第n個單詞
#eg: $(word 2,foo bar baz) #取出了bar
$(wordlist <start>,<end>,<text>) # 提取單詞範圍
#eg: $(wordlist 2,3,foo bar baz) # 返回值:bar baz
$(words <text>) # 統計單詞數
#eg: $(words foo bar baz) # 返回值:3
$(firstword <text>) # 獲取第一個單詞
#eg: $(firstword foo bar)
檔名操作#
$(dir <path...>) #提取目錄部分
#eg: $(dir src/foo.c hacks) # 返回值:src/ ./
$(notdir <path...>) #提取非目錄部分
#eg: $(notdir src/foo.c hacks) # 返回值:foo.c hacks
$(suffix <names...>) # 提取檔案後綴
#eg: $(suffix src/foo.c src-1.0/bar.c hacks) # 返回值:.c .c
$(basename <names...>) # 提取檔名前綴
#eg: $(basename src/foo.c src-1.0/bar.c hacks) # 返回值:src/foo src-1.0/bar hacks
$(addsuffix <suffix>,<names...>) # 給<names...>中的檔案添加後綴<suffix>
#eg: $(addsuffix .c,foo bar) # 返回值:foo.c bar.c
$(addprefix <prefix>,<names...>) #添加前綴
#eg: $(addprefix src/,foo bar) # 返回值:src/foo src/bar
$(join <list1>,<list2>) #連接兩個列表
#eg: $(join aaa bbb,111 222 333) # 返回值:aaa111 bbb222 333
高級函數#
$(foreach <var>,<list>,<text>) # 循環處理列表:從<list>依次取出單詞賦值給<var>,然後用表達式<text>進行計算,最終返回由每次計算結果用空格連起來的字串
#eg: files := $(foreach n,a b c d,$(n).o) # 返回值:a.o b.o c.o d.o
$(if <condition>,<then-part>,<else-part>) #條件判斷
#eg: $(if 1,yes,no) #yes
$(call <expression>,<parm1>,<parm2>,...) #自定義函數調用:用佔位符 $(1)、$(2) 等表示參數,傳遞到<expression>計算並返回
#eg:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
# 返回值:b a
$(origin <variable>) #檢查變數來源
#返回值:
undefined: 未定義
default: 默認定義
environment: 環境變數
file: Makefile 定義
command line: 命令行定義
override: override定義
automatic: 自動變數
$(shell <command>) #執行Shell命令
#eg: files := $(shell echo *.c)
$(error <text...>) #終止 make 的執行,並顯示指定的錯誤消息
#eg: 檢查變數是否為空
MY_VAR :=
ifeq ($(MY_VAR),)
$(error MY_VAR is not defined or is empty)
endif
$(warning <text...>) #輸出警告,但不會中斷構建過程
#eg: 輸出調試信息
MY_VAR :=
ifeq ($(MY_VAR),)
$(warning MY_VAR is not defined or is empty)
endif
更新函數庫檔案#
-j 並行構建庫可能會影響 ar 打包
#eg:將所有的 .o 檔案打包成 foolib 函數庫
foolib(*.o) : *.o
ar cr foolib *.o
模板#
tree#
<PROJECT_NAME >/
├── build/ # 存放編譯生成的目標檔案、依賴檔案等
├── bin/ # 存放最終生成的可執行檔案
├── inc/ # 存放頭檔案
├── lib/ # 存放庫檔案
├── src/ # 存放源程式檔案 (.c)
├── tests/ # 存放測試程式碼
├── submodule1/ # 第一个子模組
│ └── Makefile # 子模組的 Makefile
├── submodule2/ # 第二个子模組
│ └── Makefile # 子模組的 Makefile
├── Makefile # 頂層 Makefile
└── <其他檔案> # 可能包括其他文檔、配置檔案等
總控 makefile#
刪除生成的目標檔案,以便重新編譯。更穩健的做法是使用 .PHONY
聲明,並在刪除命令前加上 -
,這樣即使某些檔案不存在,也不會報錯:
.RECIPEPREFIX = >
# -------------------------------
# General settings
# -------------------------------
PROJECT_NAME :=
VERSION := 1.0.0
INC_DIR := inc
SRC_DIR := src
LIB_DIR := lib
BIN_DIR := bin
TEST_DIR := tests
BUILD_DIR := build
SUBMODULES := submodule1 submodule2
INSTALL_DIR := /usr/local/bin
TAR_FILE := $(PROJECT_NAME)-$(VERSION).tar.gz
# -------------------------------
# Compiler and flags
# -------------------------------
CC := cc
CXX := g++
AR := ar
AS := as
CO := co
CPP := $(CC) -E
LEX := lex
YACC := yacc
MAKEINFO := makeinfo
CFLAGS := -Wall -Wextra -Og
LIBS :=
ARFLAGS := rv
ASFLAGS :=
CXXFLAGS :=
COFLAGS :=
CPPFLAGS :=
LDFLAGS :=
LFLAGS :=
YFLAGS :=
# -------------------------------
# 非獨立構建加載子模組Makefile(即把子模組當靜態庫和源程式用);各模組獨立構建則不需要
# -------------------------------
#-include $(foreach submodule, $(SUBMODULES), $(submodule)/Makefile)
# -------------------------------
# top level Files
# -------------------------------
SRCS := $(shell find $(SRC_DIR) -name '*.c')
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
DEPS := $(OBJS:.o=.d)
# -------------------------------
# Targets
# -------------------------------
.PHONY: all clean install print tar dist TAGS test $(SUBMODULES) #還應該加個檢查是否安裝相關工具的目標
all: $(PROJECT_NAME)
# Build the main target executable
$(PROJECT_NAME): $(OBJS) $(SUBMODULES)
> $(CC) $(OBJS) -o $(BIN_DIR)/$@ $(LDFLAGS) $(LIBS)
# 構建子模組:遍歷子模組,進入其中執行make;如果找不到makefile,就打印skip
$(SUBMODULES):
> @$(foreach submodule, $(SUBMODULES), \
$(MAKE) -C $(submodule) || echo "Skipping $(submodule)";)
# Automatically generate dependency files
-include $(DEPS)
# Clean target: remove build artifacts
clean:
> rm -rf $(BUILD_DIR) $(BIN_DIR) $(LIB_DIR) $(TAR_FILE)
> $(foreach submodule, $(SUBMODULES), $(MAKE) -C $(submodule) clean || echo "Skipping $(submodule)";)
# Install target: install the program
install: all
> install -m 755 $(BIN_DIR)/$(PROJECT_NAME) $(INSTALL_DIR)
# Print target: list modified source files
print:
> @echo "Modified source files since last build:"
> @find $(SRC_DIR) -type f -newer $(BIN_DIR)/$(PROJECT_NAME) -print || echo "No modified source files."
# Tarball target: create a source tarball
tar:
> tar -cvzf $(TAR_FILE) $(SRC_DIR) $(INC_DIR) $(LIB_DIR) Makefile $(SUBMODULES)
# Dist target: compress the tarball
dist: tar
> gzip $(TAR_FILE)
# TAGS target: generate ctags or etags
TAGS:
> ctags -R $(SRC_DIR) $(INC_DIR) $(SUBMODULES)
# Test target: run unit tests (assuming a separate test suite)
test:
> @echo "Running tests..."
> $(MAKE) -C $(TEST_DIR)
# -------------------------------
# Compilation rules
# -------------------------------
# Rule to create object files,這裡妙在.c.h更新,則.o更新;.o更新引用這條規則,.d也更新
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
> @mkdir -p $(@D)
> $(CC) $(CFLAGS) -MD -MP -c $< -o $@
# Include all dependency files
-include $(DEPS)
獨立構建 submodule makefile#
非獨立構建 submodule makefile#
.RECIPEPREFIX = >
# 子模組配置
MODULE_NAME := submodule1
SRC_DIR := src
INC_DIR := inc
BUILD_DIR := build
LIB_DIR := ../lib
CC := cc
CFLAGS := -Wall -Wextra -Og -I$(INC_DIR)
AR := ar
ARFLAGS := rv
SRCS := $(shell find $(SRC_DIR) -name '*.c')
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
LIB := $(LIB_DIR)/lib$(MODULE_NAME).a
DEPS := $(OBJS:.o=.d)
.PHONY: all clean
# 默認目標
all: $(LIB)
# 生成靜態庫
$(LIB): $(OBJS)
> @mkdir -p $(LIB_DIR)
> $(AR) $(ARFLAGS) $@ $^
# 編譯規則
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
> @mkdir -p $(@D)
> $(CC) $(CFLAGS) -MMD -MP -c $< -o $@
# 清理規則
clean:
> rm -rf $(BUILD_DIR) $(LIB)
# 自動包含依賴檔案
-include $(DEPS)