makefile最佳實踐 - 從原理到實用技巧

Makefile 的最佳實踐

概述

make 是一個命令工具,它用來解釋 Makefile 中的規則。Makefile 中可使用系統 shell 所提供的任何命令。但注意有些像 set,setenv 等是不行的。
Makefile 最大的優勢是簡單,只須要一句話的解釋就可讓一個以前不懂的人能夠用起來併發揮做用。但只有掌握了它的內涵才能真正駕輕就熟。linux

編譯的知識

Makefile 開始實際上是爲了 C/C++的編譯而誕生的,因此它裏面的不少隱藏規則都是針對 C/C++的。在講 Makefile 以前有必要對 C/C++的編譯有一點了解
過程以下:shell

d5054ba7-7140-4fa1-9f64-96c03a0299a0

  • 預處理器:將.c 文件轉化成 .i 文件,使用的 gcc 命令是:gcc –E,對應於預處理命令 cpp;
  • 編譯器:將.c/.h 文件轉換成.s 文件,使用的 gcc 命令是:gcc –S,對應於編譯命令 cc –S;
  • 彙編器:將.s 文件轉化成 .o 文件,使用的 gcc 命令是:gcc –c,對應於彙編命令是 as;
  • 連接器:將.o 文件轉化成可執行程序,使用的 gcc 命令是: gcc,對應於連接命令是 ld;
  • 加載器:將可執行程序加載到內存並進行執行,loader 和 ld-linux.so。

Makefile 規則介紹

一個簡單的 Makefile 規則組成以下:bash

Targets...: Prerequisites...
Command
Command
...

併發

Targets: Prerequisites;Command
Command
...

下面會稱 Target 爲目標, Prerequisites 爲目標依賴, Command 爲規則的命令行
Command 必須以[Tab]開始, Command 能夠寫成多行,經過來繼行,但行尾的後不能有空格。
規則包含了文件之間的依賴關係和更新此規則 target 所須要的 Command
targets 可使用通配符, 若是格式是"A(M)"表示檔案文件(.a)中的成員「M」
在須要用$本義的時候,使用兩$$來表示函數

當規則的 target 是一個文件,它的任何一個依賴文件被修改後,在執行 make <target>時這個目標文件都會被從新編譯或從新鏈接。若是有必要此 target 的一個依賴文件也會被先從新編譯。工具

僞目標

Makefile 中把那些沒胡任何依賴只有執行動做的目標稱爲「僞目標「(Phony targets)ui

.PHONY : clean
clean :
-rm edit $(objects

經過.PHONY 將 clean 聲明爲僞目標,避免當目錄下有名爲「clean」文件時,clean 沒法執行
這樣的目標不是爲了建立或更新程序,而是執行相應動做。spa

自動推導規則

在使用 make 編譯.c 源文件時,編譯.c 源文件規則的命令能夠不用明確給出。這是由於 make 自己存在一個默認的規則,可以自動完成對.c 文件的編譯並生成對應的.o 文件。它執行命令「cc -c」來編譯.c 源文件。在 Makefile 中咱們只須要給出須要重建的目標文件名(一個.o 文件),make 會自動爲這個.o 文件尋找合適的依賴文件(對應的.c 文件。對應是指:文件名除後綴外,其他都相同的兩個文件),並且使用正確的命令來重建這個目標文件。對於上邊的例子,此默認規則就使用命令「cc -c main.c -o main.o」來建立文件「main.o」。對一個目標文件是「N.o」,倚賴文件是「N.c」的規則,徹底能夠省略其規則的命令行,而由 make 自身決定使用默認命令。此默認規則稱爲 make 的隱含規則。命令行

規則書寫建議

書寫規則建議的方式是:單目標,多依賴。就是說盡可能要作到一個規則中只存在一個目標文件,可有多個依賴文件。儘可能避免使用多目標,單依賴的方式。code

objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

上面是很差的風格

makefile 文件搜索順序

GNUmakefile
makefile
Makefile
當前目錄下不存在以「GNUmakefile 」、「makefile 」、「Makefile 」命名的任何文件,

  1. 當前目錄下存在一個源文件 foo.c 的,咱們可使用「make foo.o 」來使用 make 的隱含規則自動生成的隱含規

則自動生成 foo.o 。當執行「make foo.o 」時。咱們能夠看到其執行的命令爲:
cc –c –o foo.o foo.c
以後,foo.o 將會被建立或者更新。

  1. 若是當前目錄下沒有 foo.c 文件時,就是 make 對.o 文件目標的隱含規則中依賴文件不存在。

若是使用命令「文件目標的隱含規則中依賴文件不存在。
若是使用命令「make foo.o 」時,將回到到以下提示:

make: *** No rule to make target ‘foo.o’. Stop.
  1. 若是直接使用命令「make 」時,獲得的提示信息以下:
make: *** No targets specified and no makefile found. Stop.

include

include foo *.mk ${bar}會被展開爲include foo a.mk b.mk c.mk bish bash
能夠在 make 命令行中用-I 指定包含文件搜索目錄,默認搜索

  • /usr/gnu/include
  • /usr/local/include
  • /usr/include

能夠在 include 前加上-來使 make 不會由於未找到包含文件而退出

變量 MAKEFILES

若是定義了這個值,那麼 make 會先讀入這個變量指定的多個文件

重載另外一個 makefile

有些狀況下,存在兩個比較相似的 makefile 文件。其中一個(makefile-A)須要使用另一個(makefile-B)中所定義的變量和規則。一般咱們會想到在「makefile-A」中使用指示符「include」包含「mkaefile-B」來達到目的。但使用這種方式,若是在兩個 makefile 文件中存在相同目標,而在不一樣的文件中其描述規則使用不一樣的命令。這樣,相同的目標文件就同時存在兩個不一樣的規則命令,這是 makefile 所不容許的。遇到這種狀況,使用指示符「include」顯然是行不通的。GNU make 提供另一種途徑來實現此目的。具體的作法以下:
在須要包含的 makefile 文件(makefile-A)中,定義一個稱之爲「全部匹配模式」(參考 10.5 模式規則 一節)的規則,它用來述那些在「makefile-A」中沒有給出明確建立規則的目標的重建規則。就是說,若是在當前 makefile 文件中不能找到重建一個目標的規則時,就使用「全部匹配模式」所在的規則來重建這個目標。
看一個例子,若是存在一個命名爲「Makefile」的 makefile 文件,其中描述目標「foo」的規則和其餘的一些規,咱們也能夠書寫一個內容以下命名爲「GNUmakefile」的文件。

foo:
frobnicate > foo
%: force
@$(MAKE) -f Makefile $@
force: ;

執行命令「make foo」,make 將使用工做目錄下命名爲「GNUmakefile」的文件並執行目標「foo」所在的規則,建立目標「foo」的命令是:「frobnicate > foo」。若是執行另一個命令「make bar」,由於在「GUNmakefile」中沒有此目標的更新規則。make 將使用「全部匹配模式」規則,執行命令「$(MAKE) -f Makefile bar」。若是文件「Makefile」中存在此目標更新規則的定義,那麼這個規則會被執行。此過程一樣適用於其它 「GNUmakefile」中沒有給出的目標更新規則。此方式的靈活之處在於:若是在「Makefile」文件中存在一樣一一個目標「foo」的重建規則,因爲 make 執行時首先讀取文件「GUNmakefile」並在其中可以找到目標「foo」的重建規則,因此 make 就不會去執行這個「全部模式匹配規則」(上例中目標「%」所在的規則)。這樣就避免了使用指示符「include」包含一個 makefile 文件時所帶來的目標規則的重複定義問題。
此種方式,模式規則的模式只使用了單獨的「%」(咱們稱他爲「全部模式匹配規則」),它能夠匹配任何一個目標;它的依賴是「force」,保證了即便目標文件已經存在也會執行這個規則(文件已存在時,須要根據它的依賴文件的修改狀況決定是否須要重建這個目標文件);
「force」規則中使用空命令是爲了防止 make 程序試圖尋找一個規則去建立目標「force」時,又使用了模式規則「%: force」而陷入無限循環

make 如何解析 makefile

  • 第一階段

讀取全部的 makefile 文件(包括「MAKIFILES」變量指定的、指示符「include」指定的、以及命令行選項「-f(--file)」指定的 makefile 文件),內建全部的變量、明確規則和隱含規則,並創建全部目標和依賴之間的依賴關係結構鏈表。

  • 第二階段

根據第一階段已經創建的依賴關係結構鏈表決定哪些目標須要更新,並使用對應的規則來重建這些目標。
在 make 執行的第一階段中若是變量和函數被展開,那麼稱此展開是「當即」的,此時全部的變量和函數被展開在須要構建的結構鏈表的對應規則中(此規則在創建鏈表是須要使用)。其餘的展開稱之爲「延後」的。這些變量和函數不會被「當即」展開,而是直到後續某些規則需要使用時或者在 make 處理的第二階段它們纔會被展開。

變量取值

IMMEDIATE = DEFERRED
IMMEDIATE ?= DEFERRED
IMMEDIATE := IMMEDIATE
IMMEDIATE += DEFERRED or IMMEDIATE
define IMMEDIATE
DEFERRED
endef

條件語句

全部使用到條件語句在產生分支的地方,make 程序會根據預設條件將正確地分支展開。就是說條件分支的展開是「當即」的。其中包括:「ifdef」、「ifeq」、「ifndef」和「ifneq」所肯定的全部分支命令。

規則定義

全部的規則在 make 執行時,都按照以下的模式展開:

IMMEDIATE : IMMEDIATE ; DEFERRED
DEFERRED

其中,規則中目標和依賴若是引用其餘的變量,則被當即展開。而規則的命令行中的變量引用會被延後展開。此模板適合全部的規則,包括明確規則、模式規則、後綴規則、靜態模式規則。

依賴類型

TARGETS : NORMAL-PREREQUISITES | ORDER-ONLY-PREREQUISITES

兩種類型:

  • 常規依賴
  • 「order-only" 依賴

當"order-only"依賴更新後,不須要更新目標
好比:

LIBS = libtest.a
foo : foo.c | $(LIBS)
$(CC) $(CFLAGS) $< -o $@ $(LIBS)

文件名通配符

表示文件名時,可用的通配符有: 「*", "?", "[...]"
Makefile 中通配符能夠出如今如下兩種場合:

  • 能夠用在規則的目標、依賴中,make 在讀取 Makefile 時會自動對其進行匹配處理(通配符展開);
  • 在規則的命令中,通配符的通配處理是在 shell 在執行此命令時完成的

除上面兩種狀況以外的上下文中,不能直接使用通配符,須要經過函數"wildcard"來實現
示例一:

print: *.c
lpr -p $?
touch print

變量定義中的通配符不會被處理,好比: "objects = _.o", 它表示 objects 的值是字符串"_.o",而不是當前文件夾下的.o 文件列表。

通配符可能帶來的問題

示例以下:

objects = *.o
foo : $(objects)
cc -o foo $(CFLAGS) $(objects)

若是將工做目錄下全部的.o 文件刪除,從新執行 make 將會獲得一個相似於「沒有建立*.o 文件的規則」 的錯誤提示。
好的作法是:

objects = $(wildcard *.o)
foo : $(objects)
cc -o foo $(CFLAGS) $(objects)

函數 wildcard

在規則中, 通配符會被自動展開。但在變量定義和函數引用時, 通配符不會展開。這時候就須要用 wildcard
能夠用$(wildcard _.c)來獲取工做目錄下全部的.c 文件列表,能夠用$(patsubst %.c,%.o,$(wildcard _.c))來獲得對應.c 的目標文件

目錄搜索

在一個較大的工程中,通常會將源代碼和二進制文件(.o 文件和可執行文件)安排在不一樣的目錄來進行區分管理。這種狀況下,咱們可使用 make 提供的目錄搜索依賴文件功能(在指定的若干個目錄下自動搜索依賴文件)。在 Makefile 中,使用依賴文件的目錄搜索功能。當工程的目錄結構發生變化後,就能夠作到不更改 Makefile 的規則,只更改依賴文件的搜索目錄。

VPATH

這是一個 makefilej 裏的變量

VPATH = src:../headers

vpath

這是 make 的一個關鍵字。能夠爲不一樣類型文件指定不一樣搜索目錄,有三種形式

  • vpath PATTERN DIRECTORIES : 相似上面的 VPATH
vpath %.h ../headers
  • vpath PATTERN : 清除以前爲符合模式「PATTERN」的文件設置的搜索路徑
  • vpath: 清除全部已被設置的文件搜索路徑

當有衝突時,按順序來查找,好比

vpath %.c foo
vpath % blish
vpath %.c bar

表示對全部的.c 文件,make 依次查找目錄:「foo」、blish」、「bar」。

vpath %.c foo : bar
vpath % blish

對於全部的.c 文件 make 將依次查找目錄:「foo」、「bar」、「blish」

目錄搜索的機制

  • 首先,若是規則的目標文件在 Makefile 文件所在的目錄(工做目錄)下不存在,那麼就執行目錄搜尋。
  • 若是目錄搜尋成功,在指定的目錄下存在此規則的目標。那麼搜索到的完整的路徑名就被做爲臨時的目標文件被保存。
  • 對於規則中的全部依賴文件使用相同的方法處理
  • 後繼重建規則以下: - 當規則的目標不須要被重建時,規則中的全部的文件完整的路徑名有效 - 當規則的目標須要重建時,規則的目標文件會在工做目錄下被重建,而不是在目錄搜尋時所獲得的目錄

舉例, 有一個目錄"armgen", 它下面有一個子目錄「src", 存在"sum.c"和」memcp.c"兩個源文件,在"armgen"下的 Makefile 內容以下:

LIBS = libtest.a
VPATH = src
libtest.a : sum.o memcp.o
$(AR) $(ARFLAGS) $@ $^
  • 在"sum.c"和「memcp.c"都沒有更新的狀況下,執行 make 會搜索到 src 下的 libtest.a, 不會重建目標
  • 在.c 文件發現變量時, 它會在 armgen 裏重建 prom/libtest.a

固然咱們有一個變量 GPATH,能夠指定目標文件的目錄

命令行中的自動變量

當咱們經過目錄搜索獲得依賴文件會在其餘目錄,可是若是命令行中沒有路徑的話,就會出錯。因此必須使用自動變量

foo.o : foo.c
cc -c $(CFLAGS) $^ -o $@

規則命令行中的自動化變量「$^」表明全部經過目錄搜索獲得的依賴文件的完整路徑名(目錄 + 通常文件名)列表。「$@」表明規則的目標。

VPATH = src:../headers
foo.o : foo.c defs.h hack.h
cc -c $(CFLAGS) $< -o $@

自動化變量「$<」表明規則中經過目錄搜索獲得的依賴文件列表的第一個依賴文件

庫文件和搜索目錄

makefile 中的程序連接的靜態庫和共享庫也能夠經過搜索目錄獲得。這一特性須要咱們在書寫規則依賴時用"-I<name>"來指定一個依賴文件名

foo : foo.c -lcurses
cc $^ -o $@

上面的命令只是定義在 foo.c 和/usr/lib/libcurses.a 或.so 被更新時要重建 foo, 但不會自動重建 libcurses.a,由於 make 不知道它的依賴
若是找不到 libcurses.a 或.so 會,報出相似沒有規則能夠建立目標「foo」須要的目標「-lcurses 的錯誤
.so 或.a 能夠同變量".LIBPATTERNS"來指定,它是一個多個包含模式字符%的字,多個值之間用空格分隔。它的默認值是"lib%.so lib%.a"

僞目標

有些目標並不會建立目標,只是執行命令,因此咱們定義了僞目標,如常見的 clean

clean:
rm *.o temp
.PHONY: clean

若是沒有定義僞目標,那麼當存在文件"clean"時, "rm *.o temp"就不會被執行
另外一種使用場合是在 make 的並行和遞歸執行中。
好比

SUBDIRS = foo bar baz
subdirs:
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir; \
done

但上面的方法有 2 個問題

  1. 當子目錄執行 make 出錯時,make 不會退出,還會去其餘目錄進行 makefile, 最終會難定位第一個 make 出錯的地方, 特別是在用了-k 選項時
  2. 因爲使用了 shell 的 for 循環,它無法用到 make 對目錄的並行處理能力, 能夠改爲這樣
SUBDIRS = foo bar baz
.PHONY: subdirs $(SUBDIRS)
subdirs: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
foo: baz

上邊的實現中有一個沒有命令行的規則「foo: baz」,此規則用來限制子目錄的 make 順序。它的做用是限制同步目錄「foo」和「baz」的 make 過程(在處理「foo」目錄以前,須要等待「baz」目錄處理完成)。在書寫一個並行執行 make 的 Makefile 時,目錄的處理順序是須要特別注意的。

另外,make 存在一個內嵌隱含變量「RM」,它被定義爲:「RM = rm –f」。所以在書寫「clean」規則的命令行時可使用變量「$(RM)」來代替「rm」,這樣能夠免出現一些沒必要要的麻煩!這是推薦的用法

變量展開

makefile 裏變量的展開是循環進行的

t2 := t3
t1 := t2
t0 := t1
define bar
irun $($(t$(1)))
endef

all:
$(call bar,0)
$($($(t0)))

輸出結果是

irun t2 t3
相關文章
相關標籤/搜索