makefile詳解

2020-08-10 20:36:36

其餘相關內容可參考個人部落格

簡介

概述

該系列筆記參考《跟我一起寫makefile》,目前該筆記僅記錄工作中有經常遇到的內容,有待補充

在講述Makefile 之前,還是讓我們先來粗略地看一看 Makefile 的規則。

target ... : prerequisites ... 
command 
  • target 也就是一個目標檔案,可以是 Object File,也可以是執行檔案。還可以是一個標籤(Label),對於標籤這種特性,在後續的「僞目標」章節中會有敘述

  • prerequisites 就是要生成那個 target 所需要的檔案或是目標。

  • command 也就是make需要執行的命令(任意的 Shell 命令)。

這是一個檔案的依賴關係,也就是說target這一個或多個的目標檔案依賴於prerequisites中的檔案,其生成規則定義在command中。說白一點就是說,prerequisites中如果有一個以上的檔案比target檔案要新的話,command所定義的命令就會被執行。這就是 Makefile 的規則。

依賴關係的實質上就是說明了目標檔案是由哪些檔案生成的,換言之,目標檔案是哪些檔案更新的。

在定義好依賴關係後,後續的那一行定義瞭如何生成目標檔案的操作系統命令,一定要以一個Tab 鍵作爲開頭。記住,make並不管命令是怎麼工作的,他只管執行所定義的命令。make會比較targets檔案和prerequisites檔案的修改日期,如果prerequisites檔案的日期要比targets檔案的日期要新,或者target不存在的話,那麼make 就會執行後續定義的命令。

如何工作

在預設的方式下,也就是我們只輸入 make 命令。那麼

  1. make 會在當前目錄下找名字叫Makefilemakefile的檔案。
  2. 如果找到,它會找檔案中的第一個目標檔案(假設爲all),並把這個檔案作爲最終的目標檔案。
  3. 如果all檔案不存在,或是 all 所依賴的後面的 .o 檔案的檔案修改時間要比 all這個檔案新,那麼,他就會執行後面所定義的命令來生成 all 這個檔案。
  4. 如果 all 所依賴的.o 檔案也不存在,那麼 make 會在當前檔案中找目標爲.o 檔案的依賴性,如果找到則再根據那一個規則生成.o 檔案.(這有點像一個堆疊的過程)
  5. 當然,你的C檔案和H檔案是存在的啦,於是 make 會生成 .o 檔案,然後再用 .o 檔案生命 make 的終極任務,也就是執行檔案 all了。

這就是整個 make 的依賴性,make 會一層又一層地去找檔案的依賴關係,直到最終編譯出第一個目標檔案。在找尋的過程中,如果出現錯誤,比如最後被依賴的檔案找不到,那麼make就會直接退出並報錯,而對於所定義的命令的錯誤,或是編譯不成功,make 根本不理。make 只管檔案的依賴性,即,如果在我找了依賴關係之後,冒號後面的檔案還是不在,那麼對不起,我就不工作啦。

makefile中有什麼

  1. 顯式規則
    顯式規則說明了,如何生成一個或多個目標檔案。這是由Makefile的書寫者明顯指出:要生成的檔案,檔案的依賴檔案,生成的命令

  2. 隱晦規則
    由於make有自動推導的功能,所以隱晦的規則可以讓我們比較粗糙地簡略地書寫Makefile。

  3. 變數定義
    在Makefile中我們定義一系列的變數,變數一般都是字串,這個有點像C語言中的宏,當Makefile被執行時,其中的變數都會被擴充套件到相應的參照位置上。

  4. 檔案指示
    其包括了三個部分,一個是在一個Makefile中參照另一個Makefile,就像 C 語言中的include一樣;另一個是指根據某些情況指定Makefile中的有效部分,就像C語言中的預編譯#if一樣;還有就是定義一個多行的命令。有關這一部分的內容,我會在後續的部分中講述。

  5. 註釋
    Makefile中只有行註釋,和Shell指令碼一樣,其註釋是用「#」字元,這個就像 C/C++中的「//」一樣。如果你要在你的Makefile中使用「#」字元,可以用反斜框進行跳脫

Makefile 中的命令,必須要以 Tab 鍵開始。

檔名

預設的情況下,make 命令會在當前目錄下按順序找尋檔名爲「GNUmakefile」、「makefile」、「Makefile」的檔案,找到瞭解釋這個檔案。在這三個檔名中,最好使用「Makefile」這個檔名,因爲,這個檔名第一個字元爲大寫,這樣有一種顯目的感覺。最好不要用「GNUmakefile」,這個檔案是GNU的make識別的。有另外一些make只對全小寫的「makefile」檔名敏感。
當然,你可以使用別的檔名來書寫Makefile,比如:「Make.Linux」,如果要指定特定的Makefile,你可以使用make的「-f」和「--file」參數,如:make -f Make.Linux

參照其他的makefile

在 Makefile 使用include關鍵字可以把別的Makefile包含進來,這很像C語言的#include,被包含的檔案會原模原樣的放在當前檔案的包含位置。include的語法是:
include <filename>
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,並把其內容安置在當前的位置。如果檔案都沒有指定絕對路徑或是相對路徑的話,make 會在當前目錄下首先尋找,如果當前目錄下沒有找到,那麼,make 還會在下面 下麪的幾個目錄下找:

  1. 如果 make 執行時,有「-I」或「–include-dir」參數,那麼 make 就會在這個參數所指定的目錄下去尋找。
  2. 如果目錄prefix/include(一般是:/usr/local/bin 或/usr/include)存在的話,make 也會去找。

如果有檔案沒有找到的話,make會生成一條警告資訊,但不會馬上出現致命錯誤。它會繼續載入其它的檔案,一旦完成 makefile 的讀取,make 會再重試這些沒有找 到,或是不能讀取的檔案,如果還是不行,make 纔會出現一條致命資訊。如果你想讓make不理那些無法讀取的檔案,而繼續執行,你可以在include前加一個減號「-」,
如:

-include <filename> 

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

環境變數

如果你的當前環境中定義了環境變數MAKEFILES,那麼make會把這個變數中的值做一個類似於include的動作。這個變數中的值是其它的Makefile,用空格分隔。只是,它和include不同的是,從這個環境變數中引入的 Makefile 的「目標」不會起作用,如果環境變數中定義的檔案發現錯誤,make也會不理。

但是在這裏我還是建議不要使用這個環境變數,因爲只要這個變數一被定義,那麼當你使用 make 時,所有的 Makefile 都會受到它的影響,這絕不是你想看到的。在這裏提這個事,只是爲了告訴大家,也許有時候你的 Makefile 出現了怪事,那麼你可以看看當前環境中有沒有定義這個變數。

工作方式

  • 讀入所有的 Makefile。
  • 讀入被 include 的其它 Makefile。
  • 初始化檔案中的變數。
  • 推導隱晦規則,並分析所有規則。
  • 爲所有的目標檔案建立依賴關係鏈。
  • 根據依賴關係,決定哪些目標要重新生成。
  • 執行生成命令。

makefile的規則

概念

規則包含兩個部分,一個是依賴關係,一個是生成目標的方法。在 Makefile 中,規則的順序是很重要的,因爲,Makefile中只應該有一個最終目標,其它的目標都是被這個目標所連帶出來的,所以一定要讓make知道你的最終目標是什麼。一般來說,定義在Makefile中的目標可能會有很多,但是第一條規則中的目標將被確立爲最終的目標。如果第一條規則中的目標有很多個,那麼,第一個目標會成爲最終的目標。make所完成的也就是這個目標。

規則舉例

targets : prerequisites 
command 

萬用字元

支援 * ,? ,[…]和shell中的含義是相同的:

  • * 匹配任意字元0或無數次
  • ? 匹配任意字元1次
  • […]匹配括號中給出的任意一個字元,而在括號中加入,表示匹配不在括號中給出的字元

檔案搜尋

在一些大的工程中,有大量的原始檔,我們通常的做法是把這許多的原始檔分類,並存放在不同的目錄中。所以,當make需要去找尋檔案的依賴關係時,你可以在檔案前加上路徑,但最好的方法是把一個路徑告訴 make,讓make在自動去找。Makefile檔案中的特殊變數VPATH就是完成這個功能的,如果沒有指明這個變數,make只會在當前的目錄中去找尋依賴檔案和目標檔案。如果定義了這個變數,那麼make就會在噹噹前目錄找不到的情況下,到所指定的目錄中去找尋檔案了。

VPATH = src:../headers 

上面的的定義指定兩個目錄,「src」和「…/headers」,make會按照這個順序進行搜尋。目錄由「冒號」分隔。(當然當前目錄永遠是最高優先搜尋的地方)

另一個設定檔案搜尋路徑的方法是使用 make的vpath關鍵字(注意,它是全小寫的),這不是變數,這是一個make的關鍵字,這和上面提到的那個VPATH變數很類似,但是它更爲靈活。它可以指定不同的檔案在不同的搜尋目錄中。這是一個很靈活的功能。它的使用方法有三種:

  1. vpath <pattern> <directories>
    爲符合模式<pattern>的檔案指定搜尋目錄<directories>

  2. vpath <pattern>
    清除符合模式<pattern>的檔案的搜尋目錄。

  3. vpath
    清除所有已被設定好了的檔案搜尋目錄。

vapth 使用方法中的<pattern>需要包含%字元,意思是匹配零或若幹字元,
例如,「%.h」表示所有以「.h」結尾的檔案。指定了要搜尋的檔案集,例如:

vpath %.h ../headers 

該語句表示,要求 make 在「…/headers」目錄下搜尋所有以.h結尾的檔案。(如果某檔案在當前目錄沒有找到的話) .

補充:經過實測,檔案搜尋後,在執行命令時,將原始檔加入了相對路徑,而生成的中間檔案還是在當前目錄.

僞目標

簡單來說就是避免檔案重名,例如make clean只是想要執行下面 下麪的makefile命令,並不是要生成clean這個檔案,一般來講我們的all,clean等動作都可以加入僞目標,統一隻執行makefile中寫好的命令即可,例如:

.PHONY: clean
clean:
rm *.o -f

多目標

Makefile 的規則中的目標可以不止一個,其支援多目標,有可能我們的多個目標同時依賴於一個檔案,並且其生成的命令大體類似。於是我們就能把其合併起來。

靜態模式

<targets ...>: <target-pattern>: <prereq-patterns ...> 
<commands> 

例子:

objects = foo.o bar.o 

all: $(objects) 
$(objects): %.o: %.c 
$(CC) -c $(CFLAGS) $< -o $@ 

上面的例子中,指明瞭我們的目標從$(object) 中獲取,%.o表明要所有以.o結尾的目標,也就是foo.o bar.o,而依賴模式%.c則取模式%.o%,也就是foo bar,併爲其加下.c的後綴,於是我們的依賴目標就是foo.c bar.c

備註:這裏感覺和模式規則很像,其實把最左邊的目標去掉就是模式規則啦,可能這樣寫比較明顯吧

生成依賴關係

  1. 爲什麼要使用後綴名爲.d的依賴檔案

    在Makefile中,目標檔案的依賴關係需要包含原始檔和一系列的標頭檔案,但是一般在我們的Makefile中的依賴關係都是省略標頭檔案的,這就有一個致命的問題,就是標頭檔案修改時,目標檔案不會重新生成。

    如果是一個比較大型的工程,我們必需清楚每一個原始檔都包含了哪些標頭檔案,並且在加入或刪除某些標頭檔案時,也需要一併修改Makefile,這是一個很沒有維護性的工作,所以可以使用C/C++編譯器的-M選項自動獲取原始檔中包含的標頭檔案,並生成一個依賴關係,這個依賴關係就儲存在.d檔案中。

  2. 生成方式

選項 特點 共同點
-M 依賴關係包含標準庫 預設開啓-E參數,使得編譯器在預處理結束就停止編譯
-MM 依賴關係不包含標準庫
-MD 依賴關係包含標準庫 不開啓-E參數
-MMD 依賴關係不包含標準庫
-MF + fileName 將依賴關係寫入到fileName檔案中
-MT 在生成的依賴檔案中,指定規則中的目標
接下來簡單介紹幾個例子:
  1. 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/_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
    
  2. gcc -MM main.c
    終端輸出

    main.o: main.c defs.h
    
  3. gcc -M -MF main.d main.c
    則 「-M」 輸出的內容就儲存在 main.d 檔案中了

  4. -MT選項

    這個最爲重要,可以將.d檔案本身作爲目標加入到依賴檔案中,這樣就可在標頭檔案更新時,也更新依賴檔案

    gcc  main.c -MM -MF main.d   -MT main.d -MT main.o
    $ cat main.d    #檢視生成的依賴檔案的內容
    main.d main.o: main.c defs.h
    

    注:此時的依賴規則中 main.d 和 main.o 目標都是通過 -MT 選項指定的

makefile的命令

顯示命令

  1. 通常,make會把其要執行的命令列在命令執行前輸出到螢幕上。當我們用「@」字元在命令列前,那麼,這個命令將不被make顯示出來,最具代表性的例子是,我們用這個功能來像螢幕顯示一些資訊。如:

    @echo 正在編譯 XXX 模組...... 
    
  2. 如果 make 執行時,帶入 make 參數-n或--just-print,那麼其只是顯示命令,但不會執行命令,這個功能很有利於我們偵錯我們的Makefile,看看我們書寫的命令是執行起來是什麼樣子的或是什麼順序的。

  3. 而make參數-s或--slient則是全面禁止命令的顯示。

命令執行

當依賴目標新於目標時,也就是當規則的目標需要被更新時,make會一條一條的執行其後的命令。需要注意的是,如果你要讓上一條命令的結果應用在下一條命令時,你應該使用分號分隔這兩條命令,且寫在同一行:

範例一:

exec: 
cd /home/hchen 
pwd

範例二:

exec: 
cd /home/hchen; pwd

在範例一中cd命令未生效

命令出錯

每當命令執行完後,make會檢測每個命令的返回碼,如果命令返回成功,那麼make會執行下一條命令,有些時候,命令的出錯並不表示就是錯誤的,爲了忽略命令的出錯,在命令列前加一個-,例如:

clean: 
	-rm -f *.o 

還有一個全域性的辦法是,給make加上-i或是--ignore-errors參數,那麼Makefile中所有命令都會忽略錯誤。而如果一個規則是以「.IGNORE」作爲目標的,那麼這個規則中的所有命令將會忽略錯誤。這些是不同級別的防止命令出錯的方法,你可以根據你的不同喜歡設定。

還有一個要提一下的make的參數的是-k或是--keep-going,這個參數的意思是,如果某規則中的命令出錯了,那麼就終止該規則的執行,但繼續執行其它規則。

定義命令包

如果 Makefile 中出現一些相同命令序列,那麼我們可以爲這些相同的命令序列定義一個變數。定義這種命令序列的語法以define開始,以endef結束,例如:

define run-yacc 
yacc $(firstword $^) 
mv y.tab.c $@ 
endef 

這裏,「run-yacc」是這個命令包的名字,其不要和Makefile中的變數重名,在 define」和endef中的兩行就是命令序列,呼叫方式如下:

foo.c : foo.y 
$(run-yacc)

makefile的變數

變數的基礎

  1. 變數宣告和使用
    變數在宣告時需要給予初值,而在使用時,需要給在變數名前加上$符號,但最好用小括號()或是大括號{}把變數給包括起來,如果你要使用真實的$字元, 那麼你需要用$$來表示,例如:

    objects = program.o foo.o utils.o 
    
    program : $(objects) 
    cc -o program $(objects) 
    
    $(objects) : defs.h 
    

    變數會在使用它的地方精確地展開,就像 C/C++中的宏一樣

  2. 在定義變數的值時,我們可以使用其它變數來構造變數的值

    foo = $(bar) 
    bar = $(ugh) 
    ugh = Huh? 
    

    上面這種用 =的方法可是使用後面的變數來定義,而使用:=是無法使用後面的變數來定義的,例如

    y := $(x) bar 
    x := foo 
    

    那麼,y 的值是「bar」,而不是「foo bar」。

  3. 定義一個空格

    nullstring := 
    space := $(nullstring) # end of the line
    

    操作符的右邊是很難描述一個空格的,這裏採用的技術很管用,先用一個Empty變數來標明變數的值開始了,而後面採用「#」註釋符來表示變數定義的終止,這樣,我們可以定義出其值是一個空格的變數。請注意這裏關於#的使用,註釋符#的這種特性值得我們注意,如果我們這樣定義一個變數:

    dir := /foo/bar # directory to put the frobs in 
    

    dir這個變數的值是/foo/bar,後面還跟了4個空格,如果我們這樣使用這樣變數來指定別的目錄——$(dir)/file那麼就完蛋了。

  4. 操作符?=

    還有一個比較有用的操作符是「?=」,先看範例:

    FOO ?= bar 
    

    其含義是,如果FOO沒有被定義過,那麼變數FOO的值就是bar,如果先前被定義過,那麼這條語將什麼也不做。

變數值替換

其格式是:

$(var:a=b)

其意思是,把變數var中所有以a字串結尾a替換成b字串。這裏的結尾意思是空格或是結束符,例如:

foo := a.o b.o c.o 
bar := $(foo:.o=.c) 

結果$(bar)的值就是a.c b.c c.c

這個用法經常在定義OBJS時,把原始檔的.c換成.o(當然用putsubst函數也可以啦)

另外一種變數替換的技術是以「靜態模式」(參見前面章節)定義的,如:

foo := a.o b.o c.o 
bar := $(foo:%.o=%.c) 

這依賴於被替換字串中的有相同的模式,模式中必須包含一個%字元,這個例子的結果和上面相同。

變數巢狀

含義:把變數的值再當成變數。

例如:

x = y 
y = z 
a := $($(x))

在這個例子中,$(x)的值是y,所以$($(x))就是$(y),於是$(a)的值就是z(注意是x=y,而不是x=$(y))。

在這種方式中,或要可以使用多個變數來組成一個變數的名字,然後再取其值,例如:

first_second = Hello 
a = first 
b = second 
all = $($a_$b) 

這裏的$a_$b組成了first_second,於是,$(all)的值就是Hello

追加變數值

我們可以使用「+=」操作符給變數追加值,如:

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被追加進去了)。

如果變數之前沒有定義過,那麼,+=會自動變成=

override

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

override <variable> = <value> 
override <variable> := <value> 
override <variable> += <more text> 

多行變數

還有一種設定變數值的方法是使用define關鍵字。使用define關鍵字設定變數的值可以有換行,這有利於定義一系列的命令(前面我們講過「命令包」的技術就是利用這個關鍵字)。

define指示符後面跟的是變數的名字,而重起一行定義變數的值,定義是以endef關鍵字結束。其工作方式和=操作符一樣。變數的值可以包含函數、命令、文字,或是其它變數。因爲命令需要以Tab鍵開頭,所以如果你用define定義的命令變數中沒有以Tab鍵開頭,那麼make就不會把其認爲是命令。

環境變數

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

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

  3. 當然,我並不推薦把許多的變數都定義在系統環境中,這樣在我們執行不用的Makefile時,擁有的是同一套系統變數,這可能會帶來更多的麻煩。

總結:把這個和全域性變數和區域性變數一樣來理解就行了。

目標變數(區域性)

<target ...> : <variable-assignment> 
<target ...> : overide <variable-assignment> 

前面的基礎變數可理解爲全域性的,這個就是區域性的,例如:

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)progprog.ofoo.obar.o(CFLAGS)的值是什麼,在prog目標,以及其所引發的所有規則中(prog.o foo.o bar.o 的規則),`(CFLAGS)的值都是-g`

模式變數

在 GNU 的 make 中,還支援模式變數(Pattern-specific Variable),通過上面的目標變數中我們知道,變數可以定義在某個目標上。模式變數的好處就是,我們可以給定一種「模式」,可以把變數定義在符合這種模式的所有目標上。

我們知道,make的「模式」一般是至少含有一個%的,所以我們可以以如下方式 給所有以.o結尾的目標定義目標變數:
%.o : CFLAGS = -g

補充:其真實模式規則的來源就是這個模式變數,%取決於目標

makefile的隱含規則

概念

在我們使用Makefile時,有一些我們會經常使用,而且使用頻率非常高的東西,就是在 Makefile 中的「隱含的」,早先約定了的,不需要我們再寫出來的規則。
例如,把.c檔案編譯成.o檔案這一規則,你根本就不用寫出來,make 會自動推導出這種規則,並生成我們需要的.o檔案。

「隱含規則」會使用一些我們系統變數,我們可以改變這些系統變數的值來定製隱含規則的執行時的參數。如系統變數「CFLAGS」可以控制編譯時的編譯器參數。

  1. 如何使用

    如果要使用隱含規則生成你需要的目標,你所需要做的就是不要寫出這個目標的規則。那麼make會試圖去自動推導產生這個目標的規則和命令,如果 make 可以自動推導生成這個目標的規則和命令,那麼這個行爲就是隱含規則的自動推導。例如:

    foo : foo.o bar.o 
    cc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS) 
    

    我們可以注意到,這個 Makefile 中並沒有寫下如何生成 foo.obar.o 這兩目標的規則和命令。因爲 make 的「隱含規則」功能會自動爲我們自動去推導這兩個目標的依賴目標和生成命令。 make 會在自己的「隱含規則」庫中尋找可以用的規則,如果找到,那麼就會使用。如果找不到就會報錯。在上面的那個例子中,make 呼叫的隱含規則是,把.o的目標的依賴檔案置成.c,並使用 C 的編譯命令cc –c $(CFLAGS) [.c] 來生成.o的目標。

    也就是說,我們完全沒有必要寫下下面 下麪的兩條規則:

    foo.o : foo.c 
    cc –c foo.c $(CFLAGS) 
    bar.o : bar.c 
    cc –c bar.c $(CFLAGS) 
    

    因爲,這已經是「約定」好了的事了,這就是隱含規則。 當然,如果我們爲.o檔案書寫了自己的規則,那麼 make 就不會自動推導並呼叫隱含規則,它會按照我們寫好的規則忠實地執行。

  2. 隱含規則有優先順序

    在 make 的「隱含規則庫」中,每一條隱含規則都在庫中有其順序,越靠前的則是越被經常使用的,這會導致我們有些時候即使我們顯示地指定了目標依賴,make也不會管。如下面 下麪這條規則(沒有命令):

    foo.o : foo.p 
    

    依賴檔案「foo.p」(Pascal 程式的原始檔)有可能變得沒有意義。如果目錄下存在了foo.c檔案,那麼我們的隱含規則一樣會生效,並會通過foo.c呼叫C的編譯器生成foo.o 檔案。因爲,在隱含規則中,Pascal 的規則出現在 C 的規則之後,所以,make 找到可以生成 foo.o的 C的規則就不再尋找下一條規則了。

    如果你確實不希望任何隱含規則推導,那麼,你就不要只寫出「依賴規則」,而不寫命令。(或者使用make的-r參數禁用所有隱含規則),即使是我們指定了「-r」參數,某些隱含規則還是會生效,因爲有許多的隱含規則都是使用了「後綴規則」來定義的,所以,只要隱含規則中有「後綴列表」(也就一系統定義在目標.SUFFIXES的依賴目標 ),那麼隱含規則就會生效。 預設的後綴列表是:.out,.a, .ln, .o, .c, .cc, .C, .p, .f, .F, .r, .y, .l, .s, .S, .mod, .sym,.def, .h, .info, .dvi, .tex, .texinfo, .texi, .txinfo, .w, .ch .web, .sh, .elc, .el。

  3. 常見隱含規則

    則名稱 格式 內容
    程式的隱含規則 .o 「.o」的目標的依賴目標會自動推導爲「.c」,並且其生成命令是「$(CC) –c $(CPPFLAGS) $(CFLAGS)」

    其餘的語言的用不到,此處不表

隱含規則的變數

在隱含規則中的命令中,基本上都是使用了一些預先設定的變數。你可以在你的 makefile 中改變這些變數的值,或是在 make 的命令列中傳入這些值,或是在你的環境變數中設定這些值,無論怎麼樣,只要設定了這些特定的變數,那麼其就會對隱含規則起作用。當然,你也可以利用 make 的-R--no–builtin-variables參數來取消你所定義的變數對隱含規則的作用。

下面 下麪列出一些常用變數和其對應的參數,即這些變數在makefile中都是預先設定好的

變數 含義 預設值 對應參數 預設值
AR 函數庫打包程式(.a靜態庫) ar ARFLAGS rv
AS 彙編語言編譯程式 as ASFLAGS
CC C 語言編譯程式 cc CFLAGS
CXX C++語言編譯程式 g++ CXXFLAGS
CPP C 程式的前處理器(輸出是標準輸出裝置) $(CC) –E C 前處理器參數
YACC Yacc 文法分析器(針對於 C 程式) yacc YFLAGS
RM 刪除檔案命令 rm –f
LDFLAGS(鏈接器參數)

模式規則

  1. 介紹
    你可以使用模式規則來定義一個隱含規則。一個模式規則就好像一個一般的規則,只是在規則中,目標的定義需要有 % 字元,它的意思是表示一個或多個任意字元。在依賴目標中同樣可以使用 %,只是依賴目標中的 % 的取值,取決於其目標。

    模式規則中,至少在規則的目標定義中要包含 %,否則,就是一般的規則。目標中的 % 定義表示對檔名的匹配,表示長度任意的非空字串。例如:%.c表示以.c結尾的檔名(檔名的長度至少爲 3),而s.%.c則表示以s.開頭,.c結尾的檔名(檔名的長度至少爲 5)。

    例如有一個模式規則如下:

    %.o : %.c ; <command ......> 
    
    

    其含義是,指出了怎麼從所有的.c檔案生成相應的.o檔案的規則。如果要生成的目標是a.o b.o,那麼%c就是a.c b.c。一旦依賴目標中的%模式被確定,那麼,make 會被要求去匹配當前目錄下所有的檔名,一旦找到,make 就會執行規則下的命令,所以,在模式規則中,目標可能會是多個的,如果有模式匹配出多個目標,make 就會產生所有的模式目標,此時,make 關心的是依賴的檔名和生成目標的命令這兩件事。

  2. 自動化變數

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

老式風格的後綴規則

後綴規則是一個比較老式的定義隱含規則的方法,後綴規則會被模式規則逐步地取代,因爲模式規則更強更清晰。

  1. 雙後綴規則

    定義了一對後綴:目標檔案的後綴和依賴目標(原始檔)的後綴,例如:

    ".c.o" 
    

    相當於

    "%o : %c"。
    
  2. 單後綴
    單後綴規則只定義一個後綴,也就是原始檔的後綴,例如:
    .c相當於

    `% :%.c`。 
    

    注意:後綴規則不允許任何的依賴檔案,如果有依賴檔案的話,那就不是後綴規則,那些後綴統統被認爲是檔名,如:

    .c.o: foo.h 
    $(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $< 
    

    這個例子是說,檔案.c.o依賴於檔案foo.h,而不是我們想要的這樣:

    %.o: %.c foo.h 
    $(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $< 
    

makefile的函數

條件判斷

條件表達式的語法爲:

<conditional-directive> 
<text-if-true> 
endif 

以及

<conditional-directive> 
<text-if-true> 
else 
<text-if-false> 
endif

其中conditional-directive表示條件關鍵字,這個關鍵字有四個

ifeq (<arg1>, <arg2>) 
ifneq (<arg1>, <arg2>) 

ifdef <variable-name> 
ifndef <variable-name> 

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

這個條件判斷非常常用,比如在內核驅動裏面,用來判斷內核版本吶

字串處理常式

  1. subst

    $(subst <from>,<to>,<text>)

    名稱:字串替換函數。
    功能:把字串<text>中的<from>字串替換成<to>
    返回:函數返回被替換過後的字串。

    範例:

    $(subst ee,EE,feet on the street)
    

    把「feet on the street」中的「ee」替換成「EE」,返回結果是「fEEt on the strEEt」。

  2. patsubst

    $(patsubst <pattern>,<replacement>,<text>)

    名稱:模式字串替換函數。
    功能:查詢<text>中的單詞(單詞以「空格」、「Tab」或「回車」「換行」分隔)是否符合模式<pattern>,如果匹配的話,則以<replacement>替換。這裏,<pattern>可以包括萬用字元%,表示任意長度的字串。如果<replacement>中也包含%,那麼它的值就是<pattern>中的那個%所代表的字串。
    返回:函數返回被替換過後的字串。

    範例:

    $(patsubst %.c,%.o,x.c.c bar.c) 
    

    把字串「x.c.c bar.c」符合模式%.c的單詞替換成%.o,返回結果是「x.c.o bar.o」

    備註:「(objects:.o=.c)(objects:.o=.c)」 和 「(patsubst %.o,%.c,$(objects))」是一樣的。

  3. strip

    $(strip <string>)
    名稱:去空格函數。
    功能:去掉<string>字串中開頭和結尾的空字元。
    返回:返回被去掉空格的字串值。

    範例:

    $(strip a b c ) 
    

    把字串「a b c 」去到開頭和結尾的空格,結果是「a b c」。

  4. findstring

    $(findstring <find>,<in>)
    名稱:查詢字串函數。
    功能:在字串<in>中查詢<find>字串。
    返回:如果找到,那麼返回<find>,否則返回空字串。

    範例:

    $(findstring a,a b c) 
    $(findstring a,b c) 
    

    第一個函數返回「a」字串,第二個返回「」字串(空字串)

  5. filter

    $(filter <pattern...>,<text>)
    名稱:過濾函數。
    功能:以<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

  6. filter-out

    $(filter-out <pattern...>,<text>)

    名稱:反過濾函數。
    功能:以<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

  7. sort

    $(sort <list>)
    名稱:排序函數。
    功能:給字串<list>中的單詞排序(升序)。
    返回:返回排序後的字串。

    範例:$(sort foo bar lose)返回bar foo lose」。

    備註:sort函數會去掉中相同的單詞。

  8. word

    $(word <n>,<text>)
    名稱:取單詞函數。
    功能:取字串<text>中第<n>個單詞。(從1開始)
    返回:返回字串<text>中第<n>個單詞。如果<n><text>中的單詞數要大,那麼返回空字串。

    範例:$(word 2, foo bar baz)返回值是bar

  9. wordlist

    $(wordlist <s>,<e>,<text>)
    名稱:取單詞串函數。
    功能:從字串<text>中取從<s>開始到<e>的單詞串。<s>和<e>是一個數字。
    返回:返回字串<text>中從<s><e>的單詞字串。如果<s><text>中的單詞數要大,那麼返回空字串。如果<e>大於<text>的單詞數,那麼返回從<s>開始,到<text>結束的單詞串。

    範例: $(wordlist 2, 3, foo bar baz)返回值是bar baz

  10. words

    $(words <text>)
    名稱:單詞個數統計函數。
    功能:統計<text>中字串中的單詞個數。
    返回:返回<text>中的單詞數。

    範例:$(words, foo bar baz)返回值是3

    備註:如果我們要取<text>中最後的一個單詞,我們可以這樣:$(word $(words <text>),<text>)

  11. firstword

    $(firstword <text>)
    名稱:首單詞函數。
    功能:取字串<text>中的第一個單詞。
    返回:返回字串<text>的第一個單詞。

    範例:$(firstword foo bar)返回值是foo

  12. 字串函數範例
    以上,是所有的字串操作函數,如果搭配混合使用,可以完成比較複雜的功能。這裏舉一個現實中應用的例子。我們知道make使用VPATH變數來指定依賴檔案的搜尋路徑。我們可以利用這個搜尋路徑來指定編譯器對標頭檔案的搜尋路徑參數CFLAGS,如:

    override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH))) 
    

    如果我們的$(VPATH)值是src:../headers,那麼$(patsubst %,-I%,$(subst :, ,$(VPATH)))將返回-Isrc -I../headers,這正是gcc搜尋標頭檔案路徑的參數。

檔名操作函數

  1. dir

    $(dir <names...>)
    名稱:取目錄函數。
    功能:從檔名序列<names>中取出目錄部分。目錄部分是指最後一個反斜槓(「/」)之前的部分。如果沒有反斜槓,那麼返回「./」。
    返回:返迴檔名序列<names>的目錄部分。

    範例: $(dir src/foo.c hacks)返回值是src/ ./

  2. notdir

    $(notdir <names...>)
    名稱:取檔案函數。
    功能:從檔名序列<names>中取出非目錄部分。非目錄部分是指最後一個反斜槓(「 /」)之後的部分。
    返回:返迴檔名序列<names>的非目錄部分。

    範例: $(notdir src/foo.c hacks)返回值是foo.c hacks

  3. suffix

    $(suffix <names...>)
    名稱:取後綴函數。
    功能:從檔名序列<names>中取出各個檔名的後綴。
    返回:返迴檔名序列<names>的後綴序列,如果檔案沒有後綴,則返回空字串。

    範例:$(suffix src/foo.c src-1.0/bar.c hacks)返回值是.c .c

  4. basename
    $(basename <names...>)
    名稱:取字首函數。
    功能:從檔名序列<names>中取出各個檔名的字首部分。
    返回:返迴檔名序列<names>的字首序列,如果檔案沒有字首,則返回空字串。

    範例:$(basename src/foo.c src-1.0/bar.c hacks)返回值是src/foo src-1.0/bar hacks

  5. addsuffix

    $(addsuffix <suffix>,<names...>)
    名稱:加後綴函數。
    功能:把後綴<suffix>加到<names>中的每個單詞後面。
    返回:返回加過後綴的檔名序列。

    範例:$(addsuffix .c,foo bar)返回值是foo.c bar.c

  6. addprefix

    $(addprefix <prefix>,<names...>)
    名稱:加字首函數。
    功能:把字首<prefix>加到<names>中的每個單詞後面。
    返回:返回加過字首的檔名序列。

    範例:$(addprefix src/,foo bar)返回值是src/foo src/bar

  7. join

    $(join <list1>,<list2>)
    名稱:連線函數.
    功能:把<list2>中的單詞對應地加到<list1>的單詞後面。如果<list1>的單詞個數要比<list2>的多,那麼<list1>中的多出來的單詞將保持原樣。如果<list2>的單詞個數要比<list1>多,那麼,<list2>多出來的單詞將被複制到<list1>中。
    返回:返回連線過後的字串。

    範例:$(join aaa bbb , 111 222 333)返回值是aaa111 bbb222 333

foreach

$(foreach <var>,<list>,<text>)

把參數<list>中的單詞逐一取出放到參數<var>所指定的變數中,然後再執行<text>所包含的表達式。每一次<text>會返回一個字串,回圈過程中,<text>的所返回的每個字串會以空格分隔,最後當整個回圈結束時,<text>所返回的每個字串所組成的整個字串(以空格分隔)將會是foreach函數的返回值.

範例

names := a b c d 
files := $(foreach n,$(names),$(n).o) 

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

注意,foreach 中的<var>參數是一個臨時的區域性變數,foreach 函數執行完後,參數<var>的變數將不在作用,其作用域只在foreach 函數當中。

if

$(if <condition>,<then-part>) 
$(if <condition>,<then-part>,<else-part>) 

<condition>參數是if的表達式,如果其返回的爲非空字串,那麼這個表達式就相當於返回真,於是<then-part>會被計算,否則<else-part>會被計算。

call

$(call <expression>,<parm1>,<parm2>,<parm3>...)

當make執行這個函數時,<expression>參數中的變數,如$(1),$(2),$(3)等,會被參數<parm1>,<parm2>,<parm3>依次取代。而<expression>的返回值就是call函數的返回值。例如:

reverse = $(1) $(2) 
foo = $(call reverse,a,b) 

origin

origin函數不像其它的函數,他並不操作變數的值,他只是告訴你你的這個變數是哪裏來的,其語法是:

$(origin <variable>) 

注意,<variable>是變數的名字,不應該是參照。所以你最好不要在<variable>中使用$字元。

Origin函數會以其返回值來告訴你這個變數的「出生情況」,下面 下麪是origin函數的返回值:

  • undefined : 如果<variable>從來沒有定義過,origin函數返回這個值

  • default : 如果<variable>是一個預設的定義,比如「CC」這個變數,這種變數我們將在後面講述。

  • environment: 如果<variable>是一個環境變數,並且當Makefile被執行時-e參數沒有被開啓。

  • file : 如果<variable>這個變數被定義在Makefile中。

  • command line: 如果<variable>這個變數是被命令列定義的。

  • override : 如果<variable>是被override指示符重新定義的。

  • automatic : 如果<variable>是一個命令執行中的自動化變數。關於自動化變數將在後面講述。

這些資訊對於我們編寫Makefile 非常有用的,例如,環境變數中有一個bletch,而我們的makefile中也 有一個變數「bletch」,此時,我們想判斷一下,如果變數來源於環境,那麼我們就把它重定義了,如果來源於命令列等非環境的,那麼我們就不重新定義它。於 是,在我們的Makefile中可以這樣寫:

ifdef bletch 
ifeq "$(origin bletch)" "environment" 
bletch = barf, gag, etc. 
endif 
endif 

當然,你也許會說,使用override關鍵字不就可以重新定義環境中的變數了嗎?爲什麼需要使用這樣的步驟?是的,我們用 override 是可以達到這樣的效果,可是它過於粗暴,它同時會把從命令列定義的變數也覆蓋了,而我們只想重新定義環境傳來的,而不想重新定義命令列傳來的。

makefile的參數

指定目標檔案

  1. 指定終極目標的方法可以很方便地讓我們編譯我們的程式,例如下面 下麪這個例子:

    .PHONY: all 
    all: prog1 prog2 prog3 prog4 
    

    從這個例子中,我們可以看到,這個makefile中有四個需要編譯的程式——prog1,prog2,prog3,prog4,我們可以使用make all命令來編譯所有的目標(如果把all置成第一個目標,那麼只需執行「make」),我們也可以使用「make prog2」來單獨編譯目標「prog2」。

  2. 即然make可以指定所有makefile中的目標,那麼也包括「僞目標」,於是我們可以根據這種性質來讓我們的makefile根據指定的不同的目標來完成不同的事。在 Unix 世界中,軟體發佈時,特別是 GNU 這種開源軟體的發佈時,其 makefile 都包含了編譯、安裝、打包等功能。我們可以參照這種規則來書寫我們的 makefile 中的目標。

    • all : 這個僞目標是所有目標的目標,其功能一般是編譯所有的目標。
    • clean : 這個僞目標功能是刪除所有被 make 建立的檔案。
    • install :這個僞目標功能是安裝已編譯好的程式,其實就是把目標執行檔案拷貝到指定的目標中去。
    • print : 這個僞目標的功能是例出改變過的原始檔。
    • tar : 這個僞目標功能是把源程式打包備份。也就是一個 tar 檔案。
    • dist : 這個僞目標功能是建立一個壓縮檔案,一般是把 tar 檔案壓成 Z 檔案。或是 gz 檔案。
    • TAGS : 這個僞目標功能是更新所有的目標,以備完整地重編譯使用。
    • check和test : 這兩個僞目標一般用來測試 makefile 的流程。
  3. 在這裏我們說一個環境變數,MAKECMDGOALS,它儲存了你所指定的終極目標的列表,若在命令列中沒有指定目標,那麼這個值爲空,可以用在如下例子中

    sources = foo.c bar.c 
    ifneq ( $(MAKECMDGOALS),clean) 
    include $(sources:.c=.d) 
    endif 
    

    基於上面的這個例子,只要我們輸入的命令不是make clean,那 makefile會自動包含foo.dbar.d這兩個檔案。

make的參數

參數縮寫 參數 作用
-b
-m
忽略和其它版本 make 的相容性
-B --always-make 認爲所有的目標都需要更新(重編譯)
-C (dir) --directory=(dir) 指定讀取 makefile 的目錄dir
—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 執行命令時能執行多少就執行多少
-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都會輸出一堆資訊。
-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 相當於 UNIX 的 touch 命令,只是把目標的修改日期變成最新的,也就是阻止生成目標的命令執行
-v --version 輸出 make 程式的版本、版權等關於 make 的資訊
-w --print-directory 輸出執行 makefile 之前和之後的資訊。這個參數對於跟蹤巢狀式呼叫 make 時很有用
--no-print-directory 禁止「-w」選項
-W file --what-if= file
--new-if= file
--assume-if= file
假定目標file需要更新,如果和「-n」選項使用,那麼這個參數會輸出該目標更新時的運 行動作。如果沒有「-n」那麼就像執行 UNIX 的「touch」命令一樣,使得file的修改時間爲當前時間。
--warn-undefined-variables 只要 make 發現有未定義的變數,那麼就輸出警告資訊