makefile
一、初識makefile
想要掌握makefile,首先需要了解兩個概念,⼀個是⽬標(target),另⼀個就是依賴(dependency)。⽬標就是指要⼲什么,或說運⾏ make 后⽣成什么,⽽依賴是告訴 make 如何去做以實現⽬標。在 Makefile 中,⽬標和依賴是通過規則(rule)來表達的。
(一)、目標
首次編寫makefile
all:
echo "Hello world"
上面Makefile 中的 all 就是我們 的⽬標,⽬標放在‘:’的前⾯,其名字可以是由字⺟和下划線‘_’組成 。echo “Hello World”就是⽣成⽬標的命令,這些命令可以是任何你可以在你的環境中運⾏的命令以及 make 所定義的函數等等。all ⽬標的定義,其實是定義了如何⽣成 all ⽬標,這我們也稱之為規則.
makefile定義多個目標
all:
echo "Hello world"
test:
echo "test game"
下面是運行的結果
由此可見,⼀個 Makefile 中可以定義多個⽬標。調⽤ make 命令時,我們得告訴它我們的⽬標是什么,即要它⼲什么。當沒有指明具體的⽬標是什么 時,那么 make 以 Makefile ⽂件中定義的第⼀個⽬標作為這次運⾏的⽬標。這“第⼀個”⽬標也稱之 為默認⽬標(和是不是all沒有關系)。當 make 得到⽬標后,先找到定義⽬標的規則,然后運⾏規則中的命令來達到構建⽬標的⽬的。
makefile中取消多余的命令行顯示
在上面的指令中,多了很多的echo "......"
的內容,這部分不是我們所期望的,如果要去掉,需要對上面的makefile進行一個改動,也就是在命令前加上一個@,這個符號就是告訴make,在運行的時候這一行命令不顯示出來。
all:
@echo "Hello world"
test:
@echo "test game"
運行結果:
緊接着對makefile進行如下的改動,在all的后面加上test
all: test
@echo "Hello world"
test:
@echo "test game"
運行結果如下:
如上圖所示,此時test也被構建了。
(二)、依賴
如上面的makefile,all ⽬標后⾯的 test 是告訴 make,all ⽬標依賴 test ⽬標,這⼀依賴⽬標在 Makefile 中⼜被稱之為先決條件。出現這種⽬標依賴關系時,make⼯具會按 從左到右的先后順序先構建規則中所依賴的每⼀個⽬標。如果希望構建 all ⽬標,那么make 會在構建它之 前得先構建 test ⽬標,這就是為什么我們稱之為先決條件的原因。
(三)、規則
⼀個規則是由⽬標(targets)、先決條件(prerequisites)以及命令(commands)所組成的。
需要指出的是,⽬標和先決條件之間表達的就是依賴關系(dependency),這種依賴關系指明在構建⽬標之前,必須保證先決條件先滿⾜(或構建)。⽽先決條件可以是其它的⽬標,當先決條件是⽬標時,其必須先被構建出來。還有就是⼀個規則中⽬標可以有多個,當存在多個⽬標,且這⼀規則是 Makefile 中的第⼀個規則時,如果我們運⾏ make 命令不帶任何⽬標,那么規則中的第⼀個⽬標將被視為是缺省⽬ 標。
規則的功能就是指明 make 什么時候以及如何來為我們重新創建⽬標,在 Hello World 例⼦中,不論我們 在什么時候運⾏ make 命令(帶⽬標或是不帶⽬標),其都會在終端上打印出信息來,和我們采⽤ make 進⾏代碼編譯時的表現好象有些不同。當采⽤ Makefile 來編譯程序時,如果兩次編譯之間沒有任何代碼 的改動,理論上說來,我們是不希望看到 make 會有什么動作的,只需說“⽬標是最新的”,⽽我們的最終 ⽬標也是希望構建出⼀個“聰明的” Makefile 的。與 Hello World 相⽐不同的是,采⽤ Makefile 來進⾏ 代碼編譯時,Makefile 中所存在的先決條件都是具體的程序⽂件,后⾯我們會看到。
規則語法:
targets : prerequisites
command
...
如上, all 是⽬標,test 是 all ⽬標的依賴⽬標,⽽@echo “Hello World”則是⽤於⽣成 all ⽬標的命令。
二、makefile的原理
foo.c
#include <stdio.h>
void foo()
{
printf("this is foo() !\n");
}
main.c
extern void foo();
int main()
{
foo();
return 0;
}
makefile
all: main.o foo.o
gcc -o simple main.o foo.o
main.o: main.c
gcc -o main.o -c main.c
foo.o: foo.c
gcc -o foo.o -c foo.c
clean:
rm simple main.o foo.o
上面的三段代碼生成的依賴樹
編譯
上面的展示了測試結果,注意到了第⼆次編譯並沒有構建⽬標⽂件的動作嗎?但為什么有構建simple可執⾏程序的動作呢?為了明⽩為什么,我們需要了解 make 是如何決定哪些⽬標(這⾥是⽂件)是需要重新編譯的。為什么 make會知道我們並沒有改變 main.c 和 foo.c 呢?答案很簡單,通過⽂件的時間戳!當 make 在運⾏⼀個規則時,我們前⾯已經提到 了⽬標和先決條件之間的依賴關系,make 在檢查⼀個規則時,采⽤的⽅法是:如果先決條件中相關的⽂件的時間戳⼤於⽬標的時間戳,即先決條件中的⽂件⽐⽬標更新,則知道有變化,那么需要運⾏規則當中 的命令重新構建⽬標。這條規則會運⽤到所有與我們在 make時指定的⽬標的依賴樹中的每⼀個規則。⽐如,對於 simple 項⽬,其依賴樹中包括三個規則,make 會檢查所有三個規則當中的⽬標(⽂件)與先決條件(⽂件)之間的時間先后關系,從⽽來決定是否要重新創建規則中的⽬標。
問題一:第二次構建的時候為什么simple會被重新構建?
是因為simple文件不存在,我們在這次構建的目標是all,而all在我們編譯的過成中並不生成,所以第二次make的時候找不到,所以又重新編譯了一遍。
修改makefile
simple: main.o foo.o
gcc -o simple main.o foo.o
main.o: main.c
gcc -o main.o -c main.c
foo.o: foo.c
gcc -o foo.o -c foo.c
clean:
rm simple main.o foo.o
運行結果
一個文件是否改變不是看這個文件的大小是否改變,而是看這個文件的時間戳是否發生了變化。可以直接使用touch指令對文件的時間戳進行修改。
這時候就會進行重新編譯
假目標
如上圖,如果我們的創建了一個clean文件之后,繼續去運行make clean,這時候不是按照我們前面運行的make clean進行清理文件。
為什么出現上面的原因?
因為這個時候make 將clean單程是一個文件,並且在當前的目錄下找到了這個文件,再加上clean目標沒有任何的先決條件,這時候進行make clean時,系統會認為clean是最新的
如何解決上面的問題?使用假目標,假目標最從常用清凈就是避免所定義的目標和的已經存在文件是從重名的情況,假⽬標可以采⽤.PHONY 關鍵字來定義,需要注意的是其必須是⼤寫字⺟。使用假目標修改makefile
.PHONY: clean
simple: main.o foo.o
gcc -o simple main.o foo.o
main.o: main.c
gcc -o main.o -c main.c
foo.o: foo.c
gcc -o foo.o -c foo.c
clean:
rm simple main.o foo.o
運行結果:
采⽤.PHONY 關鍵字聲明⼀個⽬標后,make 並不會將其當作⼀個⽂件來處理,⽽只是當作⼀個概念上的⽬標。對於假⽬標,我們可以想像的是由於並不與⽂件關聯,所以每⼀次 make 這個假⽬標時,其所在的規則中的命令都會被執⾏。
變量
先看代碼
.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)
$(CC) -o $(EXE) $(OBJS)
main.o: main.c
$(CC) -o main.o -c main.c
foo.o: foo.c
$(CC) -o foo.o -c foo.c
clean:
$(RM) $(EXE) $(OBJS)
運行結果:
變量的使用可以提高makefile的可維護性。⼀個變量的定義很簡單,就是⼀個名字(變量名)后⾯跟上⼀個等號,然后在等號的后⾯放這個變量所期望的值。對於變量的引⽤,則需要采⽤$(變量名)或者${變量名}這種模式。在這個 Makefile 中,我們引⼊了 CC 和 RM 兩個變量,⼀個⽤於保存編譯器名,⽽另⼀個⽤於指示刪除⽂件的命令是什么。還有就是引⼊了 EXE 和 OBJS 兩個變量,⼀個⽤於存放可執⾏⽂件名,可另⼀個則⽤於放置所有的⽬標⽂件名。采⽤變量的話,當我們需要更改編譯器時,只需更改變量賦值的地⽅,⾮常⽅便,如果不采⽤變量,那我們得更改每⼀個使⽤編譯器的地⽅,很是麻煩。
1.自動變量(☆☆☆☆☆)
對於每⼀個規則,⽬標和先決條件的名字會在規則的命令中多次出現,每⼀次出現都是⼀種麻煩,更為麻煩的是,如果改變了⽬標或是依賴的名,那得在命令中全部跟着改。有沒有簡化這種更改的⽅法呢?這我們需要⽤到 Makefile 中的⾃動變量,最常用包括:
- $@⽤於表示⼀個規則中的⽬標。當我們的⼀個規則中有多個⽬標時,$@所指的是其中任何造成命令被運⾏的⽬標。
- $^則表示的是規則中的所有先擇條件。
- $<表示的是規則中的第⼀個先決條件。
.PHONY:all
all:first second third
@echo "\$$@ = $@"
@echo "\$$^ = $^"
@echo "\$$< = $<"
first second third:
運行結果
需要注意的是,在 Makefile 中‘$’具有特殊的意思,因此,如果想采⽤ echo 輸出‘$’,則必需⽤兩個連着的‘$’。還有就是,$@對於 Shell 也有特殊的意思,我們需要在“$$@”之前再加⼀個脫字符‘\’。
修改simple的makefile
.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)
$(CC) -o $@ $^
main.o: main.c
$(CC) -o $@ -c $^
foo.o: foo.c
$(CC) -o $@ -c $^
clean:
$(RM) $(EXE) $(OBJS)
運行結果:
2.特殊變量
(1)MAKE變量
它表示的是make 命令名是什么。當我們需要在 Makefile 中調⽤另⼀個 Makefile 時需要⽤到這個變量,采⽤這種⽅式,有利於寫⼀個容易移植的 Makefile。
.PHONY: clean
all:
@echo "MAKE = $(MAKE)"
運行結果:

(2)MAKECMDGOALS變量
它表示的是當前⽤戶所輸⼊的 make ⽬標是什么。
.PHONY: all clean
all clean:
@echo "\$$@ = $@"
@echo "MAKECMDGOALS = $(MAKECMDGOALS)"
運行結果:

從測試結果看來,MAKECMDGOALS 指的是⽤戶輸⼊的⽬標,當我們只運⾏ make 命令時,雖然根據
Makefile 的語法,第⼀個⽬標將成為缺省⽬標,即 all ⽬標,但 MAKECMDGOALS 仍然是空,⽽不是
all,這⼀點我們需要注意。
3.遞歸擴展變量
示例了使⽤等號進⾏變量定義和賦值,對於這種只⽤⼀個“=”符號定義的變量,我們稱之為遞歸擴展變量(recursively expanded variable)。
.PHONY: all
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
@echo $(foo)
運行結果:
除了遞歸擴展變量還有⼀種變量稱之為簡單擴展變量(simply expanded variables),是⽤“:=”操作符
來定義的。對於這種變量,make 只對其進⾏⼀次掃描和替換。
.PHONY: all
x = foo
y = $(x) b
x = later
xx := foo
yy := $(xx) b
xx := later
all:
@echo "x = $(y), xx = $(yy)"
運行結果:
另外還有一種條件賦值符“?=”,條件賦值的意思是當變量以前沒有定義時,就定義它並且將左邊的值賦值給它,如果已經定義了那么就不再改變其值。條件賦值類似於提供了給變量賦缺省值的功能。
.PHONY: all
foo = x
foo ?= y
bar ?= y
all:
@echo "foo = $(foo), bar = $(bar)"
運行結果:
此外,還有"+="操作符,對變量進⾏賦值的⽅法
.PHONY: all
objects = main.o foo.o bar.o utils.o
objects += another.o
all:
@echo $(objects)
運行結果
4.override指令
在設計 Makefile 時,我們並不希望⽤戶將我們在 Makefile 中定義的某個變量覆蓋掉,那就得⽤ override 指令了。
.PHONY: all
override foo = x
all:
@echo "foo = $(foo)"
運行結果:
5.模式
如果對於每⼀個⽬標⽂件都得寫⼀個不同的規則來描述,那會是⼀種“體⼒活”,太繁了!對於⼀個⼤型項⽬,就更不⽤說了。Makefile 中的模式就是⽤來解決我們的這種煩惱的。
.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) -o $@ -c $^
clean:
$(RM) $(EXE) $(OBJS)
與 simple 項⽬前⼀版本的 Makefile 相⽐,最為直觀的改變就是從⼆條構建⽬標⽂件的規則變成了⼀條。
模式類似於我們在 Windows 操作系統中所使⽤的通配符,當然是⽤“%”⽽不是“*”。采⽤了模式以后,
不論有多少個源⽂件要編譯,我們都是應⽤同⼀個模式規則的,很顯然,這⼤⼤的簡化了我們的⼯作。使
⽤了模式規則以后,你同樣可以⽤這個 Makefile 來編譯或是清除 simple 項⽬,這與前⼀版本在功能上是
完全⼀樣的。
6.函數
函數是 Makefile 中的另⼀個利器,現在我們看⼀看采⽤函數如何來簡化 simple 項⽬的 Makefile。對於
simple 項⽬的 Makefile,盡管我們使⽤了模式規則,但還有⼀件⽐較惱⼈的事,我們得在這個Makefile
中指明每⼀個需要被編譯的源程序。對於⼀個源程序⽂件⽐較多的項⽬,如果每增加或是刪除⼀個⽂件都
得更新 Makefile,其⼯作量也不可⼩視!
采⽤了 wildcard 和 patsubst 兩個函數后 simple 項⽬的 Makefile。可以先⽤它來編譯⼀下 simple 項⽬代碼以驗證其功能性。需要注意的是函數的語法形式很是特別,對於我們來說只要記住其形式就⾏了。
.PHONY: clean
CC = gcc
RM = rm
EXE = simple
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SRCS))
$(EXE): $(OBJS)
$(CC) -o $@ $^
%.o: %.c
$(CC) -o $@ -c $^
clean:
$(RM) $(EXE) $(OBJS)
現在,我們來模擬增加⼀個源⽂件的情形,看⼀看如果我們增加⼀個⽂件,在 Makefile 不做任
何更改的情況下其是否仍能正常的⼯作。增加⽂件的⽅式仍然是采⽤ touch 命令,通過 touch 命令
⽣成⼀個內容是空的 bar.c 源⽂件,然后再運⾏ make 和 make clean。
(1)addprefix函數
addprefix 函數是⽤來在給字符串中的每個⼦串前加上⼀個前綴,其形式是:$(addprefix prefix, names...)
.PHONY:all
without_dir= foo.c bar.c main.c
with_dir:=$(addprefix objs/, $(without_dir))
all:
@echo $(with_dir)
運行結果
(2)filter函數
filter 函數⽤於從⼀個字符串中,根據模式得到滿⾜模式的字符串,其形式是:$(filter pattern..., text)
.PHONY: all
sources = foo.c bar.c baz.s ugh.h
sources := $(filter %.c %.s, $(sources))
all:
@echo $(sources)
運行結果
從結果來看,經過 filter 函數的調⽤以后,source變量中只存在.c ⽂件和.s ⽂件了,⽽.h ⽂件則則被過濾掉了。
(3)filter-out函數
filter-out 函數⽤於從⼀個字符串中根據模式濾除⼀部分字符串,其形式是:$(filter-out pattern..., text)
.PHONY: all
objects = main1.o foo.o main2.o bar.o
result := $(filter-out main%.o, $(objects))
all:
@echo $(result)
(4)patsubst函數(☆☆☆)
patsubst 函數是⽤來進⾏字符串替換的,其形式是:$(patsubst pattern, replacement, text)
.PHONY:all
mixed=foo.c bar.c main.o
objects:=$(patsubst %.c, %.o, $(mixed))
all:
@echo $(objects)
運行結果:
上述代碼中 mixed 變量中包括了.c ⽂件也包括了.o ⽂件,采⽤patsubst 函數進⾏字符串替換時,我們希望將所有的.c ⽂件都替換成.o ⽂件。上圖是最后的運⾏結果。
(5)strip
strip 函數⽤於去除變量中的多余的空格,其形式是:$(strip string)
.PHONY:all
original=foo.c bar.c
stripped:=$(strip $(original))
all:
@echo "original = $(original)"
@echo "stripped = $(stripped)"
運行結果:
(6)wildcard函數(☆☆☆)
wildcard 是通配符函數,通過它可以得到我們所需的⽂件,這個函數類似我們在 Windows 或Linux 命
令⾏中的“*”。其形式是:$(wildcard pattern)
.PHONY:all
SRC=$(wildcard *.c)
all:
@echo "SRC = $(SRC)"
運行結果:
三、makefile拔高
1.創建目錄
毫⽆疑問,我們在編譯項⽬之前希望⽤於存放⽂件的⽬錄先准備好,當然,我們可以在編譯之前通過⼿動
來創建所需的⽬錄,但這⾥我們希望采⽤⾃動的⽅式。makefile的依賴樹的樣子是這樣的。
這個依賴圖從概念上說來是對的,但從 Makefile 的實現上存在⼀些問題。我們說 all 是⼀個⽬標,如果 all 直接依賴 objs 和 exes ⽬錄的話,那應該如何創建⽬錄呢?首先寫一個makefile【注意代碼的最后一行不能換行,表示一個依賴】
.PHONY:all
MKDIR=mkdir
DIRS=objs exes
all:$(DIRS)
運行結果
改進依賴關系圖
改進上面的makefile
.PHONY:all
MKDIR=mkdir
DIRS=objs exes
all:$(DIRS)
$DIRS:
$(MKDIR) $@
運行結果
在這個 Makefile 中,需要注意的是 OBJS 變量即是⼀個依賴⽬標也是⼀個⽬錄,在不同的場合其意思是不同的。⽐如,第⼀次 make 時,由於 objs 和 exes ⽬錄都不存在,所以 all ⽬標將它們視作是⼀個先決條件或者說是依賴⽬標,接着 Makefile 先根據⽬錄構建規則構建 objs 和 exes ⽬標,即Makefile 中的第⼆條規則就被派上了⽤場。構建⽬錄時,第⼆條規則中的命令被執⾏,即真正的創建了 objs 和 exes ⽬錄。當我們第⼆次進⾏ make 時,此時,make 仍以 objs 和 exes 為⽬標,但從⽬錄構建規則中發現,這兩個⽬標並沒有依賴關系,⽽且能從當前⽬錄中找到 objs 和 exes ⽬錄,即認為 objs 和 exes ⽬標都是最新的,所以不⽤再運⾏⽬錄構建規則中的命令來創建⽬錄。
更新后代碼的依賴樹關系
接下來也得為 Makefile 創建⼀個 clean ⽬標,專⻔⽤來刪除所⽣成的⽬標⽂件和可執⾏⽂件。加 clean 規則還是相當簡單,需要再增加了兩個變量,⼀個是RM,另⼀個則是 RMFLAGS。
.PHONY:all
MKDIR=mkdir
DIRS=objs exes
RM=rm
RMFLAGS=-fr
all:$(DIRS)
$(DIRS):
$(MKDIR) $@
clean:
$(RM) $(RMFLAGS) $(DIRS)
運行結果
2.增加頭文件
.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
EXE=test
DIRS=objs exes
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
all:$(DIRS) $(EXE)
$(DIRS):
$(MKDIR) $@
$(EXE):$(OBJS)
$(CC) -o $@ $^
%.o:%.c
$(CC) -o $@ -c $^
clean:
$(RM) $(RMFLAGS) $(DIRS) $(EXE) $(OBJS)
運行結果
3.將文件放進目錄
為了將⽬標⽂件或是可執⾏程序分別放⼊所創建的 objs 和 exes ⽬錄中,我們需要⽤到 Makefile中的⼀
個函數 —— addprefix。對上面的makefile進行修改。
.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
DIR_OBJS=objs
DIR_EXE=exes
DIRS=$(DIR_OBJS) $(DIR_EXE)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
all:$(DIRS) $(EXE)
$(DIRS):
$(MKDIR) $@
$(EXE):$(OBJS)
$(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c
$(CC) -o $@ -c $^
clean:
$(RM) $(RMFLAGS) $(DIRS)
運行結果
最⼤的變化除了增加了對於 addprefix 函數的運⽤為每⼀個⽬標⽂件加上“objs/”前綴外,還有⼀個很⼤的變化是,我們需要在構建⽬標⽂件的模式規則中的⽬標前也加上“objs/”前綴,即增加“$(DIR_OBJS)/”前綴。之所以要加上,是因為規則的命令中的-o 選項需要以它作為⽬標⽂件的最終⽣成位置,還有就是因為 OBJS 也加上了前綴,⽽要使得 Makefile 中的⽬標創建規則被運⽤,也需要采⽤相類似的格式,即前⾯有“objs/”。此外,由於改動后的 Makefile 會將所有的⽬標⽂件放⼊ objs ⽬錄當中,⽽我們的 clean 規則中的命令包含將 objs ⽬錄刪除的操作,所以我們可以去除命令中對 OBJS 中⽂件的刪除。這導致的改動就是 Makefile 中的最后⼀⾏中刪除了$(OBJS)。同樣的方法將 test 放入到 exes 文件夾中。
4.更復雜的依賴關系
假設我們對項⽬已經進⾏了⼀次編譯(這⼀點⾮常重要,否則你看不到將要說的問題),接着對 foo.h⽂件進⾏了修改,如下
// foo.h
#ifndef __FOO_H
#define __FOO_H
void foo(int value);
#endif
並不對 foo.c進⾏相應的更改。這樣⼀改的話,由於聲名與定義不相同,所以理論上編譯時應當出錯。【先不要進行make clean,直接進行make,否則可能出現的結果和我們預期的不一致】
運行結果
通過運行結果可以看到,第一次make的時候並沒有報錯,但是如果我們事先做了make clean,這個報錯信息又會出現,但是我們在正常的工程編譯中不能不停的進行make clean,這樣太費時。
分析一下第一次make出現異常的原因,下圖是makefile所表達的依賴關系書以及規則的映射關系圖。
從依賴關系圖中可以發現,其中並沒有出現對 foo.h 的依賴關系,這就是為什么我們改動頭⽂件時,make ⽆法生效的原因!所以我們需要在構建目標的規則中增加對於foo.h的依賴。這里我們需要使用自動變量 $< 。這個變量與$的區別是,其只表示所有的先決條件中的第⼀個,⽽$則表示全部先決條件。之所以要⽤$<是因為,我們不希望將 foo.h 也作為⼀個⽂件讓 gcc 去編譯,這樣的話會出錯。
.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
DIR_OBJS=objs
DIR_EXE=exes
DIRS=$(DIR_OBJS) $(DIR_EXE)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
all:$(DIRS) $(EXE)
$(DIRS):
$(MKDIR) $@
$(EXE):$(OBJS)
$(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c foo.h
$(CC) -o $@ -c $<
clean:
$(RM) $(RMFLAGS) $(DIRS)
現在,將 foo.h 改回以前的狀態,即去除 foo ()函數中的 int 參數,然后編譯,這次編譯當然是成功的,
接着再加⼊ int 參數,再編譯。你發現這次真的能發現問題了!更改后的依賴關系圖如下圖
編譯結果:
當項⽬復雜時,如果我們要將每⼀個頭⽂件都寫⼊到 Makefile 相對應的規則中,這將會是⼀個惡夢!所以我們需要找到另⼀種更好的⽅法。
如果有哪⼀個⼯具能幫助我們列出⼀個源程序所包含的頭⽂件那就好了,這樣的話我們或許可以在 make
時,動態的⽣成⽂件的依賴關系。還真是存在這么⼀個⼯具!就是我們的編譯器 ——gcc。下圖列出了采⽤ gcc 的-M 選項和-MM 選項列出 foo.c 對其它⽂件的依賴關系的結果,從結果你可以看出它們會列出 foo.c 中直接或是間接包含的頭⽂件。-MM 選項與-M 選項的區別是,-MM選項並不列出對於系統頭⽂件的依賴關系,⽐如 stdio.h 就屬於系統頭⽂件。其道理是,絕⼤多數情況我們並不會改變系統的頭⽂件,⽽只會對⾃⼰項⽬的頭⽂件進⾏更改。
對於采⽤ gcc 的-MM 的選項所⽣成的結果,還有⼀個問題,因為我們⽣成的⽬標⽂件是放在 objs⽬錄當
中的,因此,我們希望依賴關系中也包含這⼀⽬錄信息,否則,在我們的 Makefile 中,跟本沒有辦法做
到將⽣成的⽬標⽂件放到 objs ⽬錄中去,這在前⾯的 Makefile 中我們就是這么做的。在使⽤新的⽅法
時,我們仍然需要實現同樣的功能。這時,我們需要⽤到 sed ⼯具了,這是 Linux 中⾮常常⽤的⼀個字符
串處理⼯具。下圖是采⽤ sed 進⾏查找和替換以后的輸出結果,從結果中我們可以看到,就是在 foo.o 之前加上了“objs/”前綴。對於 sed 的⽤法說明可能超出了本⽂的范圍,如果你不熟悉其功能,可以找⼀些資料看⼀看。
gcc -MM foo.c | sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g'
執行結果
gcc 還有⼀個⾮常有⽤的選項是-E,這個命令告訴 gcc 只做預處理,⽽不進⾏程序編譯,在⽣成依賴關
系時,其實我們並不需要 gcc 去編譯,只要進⾏預處理就⾏了。這可以避免在⽣成依賴關系時出現沒有必
要的 warning,以及提⾼依賴關系的⽣成效率。
現在,我們已經有了⾃動⽣成依賴關系的⽅法了,那如何將其整合到我們的 Makefile 中呢?顯然,⾃動
⽣成的依賴信息,不可能直接出現在我們的 Makefile 中,因為我們不能動態的改變 Makefile中的內容,
那采⽤什么⽅法呢?先別急,第⼀步我們能做的是,為每⼀個源⽂件通過采⽤ gcc 和 sed⽣成⼀個依賴關
系⽂件,這些⽂件我們采⽤.dep 后綴結尾。從模塊化的⻆度來說,我們不希望.dep⽂件與.o ⽂件或是可
執⾏⽂件混放在⼀個⽬錄中。為此,創建⼀個新的 deps ⽬錄⽤於存放依賴⽂件似乎更為合理。
.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
DIR_OBJS=objs
DIR_EXE=exes
DIR_DEPS=deps
DIRS=$(DIR_OBJS) $(DIR_EXE) $(DIR_DEPS)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
all:$(DIRS) $(DEPS) $(EXE)
$(DIRS):
$(MKDIR) $@
$(EXE):$(OBJS)
$(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c
$(CC) -o $@ -c $^
$(DIR_DEPS)/%.dep: %.c
@echo "Making $@ ..."
@set -e; \
$(RM) $(RMFLAGS) $@.tmp ; \
$(CC) -E -MM $^ > $@.tmp ; \
sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g' < $@.tmp > $@ ; \
$(RM) $(RMFLAGS) $@.tmp
clean:
$(RM) $(RMFLAGS) $(DIRS)
運行結果:
5.包含文件
現在依賴⽂件已經有了,那如何為我們的 Makefile 所⽤呢?這需要⽤到 Makefile 中的 include關鍵字,
它如同 C/C++中的#include 預處理指令。現在要做的就是在 Makefile 中加⼊對所有依賴⽂件的包含功
能,更改后的 Makefile 如下所示。
.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
DIR_OBJS=objs
DIR_EXE=exes
DIR_DEPS=deps
DIRS=$(DIR_OBJS) $(DIR_EXE) $(DIR_DEPS)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS=$(SRCS:.c=.dep)
DEPS:=$(addprefix $(DIR_DEPS)/, $(DEPS))
all:$(DIRS) $(DEPS) $(EXE)
include $(DEPS)
$(DIRS):
$(MKDIR) $@
$(EXE):$(OBJS)
$(CC) -o $@ $^
$(DIR_OBJS)/%.o:%.c
$(CC) -o $@ -c $^
$(DIR_DEPS)/%.dep: %.c
@echo "Making $@ ..."
@set -e; \
$(RM) $(RMFLAGS) $@.tmp ; \
$(CC) -E -MM $^ > $@.tmp ; \
sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g' < $@.tmp > $@ ; \
$(RM) $(RMFLAGS) $@.tmp
clean:
$(RM) $(RMFLAGS) $(DIRS)
運行結果
從上圖可以看到,由於 make 在處理Makefile 的 include 命令時,發現找不到 deps/foo.dep 和 deps/main.dep,所以出錯了。如何理解這⼀錯誤呢?從這⼀錯誤我們可知,make 對於 include 的處理是先於 all ⽬標的構建的,這樣的話,由於依賴⽂件是在構建 all ⽬標時才創建的,所以很⾃然 make 在處理 include 指令時,是找不到依賴⽂件的。我們說第⼀次 make 的確沒有依賴⽂件,所以 include 出錯也是正常的,那能不能讓 make忽略這⼀錯誤呢?可以的,在 Makefile 中,如果在 include 前加上⼀個‘-’號,當 make 處理這⼀包含指示時,如果⽂件不存在就會忽略這⼀錯誤。除此之外,需要對於 Makefile 中的 include 有更為深⼊的了解。當 make 看到 include 指令時,會先找⼀下有沒有這個⽂件,如果有則讀⼊。接着,make 還會看⼀看對於包含進來的⽂件,在 Makefile 中是否存在規則來更新它。如果存在,則運⾏規則去更新需被包含進來的⽂件,當更新完了之后再將其包含進來。在我們的這個 Makefile 中,的確存在⽤於創建(或更新)依賴⽂件的規則。那為什么 make 沒有幫助我們去創建依賴⽂件,⽽只是抱怨呢?因為 make 想創建依賴⽂件時,deps ⽬錄還沒有創建,所以⽆法成功的構建依賴⽂件。
有了這些信息之后,我們需要對 Makefile 的依賴關系進⾏調整,即將 deps ⽬錄的創建放在構建依賴⽂
件之前。其改動就是在依賴⽂件的創建規則當中增加對 deps ⽬錄的信賴,且將其當作是第⼀個先決條
件。采⽤同樣的⽅法,我們將所有的⽬錄創建都放到相應的規則中去。更改后的 Makefile如下所示。
.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc
DIR_OBJS=objs
DIR_EXE=exes
DIR_DEPS=deps
DIRS=$(DIR_OBJS) $(DIR_EXE) $(DIR_DEPS)
EXE=test
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS=$(SRCS:.c=.dep)
DEPS:=$(addprefix $(DIR_DEPS)/, $(DEPS))
all: $(EXE)
-include $(DEPS)
$(DIRS):
$(MKDIR) $@
$(EXE):$(DIR_EXE) $(OBJS)
$(CC) -o $@ $(filter %.o, $^)
$(DIR_OBJS)/%.o:$(DIR_OBJS) %.c
$(CC) -o $@ -c $(filter %.c, $^)
$(DIR_DEPS)/%.dep:$(DIR_DEPS) %.c
@echo "Making $@ ..."
@set -e; \
$(RM) $(RMFLAGS) $@.tmp ; \
$(CC) -E -MM $(filter %.c, $^) > $@.tmp ; \
sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g' < $@.tmp > $@ ; \
$(RM) $(RMFLAGS) $@.tmp
clean:
$(RM) $(RMFLAGS) $(DIRS)
我們使⽤了 filter 函數將所依賴的⽬錄從先決條件中去除,否則的話會出現錯誤。正如前⾯所提及的,當make 看到 include 指令時,會試圖去構建所需包含進來的依賴⽂件,這樣⼀來,我們並不需要讓 all⽬錄依賴依賴⽂件,也就是從 all 規則中去除了對 DEPS 的依賴。6.
6.條件語法
當 make 看到條件語法時將⽴即對其進⾏分析,這包括 ifdef、ifeq、ifndef 和 ifneq 四種語句形式。這
也說明⾃動變量(參⻅ 1.5.1 節)在這些語句塊中不能使⽤,因為⾃動變量的值是在命令處理階段才被賦
值的。如果⾮得⽤條件語法,那得使⽤ Shell 所提供的條件語法⽽不是 Makefile 的。Makefile 中的條件語法有三種形式,如下所示。其中的 conditional-directive 可以是 ifdef、ifeq、ifndef 和 ifneq 中的任意⼀個。
conditional-directive
text-if-true
endif
或
conditional-directive
text-if-true
else
text-if-false
endif
或
conditional-directive
text-if-one-is-true
else conditional-directive
text-if-true
else
text-if-false
endif
舉例
.PHONY: all
sharp = square
desk = square
table = circle
ifeq ($(sharp), $(desk))
result1 = "desk == sharp"
endif
ifneq "$(table)" 'square'
result2 = "table != square"
endif
all:
@echo $(result1)
@echo $(result2)
運行結果
ifdef和ifndefg示例
.PHONY: all
foo = defined
ifdef foo
result1 = "foo is defined"
endif
ifndef bar
result2 = "bar is not defined"
endif
all:
@echo $(result1)
@echo $(result2)
運行結果