第六章:Makefile中的變量
在Makefile中,變量是一個名字(像是C語言中的宏),代表一個文本字符串(變量的值)。在Makefile的目標、依賴、命令中引用變量的地方,變量會被它的值所取代(與C語言中宏引用的方式相同,因此其他版本的make也把變量稱之爲“宏”)。在Makefile中變量有以下幾個特徵:
1. Makefile中變量和函數的展開(除規則命令行中的變量和函數以外),是在make讀取makefile文件時進行的,這裏的變量包括了使用“=”定義和使用指示符“define”定義的。
2. 變量可以用來代表一個文件名列表、編譯選項列表、程序運行的選項參數列表、搜索源文件的目錄列表、編譯輸出的目錄列表和所有我們能夠想到的事物。
3. 變量名是不包括“:”、“#”、“=”、前置空白和尾空白的任何字符串。需要注意的是,儘管在GNU make中沒有對變量的命名有其它的限制,但定義一個包含除字母、數字和下劃線以外的變量的做法也是不可取的,因爲除字母、數字和下劃線以外的其它字符可能會在make的後續版本中被賦予特殊含義,並且這樣命名的變量對於一些shell來說是不能被作爲環境變量來使用的。
4. 變量名是大小寫敏感的。變量“foo”、“Foo”和“FOO”指的是三個不同的變量。Makefile傳統做法是變量名是全採用大寫的方式。推薦的做法是在對於內部定義定義的一般變量(例如:目標文件列表objects)使用小寫方式,而對於一些參數列表(例如:編譯選項CFLAGS)採用大寫方式,但這並不是要求的。但需要強調一點:對於一個工程,所有Makefile中的變量命名應保持一種風格,否則會顯得你是一個蹩腳的程序員(就像代碼的變量命名風格一樣)。
5. 另外有一些變量名只包含了一個或者很少的幾個特殊的字符(符號)。稱它們爲自動化變量。像“$<”、“$@”、“$?”、“$*”等。
6.1 變量的引用
當我們定義了一個變量之後,就可以在Makefile的很多地方使用這個變量。變量的引用方式是:“$(VARIABLE_NAME)”或者“${ VARIABLE_NAME }”來引用一個變量的定義。例如:“$(foo) ”或者“${foo}”就是取變量“foo”的值。美元符號“$”在Makefile中有特殊的含義,所有在命令或者文件名中使用“$”時需要用兩個美元符號“$$”來表示。對一個變量的引用可以在Makefile的任何上下文中,目標、依賴、命令、絕大多數指示符和新變量的賦值中。這裏有一個例子,其中變量保存了所有.o文件的列表:
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)
$(objects) : defs.h
變量引用的展開過程是嚴格的文本替換過程,就是說變量值的字符串被精確的展開在變量被引用的地方。因此規則:
foo = c
prog.o : prog.$(foo)
$(foo) $(foo) -$(foo) prog.$(foo)
被展開後就是:
prog.c : prog.c
cc -c prog.c
通過這個例子會發現變量的展開過程和c語言中的宏展開的過程相同,是一個嚴格的文本替換過程。上例中變量“foo”被展開的過程中,變量值中的前導空格會忽略。舉這個例子的目的是爲了讓我們更清楚地瞭解變量的展開過程,而不是建議大家按照這樣的方式來書寫Makefile。在實際書寫時,最好不要這麼幹。否則將會給你帶來很多不必要的麻煩。
注意:Makefile中在對一些簡單變量的引用,我們也可以不使用“()”和“{}”來標記變量名,而直接使用“$x”的格式來實現,此種用法僅限於變量名爲單字符的情況。另外自動化變量也使用這種格式。對於一般多字符變量的引用必須使用括號了標記,否則make將把變量名的首字母作爲作爲變量而不是整個字符串(“$PATH”在Makefile中實際上是“$(P)ATH”)。這一點和shell中變量的引用方式不同。shell中變量的引用可以是“${xx}”或者“$xx”格式。但在Makefile中多字符變量名的引用只能是“$(xx)”或者“${xx}”格式。
一般在我們書寫Makefile時,各部分變量引用的格式我們建議如下:
1. make變量(Makefile中定義的或者是make的環境變量)的引用使用“$(VAR)”格式,無論“VAR”是單字符變量名還是多字符變量名。
2. 出現在規則命令行中shell變量(一般爲執行命令過程中的臨時變量,它不屬於Makefile變量,而是一個shell變量)引用使用shell的“$tmp”格式。
3. 對出現在命令行中的make變量我們同樣使用“$(CMDVAR)” 格式來引用。
例如:
# sample Makefile
……
SUBDIRS := src foo
.PHONY : subdir
Subdir :
@for dir in $(SUBDIRS); do \
$(MAKE) –C $$dir || exit 1; \
done
……
6.2 兩種變量定義(賦值)
在GNU make中,變量的定義有兩種方式(或者稱爲風格)。我們把使用這兩種方式定義的變量可以看作變量的兩種不同風格。變量的這兩種不同的風格的區別在於:1. 定義方式;2. 展開時機。下邊我們分別對這兩種不同的風格進行詳細地討論。
6.2.1 遞歸展開式變量
第一種風格的變量是遞歸方式擴展的變量。這一類型變量的定義是通過“=”或者使用指示符“define”定義的。這種變量的引用,在引用的地方是嚴格的文本替換過程,此變量值的字符串原模原樣的出現在引用它的地方。如果此變量定義中存在對其他變量的引用,這些被引用的變量會在它被展開的同時被展開。就是說在變量定義時,變量值中對其他變量的引用不會被替換展開;而是變量在引用它的地方替換展開的同時,它所引用的其它變量纔會被一同替換展開。語言的描述可能比較晦澀,讓我們來看一個例子:
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:;echo $(foo)
執行“make”將會打印出“Huh?”。整個變量的替換過程時這樣的:首先“$(foo)”被替換爲“$(bar)”,接下來“$(bar)”被替換爲“$(ugh)”,最後“$(ugh)”被替換爲“Hug?”。整個替換的過程是在執行“echo $(foo)”時完成的。
這種類型的變量是其它版本的make所支持的類型。我們可以把這種類型的變量稱爲“遞歸展開”式變量。此類型變量存有它的優點同時也存在其缺點。其優點是:
這種類型變量在定義時,可以引用其它的之前沒有定義的變量(可能在後續部分定義,或者是通過make的命令行選項傳遞的變量)。看一個這樣的例子:
CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar
“CFLAGS”會在命令中被展開爲“-Ifoo -Ibar -O”。而在“CFLAGS”的定義中使用了其後才定義的變量“include_dirs”。
其缺點是:
1. 使用此風格的變量定義,可能會由於出現變量的遞歸定義而導致make陷入到無限的變量展開過程中,最終使make執行失敗。例如,接上邊的例子,我們給這個變量追加值:
CFLAGS = $(CFLAGS) –O
它將會導致make對變量“CFLAGS”的無限展過程中去(這種定義就是變量的遞歸定義)。因爲一旦後續同樣存在對“CLFAGS”定義的追加,展開過程將是套嵌的、不能終止的(在發生這種情況時,make會提示錯誤信息並結束)。一般書寫Makefile時,這種追加變量值的方法很少使用(也不是我們推薦的方式)。看另外一個例子:
x = $(y)
y = $(x) $(z)
這種情況下變量在進行展開時,同樣會陷入死循環。所以對於此風格的變量,當在一個變量的定義中需要引用其它的同類型風格的變量時需特別注意,防止變量展開過程的死循環。
2. 第二個缺點:這種風格的變量定義中如果使用了函數,那麼包含在變量值中的函數總會在變量被引用的地方執行(變量被展開時)。
這是因爲在這種風格變量的定義中,對函數引用的替換展開發生在變量展開的過程中,而不是在定義這個變量的時候。這樣所帶來的問題是:使make的執行效率降低(每一次在變量被展開時都要展開他所引用的函數);另外在某些時候會出現一些變量和函數的引用出現非預期的結果。特別是當變量定義中引用了“shell”和“wildcard”函數的情況,可能出現不可控制或者難以預料的錯誤,因爲我們無法確定它在何時會被展開。
6.2.2 直接展開式變量
爲了避免“遞歸展開式”變量存在的問題和不方便。GNU make支持另外一種風格的變量,稱爲“直接展開”式。這種風格的變量使用“:=”定義。在使用“:=”定義變量時,變量值中對其他量或者函數的引用在定義變量時被展開(對變量進行替換)。所以變量被定義後就是一個實際需要的文本串,其中不再包含任何變量的引用。因此
x := foo
y := $(x) bar
x := later
就等價於:
y := foo bar
x := later
和遞歸展開式變量不同:此風格變量在定義時就完成了對所引用變量和函數的展開,因此不能實現對其後定義變量的引用。如:
CFLAGS := $(include_dirs) -O
include_dirs := -Ifoo -Ibar
由於變量“include_dirs”的定義出現在“CFLAGS”定義之後。因此在“CFLAGS”的定義中,“include_dirs”的值爲空。“CFLAGS”的值爲“-O”而不是“-Ifoo -Ibar -O”。這一點也是直接展開式和遞歸展開式變量的不同點。注意這裏的兩個變量都是“直接展開”式的。大家不妨試試將其中某一個變量使用遞歸展開式定義後看一下又會出現什麼樣的結果。
下邊我們來看一個複雜一點的例子。分析一下直接展開式變量定義(:=)的用法,這裏也用到了make的shell函數和變量“MAKELEVEL”(此變量在make的遞歸調用時代表make的調用深度)。
其中包括了對函數、條件表達式和系統變量“MAKELEVEL”的使用:
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami)
host-type := $(shell arch)
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif
第一行是一個條件判斷,如果是頂層Makefile,就定義下列變量。否則不定義任何變量。第二、三、四、五行分別定義了一個變量,在進行變量定義時對引用到的其它變量和函數展開。最後結束定義。利用直接展開式的特點我們可以書寫這樣一個規則:
${subdirs}:
${MAKE} cur-dir=${cur-dir}/$@ -C $@ all
它實現了在不同子目錄下變量“cur_dir”使用不同的值(爲當前工作目錄)。
在複雜的Makefile中,推薦使用直接展開式變量。因爲這種風格變量的使用方式和大多數編程語言中的變量使用方式基本上相同。它可以使一個比較複雜的Makefile在一定程度上具有可預測性。而且這種變量允許我們利用之前所定義的值來重新定義它(比如使用某一個函數來對它以前的值進行處理並重新賦值),此方式在Makefile中經常用到。儘量避免和減少遞歸式變量的使用。
6.2.3 如何定義一個空格
使用直接擴展式變量定義我們可以實現將一個前導空格定義在變量值中。一般變量值中的前導空格字符在變量引用和函數調用時被丟棄。利用直接展開式變量在定義時對引用的其它變量或函數進行展開的特點,我們可以實現在一個變量中包含前導空格並在引用此變量時對空格加以保護。像這樣:
nullstring :=
space := $(nullstring) # end of the line
這裏,變量“space”就表示一個空格。在“space”定義行中的註釋使得我們的目的更清晰(明確地描述一個空格字符比較困難),註釋和變量引用“$(nullstring)”之間存在一個空格。通過這種方式我們就明確的指定了一個空格。這是一個很好地實現方式。通過引用變量“nullstring”標明變量值的開始,採用“#”註釋來結束,中間是一個空格字符。
make對變量進行處理時變量值中尾空格是不被忽略的,因此定義一個包含一個或者多個空格的變量定義時,上邊的實現就是一個簡單並且非常直觀的方式。但是需要注意:當定義不包含尾空格的變量時,就不能使用這種方式,將變量定義和註釋書寫在同一行並使用若干空格分開。否則,註釋之前的空格會被作爲變量值的一部分。例如下邊的做法就是不正確的:
dir := /foo/bar # directory to put the frobs in
變量“dir”的值是“/foo/bar ”(後面有4個空格),這可能並不是想要實現的。如果一個文件以它作爲路徑來表示“$(dir)/file”,那麼大錯特錯了。
在書寫Makefile時。推薦將註釋書寫在獨立的行或者多行,防止出現上邊例子中的意外情況,而且將註釋書寫在獨立的行也使得Makefile清晰,便於閱讀。對於特殊的定義,比如定義包含一個或者多個空格空格的變量時進行詳細地說明和註釋。
6.2.4 “?=”操作符
GNU make中,還有一個被稱爲條件賦值的賦值操作符“?=”。被稱爲條件賦值是因爲:只有此變量在之前沒有賦值的情況下才會對這個變量進行賦值。例如:
FOO ?= bar
其等價於:
ifeq ($(origin FOO), undefined)
FOO = bar
endif
含義是:如果變量“FOO”在之前沒有定義,就給它賦值“bar”。否則不改變它的值。
6.3 變量的高級用法
本節討論關於變量的高級用法,這些高級的用法使我們可以更靈活的使用變量。
6.3.1 變量的替換引用
對於一個已經定義的變量,可以使用“替換引用”將其值中的後綴字符(串)使用指定的字符(字符串)替換。格式爲“$(VAR:A=B)”(或者“${VAR:A=B}”),意思是,替換變量“VAR”中所有“A”字符結尾的字爲“B”結尾的字。“結尾”的含義是空格之前(變量值多個字之間使用空格分開)。而對於變量其它部分的“A”字符不進行替換。例如:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
在這個定義中,變量“bar”的值就爲“a.c b.c c.c”。使用變量的替換引用將變量“foo”以空格分開的值中的所有的字的尾字符“o”替換爲“c”,其他部分不變。如果在變量“foo”中如果存在“o.o”時,那麼變量“bar”的值爲“a.c b.c c.c o.c”而不是“a.c b.c c.c c.c”。
變量的替換引用其實是函數“patsubst”的一個簡化實現。在GNU make中同時提供了這兩種方式來實現同樣的目的,以兼容其它版本make。
另外一種引用替換的技術使用功能更強大的“patsubst”函數。它的格式和上面“$(VAR:A=B)”的格式相類似,不過需要在“A”和“B”中需要包含模式字符“%”。這時它和“$(patsubst A,B $(VAR))”所實現功能相同。例如:
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
這個例子同樣使變量“bar”的值爲“a.c b.c c.c”。這種格式的替換引用方式比第一種方式更通用。
6.3.2 變量的套嵌引用
計算的變量名是一個比較複雜的概念,僅用在那些複雜的Makefile中。通常我們不需要對它的計算過程進行深入地瞭解,只要知道當一個被引用的變量名之中含有“$”時,可得到另外一個值。如果您是一個比較喜歡追根問底的人,或者想弄清楚make計算變量的過程。那麼就可以參考本節的內容。
一個變量名(文本串)之中可以包含對其它變量的引用。這種情況我們稱之爲“變量的套嵌引用”或者“計算的變量名”。先看一個例子:
x = y
y = z
a := $($(x))
這個例子中,最終定義了“a”的值爲“z”。來看一下變量的引用過程:首先最裏邊的變量引用“$(x)”被替換爲變量名“y”(就是“$($(x))”被替換爲了“$(y)”),之後“$(y)”被替換爲“z”(就是a := z)。這個例子中(a:=$($(x)))所引用的變量名不是明確聲明的,而是由$(x)擴展得到。這裏“$(x)”相對於外層的引用就是套嵌的變量引用。
上個例子我們看到是一個兩層的套嵌引用的例子,具有多層的套嵌引用在Makefile中也是允許的。下邊我們在來看一個三層套嵌引用的例子:
x = y
y = z
z = u
a := $($($(x)))
這個例子最終是定義了“a”的值爲“u”。它的擴展過程和上邊第一個例子的過程相同。首先“$(x)”被替換爲“y”,則“$($(x))”就是“$(y)”,“$(y)”再被替換爲“z”,所以就有“a:=$(z)”;“$(z)”最後被替換爲“u”。
以上兩個套嵌引用的例子中沒有用到遞歸展開式變量的特點。遞歸展開式變量的變量名的計算過程,也是按照相同的方式被擴展的。例如:
x = $(y)
y = z
z = Hello
a := $($(x))
此例最終實現了“a:=Hello”這麼一個定義。這裏$($(x))被替換成了$($(y)),因爲$(y)值是“z”,所以,最終結果是:a:=$(z),也就是“Hello”。
遞歸變量的套嵌引用過程,也可以包含變量的修改引用和函數調用。看下邊的例子,其中使用了make的文本處理函數:
x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))
此例同樣的實現“a:=Hello”。“$($($(z)))”首先被替換爲“$($(y))”,之後再次被替換爲“$($(subst 1,2,$(x)))”(“$(x)”的值是“variable1”,所以有“$($(subst 1,2,$(variable1)))”)。函數處理之後爲“$(variable2)”。之後對它在進行替換展開。最終,變量“a”的值就是“Hello”。從上邊的例子中我們看到,計算的變量名的引用過程存在多層套嵌,也使用了文本處理函數。這個複雜的計算變量的過程,會使很多人感到混亂甚至迷惑。上例中所要實現的目的就沒有直接使用“a:=Hello”來的直觀。在書寫Makefile時,應儘量避免使用套嵌的變量引用。在一些必需的地方,也最好不要使用高於兩級的套嵌引用。使用套嵌的變量引用時,如果涉及到遞歸展開式變量的引用時需要特別注意。一旦處理不當就可能導致遞歸展開錯誤,從而導致難以預料的結果。
一個計算的變量名可以不是對一個完整、單一的其他變量的引用。其中可以包含多個變量的引用,也可以包含一些文本字符串。就是說,計算變量的名字可以由一個或者多個變量引用同時加上字符串混合組成。例如:
a_dirs := dira dirb
1_dirs := dir1 dir2
a_files := filea fileb
1_files := file1 file2
ifeq "$(use_a)" "yes"
a1 := a
else
a1 := 1
endif
ifeq "$(use_dirs)" "yes"
df := dirs
else
df := files
endif
dirs := $($(a1)_$(df))
這個例子對變量“dirs”進行定義,變量的可能取值爲“a_dirs”、“1_dirs”、“a_files”和“a_files”四個之一,具體依賴於“use_a”和“use_dirs”的定義。
計算的變量名也可以使用上一小節我們討論過的“變量的替換引用”。例如:
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)
這個例子實現了變量“sources”的定義,它的可能取值爲“a.c b.c c.c”和“1.c 2.c 3.c”,具體依賴於“a1”的定義。大家自己分析一下計算變量名的過程。
使用嵌套的變量引用的唯一限制是,不能通過指定部分需要調用的函數名稱(調用的函數包括了函數名本身和執行的參數)來實現對這個函數的調用。這是因爲套嵌引用在展開之前已經完成了對函數名的識別測試。我們來看一個例子,此例子試圖將函數執行的結果賦值給一個變量:
ifdef do_sort
func := sort
else
func := strip
endif
bar := a d b g q c
foo := $($(func) $(bar))
此例的本意是將“sort”或者“strip”(依賴於是否定義了變量“do_sort”)以“a d b g q c”的執行結果賦值變量“foo”。在這裏使用了套嵌引用方式來實現,但是本例的結果是:變量“foo”的值爲字符串“sort a d b g q c”或者“strip a d g q c”。這是目前版本的make在處理套嵌變量引用時的限制。
計算的變量名可以用在:1. 一個使用賦值操作符定義變量的左值部分;2. 使用“define”定義的變量名中。例如:
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print
lpr $($(dir)_sources)
endef
在這個例子中我們定義了三個變量:“dir”,“foo_sources”和“foo_print”。
計算的變量名在進行替換時的順序是:從最裏層的變量引用開始,逐步向外進行替換。一層層展開直到最後計算出需要應用的具體的變量,之後進行替換展開得到實際的引用值。
變量的套嵌引用(計算的變量名)在我們的Makefile中應該儘量避免使用。在必需的場合使用時掌握的原則是:套嵌使用的層數越少越好,使用多個兩層套嵌引用代替一個多層的套嵌引用。如果在你的Makefile中存在一個層次很深的套嵌引用。會給其他人閱讀造成很大的困難。而且變量的多級套嵌引用在某些時候會使簡單問題複雜化。
作爲一個優秀的程序員,在面對一個複雜問題時,應該是尋求一種儘可能簡單、直接並且高效的處理方式來解決,而不是將一個簡單問題在實現上複雜化。如果想在簡單問題上突出自己使用某種語言的熟練程度,是一種非常愚蠢、且不成熟的行爲。
注意:
套嵌引用的變量和遞歸展開的變量在本質上存在區別。套嵌的引用就是使用一個變量表示另外一個變量,或者更多的層次;而遞歸展開的變量表示當一個變量存在對其它變量的引用時,對這變量替換的方式。遞歸展開在另外一個角度描述了這個變量在定義是賦予它的一個屬性或者風格。並且我們可以在定義個一個遞歸展開式的變量時使用套嵌引用的方式,但是建議你的實際編寫Makefile時要儘量避免這種複雜的用法。
6.4 變量取值
一個變量可以通過以下幾種方式來獲得值:
² 在運行make時通過命令行選項來取代一個已定義的變量值。
² 在makefile文件中通過賦值的方式或者使用“define”來爲一個變量賦值。
² 將變量設置爲系統環境變量。所有系統環境變量都可以被make使用。
² 自動化變量,在不同的規則中自動化變量會被賦予不同的值。它們每一個都有單一的習慣性用法。
² 一些變量具有固定的值。
6.5 如何設置變量
Makefile中變量的設置(也可以稱之爲定義)是通過“=”(遞歸方式)或者“:=”(靜態方式)來實現的。“=”和“:=”左邊的是變量名,右邊是變量的值。下邊就是一個變量的定義語句:
objects = main.o foo.o bar.o utils.o
這個語句定義了一個變量“objects”,其值爲一個.o文件的列表。變量名兩邊的空格和“=”之後的空格在make處理時被忽略。
使用“=”定義的變量稱之爲“遞歸展開”式變量;使用“:=”定義的變量稱爲“直接展開”式變量,“直接展開”式的變量如果其值中存其他變量或者函數的引用,在定義時這些引用將會被替換展開。
定義一個變量時需要明確以下幾點:
1. 變量名之中可以包含函數或者其它變量的引用,make在讀入此行時根據已定義情況進行替換展開而產生實際的變量名。
2. 變量的定義值在長度上沒有限制。不過在使用時還是需要根據實際情況考慮,保證你的機器上有足夠的可用的交換空間來處理一個超常的變量值。變量定義較長時,一個好的做法就是將比較長的行分多個行來書寫,除最後一行外行與行之間使用反斜槓(\)連接,表示一個完整的行。這樣的書寫方式對make的處理不會造成任何影響,便於後期修改維護而且使得你的Makefile更清晰。例如上邊的例子就可以這樣寫:
ojects = main.o foo.o \
bar.o utils.o
3. 當引用一個沒有定義的變量時,make默認它的值爲空。
4. 一些特殊的變量在make中有內嵌固定的值,不過這些變量允許我們在Makefile中顯式得重新給它賦值。
5. 還存在一些由兩個符號組成的特殊變量,稱之爲自動環變量。它們的值不能在Makefile中進行顯式的修改。這些變量使用在規則中時,不同的規則中它們會被賦予不同的值。
6. 如果你希望實現這樣一個操作,僅對一個之前沒有定義過的變量進行賦值。那麼可以使用速記符“?=”(條件方式)來代替“=”或者“:=”來實現。
6.6 追加變量值
通常,一個通用變量在定義之後的其他一個地方,可以對其值進行追加。這是非常有用的。我們可以在定義時(也可以不定義而直接追加)給它賦一個基本值,後續根據需要可隨時對它的值進行追加(增加它的值)。在Makefile中使用“+=”(追加方式)來實現對一個變量值的追加操作。像下邊那樣:
objects += another.o
這個操作把字符串“another.o”添加到變量“objects”原有值的末尾,使用空格和原有值分開。因此我們可以看到:
objects = main.o foo.o bar.o utils.o
objects += another.o
上邊的兩個操作之後變量“objects”的值就爲:“main.o foo.o bar.o utils.o another.o”。使用“+=”操作符,相當於:
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o
但是,這兩種方式可能在簡單一些的Makefile有相同的效果,複雜的Makefile中它們之間的差異就會導致一些問題。爲了方便我們調試,瞭解這兩種實現的差異還是很有必要的。
1. 如果被追加值的變量之前沒有定義,那麼,“+=”會自動變成“=”,此變量就被定義爲一個遞歸展開式的變量。如果之前存在這個變量定義,那麼“+=”就繼承之前定義時的變量風格。
2. 直接展開式變量的追加過程:變量使用“:=”定義,之後“+=”操作將會首先替換展開之前此變量的值,爾後在末尾添加需要追加的值,並使用“:=”重新給此變量賦值。實際的過程像下邊那樣:
variable := value
variable += more
就是:
variable := value
variable := $(variable) more
3. 遞歸展開式變量的追加過程:一個變量使用“=”定義,之後“+=”操作時不對之前此變量值中的任何引用進行替換展開,而是按照文本的擴展方式(之前等號右邊的文本未發生變化)替換,爾後在末尾添加需要追加的值,並使用“=”給此變量重新賦值。實際的過程和上邊的相類似:
variable = value
variable += more
相當於:
temp = value
variable = $(temp) more
當然了,上邊的過程並不會存在中間變量:“temp”,使用它的目的時方便描述。這種情況時如果“value”中存在某種引用,情況就有些不同了。看我們通常一個會用到的例子:
CFLAGS = $(includes) -O
...
CFLAGS += -pg # enable profiling
第一行定義了變量“CFLAGS”,它是一個遞歸展開式的變量。因此make在處理它的定義時不會對其值中的引用“$(includes)”進行展開,它的替換展開是在變量“CFLAGS”被引用的規則中。因此,變量“include”可以在“CFLAGS”之前不進行定義,只要它在實際引用“CFLAGS”之前定義就可以了。但是如果給“CFLAGS”追加值使用“:=”操作符,我們按照下邊那樣實現:
CFLAGS := $(CFLAGS) -pg # enable profiling
這樣似乎好像很正確,但是實際上它在有些情況時卻不是你所要實現的。來看看,因爲“:=”操作符定義的是直接展開式變量,因此變量值中對其它變量或者函數的引用會在定義時進行展開。在這種情況下,如果變量“includes”在之前沒有進行定義的話,變量“CFLAGS”的值爲“-O -pg”($(includes)被替換展開爲空字符)。而其後出現的“includes”的定義對“CFLAGS”將不產生影響。相反的情況,如果在這裏使用“+=”實現:
CFLAGS += -pg # enable profiling
那麼變量“CFLAGS”的值就是文本串“$(includes) –O -pg”,因爲之前“CFLAGS”定義爲遞歸展開式,所以追加值時不會對其值的引用進行替換展開。因此變量“includes”只要出現在規則對“CFLAGS”的引用之前定義,它都可以對“CFLAGS”的值起作用。對於遞歸展開式變量的追加,make程序會同樣會按照遞歸展開式的定義來實現對變量的重新賦值,不會發生遞歸展開式變量展開過程的無限循環。
6.7 override 指示符
通常在執行make時,如果通過命令行定義了一個變量,那麼它將替代在Makefile中出現的同名變量的定義。就是說,對於一個在Makefile中使用常規方式(使用“=”、“:=”或者“define”)定義的變量,我們可以在執行make時通過命令行方式重新指定這個變量的值,命令行指定的值將替代出現在Makefile中此變量的值。如果不希望命令行指定的變量值替代在Makefile中的變量定義,那麼我們需要在Makefile中使用指示符“override”來對這個變量進行聲明,像下邊那樣:
override VARIABLE = VALUE
或者:
override VARIABLE := VALUE
也可以對變量使用追加方式:
override VARIABLE += MORE TEXT
對於追加方式需要說明的是:變量在定義時使用了“override”,則後續對它值進行追加時,也需要使用帶有“override”指示符的追加方式。否則對此變量值的追加不會生效。
指示符“override”並不是用來調整Makefile和執行時命令參數的衝突,其存在的目的是爲了使用戶可以改變或者追加那些使用make的命令行指定的變量的定義。從另外一個角度來說,就是實現了在Makefile中增加或者修改命令行參數的一種機制。我們可能會有這樣的需求;可以通過命令行來指定一些附加的編譯參數,對一些通用的參數或者必需的編譯參數在Makefile中指定,而在命令行中指定一些特殊的參數。對於這種需求,我們就需要使用指示符“override”來實現。
例如:無論命令行指定那些編譯參數,編譯時必須打開“-g”選項,那麼在Makefile中編譯選項“CFLAGS”應該這樣定義:
override CFLAGS += -g
這樣,在執行make時無論在命令行中指定了那些編譯選項(“指定CFLAGS”的值),編譯時“-g”參數始終存在。
同樣,使用“define”定義變量時同樣也可以使用“override”進行聲明。例如:
override define foo
bar
endef
最後我們來看一個例子:
# sample Makefile
EXEF = foo
override CFLAGS += -Wall –g
.PHONY : all debug test
all : $(EXEF)
foo : foo.c
………..
………..
$(EXEF) : debug.h
$(CC) $(CFLAGS) $(addsuffix .c,$@) –o $@
debug :
@echo ”CFLAGS = $(CFLAGS)”
執行:make CFLAGS=-O2 將顯式編譯“foo”的過程是“cc –O2 –Wall –g foo.c –o foo”。執行“make CFLAGS=-O2 debug”可以查看到變量“CFLAGS”的值爲“–O2 –Wall –g”。另外,這個例子中,如果把變量“CFLAGS”之前的指示符“override”去掉,使用相同的命令將得到不同的結果。大家試試看!
6.8 多行定義
定義變量的另外一種方式是使用“define”指示符。它定義一個包含多行字符串的變量,我們就是利用它的這個特點實現了一個完整命令包的定義。使用“define”定義的命令包可以作爲“eval”函數的參數來使用。
本文的前些章節已經不止一次的提到並使用了“define”。相信大家已經有所瞭解。本節就“define”定義變量從以下幾個方面來討論:
1. “define”定義變量的語法格式:以指示符“define”開始,“endif”結束,之間的所有內容就是所定義變量的值。所要定義的變量名字和指示符“define”在同一行,使用空格分開;指示符所在行的下一行開始一直到“endif”所在行的上一行之間的若干行,是變量值。
define two-lines
echo foo
echo $(bar)
endef
如果將變量“two-lines”作爲命令包執行時,其相當於:
two-lines = echo foo; echo $(bar)
大家應該對這個命令的執行比較熟悉。它把變量“two-lines”的值作爲一個完整的shell命令行來處理(是使用分號“;”分開的在同一行中的兩個命令而不是作爲兩個命令行來處理),保證了變量完整。
2. 變量的風格:使用“define”定義的變量和使用“=”定義的變量一樣,屬於“遞歸展開”式的變量,兩者只是在語法上不同。因此“define”所定義的變量值中,對其它變量或者函數引用不會在定義變量時進行替換展開,其展開是在“define”定義的變量被展開的同時完成的。
3. 可以套嵌引用。因爲是遞歸展開式變量,所以在嵌套引用時“$(x)”將是變量的值的一部分。
4. 變量值中可以包含:換行符、空格等特殊符號(注意如果定義中某一行是以[Tab]字符開始時,當引用此變量時這一行會被作爲命令行來處理)。
5. 可以使用“override”在定義時聲明變量:這樣可以防止變量的值被命令行指定的值替代。例如:
override define two-lines
foo
$(bar)
endef
6.9 系統環境變量
make在運行時,系統中的所有環境變量對它都是可見的。在Makefile中,可以引用任何已定義的系統環境變量。(這裏我們區分系統環境變量和make的環境變量,系統環境變量是這個系統所有用戶所擁有的,而make的環境變量只是對於make的一次執行過程有效,以下正文中出現沒有限制的“環境變量”時默認指的是“系統環境變量”,在特殊的場合我們會區分兩者)正因爲如此,我們就可以設置一個命名爲“CFLAGS”的環境變量,用它來指定一個默認的編譯選項。就可以在所有的Makefile中直接使用這個變量來對c源代碼就行編譯。通常這種方式是比較安全的,但是它的前提是大家都明白這個變量所代表的含義,沒有人在Makefile中把它作其他的用途。當然了,你也可以在你的Makefile中根據你的需要對它進行重新定義。
使用環境變量需要注意以下幾點:
1. 在Makefile中對一個變量的定義或者以make命令行形式對一個變量的定義,都將覆蓋同名的環境變量(注意:它並不改變系統環境變量定義,被修改的環境變量只在make執行過程有效)。而make使用“-e”參數時,Makefile和命令行定義的變量不會覆蓋同名的環境變量,make將使用系統環境變量中這些變量的定義值。
2. make的遞歸調用中,所有的系統環境變量會被傳遞給下一級make。默認情況下,只有環境變量和通過命令行方式定義的變量纔會被傳遞給子make進程。在Makefile中定義的普通變量需要傳遞給子make時需要使用“export”指示符來對它聲明。
3. 一個比較特殊的是環將變量“SHELL”。在系統中這個環境變量的用途是用來指定用戶和系統的交互接口,顯然對於make是不合適的。因此make的執行環境變量“SHELL”沒有使用同名的環境變量定義,而是“/bin/sh”。make默認“/bin/sh”作爲它的命令行解釋程序(make在執行之前將變量“SHELL”設置爲“/bin/sh”)。
我們不推薦使用環境變量的方式來完成普通變量的工作,特別是在make的遞歸調用中。任何一個環境變量的錯誤定義都對系統上的所有make產生影響,甚至是毀壞性的。因爲環境變量具有全局的特徵。所以儘量不要污染環境變量,造成環境變量名字污染。我想大多數系統管理員都明白環境變量對系統是多麼的重要。
我們來看一個例子,結束本節。假如我們的機器名爲“server-cc”;我們的Makefile內容如下:
# test makefile
HOSTNAME = server-http
…………
…………
.PHONY : debug
debug :
@echo “hostname is : $( HOSTNAME)”
@echo “shell is $(SHELL)”
1. 執行“make debug”將顯示:
hostname is : server-http
shell is /bin/sh
2. 執行“make –e debug”;將顯示:
hostname is : server-cc
shell is /bin/sh
3. 執行“make –e HOSTNAEM=server-ftp”;將顯示:
hostname is : server-ftp
shell is /bin/sh
記住:除非必須,否則在你的Makefile中不要重置環境變量“SHELL”的值。因爲一個不正確的命令行解釋程序可能會導致規則定義的命令執行失敗,甚至是無法執行!當需要重置它時,必須有充分的理由和配套的規則命令來適應這個新指定的命令行解釋程序。
6.10目標指定變量
在Makefile中定義一個變量,那麼這個變量對此Makefile的所有規則都是有效的。它就像是一個“全局的”變量(僅限於定義它的那個Makefile中的所有規則,如果需要對其它的Makefile中的規則有效,就需要使用“export”對它進行聲明。類似於c語言中的全局靜態變量,使用static聲明的全局變量)。當然“自動化變量”除外。
另外一個特殊的變量定義就是所謂的“目標指定變量(Target-specific Variable)”。此特性允許對於相同變量根據目標指定不同的值,有點類似於自動化變量。目標指定的變量值只在指定它的目標的上下文中有效,對於其他的目標沒有影響。就是說目標指定的變量具有隻對此目標上下文有效的“局部性”。
設置一個目標指定變量的語法爲:
TARGET ... : VARIABLE-ASSIGNMENT
或者:
TARGET ... : override VARIABLE-ASSIGNMENT
一個多目標指定的變量的作用域是所有這些目標的上下文,它包括了和這個目標相關的所有執行過程。
目標指定變量的一些特點:
1. “VARIABLE-ASSIGNMENT”可以使用任何一個有效的賦值方式,“=”(遞歸)、“:=”(靜態)、“+=”(追加)或者“?=”(條件)。
2. 使用目標指定變量值時,目標指定的變量值不會影響同名的那個全局變量的值。就是說目標指定一個變量值時,如果在Makefile中之前已經存在此變量的定義(非目標指定的),那麼對於其它目標全局變量的值沒有變化。變量值的改變只對指定的這些目標可見。
3. 目標指定變量和普通變量具有相同的優先級。就是說,當我們使用make命令行的方式定義變量時,命令行中的定義將替代目標指定的同名變量定義(和普通的變量一樣會被覆蓋)。另外當使用make的“-e”選項時,同名的環境變量也將覆蓋目標指定的變量定義。因此爲了防止目標指定的變量定義被覆蓋,可以使用第二種格式,使用指示符“override”對目標指定的變量進行聲明。
4. 目標指定的變量和同名的全局變量屬於兩個不同的變量,它們在定義的風格(遞歸展開式和直接展開式)上可以不同。
5. 目標指定的變量變量會作用到由這個目標所引發的所有的規則中去。例如:
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
這個例子中,無論Makefile中的全局變量“CFLAGS”的定義是什麼。對於目標“prog”以及其所引發的所有(包含目標爲“prog.o”、“foo.o”和“bar.o”的所有規則)規則,變量“CFLAGS”值都是“-g”。
使用目標指定變量可以實現對於不同的目標文件使用不同的編譯參數。看一個例子:
# sample Makefile
CUR_DIR = $(shell pwd)
INCS := $(CUR_DIR)/include
CFLAGS := -Wall –I$(INCS)
EXEF := foo bar
.PHONY : all clean
all : $(EXEF)
foo : foo.c
foo : CFLAGS+=-O2
bar : bar.c
bar : CFLAGS+=-g
………..
………..
$(EXEF) : debug.h
$(CC) $(CFLAGS) $(addsuffix .c,$@) –o $@
clean :
$(RM) *.o *.d $(EXES)
這個Makefile文件實現了在編譯程序“foo”使用優化選項“-O2”但不使用調試選項“-g”,而在編譯“bar”時採用了“-g”但沒有“-O2”。這就是目標指定變量的靈活之處。目標指定變量的其它特性大家可以修改這個簡單的Makefile來進行驗證!
6.11模式指定變量
GNU make除了支持上一節所討論的模式指定變量之外,還支持另外一種方式:模式指定變量(Pattern-specific Variable)。使用目標定變量定義時,此變量被定義在某個具體目標和由它所引發的規則的目標上。而模式指定變量定義是將一個變量值指定到所有符合此模式的目標上。對於同一個變量如果使用追加方式,通常對於一個目標,它的局部變量值是:(爲所有規則定義的全局值)+(引發它所在規則被執行的目標所指定的值)+(它所符合的模式指定值)+(此目標所指定的值)。這個大家也不需要深入瞭解。
設置一個模式指定變量的語法和設置目標變量的語法相似:
PATTERN ... : VARIABLE-ASSIGNMENT
或者:
PATTERN ... : override VARIABLE-ASSIGNMENT
和目標指定變量語法的唯一區別就是:這裏的目標是一個或者多個“模式”目標(包含模式字符“%”)。例如我們可以爲所有的.o文件指定變量“CFLAGS”的值:
%.o : CFLAGS += -O
它指定了所有.o文件的編譯選項包含“-O”選項,不改變對其它類型文件的編譯選項。
需要說明的是:在使用模式指定的變量定義時。目標文件一般除了模式字符(%)以外需要包含某種文件名的特徵字符(例如:“a%”、“%.o”、“%.a”等)。當單獨使用“%”作爲目標時,指定的變量會對所有類型的目標文件有效。