banner
Zein

Zein

x_id

make最佳實踐手冊

# 假目標
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>包括 allbasicverboseimplicitjobsmakefile
-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 變數,不必修改規則;

命名規則: 可包含字母、數字、下劃線 (_),且可以以數字開頭。大小寫敏感;系統變數通常全大寫,如 CCCFLAGS。用戶自定義變數建議駝峰命名,如 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 $? dependency1target 新,則 $?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.cdir/file2.c,則 $(^D)dir
$(^F)所有依賴檔案的檔名部分的集合(去重)target: dir/file1.c dir/file2.c command $(^F) 如依賴檔案是 dir/file1.cdir/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 的形式重載。

以下是常用內置自動推導規則:

targetprerequisitescommand
<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
CCC 语言编译器cc
CXXC++ 语言编译器g++
CO從 RCS 檔案中擴展檔案程序co
CPPC 程式的預處理器$(CC) -E
FCFortran 和 Ratfor 編譯器f77
LEXLex 文法分析器(針對 C 或 Ratfor)lex
PCPascal 編譯器pc
YACCYacc 文法分析器(針對 C 程式)yacc
MAKEINFO將 Texinfo 檔案轉換為 Info 格式的程序makeinfo
參數變數描述默認值
ARFLAGSAR 命令的參數rv
ASFLAGS匯編器的參數
CFLAGSC 语言编译器的參數
CXXFLAGSC++ 语言编译器的參數
COFLAGSRCS 命令的參數
CPPFLAGSC 預處理器的參數
FFLAGSFortran 編譯器的參數
LDFLAGS連結器的參數
LFLAGSLex 文法分析器的參數
PFLAGSPascal 編譯器的參數
RFLAGSRatfor 編譯器的參數
YFLAGSYacc 文法分析器的參數

自動推導構建規則#

GNUmake 可自動推導某些 prerequisites 到 target 的構建規則,比如 C 專案每個 x.o 檔案後必然有一個同名x.c依賴,必然有一條cc -c x.c

自動推導有個問題就是規則匹配優先級:比如你顯式指定foo.o : foo.p; 但檔案目錄下存在foo.c;此時會自動推導出foo.c構建foo.o的規則;make -rmake --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: 一個佔位規則,可以集成ctagsetags來生成代碼索引。
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)

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。