Makefile 讀書筆記

sgy618發表於2011-03-16
Makefile 讀書筆記[@more@]

編譯器只檢測程式語法,和函式、變數是否被宣告。
Makefile裡主要包含了五個東西:顯式規則、隱晦規則、變數定義、檔案指示和註釋。
其中,檔案指示。其包括了三個部分,一個是在一個Makefile中引用另一個Makefile,就像C語言中的include一樣;另一個是指根據某些情況指定Makefile中的有效部分,就像C語言中的預編譯#if一樣;
。Makefile中只有行註釋,和UNIX的Shell指令碼一樣,其註釋是用“#”字元,這個就像C/C++中的“//”一樣。如果你要在你的Makefile中使用“#”字元,可以用反斜框進行轉義,如:“#”。最後,還值得一提的是,在Makefile中的命令,必須要以[Tab]鍵開始。

在Makefile 使用include 關鍵字可以把別的Makefile 包含進來,
include
filename可以是當前作業系統Shell的檔案模式(可以保含路徑和萬用字元)
在include前面可以有一些空字元,但是絕不能是[Tab]鍵開始。include和可以用一個或多個空格隔開。

舉個例子,你有這樣幾個Makefile:a.mk、b.mk、c.mk,還有一個檔案叫foo.make,以及一個變數$(bar),其包含了e.mk和f.mk,那麼,下面的語句:

include foo.make *.mk $(bar)
等價於:
include foo.make a.mk b.mk c.mk e.mk f.mk

make命令開始時,會把找尋include所指出的其它Makefile,並把其內容安置在當前的位置。就好像C/C++的#include指令一樣。如果檔案都沒有指定絕對路徑或是相對路徑的話,make會在當前目錄下首先尋找,如果當前目錄下沒有找到,那麼,make還會在下面的幾個目錄下找:
1、如果make執行時,有“-I”或“--include-dir”引數,那麼make就會在這個引數所指定的目錄下去尋找。
2、如果目錄/include(一般是:/usr/local/bin或/usr/include)存在的話,make也會去找。

-include
其表示,無論include過程中出現什麼錯誤,都不要報錯繼續執行。和其它版本make相容的相關命令是sinclude,其作用和這一個是一樣的。

一般來說,make會以UNIX的標準Shell,也就是/bin/sh來執行命令。

在規則中使用萬用字元
make支援三各萬用字元:“*”,“?”和“[...]”。

波浪號(“~”)字元在檔名中也有比較特殊的用途。如果是“~/test”,這就表示當前
使用者的$HOME目錄下的test目錄。而“~hchen/test”則表示使用者hchen的宿主目錄下的
test目錄。

Makefile中的變數其實就是C/C++中的宏。如果你要讓萬用字元在變數中展開,也就是讓objects的值是所有[.o]的檔名的集合,那麼,你可以這樣:
objects := $(wildcard *.o)

vpath %.c foo:bar
vpath % blish
而上面的語句則表示“.c”結尾的檔案,先在“foo”目錄,然後是“bar”目錄,最後才是“blish”目錄。

偽目標同樣也可成為依賴。看下面的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
“make clean”將清除所有要被清除的檔案。“cleanobj”和“cleandiff”這兩個偽目標有點像“子程式”的意思。我們可以輸入“make cleanall”和“make cleanobj”和“make cleandiff”命令來達到清除不同種類檔案的目的。

多目標
自動化變數“$@”
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述規則等價於:
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
其中,-$(subst output,,$@)中的“$”表示執行一個Makefile的函式,函式名為subst,後面的為引數。關於函式,將在後面講述。這裡的這個函式是擷取字串的意思,“$@”表示目標的集合,就像一個陣列,“$@”依次取出目標,並執於命令。

靜態模式
例子1:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
上面的例子中,指明瞭我們的目標從$object中獲取,“%.o”表明要所有以“.o”結尾的目標,也就是“foo.o bar.o”,也就是變數$object集合的模式,而依賴模式“%.c”則取模式“%.o”的“%”,也就是“foo bar”,併為其加下“.c”的字尾,於是,我們的依賴目標就是“foo.c bar.c”。而命令中的“$例子2:
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
$(filter %.o,$(files))表示呼叫Makefile的filter函式,過濾“$filter”集,只要其中模式為
“%.o”的內容。

自動生成依賴
依賴關係可能會需要包含一系列的標頭檔案,比如,如果我們的main.c中有一句“#include "defs.h"”,那麼我們的依賴關係應該是:
main.o : main.c defs.h
但是,如果是一個比較大型的工程,你必需清楚哪些C檔案包含了哪些標頭檔案,並且,你在加入或刪除標頭檔案時,也需要小心地修改Makefile,這是一個很沒有維護性的工作。為了避免這種繁重而又容易出錯的事情,我們可以使用C/C++編譯的一個功能。大多數的C/C++編譯器都支援一個“-M”的選項,即自動找尋原始檔中包含的標頭檔案,並生成一個依賴關係。
執行下面的命令:
cc -M main.c
其輸出是:
main.o : main.c defs.h
於是由編譯器自動生成的依賴關係,這樣一來,你就不必再手動書寫若干檔案的依賴關係,而由編譯器自動生成了。需要提醒一句的是,如果你使用GNU的C/C++編譯器,你得用“-MM”引數,不然,“-M”引數會把一些標準庫的標頭檔案也包含進來。
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
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h
/usr/include/bits/types.h /usr/include/bits/pthreadtypes.h
/usr/include/bits/sched.h /usr/include/libio.h
/usr/include/_G_config.h /usr/include/wchar.h
/usr/include/bits/wchar.h /usr/include/gconv.h
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h
/usr/include/bits/stdio_lim.h
gcc -MM main.c的輸出則是:
main.o: main.c defs.h
GNU組織建議把編譯器為每一個原始檔的自動生成的依賴關係放到一個檔案中,為每一個“name.c”的檔案都生成一個“name.d”的Makefile檔案,[.d]檔案中就存放對應[.c]檔案的依賴關係。於是,我們可以寫出[.c]檔案和[.d]檔案的依賴關係,並讓make自動更新或自成[.d]檔案,並把其包含在我們的主Makefile中,這樣,我們就可以自動化地生成每個檔案的依賴關係了。
%.d: %.c
@set -e; rm -f $@;
$(CC) -M $(CPPFLAGS) $< > ;
sed 's,($*).o[ :]*,1.o $@ : ,g' < > $@;
rm -f
這個規則的意思是,所有的[.d]檔案依賴於[.c]檔案,“rm -f $@”的意思是刪除所有的目標,也就是[.d]檔案,第二行的意思是,為每個依賴檔案“$這個模式要做的事就是在編譯器生成的依賴關係中加入[.d]檔案的依賴,即
把依賴關係:
main.o : main.c defs.h
轉成:
main.o main.d : main.c defs.h
於是,我們的[.d]檔案也會自動更新了,並會自動生成了,當然,你還可以在這個[.d]檔案中加入的不只是依賴關係,包括生成的命令也可一併加入,讓每個[.d]檔案都包含一個完賴的規則。一旦我們完成這個工作,接下來,我們就要把這些自動生成的規則放
進我們的主Makefile 中。我們可以使用Makefile 的“include”命令,來引入別的Makefile檔案(前面講過),例如:
sources = foo.c bar.c
include $(sources:.c=.d)
上述語句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做一個替換,把變數$(sources)所有[.c]的字串都替換成[.d],

make 的命令預設是被“/bin/sh”——UNIX的標準Shell解釋執行的。

顯示命令
當我們用“@”字元在命令列前,那麼,這個命令將不被make顯示出來,最具代表性的例子是,我們用這個功能來像螢幕顯示一些資訊。如:
@echo 正在編譯XXX模組......
當make執行時,會輸出“正在編譯XXX模組......”字串,但不會輸出命令,如果沒有
“@”,那麼,make將輸出:
echo 正在編譯XXX模組......
正在編譯XXX模組......
如果make執行時,帶入make引數“-n”或“--just-print”,那麼其只是顯示命令,但不會執行命令,這個功能很有利於我們除錯我們的Makefile,看看我們書寫的命令是執行起來是什麼樣子的或是什麼順序的。而make引數“-s”或“--slient”則是全面禁止命令的顯示。

命令出錯
有些時候,命令的出錯並不表示就是錯誤的。例如mkdir命令,我們一定需要建立一個目錄,如果目錄不存在,那麼mkdir就成功執行,萬事大吉,如果目錄存在,那麼就出錯了。我們之所以使用mkdir的意思就是一定要有這樣的一個目錄,於是我們就不希望mkdir出錯而終止規則的執行。為了做到這一點,忽略命令的出錯,我們可以在Makefile的命令列前加一個減號“-”(在Tab鍵之後),標記為不管命令出不出錯都認為是成功的。如:
clean:
-rm -f *.o
還有一個全域性的辦法是,給make加上“-i”或是“--ignore-errors”引數,那麼,Makefile中所有命令都會忽略錯誤。而如果一個規則是以“.IGNORE”作為目標的,那麼這個規則中的所有命令將會忽略錯誤。這些是不同級別的防止命令出錯的方法,你可以根據你的不同喜歡設定。
還有一個要提一下的make的引數的是“-k”或是“--keep-going”,這個引數的意思是,如果某規則中的命令出錯了,那麼就終目該規則的執行,但繼續執行其它規則。

巢狀執行make
我們可以在每個目錄中都書寫一個該目錄的Makefile,這有利於讓我們的Makefile變得更加地簡潔,而不至於把所有的東西全部寫在一個Makefile中,這樣會很難維護我們的Makefile。
例如,我們有一個子目錄叫subdir,這個目錄下有個Makefile檔案,來指明瞭這個目
錄下檔案的編譯規則。那麼我們總控的Makefile可以這樣書寫:
subsystem:
cd subdir && $(MAKE)
其等價於:
subsystem:
$(MAKE) -C subdir
定義$(MAKE)宏變數的意思是,也許我們的make需要一些引數,所以定義成一個變數比較利於維護。這兩個例子的意思都是先進入“subdir”目錄,然後執行make命令。我們把這個Makefile 叫做“總控Makefile”,總控Makefile 的變數可以傳遞到下級的Makefile中(如果你顯示的宣告),但是不會覆蓋下層的Makefile中所定義的變數,除非指定了“-e”引數。
如果你要傳遞變數到下級Makefile中,那麼你可以使用這樣的宣告:
export
如果你不想讓某些變數傳遞到下級Makefile中,那麼你可以這樣宣告:
unexport
如:
示例一:
export variable = value
其等價於:
variable = value
export variable
其等價於:
export variable := value
其等價於:
variable := value
export variable
示例二:
export variable += value
其等價於:
variable += value
export variable
如果你要傳遞所有的變數,那麼,只要一個export就行了。後面什麼也不用跟,表示傳遞所有的變數。
需要注意的是,有兩個變數,一個是SHELL,一個是MAKEFLAGS,這兩個變數不管你是否export,其總是要傳遞到下層Makefile中,特別是MAKEFILES變數,其中包含了make 的引數資訊,如果我們執行“總控Makefile”時有make 引數或是在上層Makefile中定義了這個變數,那麼MAKEFILES變數將會是這些引數,並會傳遞到下層Makefile中,這是一個系統級的環境變數。
但是make命令中的有幾個引數並不往下傳遞,它們是“-C”,“-f”,“-h”“-o”和“-W”(有關Makefile引數的細節將在後面說明),如果你不想往下層傳遞引數,那麼,你可以這樣來:
subsystem:
cd subdir && $(MAKE) MAKEFLAGS=
如果你定義了環境變數MAKEFLAGS,那麼你得確信其中的選項是大家都會用到的,如果其中有“-t”,“-n”,和“-q”引數,那麼將會有讓你意想不到的結果,或許會讓你異常地恐慌。還有一個在“巢狀執行”中比較有用的引數,“-w”或是“--print-directory”會在make的過程中輸出一些資訊,讓你看到目前的工作目錄。比如,如果我們的下級make目錄是“/home/hchen/gnu/make”,如果我們使用“make -w”來執行,那麼當進入該目錄時,
我們會看到:
make: Entering directory `/home/hchen/gnu/make'.
而在完成下層make後離開目錄時,我們會看到:
make: Leaving directory `/home/hchen/gnu/make'
當你使用“-C”引數來指定make下層Makefile時,“-w”會被自動開啟的。如果引數中有“-s”(“--slient”)或是“--no-print-directory”,那麼,“-w”總是失效的。

定義命令包
如果Makefile中出現一些相同命令序列,那麼我們可以為這些相同的命令序列定義一個變數。定義這種命令序列的語法以“define”開始,以“endef”結束,如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
這裡,“run-yacc”是這個命令包的名字,其不要和Makefile中的變數重名。在“define”和“endef”中的兩行就是命令序列。這個命令包中的第一個命令是執行Yacc程式,因為Yacc程式總是生成“y.tab.c”的檔案,所以第二行的命令就是把這個檔案改改名字。還是把這個命令包放到一個示例中來看看吧。
foo.c : foo.y
$(run-yacc)
我們可以看見,要使用這個命令包,我們就好像使用變數一樣。在這個命令包的使用中,命令包“run-yacc”中的“$^”就是“foo.y”,“$@”就是“foo.c”(有關這種以“$”開頭的特殊變數,我們會在後面介紹),make在執行命令包時,命令包中的每個命令會被依次獨立執行。

變數的命名字可以包含字元、數字,下劃線(可以是數字開頭),但不應該含有“:”、“#”、“=”或是空字元(空格、回車等)。變數是大小寫敏感的,“foo”、“Foo”和“FOO”是三個不同的變數名。傳統的Makefile的變數名是全大寫的命名方式,但我推薦使用大小寫搭配的變數名,如:MakeFlags。這樣可以避免和系統的變數衝突,而發生意外的事情。

變數在宣告時需要給予初值,而在使用時,需要給在變數名前加上“$”符號,但最好用小括號“()”或是大括號“{}”把變數給包括起來。如果你要使用真實的“$”字元,那麼你需要用“$$”來表示。
A = $(B)
B = $(A)
這會讓make陷入無限的變數展開過程中去,當然,我們的make是有能力檢測這樣的定義,並會報錯。還有就是如果在變數中使用函式,那麼,這種方式會讓我們的make執行時非常慢,更糟糕的是,他會使用得兩個make的函式“wildcard”和“shell”發生不可預知的錯誤。因為你不會知道這兩個函式會被呼叫多少次。為了避免上面的這種方法,我們可以使用make中的另一種用變數來定義變數的方法。這種方法使用的是“:=”運算子,如:
x := foo
y := $(x) bar
x := later
其等價於:
y := foo bar
x := later
值得一提的是,這種方法,前面的變數不能使用後面的變數,只能使用前面已定義好了的變數。如果是這樣:
y := $(x) bar
x := foo
那麼,y的值是“bar”,而不是“foo bar”。

下面再介紹兩個定義變數時我們需要知道的,請先看一個例子,如果我們要定義一個變數,其值是一個空格,那麼我們可以這樣來:
nullstring :=
space := $(nullstring) # end of the line
nullstring是一個Empty變數,其中什麼也沒有,而我們的space的值是一個空格。因為在運算子的右邊是很難描述一個空格的,這裡採用的技術很管用,先用一個Empty變數來標明變數的值開始了,而後面採用“#”註釋符來表示變數定義的終止,這樣,我們可以定義出其值是一個空格的變數。請注意這裡關於“#”的使用,註釋符“#”的這種特性值得我們注意,如果我們這樣定義一個變數:
dir := /foo/bar # directory to put the frobs in
dir這個變數的值是“/foo/bar”,後面還跟了4個空格,如果我們這樣使用這樣變數來指定別的目錄——“$(dir)/file”那麼就完蛋了。

還有一個比較有用的運算子是“?=”,先看示例:
FOO ?= bar
其含義是,如果FOO沒有被定義過,那麼變數FOO的值就是“bar”,如果FOO先前被定義過,那麼這條語將什麼也不做,其等價於:
ifeq ($(origin FOO), undefined)
FOO = bar
endif

變數高階用法
1.變數值的替換
替換變數中的共有的部分,
foo := a.o b.o c.o
bar := $(foo:.o=.c)
這個示例中,我們先定義了一個“$(foo)”變數,而第二行的意思是把“$(foo)”中所有以“.o”字串“結尾”全部替換成“.c”,所以我們的“$(bar)”的值就是“a.c b.c c.c”。
另外一種變數替換的技術是以“靜態模式”(參見前面章節)定義的,如:
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
這依賴於被替換字串中的有相同的模式,模式中必須包含一個“%”字元,這個例子同樣讓$(bar)變數的值為“a.c b.c c.c”。

2.把變數的值當成變數
x = y
y = z
a := $($(x))
在這個例子中,$(x)的值是“y”,所以$($(x))就是$(y),於是$(a)的值就是“z”。(注意是“x=y”,而不是“x=$(y)”)

x = $(y)
y = z
z = Hello
a := $($(x))
這裡的$($(x))被替換成了$($(y)),因為$(y)值是“z”,所以,最終結果是:a:=$(z),也就是“Hello”。

x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))
這個例子中,“$($($(z)))”擴充套件為“$($(y))”,而其再次被擴充套件為“$($(subst 1,2,$(x)))”。$(x)的值是“variable1”,subst函式把“variable1”中的所有“1”字串替換成“2”字串,於是,“variable1”變成“variable2”,再取其值,所以,最終,$(a)的值就是$(variable2)的值——“Hello”。(喔,好不容易)

在這種方式中,或要可以使用多個變數來組成一個變數的名字,然後再取其值:
first_second = Hello
a = first
b = second
all = $($a_$b)
這裡的“$a_$b”組成了“first_second”,於是,$(all)的值就是“Hello”。

a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)
這個例子中,如果$(a1)的值是“a”的話,那麼,$(sources)的值就是“a.c b.c c.c”;如果$(a1)的值是“1”,那麼$(sources)的值是“1.c 2.c 3.c”。再來看一個這種技術和“函式”與“條件語句”一同使用的例子:
ifdef do_sort
func := sort
else
func := strip
endif
bar := a d b g q c
foo := $($(func) $(bar))
這個示例中,如果定義了“do_sort”,那麼:foo := $(sort a d b g q c),於是$(foo)的值就是“a b c d g q”,而如果沒有定義“do_sort”,那麼:foo := $(sort a d b g q c),呼叫的就是strip函式。

dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print
lpr $($(dir)_sources)
endef
這個例子中定義了三個變數:“dir”,“foo_sources”和“foo_print”。

追加變數值

我們可以使用“+=”運算子給變數追加值,如:
objects = main.o foo.o bar.o utils.o
objects += another.o
於是,我們的$(objects)值變成:“main.o foo.o bar.o utils.o another.o”(another.o被追加進去了)

使用“+=”運算子,可以模擬為下面的這種例子:
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o

如果變數之前沒有定義過,那麼,“+=”會自動變成“=”,如果前面有變數定義,那麼“+=”會繼承於前次操作的賦值符。如果前一次的是“:=”,那麼“+=”會以“:=”作為其賦值符,如:
variable := value
variable += more
等價於:
variable := value
variable := $(variable) more

override 指示符
如果有變數是通常make的命令列引數設定的,那麼Makefile中對這個變數的賦值會被忽略。如果你想在Makefile中設定這類引數的值,那麼,你可以使用“override”指示符。其語法是:
override =
override :=
當然,你還可以追加:
override +=

對於多行的變數定義,我們用define 指示符,在define 指示符前,也同樣可以使用overide指示符,如:
override define foo
bar
endef

多行變數
define指示符後面跟的是變數的名字,而重起一行定義變數的值,定義是以endef關鍵字結束。其工作方式和“=”運算子一樣。變數的值可以包含函式、命令、文字,或是其它變數。因為命令需要以[Tab]鍵開頭,所以如果你用define 定義的命令變數中沒有以[Tab]鍵開頭,那麼make就不會把其認為是命令。
下面的這個示例展示了define的用法:
define two-lines
echo foo
echo $(bar)
endef

環境變數
make執行時的系統環境變數可以在make開始執行時被載入到Makefile檔案中,但是如果Makefile中已定義了這個變數,或是這個變數由make命令列帶入,那麼系統的環境變數的值將被覆蓋。(如果make 指定了“-e”引數,那麼,系統環境變數將覆蓋Makefile中定義的變數)

當make巢狀呼叫時(參見前面的“巢狀呼叫”章節),上層Makefile中定義的變數會以系統環境變數的方式傳遞到下層的Makefile中。當然,預設情況下,只有透過命令列設定的變數會被傳遞。而定義在檔案中的變數,如果要向下層Makefile傳遞,則需要使用exprot關鍵字來宣告。(參見前面章節)

目標變數
我樣同樣可以為某個目標設定區域性變數,它可以和“全域性變數”同名,因為它的作用範圍只在這條規則以及連帶規則中,所以其值也只在作用範圍內有效。而不會影響規則鏈以外的全域性變數的值。
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
在這個示例中,不管全域性的$(CFLAGS)的值是什麼,在prog目標,以及其所引發的所有規則中(prog.o foo.o bar.o的規則),$(CFLAGS)的值都是“-g”

模式變數
make的“模式”一般是至少含有一個“%”的,所以,我們可以以如下方式給所有以[.o]結尾的目標定義目標變數:
%.o : CFLAGS = -O
同樣,模式變數的語法和“目標變數”一樣:
:
: override
override同樣是針對於系統環境傳入的變數,或是make命令列指定的變數。

使用條件判斷
判斷$(CC)變數是否“gcc”,如果是的話,則使用GNU函式編譯目標。
libs_for_gcc = -lgnu
normal_libs =
foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif
可見,在上面示例的這個規則中,目標“foo”可以根據變數“$(CC)”值來選取不同的函式庫來編譯程式。
我們可以從上面的示例中看到三個關鍵字:ifeq、else和endif
當然,我們還可以把上面的那個例子寫得更簡潔一些:
libs_for_gcc = -lgnu
normal_libs =
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
foo: $(objects)
$(CC) -o foo $(objects) $(libs)

ifeq ($(strip $(foo)),)

endif
這個示例中使用了“strip”函式,如果這個函式的返回值是空(Empty),那麼就生效。

第二個條件關鍵字是“ifneq”。

第三個條件關鍵字是“ifdef”。
示例一:
bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif
示例二:
foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif
第一個例子中,“$(frobozz)”值是“yes”,第二個則是“no”。

第四個條件關鍵字是“ifndef”。

“else”和“endif”也一樣,只要不是以[Tab]鍵開始就行了。

特別注意的是,make是在讀取Makefile時就計算條件表示式的值,並根據條件表示式的值來選擇語句,所以,你最好不要把自動化變數(如“$@”等)放入條件表示式中,因為自動化變數是在執行時才有的。


使用函式
一、函式的呼叫語法
函式呼叫,很像變數的使用,也是以“$”來標識的,其語法如下:
$( )
或是
${ }
這裡,就是函式名,make支援的函式不多。是函式的引數,引數間以逗號“,”分隔,而函式名和引數之間以“空格”分隔。函式呼叫以“$”開頭,以圓括號或花括號把函式名和引數括起。

還是來看一個示例:
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
在這個示例中,$(comma)的值是一個逗號。$(space)使用了$(empty)定義了一個空格,$(foo)的值是“a b c”,$(bar)的定義用,呼叫了函式“subst”,這是一個替換函式,這個函式有三個引數,第一個引數是被替換字串,第二個引數是替換字串,第三個引數是替換操作作用的字串。這個函式也就是把$(foo)中的空格替換成逗號,所以$(bar)的值是“a,b,c”。

二、字串處理函式
$(subst ,, )
名稱:字串替換函式——subst。
功能:把字串中的字串替換成
返回:函式返回被替換過後的字串。
示例:
$(subst ee,EE,feet on the street),
把“feet on the street”中的“ee”替換成“EE”,返回結果是“fEEt on the strEEt”。

$(patsubst ,, )
名稱:模式字串替換函式——patsubst。
功能:查詢中的單詞(單詞以“空格”、“Tab”或“回車”“換行”分隔)是否
符合模式,如果匹配的話,則以替換。這裡,可以包括萬用字元“%”,表示任意長度的字串。如果中也包含“%”,那麼,中的這個“%”將是中的那個“%”所代表的字串。(可以用“”來轉義,以“%”來表示真實含義的“%”字元)
返回:函式返回被替換過後的字串。
示例:
$(patsubst %.c,%.o,x.c.c bar.c)
把字串“x.c.c bar.c”符合模式[%.c]的單詞替換成[%.o],返回結果是“x.c.o bar.o”

這和我們前面“變數章節”說過的相關知識有點相似。如:
“$(var:= )”
相當於
“$(patsubst ,,$(var))”,
而“$(var: = )”
則相當於
“$(patsubst %,%,$(var))”。

例如有:objects = foo.o bar.o baz.o,
那麼,“$(objects:.o=.c)”和“$(patsubst %.o,%.c,$(objects))”是一樣的。

$(strip )
名稱:去空格函式——strip。
功能:去掉字串中開頭和結尾的空字元。
返回:返回被去掉空格的字串值。
示例:
$(strip a b c )
把字串“a b c ”去到開頭和結尾的空格,結果是“a b c”。

$(findstring , )
名稱:查詢字串函式——findstring。
功能:在字串中查詢字串。
返回:如果找到,那麼返回,否則返回空字串。
示例:
$(findstring a,a b c)
$(findstring a,b c)
第一個函式返回“a”字串,第二個返回“”字串(空字串)

$(filter , )
名稱:過濾函式——filter。
功能:以模式過濾字串中的單詞,保留符合模式的單詞。可以有多個模式。
返回:返回符合模式的字串。
示例:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
$(filter %.c %.s,$(sources))返回的值是“foo.c bar.c baz.s”。

$(filter-out , )
名稱:反過濾函式——filter-out。
功能:以模式過濾字串中的單詞,去除符合模式的單詞。可以有多個模式。
返回:返回不符合模式的字串。
示例:
objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
$(filter-out $(mains),$(objects)) 返回值是“foo.o bar.o”。

$(sort )
名稱:排序函式——sort。
功能:給字串中的單詞排序(升序)。
返回:返回排序後的字串。
示例:$(sort foo bar lose)返回“bar foo lose” 。
備註:sort函式會去掉中相同的單詞。

$(word , )
名稱:取單詞函式——word。
功能:取字串中第個單詞。(從一開始)
返回:返回字串中第個單詞。如果中的單詞數要大,那麼返回
空字串。
示例:$(word 2, foo bar baz)返回值是“bar”。

$(wordlist ,, )
名稱:取單詞串函式——wordlist。
功能:從字串中取從開始到的單詞串。是一個數字。
返回:返回字串中從的單詞字串。如果中的單詞數要大,
那麼返回空字串。如果大於的單詞數,那麼返回從開始,到結束
的單詞串。
示例: $(wordlist 2, 3, foo bar baz)返回值是“bar baz”。

$(words )
名稱:單詞個數統計函式——words。
功能:統計中字串中的單詞個數。
返回:返回中的單詞數。
示例:$(words, foo bar baz)返回值是“3”。
備註:如果我們要取中最後的一個單詞,我們可以這樣:$(word $(words ), )。

$(firstword )
名稱:首單詞函式——firstword。
功能:取字串中的第一個單詞。
返回:返回字串的第一個單詞。
示例:$(firstword foo bar)返回值是“foo”。
備註:這個函式可以用word函式來實現:$(word 1, )。

我們可以利用這個搜尋路徑來指定編譯器對標頭檔案的搜尋路徑引數CFLAGS,如:
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))
如果我們的“$(VPATH)”值是“src:../headers”,那麼“$(patsubst %,-I%,$(subst :, ,$(VPATH)))”將返回“-Isrc -I../headers”,這正是cc或gcc搜尋標頭檔案路徑的引數。

三、檔名操作函式
$(dir )
名稱:取目錄函式——dir。
功能:從檔名序列中取出目錄部分。目錄部分是指最後一個反斜槓(“/”)之前的部分。如果沒有反斜槓,那麼返回“./”。
返回:返回檔名序列的目錄部分。
示例: $(dir src/foo.c hacks)返回值是“src/ ./”。

$(notdir )
名稱:取檔案函式——notdir。
功能:從檔名序列中取出非目錄部分。非目錄部分是指最後一個反斜槓(“/”)之後的部分。
返回:返回檔名序列的非目錄部分。
示例: $(notdir src/foo.c hacks)返回值是“foo.c hacks”。

$(suffix )
名稱:取字尾函式——suffix。
功能:從檔名序列中取出各個檔名的字尾。
返回:返回檔名序列的字尾序列,如果檔案沒有字尾,則返回空字串。
示例:$(suffix src/foo.c src-1.0/bar.c hacks)返回值是“.c .c”。

$(basename )
名稱:取字首函式——basename。
功能:從檔名序列中取出各個檔名的字首部分。
返回:返回檔名序列的字首序列,如果檔案沒有字首,則返回空字串。
示例:$(basename src/foo.c src-1.0/bar.c hacks)返回值是“src/foo src-1.0/bar hacks”。

$(addsuffix , )
名稱:加字尾函式——addsuffix。
功能:把字尾加到中的每個單詞後面。
返回:返回加過字尾的檔名序列。
示例:$(addsuffix .c,foo bar)返回值是“foo.c bar.c”。

$(addprefix , )
名稱:加字首函式——addprefix。
功能:把字首加到中的每個單詞後面。
返回:返回加過字首的檔名序列。
示例:$(addprefix src/,foo bar)返回值是“src/foo src/bar”。

$(join , )
名稱:連線函式——join。
功能:把中的單詞對應地加到的單詞後面。如果的單詞個數要比的多,那麼,中的多出來的單詞將保持原樣。如果的單詞個數要比多,那麼,多出來的單詞將被複制到中。
返回:返回連線過後的字串。
示例:$(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”。

四、foreach 函式
names := a b c d
files := $(foreach n,$(names),$(n).o)
上面的例子中,$(name)中的單詞會被挨個取出,並存到變數“n”中,“$(n).o”每次根據“$(n)”計算出一個值,這些值以空格分隔,最後作為foreach函式的返回,所以,$(files)的值是“a.o b.o c.o d.o”。

//五、if 函式
六、call函式
call函式是唯一一個可以用來建立新的引數化的函式。
$(call ,,,...)
當make執行這個函式時,引數中的變數,如$(1),$(2),$(3)等,會被引數依次取代。而的返回值就是call函式的返回值。例如:
reverse = $(1) $(2)
foo = $(call reverse,a,b)
那麼,foo的值就是“a b”。當然,引數的次序是可以自定義的,不一定是順序的,如:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
此時的foo的值就是“b a”。

七、origin函式
origin函式不像其它的函式,他並不操作變數的值,他只是告訴你你的這個變數是哪裡來的?其語法是:
$(origin )
注意,是變數的名字,不應該是引用。所以你最好不要在中使用“$”字元。Origin函式會以其返回值來告訴你這個變數的“出生情況”,下面,是origin函式的返回值:
“undefined”
如果從來沒有定義過,origin函式返回這個值“undefined”。
“default”
如果是一個預設的定義,比如“CC”這個變數。
“environment”
如果是一個環境變數,並且當Makefile被執行時,“-e”引數沒有被開啟。
“file”
如果這個變數被定義在Makefile中。
“command line”
如果這個變數是被命令列定義的。
“override”
如果是被override指示符重新定義的。
“automatic”
如果是一個命令執行中的自動化變數。關於自動化變數將在後面講述。

這些資訊對於我們編寫Makefile是非常有用的,例如,假設我們有一個Makefile其包了一個定義檔案Make.def,在Make.def中定義了一個變數“bletch”,而我們的環境中也有一個環境變數“bletch”,此時,我們想判斷一下,如果變數來源於環境,那麼我們就把之重定義了,如果來源於Make.def或是命令列等非環境的,那麼我們就不重新定義它。
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf, gag, etc.
endif
endif
當然,你也許會說,使用override關鍵字不就可以重新定義環境中的變數了嗎?為什麼需要使用這樣的步驟?是的,我們用override是可以達到這樣的效果,可是override過於粗暴,它同時會把從命令列定義的變數也覆蓋了,而我們只想重新定義環境傳來的,而不想重新定義命令列傳來的。

八、shell函式
它的引數應該就是作業系統Shell的命令。它和反引號“`”是相同的功能。這就是說,shell函式把執行作業系統命令後的輸出作為函式返回。於是,我們可以用作業系統命令以及字串處理命令awk,sed等等命令來生成一個變數,如:
contents := $(shell cat foo)
files := $(shell echo *.c)
注意,這個函式會新生成一個Shell程式來執行命令,所以你要注意其執行效能,如果你的Makefile中有一些比較複雜的規則,並大量使用了這個函式,那麼對於你的系統效能是有害的。特別是Makefile的隱晦的規則可能會讓你的shell函式執行的次數比你想像的多得多。

//九、控制make的函式
make提供了一些函式來控制make的執行。通常,你需要檢測一些執行Makefile時的執行時資訊,並且根據這些資訊來決定,你是讓make繼續執行,還是停止。
$(error )
產生一個致命的錯誤,是錯誤資訊。注意,error函式不會在一被使用就會產生錯誤資訊,所以如果你把其定義在某個變數中,並在後續的指令碼中使用這個變數,那麼也是可以的。例如:
示例一:
ifdef ERROR_001
$(error error is $(ERROR_001))
endif
示例二:
ERR = $(error found an error!)
.PHONY: err
err: ; $(ERR)
示例一會在變數ERROR_001定義了後執行時產生error呼叫,而示例二則在目錄err被執行時才發生error呼叫。
$(warning )
這個函式很像error 函式,只是它並不會讓make退出,只是輸出一段警告資訊,而make繼續執行。

make命令執行後有三個退出碼:
0 —— 表示成功執行。
1 —— 如果make執行時出現任何錯誤,其返回1。
2 —— 如果你使用了make的“-q”選項,並且make使得一些目標不需要更新,那麼返回2。

指定目標
sources = foo.c bar.c
ifneq ( $(MAKECMDGOALS),clean)
include $(sources:.c=.d)
endif
基於上面的這個例子,只要我們輸入的命令不是“make clean”,那麼makefile會自動包含“foo.d”和“bar.d”這兩個makefile。


隱含規則
foo : foo.o bar.o
cc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)
我們可以注意到,這個Makefile中並沒有寫下如何生成foo.o和bar.o這兩目標的規則和命令。因為make的“隱含規則”功能會自動為我們自動去推導這兩個目標的依賴目標和生成命令。

當然,我們也可以使用make的引數“-r”或“--no-builtin-rules”選項來取消所有的預設定的隱含規則。

隱含規則使用的變數
第一條隱含規則——編譯C程式的隱含規則的命令是“$(CC) –c $(CFLAGS)$(CPPFLAGS)”。Make 預設的編譯命令是“cc”,如果你把變數“$(CC)”重定義成“gcc”,把變數“$(CFLAGS)”重定義成“-g”,那麼,隱含規則中的命令全部會以“gcc –c -g $(CPPFLAGS)”的樣子來執行了。

1、關於命令的變數。
AR
函式庫打包程式。預設命令是“ar”。
AS
組合語言編譯程式。預設命令是“as”。
CC
C語言編譯程式。預設命令是“cc”。
CXX
C++語言編譯程式。預設命令是“g++”。
CO
從 RCS檔案中擴充套件檔案程式。預設命令是“co”。
CPP
C程式的前處理器(輸出是標準輸出裝置)。預設命令是“$(CC) –E”。
FC
Fortran 和 Ratfor 的編譯器和預處理程式。預設命令是“f77”。
GET
從SCCS檔案中擴充套件檔案的程式。預設命令是“get”。
LEX
Lex方法分析器程式(針對於C或Ratfor)。預設命令是“lex”。
PC
Pascal語言編譯程式。預設命令是“pc”。
YACC
Yacc文法分析器(針對於C程式)。預設命令是“yacc”。
YACCR
Yacc文法分析器(針對於Ratfor程式)。預設命令是“yacc –r”。
MAKEINFO
轉換Texinfo原始檔(.texi)到Info檔案程式。預設命令是“makeinfo”。
TEX
從TeX原始檔建立TeX DVI檔案的程式。預設命令是“tex”。
TEXI2DVI
從Texinfo原始檔建立軍TeX DVI 檔案的程式。預設命令是“texi2dvi”。
WEAVE
轉換Web到TeX的程式。預設命令是“weave”。
CWEAVE
轉換C Web 到 TeX的程式。預設命令是“cweave”。
TANGLE
轉換Web到Pascal語言的程式。預設命令是“tangle”。
CTANGLE
轉換C Web 到 C。預設命令是“ctangle”。
RM
刪除檔案命令。預設命令是“rm –f”。

2、關於命令引數的變數
ARFLAGS
函式庫打包程式AR命令的引數。預設值是“rv”。
ASFLAGS
組合語言編譯器引數。(當明顯地呼叫“.s”或“.S”檔案時)。
CFLAGS
C語言編譯器引數。
CXXFLAGS
C++語言編譯器引數。
COFLAGS
RCS命令引數。
CPPFLAGS
C前處理器引數。( C 和 Fortran 編譯器也會用到)。
FFLAGS
Fortran語言編譯器引數。
GFLAGS
SCCS “get”程式引數。
LDFLAGS
連結器引數。(如:“ld”)
LFLAGS
Lex文法分析器引數。
PFLAGS
Pascal語言編譯器引數。
RFLAGS
Ratfor 程式的Fortran 編譯器引數。
YFLAGS
Yacc文法分析器引數。

四、隱含規則鏈
一個目標可能被一系列的隱含規則所作用。例如,一個[.o]的檔案生成,可能會是先被Yacc的[.y]檔案先成[.c],然後再被C的編譯器生成。我們把這一系列的隱含規則叫做“隱含規則鏈”。
如果檔案[.c]存在,那麼就直接呼叫C的編譯器的隱含規則,如果沒有[.c]檔案,但有一個[.y]檔案,那麼Yacc的隱含規則會被呼叫,生成[.c]檔案,然後,再呼叫C編譯的隱含規則最終由[.c]生成[.o]檔案,達到目標。

下面這個例子表示了,把所有的[.c]檔案都編譯成[.o]檔案.
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

自動化變數及其說明:
$@
表示規則中的目標檔案集。在模式規則中,如果有多個目標,那麼,"$@"就是匹配於目標中模式定義的集合。
$%
僅當目標是函式庫檔案中,表示規則中的目標成員名。例如,如果一個目標是"foo.a(bar.o)",那麼,"$%"就是"bar.o","$@"就是"foo.a"。如果目標不是函式庫檔案(Unix下是[.a],Windows下是[.lib]),那麼,其值為空。
$<
依賴目標中的第一個目標名字。如果依賴目標是以模式(即"%")定義的,那麼"$$?
所有比目標新的依賴目標的集合。以空格分隔。
$^
所有的依賴目標的集合。以空格分隔。如果在依賴目標中有多個重複的,那個這個變數會去除重複的依賴目標,只保留一份。
$+
這個變數很像"$^",也是所有依賴目標的集合。只是它不去除重複的依賴目標。
$*
這個變數表示目標模式中"%"及其之前的部分。如果目標是"dir/a.foo.b",並且目標的模式是"a.%.b",那麼,"$*"的值就是"dir/a.foo"。這個變數對於構造有關聯的檔名是比較有較。如果目標中沒有模式的定義,那麼"$*"也就不能被推匯出,但是,如果目標檔案的字尾是make 所識別的,那麼"$*"就是除了字尾的那一部分。例如:如果目標是"foo.c",因為".c"是make所能識別的字尾名,所以,"$*"的值就是"foo"。這個特性是GNU make的,很有可能不相容於其它版本的make,所以,你應該儘量避免使用"$*",除非是在隱含規則或是靜態模式中。如果目標中的字尾是make所不能識別的,那麼"$*"就是空值。

當你希望只對更新過的依賴檔案進行操作時,"$?"在顯式規則中很有用,例如,假設有一個函式庫檔案叫"lib",其由其它幾個object檔案更新。那麼把object檔案打包的比較有效率的Makefile規則是:
lib : foo.o bar.o lose.o win.o
ar r lib $?

在上述所列出來的自動量變數中。四個變數($@、$$(@D)
表示"$@"的目錄部分(不以斜槓作為結尾),如果"$@"值是"dir/foo.o",那麼"$(@D)"就是"dir",而如果"$@"中沒有包含斜槓的話,其值就是"."(當前目錄)。
$(@F)
表示"$@" 的檔案部分, 如果"$@" 值是"dir/foo.o" , 那麼"$(@F)" 就是"foo.o","$(@F)"相當於函式"$(notdir $@)"。

"$(*D)"
"$(*F)"
和上面所述的同理,也是取檔案的目錄部分和檔案部分。對於上面的那個例子,"$(*D)"返回"dir",而"$(*F)"返回"foo"
"$(%D)"
"$(%F)"
分別表示了函式包檔案成員的目錄部分和檔案部分。這對於形同"archive(member)"形式的目標中的"member"中包含了不同的目錄很有用。
"$("$(分別表示依賴檔案的目錄部分和檔案部分。
"$(^D)"
"$(^F)"
分別表示所有依賴檔案的目錄部分和檔案部分。(無相同的)
"$(+D)"
"$(+F)"
分別表示所有依賴檔案的目錄部分和檔案部分。(可以有相同的)
"$(?D)"
"$(?F)"
分別表示被更新的依賴檔案的目錄部分和檔案部分。

最後想提醒一下的是,對於"$


4、模式的匹配
一般來說,一個目標的模式有一個有字首或是字尾的"%",或是沒有前字尾,直接就是一個"%"。因為"%"代表一個或多個字元,所以在定義好了的模式中,我們把"%"所匹配的內容叫做"莖",例如"%.c"所匹配的檔案"test.c"中"test"就是"莖"。因為在目標和依賴目標中同時有"%"時,依賴目標的"莖"會傳給目標,當做目標中的"莖"。當一個模式匹配包含有斜槓(實際也不經常包含)的檔案時,那麼在進行模式匹配時,目錄部分會首先被移開,然後進行匹配,成功後,再把目錄加回去。在進行"莖"的傳遞時,我們需要知道這個步驟。例如有一個模式"e%t",檔案"src/eat"匹配於該模式,於是"src/a"就是其"莖",如果這個模式定義在依賴目標中,而被依賴於這個模式的目標中又有個模式"c%r",那麼,目標就是"src/car"。("莖"被傳遞)

5、過載內建隱含規則
你可以過載內建的隱含規則(或是定義一個全新的),例如你可以重新構造和內建隱含規則不同的命令,如:
%.o : %.c
$(CC) -c $(CPPFLAGS) $(CFLAGS) -D$(date)
你可以取消內建的隱含規則,只要不在後面寫命令就行。如:
%.o : %.s
同樣,你也可以重新定義一個全新的隱含規則,其在隱含規則中的位置取決於你在哪裡寫下這個規則。朝前的位置就靠前。


函式庫檔案也就是對Object檔案(程式編譯的中間檔案)的打包檔案。在Unix下,一般是由命令"ar"來完成打包工作。


使用make更新函式庫檔案
一、函式庫檔案的成員
一個函式庫檔案由多個檔案組成。你可以以如下格式指定函式庫檔案及其組成:
archive(member)
這個不是一個命令,而一個目標和依賴的定義。一般來說,這種用法基本上就是為了"ar"命令來服務的。如:
foolib(hack.o) : hack.o
ar cr foolib hack.o

如果要指定多個member,那就以空格分開,如:
foolib(hack.o kludge.o)
其等價於:
foolib(hack.o) foolib(kludge.o)


你還可以使用Shell的檔案萬用字元來定義,如:
foolib(*.o)

二、函式庫成員的隱含規則
當make搜尋一個目標的隱含規則時,一個特殊的特性是,如果這個目標是"a(m)"形式的,其會把目標變成"(m)"。於是,如果我們的成員是"%.o"的模式定義,並且如果我們使用"make foo.a(bar.o)"的形式呼叫Makefile時,隱含規則會去找"bar.o"的規則,如果沒有定義bar.o的規則,那麼內建隱含規則生效,make會去找bar.c檔案來生成bar.o,如果找得到的話,make執行的命令大致如下:
cc -c bar.c -o bar.o
ar r foo.a bar.o
rm -f bar.o

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/23168012/viewspace-1047360/,如需轉載,請註明出處,否則將追究法律責任。