網頁

顯示具有 makefile 標籤的文章。 顯示所有文章
顯示具有 makefile 標籤的文章。 顯示所有文章

2008年3月1日 星期六

介紹Makefile

Linux的內核配置文件有兩個,一個是隱含的.config文件,嵌入到主Makefile中;
另一個是include/linux/autoconf.h,嵌入到各個c源文件中,它們由make config
make menuconfigmake xconfig這些過程創建。幾乎所有的源文件都會通過linux/config.h
而嵌入autoconf.h,如果按照通常方法建立文件依賴關係(.depend),只要更新過autoconf.h,就會造成所有源代碼的重新編繹。

為了優化make過程,減少不必要的重新編繹,Linux開發了專用的mkdep工具,用它來取代gcc來生成.depend文件。
mkdep在處理源文件時,忽略linux/config.h這樣的頭文件,識別源文件宏指令中具有」CONFIG_」特徵的行。
例如,如果有」#ifdef CONFIG_SMP」這樣的行,它就會在.depend文件中輸出$(wildcard /usr/src/linux/include/config/smp.h)

include/config/下的文件是另一個工具split-includeautoconf.h中生成,
它利用autoconf.h中的CONFIG_標記,生成與mkdep相對應的文件。例如,如果autoconf.h中有」#undef CONFIG_SMP」這一行,
它就生成include/config/smp.h文件,內容為」#undef CONFIG_SMP」。這些文件名只在.depend文件中出現,
內核源文件是不會嵌入它們的。每配置一次內核,運行split-include一次。split-include會檢查舊的子文件的內容,
確定是不是要更新它們。這樣,不管autoconf.h修改日期如何,只要其配置不變,make就不會重新編繹內核。

如果系統的編繹選項發生了變化,Linux也能進行增量編繹。為了做到這一點,make每編繹一個源文件時生成一個flags文件。
例如編繹sched.c時,會在相同的目錄下生成隱含的.sched.o.flags文件。它是Makefile的一個片斷,
make進入某個子目錄編繹時,會搜索其中的flags文件,將它們嵌入到Makefile中。
這些flags代碼測試當前的編繹選項與原來的是不是相同,如果相同,就將自已對應的目標文件加入FILES_FLAGS_UP_TO_DATE列表,
然後,系統從編繹對像表中刪除它們,得到FILES_FLAGS_CHANGED列表,最後,將它們設為目標進行更新。

下一步準備逐步深入的剖析Makefile代碼。

==========================================
Makefile解讀之二: sub-make
==========================================
Linux各級內核源代碼的子目錄下都有Makefile,大多數Makefile要嵌入主目錄下的Rule.make
Rule.make將識別各個Makefile中所定義的一些變量。變量obj-y表示需要編繹到內核中的目標文件名集合,
定義O_TARGET表示將obj-y連接為一個O_TARGET名稱的目標文件,定義L_TARGET表示將obj-y合併為一個L_TARGET名稱的庫文件。
同樣obj-m表示需要編繹成模塊的目標文件名集合。如果還需進行子目錄make,則需要定義subdir-ysubdir-m
Makefile中,用」obj-$(CONFIG_BINFMT_ELF) += binfmt_elf.o」和」subdir-$(CONFIG_EXT2_FS) += ext2
這種形式自動為obj-yobj-msubdir-ysubdir-m添加文件名。有時,情況沒有這麼單純,還需要使用條件語句個別對待。
Makefile中還有其它一些變量,如mod-subdirs定義了subdir-m以外的所有模塊子目錄。

Rules.make是如何使make進入子目錄的呢? 先來看subdir-y是如何處理的,
Rules.make中,先對subdir-y中的每一個文件名加上前綴」_subdir_」再進行排序生成subdir-list集合,再以它作為目標集,
對其中每一個目標產生一個子make,同時將目標名的前綴去掉得到子目錄名,作為子make的起始目錄參數。subdir-msubdir-y類似,
但情況稍微複雜一些。由於subdir-y中可能有模塊定義,因此利用mod-subdirs變量將subdir-y中模塊目錄提取出來,
再與subdir-m合成一個大的MOD_SUB_DIRS集合。subdir-m的目標所用的前綴是」_modsubdir_」。

一點說明,子目錄中的MakefileRules.make都沒有嵌入.config文件,它是通過主Makefile向下傳遞MAKEFILES變量完成的。
MAKEFILESmake自已識別的一個變量,在執行新的Makefile之前,make會首先加載MAKEFILES所指的文件。
在主Makefile中它即指向.config


==========================================
Makefile解讀之三: 模塊的版本化處理
==========================================
模塊的版本化是內核與模塊接口之間進行嚴格類型匹配的一種方法。當內核配置了CONFIG_MODVERSIONS之後,
make dep操作會在include/linux/modules/目錄下為各級Makefileexport-objs變量所對應的源文件生成擴展名為.ver的文件。

例如對於kernel/ksyms.cmake用以下命令生成對應的ksyms.ver

gcc -E -D__KERNEL__ -D__GENKSYMS__ ksyms.c /sbin/genksyms -k 2.4.1 >; ksyms.ver

-D__GENKSYMS__的作用是使ksyms.c中的EXPORT_SYMBOL宏不進行擴展。genksyms命令識別EXPORT_SYMBOL()中的函數名和對應的原型,
再根據其原型計算出該函數的版本號。

例如ksyms.c中有一行:
EXPORT_SYMBOL(kmalloc);
kmalloc原型是:
void *kmalloc(size_t, int);
genksyms程序對應的輸出為:
#define __ver_kmalloc 93d4cfe6
#define kmalloc _set_ver(kmalloc)
在內核符號表和模塊中,kmalloc將變成kmalloc_R93d4cfe6

在生成完所有的.ver文件後,make將重建include/linux/modversions.h文件,它包含一系列#include指令行嵌入各個.ver文件。
在編繹內核本身export-objs中的文件時,make會增加一個」-DEXPORT_SYMTAB」編繹標誌,它使源文件嵌入modversions.h文件,
EXPORT_SYMBOL宏展開中的函數名字符串進行版本名擴展;同時,它也定義_set_ver()宏為一空操作,使代碼中的函數名不受其影響。
在編繹模塊時,make會增加」-include=linux/modversion.h -DMODVERSIONS」編繹標誌,使模塊中代碼的函數名得到相應版本擴展。

由於生成.ver文件比較費時,make還為每個.ver創建了一個後綴為.stamp時戳文件。在make dep時,
如果其.stamp文件比源文件舊才重新生成.ver文件,否則只是更新.stamp文件時戳。另外,在生成.vermodversions.h文件時,
make都會比較新文件和舊文件的內容,保持它們修改時間為最舊。

Config.in的用法

1) 頂層菜單mainmenu_name /prompt/ /prompt/ 是用'或"包圍的字符串,'與"的區別是'…'中可使用$引用變量的值。mainmenu_name 設置最高層菜單的名字,它只在 make xconfig 時才會顯示。
2) 詢問語句
bool /prompt/ /symbol/ hex /prompt/ /symbol/ /word/ int /prompt/ /symbol/ /word/ string /prompt/ /symbol/ /word/ tristate /prompt/ /symbol/
詢問語句首先顯示一串提示符 /prompt/,等待用戶輸入,並把輸入的結果賦給 /symbol/ 所代表的配置變量。不同的詢問語句的區別在於它們接受的輸入數據類型不同,比如 bool 接受布爾類型( y 或 n ),hex 接受 16 進制數據。有些詢問語句還有第三個參數 /word/,用來給出缺省值。
3) 定義語句
define_bool /symbol/ /word/ define_hex /symbol/ /word/ define_int /symbol/ /word/ define_string /symbol/ /word/ define_tristate /symbol/ /word/
不同於詢問語句等待用戶輸入,定義語句顯式的給配置變量 /symbol/ 賦值 /word/。
4) 依賴語句
dep_bool /prompt/ /symbol/ /dep/ ... dep_mbool /prompt/ /symbol/ /dep/ ... dep_hex /prompt/ /symbol/ /word/ /dep/ ... dep_int /prompt/ /symbol/ /word/ /dep/ ... dep_string /prompt/ /symbol/ /word/ /dep/ ... dep_tristate /prompt/ /symbol/ /dep/ ...
與詢問語句類似,依賴語句也是定義新的配置變量。不同的是,配置變量/symbol/的取值範圍將依賴於配置變量列表/dep/ …。這就意味著:被定義的配置變量所對應功能的取捨取決於依賴列表所對應功能的選擇。以dep_bool為例,如果/dep/ …列表的所有配置變量都取值y,則顯示/prompt/,用戶可輸入任意的值給配置變量/symbol/,但是只要有一個配置變量的取值為n,則/symbol/被強製成n。不同依賴語句的區別在於它們由依賴條件所產生的取值範圍不同。
5) 選擇語句
choice /prompt/ /word/ /word/
choice 語句首先給出一串選擇列表,供用戶選擇其中一種。比如 Linux for ARM 支持多種基於 ARM core 的 CPU,Linux 使用 choice 語句提供一個 CPU 列表,供用戶選擇:
choice 'ARM system type' "Anakin CONFIG_ARCH_ANAKIN Archimedes/A5000 CONFIG_ARCH_ARCA5K Cirrus-CL-PS7500FE CONFIG_ARCH_CLPS7500 …… SA1100-based CONFIG_ARCH_SA1100 Shark CONFIG_ARCH_SHARK" RiscPC
Choice 首先顯示 /prompt/,然後將 /word/ 分解成前後兩個部分,前部分為對應選擇的提示符,後部分是對應選擇的配置變量。用戶選擇的配置變量為 y,其餘的都為 n。
6) if語句
if [ /expr/ ] ; then /statement/ ... fi if [ /expr/ ] ; then /statement/ ... else /statement/ ... fi
if 語句對配置變量(或配置變量的組合)進行判斷,並作出不同的處理。判斷條件 /expr/ 可以是單個配置變量或字符串,也可以是帶操作符的表達式。操作符有:=,!=,-o,-a 等。
7) 菜單塊(menu block)語句
mainmenu_option next_commentcomment '…..'…endmenu
引入新的菜單。在向內核增加新的功能後,需要相應的增加新的菜單,並在新菜單下給出此項功能的配置選項。Comment 後帶的註釋就是新菜單的名稱。所有歸屬於此菜單的配置選項語句都寫在 comment 和 endmenu 之間。
8) Source 語句source /word//word/ 是文件名,source 的作用是調入新的文件。
3.3 預設值配置
Linux 內核支持非常多的硬件平台,對於具體的硬件平台而言,有些配置就是必需的,有些配置就不是必需的。另外,新增加功能的正常運行往往也需要一定的先決條件,針對新功能,必須作相應的配置。因此,特定硬件平台能夠正常運行對應著一個最小的基本配置,這就是缺省配置。
Linux 內核中針對每個 ARCH 都會有一個缺省配置。在向內核代碼增加了新的功能後,如果新功能對於這個 ARCH 是必需的,就要修改此 ARCH 的缺省配置。修改方法如下(在 Linux 內核根目錄下):

  1. 備份 .config 文件

  2. cp arch/arm/deconfig .config

  3. 修改 .config

  4. cp .config arch/arm/deconfig

  5. 恢復 .config
如果新增的功能適用於許多的 ARCH,只要針對具體的 ARCH,重複上面的步驟就可以了。
3.4 help file
大家都有這樣的經驗,在配置 Linux 內核時,遇到不懂含義的配置選項,可以查看它的幫助,從中可得到選擇的建議。下面我們就看看如何給給一個配置選項增加幫助信息。
所有配置選項的幫助信息都在 Documentation/Configure.help 中,它的格式為:
<description><variable name><help file>
<description> 給出本配置選項的名稱,<variable name> 對應配置變量,<help file> 對應配置幫助信息。在幫助信息中,首先簡單描述此功能,其次說明選擇了此功能後會有什麼效果,不選擇又有什麼效果,最後,不要忘了寫上"如果不清楚,選擇 N(或者)Y",給不知所措的用戶以提示。

Makefile變數的傳遞

. 兩種風格的變量定義
GNU有兩種定義變量的方式,它們的不同體現在定義它們的風格和他們被展開的方式。
第一類叫做遞歸展開變量(Recursively Expanded Variable)。用=define關鍵字都
可以定義這種變量,如果變量的定義引用了其它的變量,那麼引用會一直展開下去,
直到找到被引用的變量的最新的定義,並以此作為改變量的值返回。例如:
代碼:
foo = $(bar)
bar = $(ugh)
ugh = Huh?
$(foo)的值究竟是什麼呢? $(foo)被展開成$(bar)$(bar)被展開成$(ugh),最終$(ugh)
被展開成「Huh?」,那麼$(foo)的值就是「Huh?」這種類型的變量是所有其他make工具支持
的變量類型。它有著自己的有點和缺點:
優點:
它可以向後引用變量
缺點:
1).你不能對該變量進行任何擴展,例如
CFLAGS=$(CFLAGS) -O
會造成無限循環展開,所有對於遞歸展開變量,這樣做是不允許的。
2).如果makefile中的某個變量調用了某些函數,那麼在變量被展開的每一次,函數都會被調用,
說的好些,無非是make慢點,更壞的情況是一些 shell函數和通配符的重複調用的結果往往
是難以預知的。
最後,用一個例子說明上面的問題。用這個makefile試一下就明白了(分別開啟和註銷掉第三行,
你就能看到結果)
代碼:
bar = test
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all: ;echo foo
為了解決這些問題,GNU定義了另一中其它風格的變量,叫做簡單擴展變量(Simply Expanded Variables)
簡單擴展變量用符號:=來定義,用這種方式定義的變量,會在變量的定義點,按照被引用的變量的當前值
進行展開。同樣寫個例子,測試一下,這東西用文字表達太困難了
代碼:
m := mm
x := $(m)
y := $(x) bar
x := later
all:;echo $(x) $(y)

這種定義變量的方式更適合在大的編程項目中使用,因為它更像我們一般的編程語言。
其實,GNU還對makefile中定義變量的方式進行了第三種擴展,用?=定義變量,它的含義是如果變量還沒定義
就定義它,例如 FOO ?= Am I defined?,那麼,它相當於下面的代碼片斷
代碼:
ifeq ($(origin FOO), undefined)
FOO = Am I defined?
endif
這種寫法可以用於為用戶提供默認的選項,即如果用戶不定義,就提供給他默認的。另外,要說明的是把變量
定義成NULl,也是定義了變量,此時?=將不再起作用。
注意:
最後要說的是,無論我們使用那種風格定義變量,都不要在定義變量的行後面加上隨機數目的空格,之後寫註釋,
這個我們想像的是不一樣的,例如
ml = magic_linux

ml = magic_linux # My favourite linux distribution
這是兩個完全不同的變量,其中第一個ml的值是magic_linux,而後一個的值是『magic_linux 』,所以除非
你有意要定義末尾帶有空格的變量,否則不要在定義變量行末尾隨便添加空格和註釋。
. 高級的變量引用方法
有兩種方法: 1. 替換變量引用 2. 計算變量的值(更確切的說是推導)
第一種
定義方法: $(var: a=b) ${var: a=b}
意義:把其變量a的值中,每一個詞的最後一個字幕換成b
例如:
代碼:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
all:;echo $(bar)
這時,bar的值等於a.c b.c c.c
另外,我們還可以用bar := $(foo:%.o=%.c)的形式
第二種
這是一種高級的makefile編程技術,一般我們很少使用它,但是它並不難,先看個例子
代碼:
x = y
y = z
a := $($(x))
此時,a的值是什麼呢?答案是z。因為內層的$(x)=y,而外層的$(y)=z,也就是說a的值是通過$(x)計算出來的,所以
叫做計算變量的值。當然嵌套可是有很多層,自己試試吧,不多說了。在嵌套變量引用的時候,還可以包含函數調用,
例如:
代碼:
x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))
一番推導,a的值等於Hello
整個變量的推導過程中可以涉及到多個變量,這樣,一個變量就可以有多個字面值,例如:
代碼:
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))
根據usa_ause_dirs這兩個兩個變量的值,dirs可以有不同的值
另外,推導變量值還可以用在替換變量引用中,例如:
代碼:
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)
根據上面a1值的不同,source可以等於a.c b.c c.c1.c 2.c 3.c
這種內嵌變量的限制是你不能把函數調用作為定義變量的一種方式,舉個例子
代碼:
ifdef do_sort
func := sort
else
func := strip
endif
bar := a d b g q c
foo := $($(func) $(bar))
foo的是什麼呢?它是sort a d b g q cstrip a d b g q c,而並不是我們想像的把a d b g q c作為參數
傳遞給sortstrip
你還可以把推導出來的變量名作為變量的左值
代碼:
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
最後要說明的是,要注意把遞歸擴展變量和嵌套推廣變量區分開。
. 變量得到值的辦法

1) 在你運行make的時候覆蓋變量的值,例如make CFLAGS='-g -O'
2) makefile中用上面說的方式為變量賦值
3) 設定的環境變量可以在makefile中成為變量
4) GNU定義的一些自動變量,詳見http://www.gnu.org/software/make/manual/html_chapter/make_10.html#SEC111
5) 一些變量有固定的初始值,詳見http://www.gnu.org/software/make/manual/html_chapter/make_10.html#SEC106
. 為變量添加值
你可以通過+=為已定義的變量添加新的值,例如
代碼:
objects = main.o foo.o bar.o utils.o
objects += another.o
看上去,+=有些和下面的代碼類似
代碼:
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o
但是,它們還是存在這一些不同
當變量從前沒有被定義過, +==是一樣的,它定義一個遞歸展開的變量,但是,當變量已經有定義的時候,+=只是簡單
的進行字符的添加工作。
如果起初你用:=定義變量,那麼+=只是利用變量的當前值進行添加,這和我們的直覺是一樣的,例如:
variable := magic_
variable += linux
此時variable的值就是magic_linux
如果起初用=定義變量,+=的行為就變得有些古怪,它並不會在使用+=的地方馬上進行變量展開,而是會把展開工作推後,
直到它找到最後變量的定義,這和=定義變量的行為是類似的,但是總覺得不合直覺,例如:
代碼:
var = I love
variable = linux
var += $(variable)
variable = magic
all:;echo $(var)
當你使用var := I love和上面的情況作對比的時候,這種差別就明顯了。這種定義方式在當變量中引用了其他的變量時是
很有用的。例如:
代碼:
CFLAGS = $(includes) -O
...
CFLAGS += -pg # enable profiling
由於CFLAGS是遞歸擴展的,所以make處理CFLAGS時,並不會對其進行擴展,所以只要在使用CFLAGS前,把include定義就好了。
看似我們可以用CFLAGS := $(CFLAGS) -O來完成上面的任務,但是他們之間仍有差別,這會使CFLAGS變成一個簡單擴展型的變
量,如果此時include尚未定義,整個CFLAGS就會變成-O -pg,而我們用+=是想把CFLAGS設定成$(include) -O-pg,這顯然和
我們想像的有差距。
. 使用override關鍵字
一般情況下,如果你是用命令行參數定義一個變量的話,在makefile中的定義就會被忽略,如果你想讓你的定義仍然有效,就要
使用override關鍵字,向下面這樣:
override variable = magic override variable := magic
此時,用戶在命令行中指定的值就會被忽略,如果你想在用戶指定的命令行後面添加上自己的文字,用下面的方法
override variable += magic
這個東西的使用動機就是,你可以為用戶指定一些你默認想讓他們使用的選項,例如:
override CFLAGS += -g

. 使用define定義變量
define可以定義一個代表多行指令的變量,例如
代碼:
define two-lines
echo foo
echo $(bar)
endef
它相當於
代碼:
two-lines = echo foo; echo $(bar)
. 為特定目標的指定變量值
我們可以在編譯特定的目標文件的時候,為變量設定特殊的值,例如:
代碼:
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
在這個依賴關係中,編譯prog時,就會為編譯參數加上-g選項。當然你也可以使用override關鍵字
代碼:
prog : override CFLAGS = -g
prog : prog.o foo.o bar.o
十一. 為特定樣式的文件指定變量值
我們還可以為生成特定各式的文件指定特殊的編譯參數,例如
%.o : CFLAGS = -O
這樣,所有生成.o的編譯參數都會被加上-O的選項。

Makefile(VPATH及PHONY)

在一些大的工程中,有大量的源文件,我們通常的做法是把這許多的源文件分類,並存放在不同的目錄中。所以,當make需要去找尋文件的依賴關係時,你可以在文件前加上路徑,但最好的方法是把一個路徑告訴make,讓make在自動去找。
Makefile文件中的特殊變量「VPATH」就是完成這個功能的,如果沒有指明這個變量,make只會在當前的目錄中去找尋依賴文件和目標文件。如果定義了這個變量,那麼,make就會在當當前目錄找不到的情況下,到所指定的目錄中去找尋文件了。
VPATH = src:../headers
上面的的定義指定兩個目錄,「src」和「../headers」,make會按照這個順序進行搜索。目錄由「冒號」分隔。(當然,當前目錄永遠是最高優先搜索的地方)
另一個設置文件搜索路徑的方法是使用make的「vpath」關鍵字(注意,它是全小寫的),這不是變量,這是一個make的關鍵字,這和上面提到的那個VPATH變量很類似,但是它更為靈活。它可以指定不同的文件在不同的搜索目錄中。這是一個很靈活的功能。它的使用方法有三種:
1vpath <pattern> <directories>
為符合模式<pattern>的文件指定搜索目錄<directories>
2vpath <pattern>
清除符合模式<pattern>的文件的搜索目錄。
3vpath
清除所有已被設置好了的文件搜索目錄。
vapth使用方法中的<pattern>需要包含「%」字符。「%」的意思是匹配零或若干字符,例如,「%.h」表示所有以「.h」結尾的文件。<pattern>指定了要搜索的文件集,而<directories>則指定了<pattern>的文件集的搜索的目錄。例如:
vpath %.h ../headers
該語句表示,要求make在「../headers」目錄下搜索所有以「.h」結尾的文件。(如果某文件在當前目錄沒有找到的話)
我們可以連續地使用vpath語句,以指定不同搜索策略。如果連續的vpath語句中出現了相同的<pattern>,或是被重複了的<pattern>,那麼,make會按照vpath語句的先後順序來執行搜索。如:
vpath %.c foo
vpath % blish
vpath %.c bar
其表示「.c」結尾的文件,先在「foo」目錄,然後是「blish」,最後是「bar」目錄。
vpath %.c foo:bar
vpath % blish
而上面的語句則表示「.c」結尾的文件,先在「foo」目錄,然後是「bar」目錄,最後才是「blish」目錄。
五、偽目標
最早先的一個例子中,我們提到過一個「clean」的目標,這是一個「偽目標」,
clean:
rm *.o temp
正像我們前面例子中的「clean」一樣,即然我們生成了許多文件編譯文件,我們也應該提供一個清除它們的「目標」以備完整地重編譯而用。 (以「make clean」來使用該目標)
因為,我們並不生成「clean」這個文件。「偽目標」並不是一個文件,只是一個標籤,由於「偽目標」不是文件,所以make無法生成它的依賴關係和決定它是否要執行。我們只有通過顯示地指明這個「目標」才能讓其生效。當然,「偽目標」的取名不能和文件名重名,不然其就失去了「偽目標」的意義了。
當然,為了避免和文件重名的這種情況,我們可以使用一個特殊的標記「.PHONY」來顯示地指明一個目標是「偽目標」,向make說明,不管是否有這個文件,這個目標就是「偽目標」。
.PHONY : clean
只要有這個聲明,不管是否有「clean」文件,要運行「clean」這個目標,只有「make clean」這樣。於是整個過程可以這樣寫:
.PHONY: clean
clean:
rm *.o temp
偽目標一般沒有依賴的文件。但是,我們也可以為偽目標指定所依賴的文件。偽目標同樣可以作為「默認目標」,只要將其放在第一個。一個示例就是,如果你的Makefile需要一口氣生成若干個可執行文件,但你只想簡單地敲一個make完事,並且,所有的目標文件都寫在一個Makefile中,那麼你可以使用「偽目標」這個特性:
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
我們知道,Makefile中的第一個目標會被作為其默認目標。我們聲明了一個「all」的偽目標,其依賴於其它三個目標。由於偽目標的特性是,總是被執行的,所以其依賴的那三個目標就總是不如「all」這個目標新。所以,其它三個目標的規則總是會被決議。也就達到了我們一口氣生成多個目標的目的。「.PHONY : all」聲明了「all」這個目標為「偽目標」。
隨便提一句,從上面的例子我們可以看出,目標也可以成為依賴。所以,偽目標同樣也可成為依賴。看下面的例子:
.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」命令來達到清除不同種類文件的目的。

Makefile自動生成依賴及多目標

六、多目標

Makefile的規則中的目標可以不止一個,其支持多目標,有可能我們的多個目標同時依賴於一個文件,並且其生成的命令大體類似。於是我們就能把其合併起來。當然,多個目標的生成規則的執行命令是同一個,這可能會可我們帶來麻煩,不過好在我們的可以使用一個自動化變量「$@」(關於自動化變量,將在後面講述),這個變量表示著目前規則中所有的目標的集合,這樣說可能很抽像,還是看一個例子吧。
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,後面的為參數。關於函數,將在後面講述。這裡的這個函數是截取字符串的意思,「$@」表示目標的集合,就像一個數組,「$@」依次取出目標,並執於命令。
七、靜態模式
靜態模式可以更加容易地定義多目標的規則,可以讓我們的規則變得更加的有彈性和靈活。我們還是先來看一下語法:
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
...
targets定義了一系列的目標文件,可以有通配符。是目標的一個集合。
target-parrtern是指明了targets的模式,也就是的目標集模式。
prereq-parrterns是目標的依賴模式,它對target-parrtern形成的模式再進行一次依賴目標的定義。
這樣描述這三個東西,可能還是沒有說清楚,還是舉個例子來說明一下吧。如果我們的<target-parrtern>定義成「%.o」,意思是我們的<target>集合中都是以「.o」結尾的,而如果我們的<prereq-parrterns>定義成「%.c」,意思是對<target-parrtern>所形成的目標集進行二次定義,其計算方法是,取<target-parrtern>模式中的「%」(也就是去掉了[.o]這個結尾),並為其加上[.c]這個結尾,形成的新集合。
所以,我們的「目標模式」或是「依賴模式」中都應該有「%」這個字符,如果你的文件名中有「%」那麼你可以使用反斜槓「」進行轉義,來標明真實的「%」字符。
看一個例子:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $(CC) -c $(CFLAGS) $(CC) -c $(CFLAGS) $(CC) -c $(CFLAGS) $(CC) -c $(CFLAGS) $(CC) -c $(CFLAGS) $(CC) -c $(CFLAGS) $< -o $@
amp;lt; -o $@
amp;lt; -o $@
amp;lt; -o $@
amp;lt; -o $@
上面的例子中,指明了我們的目標從$object中獲取,「%.o」表明要所有以「.o」結尾的目標,也就是「foo.o bar.o」,也就是變量$object集合的模式,而依賴模式「%.c」則取模式「%.o」的「%」,也就是「foo bar」,並為其加下「.c」的後綴,於是,我們的依賴目標就是「foo.c bar.c」。而命令中的「$<」和「$@」則是自動化變量,「$<」表示所有的依賴目標集(也就是「foo.c bar.c」),「$@」表示目標集(也就是「foo.o bar.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
試想,如果我們的「%.o」有幾百個,那種我們只要用這種很簡單的「靜態模式規則」就可以寫完一堆規則,實在是太有效率了。「靜態模式規則」的用法很靈活,如果用得好,那會一個很強大的功能。再看一個例子:
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 emacs -f batch-byte-compile emacs -f batch-byte-compile emacs -f batch-byte-compile $<
amp;lt;
amp;lt;
$(filter %.o,$(files))表示調用Makefilefilter函數,過濾「$filter」集,只要其中模式為「%.o」的內容。其的它內容,我就不用多說了吧。這個例字展示了Makefile中更大的彈性。
八、自動生成依賴性
Makefile中,我們的依賴關係可能會需要包含一系列的頭文件,比如,如果我們的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
於是由編譯器自動生成的依賴關係,這樣一來,你就不必再手動書寫若干文件的依賴關係,而由編譯器自動生成了。需要提醒一句的是,如果你使用GNUC/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
那麼,編譯器的這個功能如何與我們的Makefile聯繫在一起呢。因為這樣一來,我們的Makefile也要根據這些源文件重新生成,讓Makefile自已依賴於源文件?這個功能並不現實,不過我們可以有其它手段來迂迴地實現這一功能。GNU組織建議把編譯器為每一個源文件的自動生成的依賴關係放到一個文件中,為每一個「name.c」的文件都生成一個「name.d」的Makefile文件,[.d]文件中就存放對應[.c]文件的依賴關係。
於是,我們可以寫出[.c]文件和[.d]文件的依賴關係,並讓make自動更新或自成[.d]文件,並把其包含在我們的主Makefile中,這樣,我們就可以自動化地生成每個文件的依賴關係了。
這裡,我們給出了一個模式規則來產生[.d]文件:
%.d: %.c
@set -e; rm -f $@; $(CC) -M $(CPPFLAGS) @set -e; rm -f $@; $(CC) -M $(CPPFLAGS) @set -e; rm -f $@; $(CC) -M $(CPPFLAGS) @set -e; rm -f $@; $(CC) -M $(CPPFLAGS) $< > $@.$$$$; sed 's,($*).o[ :]*,1.o $@ : ,g' < $@.$$$$ > $@; rm -f $@.$$$$
amp;lt; > $@.$$; sed 's,($*).o[ :]*,1.o $@ : ,g' < $@.$$ > $@; rm -f $@.$$
amp;lt; > $@.$; sed 's,($*).o[ :]*,1.o $@ : ,g' < $@.$ > $@; rm -f $@.$
這個規則的意思是,所有的[.d]文件依賴於[.c]文件,「rm -f $@」的意思是刪除所有的目標,也就是[.d]文件,第二行的意思是,為每個依賴文件「$<」,也就是[.c]文件生成依賴文件,「$@」表示模式「%.d」文件,如果有一個C文件是name.c,那麼「%」就是「name」,「$$$$」意為一個隨機編號,第二行生成的文件有可能是「name.d.12345」,第三行使用sed命令做了一個替換,關於sed命令的用法請參看相關的使用文檔。第四行就是刪除臨時文件。
總而言之,這個模式要做的事就是在編譯器生成的依賴關係中加入[.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],關於這個「替換」的內容,在後面我會有更為詳細的講述。當然,你得注意次序,因為include是按次來載入文件,最先載入的[.d]文件中的目標會成為默認目標

Makefile字串函式的用法

字符串處理函數
$(subst <from>,<to>,<text>)
名稱:字符串替換函數——subst
功能:把字串<text>中的<from>字符串替換成<to>
返回:函數返回被替換過後的字符串。
示例:

$(subst ee,EE,feet on the street)
把「feet on the street」中的「ee」替換成「EE」,返回結果是「fEEt on the strEEt」。
$(patsubst <pattern>,<replacement>,<text>)
名稱:模式字符串替換函數——patsubst
功能:查找<text>中的單詞(單詞以「空格」、「Tab」或「回車」「換行」分隔)是否符合模式<pattern>,如果匹配的話,則以<replacement>替換。這裡,<pattern>可以包括通配符「%」,表示任意長度的字串。如果<replacement>中也包含「%」,那麼,<replacement>中的這個「%」將是<pattern>中的那個「%」所代表的字串。(可以用「」來轉義,以「%」來表示真實含義的「%」字符)
返回:函數返回被替換過後的字符串。
示例:
$(patsubst %.c,%.o,x.c.c bar.c)
把字串「x.c.c bar.c」符合模式[%.c]的單詞替換成[%.o],返回結果是「x.c.o bar.o
備註:
這和我們前面「變量章節」說過的相關知識有點相似。如:
$(var:<pattern>=<replacement>)
相當於
$(patsubst <pattern>,<replacement>,$(var))」,
而「$(var: <suffix>=<replacement>)
則相當於
$(patsubst %<suffix>,%<replacement>,$(var))」。
例如有:objects = foo.o bar.o baz.o
那麼,「$(objects:.o=.c)」和「$(patsubst %.o,%.c,$(objects))」是一樣的。
$(strip <string>)
名稱:去空格函數——strip
功能:去掉<string>字串中開頭和結尾的空字符。
返回:返回被去掉空格的字符串值。
示例:
$(strip a b c )
把字串「a b c 」去到開頭和結尾的空格,結果是「a b c」。
$(findstring <find>,<in>)
名稱:查找字符串函數——findstring
功能:在字串<in>中查找<find>字串。
返回:如果找到,那麼返回<find>,否則返回空字符串。
示例:
$(findstring a,a b c)
$(findstring a,b c)
第一個函數返回「a」字符串,第二個返回「」字符串(空字符串)
$(filter <pattern...>,<text>)
名稱:過濾函數——filter
功能:以<pattern>模式過濾<text>字符串中的單詞,保留符合模式<pattern>的單詞。可以有多個模式。
返回:返回符合模式<pattern>的字串。
示例:
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 <pattern...>,<text>)
名稱:反過濾函數——filter-out
功能:以<pattern>模式過濾<text>字符串中的單詞,去除符合模式<pattern>的單詞。可以有多個模式。
返回:返回不符合模式<pattern>的字串。
示例:
objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
$(filter-out $(mains),$(objects)) 返回值是「foo.o bar.o」。
$(sort <list>)
名稱:排序函數——sort
功能:給字符串<list>中的單詞排序(升序)。
返回:返回排序後的字符串。
示例:$(sort foo bar lose)返回「bar foo lose」 。
備註:sort函數會去掉<list>中相同的單詞。
$(word <n>,<text>)
名稱:取單詞函數——word
功能:取字符串<text>中第<n>個單詞。(從一開始)
返回:返回字符串<text>中第<n>個單詞。如果<n><text>中的單詞數要大,那麼返回空字符串。
示例:$(word 2, foo bar baz)返回值是「bar」。
$(wordlist <s>,<e>,<text>)
名稱:取單詞串函數——wordlist
功能:從字符串<text>中取從<s>開始到<e>的單詞串。<s><e>是一個數字。
返回:返回字符串<text>中從<s><e>的單詞字串。如果<s><text>中的單詞數要大,那麼返回空字符串。如果<e>大於<text>的單詞數,那麼返回從<s>開始,到<text>結束的單詞串。
示例: $(wordlist 2, 3, foo bar baz)返回值是「bar baz」。
$(words <text>)
名稱:單詞個數統計函數——words
功能:統計<text>中字符串中的單詞個數。
返回:返回<text>中的單詞數。
示例:$(words, foo bar baz)返回值是「3」。
備註:如果我們要取<text>中最後的一個單詞,我們可以這樣:$(word $(words <text>),<text>)
$(firstword <text>)
名稱:首單詞函數——firstword
功能:取字符串<text>中的第一個單詞。
返回:返回字符串<text>的第一個單詞。
示例:$(firstword foo bar)返回值是「foo」。
備註:這個函數可以用word函數來實現:$(word 1,<text>)
以上,是所有的字符串操作函數,如果搭配混合使用,可以完成比較複雜的功能。這裡,舉一個現實中應用的例子。我們知道,make使用「VPATH」變量來指定「依賴文件」的搜索路徑。於是,我們可以利用這個搜索路徑來指定編譯器對頭文件的搜索路徑參數CFLAGS,如:
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))
如果我們的「$(VPATH)」值是「src:../headers」,那麼「$(patsubst %,-I%,$(subst :, ,$(VPATH)))」將返回「-Isrc -I../headers」,這正是ccgcc搜索頭文件路徑的參數。
三、文件名操作函數
下面我們要介紹的函數主要是處理文件名的。每個函數的參數字符串都會被當做一個或是一系列的文件名來對待。
$(dir <names...>)
名稱:取目錄函數——dir
功能:從文件名序列<names>中取出目錄部分。目錄部分是指最後一個反斜槓(「/」)之前的部分。如果沒有反斜槓,那麼返回「./」。
返回:返回文件名序列<names>的目錄部分。
示例: $(dir src/foo.c hacks)返回值是「src/ ./」。
$(notdir <names...>)
名稱:取文件函數——notdir
功能:從文件名序列<names>中取出非目錄部分。非目錄部分是指最後一個反斜槓(「/」)之後的部分。
返回:返回文件名序列<names>的非目錄部分。
示例: $(notdir src/foo.c hacks)返回值是「foo.c hacks」。
$(suffix <names...>)
名稱:取後綴函數——suffix
功能:從文件名序列<names>中取出各個文件名的後綴。
返回:返回文件名序列<names>的後綴序列,如果文件沒有後綴,則返回空字串。
示例:$(suffix src/foo.c src-1.0/bar.c hacks)返回值是「.c .c」。
$(basename <names...>)
名稱:取前綴函數——basename
功能:從文件名序列<names>中取出各個文件名的前綴部分。
返回:返回文件名序列<names>的前綴序列,如果文件沒有前綴,則返回空字串。
示例:$(basename src/foo.c src-1.0/bar.c hacks)返回值是「src/foo src-1.0/bar hacks」。
$(addsuffix <suffix>,<names...>)
名稱:加後綴函數——addsuffix
功能:把後綴<suffix>加到<names>中的每個單詞後面。
返回:返回加過後綴的文件名序列。
示例:$(addsuffix .c,foo bar)返回值是「foo.c bar.c」。
$(addprefix <prefix>,<names...>)
名稱:加前綴函數——addprefix
功能:把前綴<prefix>加到<names>中的每個單詞後面。
返回:返回加過前綴的文件名序列。
示例:$(addprefix src/,foo bar)返回值是「src/foo src/bar」。
$(join <list1>,<list2>)
名稱:連接函數——join
功能:把<list2>中的單詞對應地加到<list1>的單詞後面。如果<list1>的單詞個數要比<list2>的多,那麼,<list1>中的多出來的單詞將保持原樣。如果<list2>的單詞個數要比<list1>多,那麼,<list2>多出來的單詞將被複製到<list2>中。
返回:返回連接過後的字符串。
示例:$(join aaa bbb , 111 222 333)返回值是「aaa111 bbb222 333

Makefile的函式call,origin,shell,foreach

Makefile用法(call,origin,shell,foreach)

四、foreach 函數
foreach函數和別的函數非常的不一樣。因為這個函數是用來做循環用的,Makefile中的foreach函數幾乎是仿照於Unix標準Shell/bin/sh)中的for語句,或是C-Shell/bin/csh)中的foreach語句而構建的。它的語法是:
$(foreach <var>,<list>,<text>)
這個函數的意思是,把參數<list>中的單詞逐一取出放到參數<var>所指定的變量中,然後再執行<text>所包含的表達式。每一次<text>會返回一個字符串,循環過程中,<text>的所返回的每個字符串會以空格分隔,最後當整個循環結束時,<text>所返回的每個字符串所組成的整個字符串(以空格分隔)將會是foreach函數的返回值。
所以,<var>最好是一個變量名,<list>可以是一個表達式,而<text>中一般會使用<var>這個參數來依次枚舉<list>中的單詞。舉個例子:
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」。
注意,foreach中的<var>參數是一個臨時的局部變量,foreach函數執行完後,參數<var>的變量將不在作用,其作用域只在foreach函數當中。
五、if 函數
if函數很像GNUmake所支持的條件語句——ifeq(參見前面所述的章節),if函數的語法是:
$(if <condition>,<then-part>)
或是
$(if <condition>,<then-part>,<else-part>)
可見,if函數可以包含「else」部分,或是不含。即if函數的參數可以是兩個,也可以是三個。<condition>參數是if的表達式,如果其返回的為非空字符串,那麼這個表達式就相當於返回真,於是,<then-part>會被計算,否則<else-part>會被計算。
if函數的返回值是,如果<condition>為真(非空字符串),那個<then-part>會是整個函數的返回值,如果<condition>為假(空字符串),那麼<else-part>會是整個函數的返回值,此時如果<else-part>沒有被定義,那麼,整個函數返回空字串。
所以,<then-part><else-part>只會有一個被計算。
六、call函數
call函數是唯一一個可以用來創建新的參數化的函數。你可以寫一個非常複雜的表達式,這個表達式中,你可以定義許多參數,然後你可以用call函數來向這個表達式傳遞參數。其語法是:

$(call <expression>,<parm1>,<parm2>,<parm3>...)
make執行這個函數時,<expression>參數中的變量,如$(1)$(2)$(3)等,會被參數<parm1><parm2><parm3>依次取代。而<expression>的返回值就是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 <variable>)
注意,<variable>是變量的名字,不應該是引用。所以你最好不要在<variable>中使用「$」字符。Origin函數會以其返回值來告訴你這個變量的「出生情況」,下面,是origin函數的返回值:
undefined
如果<variable>從來沒有定義過,origin函數返回這個值「undefined」。
default
如果<variable>是一個默認的定義,比如「CC」這個變量,這種變量我們將在後面講述。
environment
如果<variable>是一個環境變量,並且當Makefile被執行時,「-e」參數沒有被打開。
file
如果<variable>這個變量被定義在Makefile中。
command line
如果<variable>這個變量是被命令行定義的。
override
如果<variable>是被override指示符重新定義的。
automatic
如果<variable>是一個命令運行中的自動化變量。關於自動化變量將在後面講述。
這些信息對於我們編寫Makefile是非常有用的,例如,假設我們有一個Makefile其包了一個定義文件Make.def,在Make.def中定義了一個變量「bletch」,而我們的環境中也有一個環境變量「bletch」,此時,我們想判斷一下,如果變量來源於環境,那麼我們就把之重定義了,如果來源於Make.def或是命令行等非環境的,那麼我們就不重新定義它。於是,在我們的Makefile中,我們可以這樣寫:
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf, gag, etc.
endif
endif
當然,你也許會說,使用override關鍵字不就可以重新定義環境中的變量了嗎?為什麼需要使用這樣的步驟?是的,我們用override是可以達到這樣的效果,可是override過於粗暴,它同時會把從命令行定義的變量也覆蓋了,而我們只想重新定義環境傳來的,而不想重新定義命令行傳來的。
八、shell函數
shell函數也不像其它的函數。顧名思義,它的參數應該就是操作系統Shell的命令。它和反引號「`」是相同的功能。這就是說,shell函數把執行操作系統命令後的輸出作為函數返回。於是,我們可以用操作系統命令以及字符串處理命令awksed等等命令來生成一個變量,如:
contents := $(shell cat foo)
files := $(shell echo *.c)
注意,這個函數會新生成一個Shell程序來執行命令,所以你要注意其運行性能,如果你的Makefile中有一些比較複雜的規則,並大量使用了這個函數,那麼對於你的系統性能是有害的。特別是Makefile的隱晦的規則可能會讓你的shell函數執行的次數比你想像的多得多。
九、控制make的函數
make提供了一些函數來控制make的運行。通常,你需要檢測一些運行Makefile時的運行時信息,並且根據這些信息來決定,你是讓make繼續執行,還是停止。
$(error <text ...>)
產生一個致命的錯誤,<text ...>是錯誤信息。注意,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 <text ...>)
這個函數很像error函數,只是它並不會讓make退出,只是輸出一段警告信息,而make繼續執行。

2006年3月24日 星期五

Kernel Makefile的解釋

==========================================
Makefile 初探
==========================================
Linux的內核配置文件有兩個,一個是隱含的.config文件,嵌入到主Makefile中;另一個是include/linux/autoconf.h,嵌入到各個c源文件中,它們由make configmake menuconfigmake xconfig這些過程創建。幾乎所有的源文件都會通過linux/config.h而嵌入autoconf.h,如果按照通常方法建立文件依賴關係(.depend),只要更新過autoconf.h,就會造成所有源代碼的重新編繹。

為了優化make過程,減少不必要的重新編繹,Linux開發了專用的mkdep工具,用它來取代gcc來生成.depend文件。mkdep在處理源文件時,忽略linux/config.h這樣的頭文件,識別源文件宏指令中具有〞CONFIG_〞特徵的行。例如,如果有〞#ifdef CONFIG_SMP〞這樣的行,它就會在.depend文件中輸出$(wildcard /usr/src/linux/include/config/smp.h)

include/config/下的文件是另一個工具split-includeautoconf.h中生成,它利用autoconf.h中的CONFIG_標記,生成與mkdep相對應的文件。例如,如果autoconf.h中有〞#undef CONFIG_SMP〞這一行,它就生成include/config/smp.h文件,內容為〞#undef CONFIG_SMP〞。這些文件名只在.depend文件中出現,內核源文件是不會嵌入它們的。每配置一次內核,運行split-include一次。split-include會檢查舊的子文件的內容,確定是不是要更新它們。這樣,不管autoconf.h修改日期如何,只要其配置不變,make就不會重新編繹內核。

如果系統的編繹選項發生了變化,Linux也能進行增量編繹。為了做到這一點,make每編繹一個源文件時生成一個flags文件。例如編繹sched.c時,會在相同的目錄下生成隱含的.sched.o.flags文件。它是Makefile的一個片斷,當make進入某個子目錄編繹時,會搜索其中的flags文件,將它們嵌入到Makefile中。這些flags代碼測試當前的編繹選項與原來的是不是相同,如果相同,就將自已對應的目標文件加入FILES_FLAGS_UP_TO_DATE列表,然後,系統從編繹對像表中刪除它們,得到FILES_FLAGS_CHANGED列表,最後,將它們設為目標進行更新。

下一步準備逐步深入的剖析Makefile代碼。

==========================================
Makefile解讀之二: sub-make
==========================================
Linux各級內核源代碼的子目錄下都有Makefile,大多數Makefile要嵌入主目錄下的Rule.makeRule.make將識別各個Makefile中所定義的一些變量。變量obj-y表示需要編繹到內核中的目標文件名集合,定義O_TARGET表示將obj-y連接為一個O_TARGET名稱的目標文件,定義L_TARGET表示將obj-y合併為一個L_TARGET名稱的庫文件。同樣obj-m表示需要編繹成模塊的目標文件名集合。如果還需進行子目錄make,則需要定義subdir-ysubdir-m。在Makefile中,用〞obj-$(CONFIG_BINFMT_ELF) += binfmt_elf.o〞和〞subdir-$(CONFIG_EXT2_FS) += ext2〞這種形式自動為obj-yobj-msubdir-ysubdir-m添加文件名。有時,情況沒有這麼單純,還需要使用條件語句個別對待。Makefile中還有其它一些變量,如mod-subdirs定義了subdir-m以外的所有模塊子目錄。

Rules.make是如何使make進入子目錄的呢? 先來看subdir-y是如何處理的,在Rules.make中,先對subdir-y中的每一個文件名加上前綴〞_subdir_〞再進行排序生成subdir-list集合,再以它作為目標集,對其中每一個目標產生一個子make,同時將目標名的前綴去掉得到子目錄名,作為子make的起始目錄參數。subdir-msubdir-y類似,但情況稍微複雜一些。由於subdir-y中可能有模塊定義,因此利用mod-subdirs變量將subdir-y中模塊目錄提取出來,再與subdir-m合成一個大的MOD_SUB_DIRS集合。subdir-m的目標所用的前綴是〞_modsubdir_〞。

一點說明,子目錄中的MakefileRules.make都沒有嵌入.config文件,它是通過主Makefile向下傳遞MAKEFILES變量完成的。MAKEFILESmake自已識別的一個變量,在執行新的Makefile之前,make會首先加載MAKEFILES所指的文件。在主Makefile中它即指向.config


==========================================
Makefile解讀之三: 模塊的版本化處理
==========================================
模塊的版本化是內核與模塊接口之間進行嚴格類型匹配的一種方法。當內核配置了CONFIG_MODVERSIONS之後,make dep操作會在include/linux/modules/目錄下為各級Makefileexport-objs變量所對應的源文件生成擴展名為.ver的文件。

例如對於kernel/ksyms.cmake用以下命令生成對應的ksyms.ver

gcc -E -D__KERNEL__ -D__GENKSYMS__ ksyms.c /sbin/genksyms -k 2.4.1 > ksyms.ver

-D__GENKSYMS__的作用是使ksyms.c中的EXPORT_SYMBOL宏不進行擴展。genksyms命令識別EXPORT_SYMBOL()中的函數名和對應的原型,再根據其原型計算出該函數的版本號。

例如ksyms.c中有一行:
EXPORT_SYMBOL(kmalloc);
kmalloc原型是:
void *kmalloc(size_t, int);
genksyms程序對應的輸出為:
#define __ver_kmalloc 93d4cfe6
#define kmalloc _set_ver(kmalloc)
在內核符號表和模塊中,kmalloc將變成kmalloc_R93d4cfe6

在生成完所有的.ver文件後,make將重建include/linux/modversions.h文件,它包含一系列#include指令行嵌入各個.ver文件。在編繹內核本身export-objs中的文件時,make會增加一個〞-DEXPORT_SYMTAB〞編繹標誌,它使源文件嵌入modversions.h文件,將EXPORT_SYMBOL宏展開中的函數名字符串進行版本名擴展;同時,它也定義_set_ver()宏為一空操作,使代碼中的函數名不受其影響。
在編繹模塊時,make會增加〞-include=linux/modversion.h -DMODVERSIONS〞編繹標誌,使模塊中代碼的函數名得到相應版本擴展。

由於生成.ver文件比較費時,make還為每個.ver創建了一個後綴為.stamp時戳文件。在make dep時,如果其.stamp文件比源文件舊才重新生成.ver文件,否則只是更新.stamp文件時戳。另外,在生成.vermodversions.h文件時,make都會比較新文件和舊文件的內容,保持它們修改時間為最舊。

2006年3月23日 星期四

Makefile用法(make的參數)

make 的運行
——————
一般來說,最簡單的就是直接在命令行下輸入make命令,make命令會找當前目錄的makefile來執行,一切都是自動的。但也有時你也許只想讓make重編譯某些文件,而不是整個工程,而又有的時候你有幾套編譯規則,你想在不同的時候使用不同的編譯規則,等等。本章節就是講述如何使用make命令的。
一、make的退出碼
make命令執行後有三個退出碼:
0 —— 表示成功執行。
1 —— 如果make運行時出現任何錯誤,其返回1
2 —— 如果你使用了make的「-q」選項,並且make使得一些目標不需要更新,那麼返回2
Make的相關參數我們會在後續章節中講述。
二、指定Makefile
前面我們說過,GNU make找尋默認的Makefile的規則是在當前目錄下依次找三個文件——「GNUmakefile」、「makefile」和「Makefile」。其按順序找這三個文件,一旦找到,就開始讀取這個文件並執行。
當前,我們也可以給make命令指定一個特殊名字的Makefile。要達到這個功能,我們要使用make的「-f」或是「--file」參數(「--makefile」參數也行)。例如,我們有個makefile的名字是「hchen.mk」,那麼,我們可以這樣來讓make來執行這個文件:
make –f hchen.mk
如果在make的命令行是,你不只一次地使用了「-f」參數,那麼,所有指定的makefile將會被連在一起傳遞給make執行。
三、指定目標
一般來說,make的最終目標是makefile中的第一個目標,而其它目標一般是由這個目標連帶出來的。這是make的默認行為。當然,一般來說,你的makefile中的第一個目標是由許多個目標組成,你可以指示make,讓其完成你所指定的目標。要達到這一目的很簡單,需在make命令後直接跟目標的名字就可以完成(如前面提到的「make clean」形式)
任何在makefile中的目標都可以被指定成終極目標,但是除了以「-」打頭,或是包含了「=」的目標,因為有這些字符的目標,會被解析成命令行參數或是變量。甚至沒有被我們明確寫出來的目標也可以成為make的終極目標,也就是說,只要make可以找到其隱含規則推導規則,那麼這個隱含目標同樣可以被指定成終極目標。
有一個make的環境變量叫「MAKECMDGOALS」,這個變量中會存放你所指定的終極目標的列表,如果在命令行上,你沒有指定目標,那麼,這個變量是空值。這個變量可以讓你使用在一些比較特殊的情形下。比如下面的例子:
sources = foo.c bar.c
ifneq ( $(MAKECMDGOALS),clean)
include $(sources:.c=.d)
endif
基於上面的這個例子,只要我們輸入的命令不是「make clean」,那麼makefile會自動包含「foo.d」和「bar.d」這兩個makefile
使用指定終極目標的方法可以很方便地讓我們編譯我們的程序,例如下面這個例子:
.PHONY: all
all: prog1 prog2 prog3 prog4
從這個例子中,我們可以看到,這個makefile中有四個需要編譯的程序——「prog1」, 「prog2」, 「prog3」和 「prog4」,我們可以使用「make all」命令來編譯所有的目標(如果把all置成第一個目標,那麼只需執行「make」),我們也可以使用「make prog2」來單獨編譯目標「prog2」。
即然make可以指定所有makefile中的目標,那麼也包括「偽目標」,於是我們可以根據這種性質來讓我們的makefile根據指定的不同的目標來完成不同的事。在Unix世界中,軟件發佈時,特別是GNU這種開源軟件的發佈時,其makefile都包含了編譯、安裝、打包等功能。我們可以參照這種規則來書寫我們的makefile中的目標。
all
這個偽目標是所有目標的目標,其功能一般是編譯所有的目標。
clean
這個偽目標功能是刪除所有被make創建的文件。
install
這個偽目標功能是安裝已編譯好的程序,其實就是把目標執行文件拷貝到指定的目標中去。
print
這個偽目標的功能是例出改變過的源文件。
tar
這個偽目標功能是把源程序打包備份。也就是一個tar文件。
dist
這個偽目標功能是創建一個壓縮文件,一般是把tar文件壓成Z文件。或是gz文件。
TAGS
這個偽目標功能是更新所有的目標,以備完整地重編譯使用。
check」和「test
這兩個偽目標一般用來測試makefile的流程。
當然一個項目的makefile中也不一定要書寫這樣的目標,這些東西都是GNU的東西,但是我想,GNU搞出這些東西一定有其可取之處(等你的UNIX下的程序文件一多時你就會發現這些功能很有用了),這裡只不過是說明了,如果你要書寫這種功能,最好使用這種名字命名你的目標,這樣規範一些,規範的好處就是——不用解釋,大家都明白。而且如果你的makefile中有這些功能,一是很實用,二是可以顯得你的makefile很專業(不是那種初學者的作品)。
四、檢查規則
有時候,我們不想讓我們的makefile中的規則執行起來,我們只想檢查一下我們的命令,或是執行的序列。於是我們可以使用make命令的下述參數:
-n
--just-print
--dry-run
--recon
不執行參數,這些參數只是打印命令,不管目標是否更新,把規則和連帶規則下的命令打印出來,但不執行,這些參數對於我們調試makefile很有用處。
-t
--touch
這個參數的意思就是把目標文件的時間更新,但不更改目標文件。也就是說,make假裝編譯目標,但不是真正的編譯目標,只是把目標變成已編譯過的狀態。
-q
--question
這個參數的行為是找目標的意思,也就是說,如果目標存在,那麼其什麼也不會輸出,當然也不會執行編譯,如果目標不存在,其會打印出一條出錯信息。
-W <file>
--what-if=<file>
--assume-new=<file>
--new-file=<file>
這個參數需要指定一個文件。一般是是源文件(或依賴文件),Make會根據規則推導來運行依賴於這個文件的命令,一般來說,可以和「-n」參數一同使用,來查看這個依賴文件所發生的規則命令。

另外一個很有意思的用法是結合「-p」和「-v」來輸出makefile被執行時的信息(這個將在後面講述)。
五、make的參數
下面列舉了所有GNU make 3.80版的參數定義。其它版本和產商的make大同小異,不過其它產商的make的具體參數還是請參考各自的產品文檔。
-b
-m
這兩個參數的作用是忽略和其它版本make的兼容性。
-B
--always-make
認為所有的目標都需要更新(重編譯)。
-C <dir>
--directory=<dir>
指定讀取makefile的目錄。如果有多個「-C」參數,make的解釋是後面的路徑以前面的作為相對路徑,並以最後的目錄作為被指定目錄。如:「make C ~hchen/test C prog」等價於「make C ~hchen/test/prog」。
「—debug[=<options>]
輸出make的調試信息。它有幾種不同的級別可供選擇,如果沒有參數,那就是輸出最簡單的調試信息。下面是<options>的取值:
a —— 也就是all,輸出所有的調試信息。(會非常的多)
b —— 也就是basic,只輸出簡單的調試信息。即輸出不需要重編譯的目標。
v —— 也就是verbose,在b選項的級別之上。輸出的信息包括哪個makefile被解析,不需要被重編譯的依賴文件(或是依賴目標)等。
i —— 也就是implicit,輸出所以的隱含規則。
j —— 也就是jobs,輸出執行規則中命令的詳細信息,如命令的PID、返回碼等。
m —— 也就是makefile,輸出make讀取makefile,更新makefile,執行makefile的信息。
-d
相當於「--debug=a」。
-e
--environment-overrides
指明環境變量的值覆蓋makefile中定義的變量的值。
-f=<file>
--file=<file>
--makefile=<file>
指定需要執行的makefile
-h
--help
顯示幫助信息。
-i
--ignore-errors
在執行時忽略所有的錯誤。
-I <dir>
--include-dir=<dir>
指定一個被包含makefile的搜索目標。可以使用多個「-I」參數來指定多個目錄。
-j [<jobsnum>]
--jobs[=<jobsnum>]
指同時運行命令的個數。如果沒有這個參數,make運行命令時能運行多少就運行多少。如果有一個以上的「-j」參數,那麼僅最後一個「-j」才是有效的。(注意這個參數在MS-DOS中是無用的)
-k
--keep-going
出錯也不停止運行。如果生成一個目標失敗了,那麼依賴於其上的目標就不會被執行了。
-l <load>
--load-average[=<load]
「—max-load[=<load>]
指定make運行命令的負載。
-n
--just-print
--dry-run
--recon
僅輸出執行過程中的命令序列,但並不執行。
-o <file>
--old-file=<file>
--assume-old=<file>
不重新生成的指定的<file>,即使這個目標的依賴文件新於它。
-p
--print-data-base
輸出makefile中的所有數據,包括所有的規則和變量。這個參數會讓一個簡單的makefile都會輸出一堆信息。如果你只是想輸出信息而不想執行makefile,你可以使用「make -qp」命令。如果你想查看執行makefile前的預設變量和規則,你可以使用「make p f /dev/null」。這個參數輸出的信息會包含著你的makefile文件的文件名和行號,所以,用這個參數來調試你的makefile會是很有用的,特別是當你的環境變量很複雜的時候。
-q
--question
不運行命令,也不輸出。僅僅是檢查所指定的目標是否需要更新。如果是0則說明要更新,如果是2則說明有錯誤發生。
-r
--no-builtin-rules
禁止make使用任何隱含規則。
-R
--no-builtin-variabes
禁止make使用任何作用於變量上的隱含規則。
-s
--silent
--quiet
在命令運行時不輸出命令的輸出。
-S
--no-keep-going
--stop
取消「-k」選項的作用。因為有些時候,make的選項是從環境變量「MAKEFLAGS」中繼承下來的。所以你可以在命令行中使用這個參數來讓環境變量中的「-k」選項失效。
-t
--touch
相當於UNIXtouch命令,只是把目標的修改日期變成最新的,也就是阻止生成目標的命令運行。
-v
--version
輸出make程序的版本、版權等關於make的信息。
-w
--print-directory
輸出運行makefile之前和之後的信息。這個參數對於跟蹤嵌套式調用make時很有用。
--no-print-directory
禁止「-w」選項。
-W <file>
--what-if=<file>
--new-file=<file>
--assume-file=<file>
假定目標<file>需要更新,如果和「-n」選項使用,那麼這個參數會輸出該目標更新時的運行動作。如果沒有「-n」那麼就像運行UNIX的「touch」命令一樣,使得<file>的修改時間為當前時間。
--warn-undefined-variables
只要make發現有未定義的變量,那麼就輸出警告信息。