GNU make(2)

GNU make(2)

參考:
GNU Make學習總結(二)java

變量

變量由一個前導符號$加上字符或者是括號字符組成, 名稱區分大小寫.
命名: 習慣上用所有大寫字符表示常量, 小寫字符表示變量, 單詞之間用下劃線.shell

變量賦值

有四種方式:數據庫

CC = gcc
CC := gcc
CC ?= gcc
CC += gcc

CC = gcc 這種方式和普通語言不同, 並非直接將右值賦給左值, 而是在變量CC在使用時賦值, 因此將變值賦給變量時, 有可能不一樣時候獲得不一樣的值.vim

CC := gcc 和普通賦值同樣, 直接將右值賦給左邊.bash

CC ?= gcc 條件賦值, 只有在變量不存在的時候纔會賦值, 這種方式很容易和環境變量交互.ide

CC += gcc 同其餘語言, 將右值增長到左邊的變量後面.函數

除了單個變量, Makefile中還能夠用define來定義一個宏來保存一組操做, 用來重用. 例子:學習

define newdir
    mkdir tmp
    cd tmp
endef

拓展變量

make對於變量的拓展不是即時的, 而是有內部的數據庫和依存圖(Depency Parsing Gragh)的, 須要知道什麼時候纔會對變量進行拓展.測試

拓展規則:
make會分爲兩個階段來完成工做,第一階段讀進makefile以及include進來的makefile,其中定義的變量和規則都會被加進make的內部數據庫,並創建依存圖,第二階段會根據依存圖判斷須要進行哪些更新操做。
下面是用來處理什麼時候擴展變量的幾條準則:spa

  1. 在變量左側的部分,會在第一階段當即擴展。(變量左側也能夠是一個\(x形式的變量,例如x=y,\)(x)=z就等於y=z)
  2. =和?=都在使用的時候,也就是第二階段才擴展。
  3. :=的右邊的值會在第一階段當即擴展。
  4. +=左邊若是是簡單變量,右邊就會當即擴展,不然會延到第二階段擴展。(所謂簡單變量就是指用:=賦值的變量,相對的,遞歸變量指用=賦值的變量)
  5. 宏定義的變量名會被當即擴展,宏的主體會被延後到使用時擴展。
  6. 對於每條規則,工做目標和必要條件都是當即擴展,命令老是延後擴展。

能夠總結成下表:

定義 擴展a 擴展b
a=b 當即 延後
a?=b 當即 延後
a:=b 當即 當即
a+=b 當即 當即或延後
define a
b..
endef
當即 延後

專屬變量

有時候一個變量對於多數規則都適用,但在某條規則下須要對這個變量進行擴展或者改變,這時就能夠用到條件專屬變量。好比說咱們在編譯a.o時要DEFINE DEBUG,能夠這樣寫:

a.o: CFLAGS += -DDEBUG
a.o: a.h

這樣的話在編譯a.o時CFLAGS會在原來的CFLAGS後面加上-DDEBUG,編譯完a.o後就還原了。
擴展到不一樣的賦值方式,共有如下四種格式,注意這些賦值操做都會延後到開始處理工做目標的時候進行,變量的值也只在處理該工做目標的時候有效。

注意:
在一個target內部的改變的規則只在編譯該target時纔有效, 不是全局的.
該規則和給變量的賦值同.以下:

target...: v = value
target...: v := value
target...: v += value
target...: v ?= value

include指令

在上一章生成自動依賴的時候用到了include指令,這裏對include指令詳細介紹。
在make讀到include指令的時候,若是include文件存在,則會讀取文件內容並繼續執行下去,若不存在,會在彙報問題後繼續讀取剩下的makefile。讀取完成後,make會從規則庫中找出任何可用來更新引入文件的規則,若是找到了就執行更新操做,若是一個引入文件被規則更新,則make會清除內部數據庫而且從新讀進整個makefile,若是這以後include的文件仍然不存在,則報錯終止執行。
使用-include或者sinclude指令來代替include可讓make忽略沒法加載的引入文件。

條件指令

選擇性執行:

if-condition
    ...
endif

if-condition
    ...
else
    ...
endif

其中if-condition能夠是如下之一

ifdef variable-name
ifndef variable-name
ifeq test
ifneq test

注意的是variable-name不須要加前導$符,而test能夠表示成」a」 「b」或 (a,b)。

標準make變量

除了自動變量以外,make會爲本身的狀態和內置規則的定義提供變量。
爲內置規則提供的變量經過make -p能夠看到,咱們能夠經過直接修改這些參數來改變內置規則參數,好比CFLAGS修改編譯C時的參數,CC修改C編譯器等等。
而爲表示Makefile本身狀態提供的變量主要有如下幾個。

MAKE_VERSION    GNU Make版本號
CURDIR          正在執行make進程的工做目錄
MAKEFILE_LIST   make所讀進的各個makefile文件名稱構成的列表,最後一個是自身文件名
MAKECMDGOALS    make命令指定了哪些工做目標
.VARIABLES      make從各個makefile文件讀進的名稱所構成的列表

例子:
好比,咱們在clean的時候是不執行include指令的,能夠經過如下幾行來完成

ifneq "$(MAKECMDGOALS)" "clean"
 -include ...
endif

函數

內置函數

如下只作簡單的介紹,具體怎麼用試一下就記住了。

內置函數在語法上基本都是如同$(func-name arg1[, argn..])的形式,不一樣參數之間用逗號隔開,須要注意的是,除了第一個參數,逗號後的空格都會被保留下來,若是不當心多個空格會引發各類問題。另外,對於處理單詞或文件名的函數,參數常常是一串空格隔開的單詞,函數對每一個單詞進行匹配或處理.

雜項函數

$(sort list)
對list參數排序並去重,此外,它還會刪除前導及結尾的空格。雖然這個函數叫sort,但更多的時候是用它來去重。

$(shell command)
將command傳遞給subshell執行,並將標準輸出值做爲結果返回,其中換行符都會被替換爲空格。

$(strip text)
去掉前導和後面空格,並將內部連續空格轉化爲單一空格。

$(origin variable)
返回變量的來源,也能夠測試變量是否認義,返回值有如下幾個:

undefined 未定義
default 來自make內置數據庫
environment 來自環境變量
environment override 來自環境變量,並且使用了–environment-overrides指令
file 來自makefile
command line 來自命令行
override 來自override指令
automatic make所定義的自動變量

這裏override變量是指在變量賦值前加override,使得該賦值比命令行賦值優先級高,而–environment-overrides選項是使默認環境變量比makefile中環境變量賦值優先級高。

$(warning text)
打印警告信息.

字符串函數
$(filter pattern ...,text)
$(filter-out pattern ...,text)

filter將text視爲一系列空格隔開的單詞,返回與pattern符合的單詞,pattern中可使用模式通配符。而filter-out找的是filter的補集。

$(findstring string...,text)
用處不是太大,在文本中找一個字符串,還不能使用通配符。。

$(subst search-string,replace-string,text)
$(patsubst search-string,replace-string,text)
$(variable:search-string=replace-string)

subst將text中出現search-string的地方所有替換成replace-string,而patsubst與subst的不一樣的是可使用一個模式通配符%。第三個函數叫作替換引用,主要是用來替換文件後綴的,與subst不一樣的是,search-string必定出如今文件結尾。
如下嘗試使用這三個函數替換文件後綴,以理解這幾個函數之間的區別:

.PHONY: test
sc = a.c b.c c.c.c
to1 = $(subst .c,.o,$(sc));
to2 = $(patsubst %.c,%.o,$(sc));
to3 = $(sc:.c=.o);
test:
    @echo $(to1)
    @echo $(to2)
    @echo $(to3)

輸出結果以下,能夠發現subst函數對於文件名中也出現.c的單詞會更改文件名,另外兩個函數的效果相同,都能替換文件後綴。

a.o b.o c.o.o
a.o b.o c.c.o
a.o b.o c.c.o
$(words text)
$(words n,text)
$(firstword text)
$(wordlist start,end,text)

這幾個函數中,text都是用空格隔開的單詞列表。第一個函數返回單詞的個數,第二個函數返回第n個單詞,第三個函數返回第一個單詞,第四個返回從start到end的單詞。

文件名函數

Makefile中不少時候都是在對文件名進行處理,這些函數常常會用一個變量包含一組文件名,中間用空格隔開,下面的函數中變量後加…都表示這個變量是一個由空格隔開的字符串。

$(wildcard pattern...)
$(dir list...)
$(notdir name...)

第一個函數比較經常使用,可使用通配符來匹配一組文件,這些文件名之間用空格隔開做爲函數返回值,好比$(wildcard *.cpp)能夠得到當前目錄下全部的cpp文件。第二個函數返回list中每一個文件的目錄部分,而第三個函數會返回文件名部分。

$(suffix name...)
$(basename name...)
$(addsuffix suffix,name...)
$(addprefix prefix,name...)
$(join prefix-list,suffix-list)

suffix函數返回name列表中的全部後綴,basename返回不帶後綴的部分,而addsuffix和addprefix顧名思義,就是爲name列表中全部單詞添加後綴或前綴。而join就是將prefix-list和suffix-list中的單詞按順序組合,可用來重建被dir和notdir分解的列表。

下面舉幾個例子來看一下文件名函數的使用方法

#當前目錄下以.c和.h文件組成的變量
sources := $(wildcard *.c *.h)
#判斷主目錄下是否存在.emacs文件
dot-eamcs-exists := $(wildcard ~/.emacs)
#顯示包含C文件的子目錄(使用find查找,sort去重)
source-dirs := $(sort $(dir $(shell find . -name '*.c')))
#返回$JAVAFILE變量(a.java的形式之間用空格隔開組成)中的JAVA類名
class-name := $(notdir $(subst .java,,$(JAVAFILE)))
#測試$files中是否全部單詞具備相同的後綴(判斷去重後是否只有一種後綴)
same-suffix = $(filter 1,$(words $(sort $(suffix $files))))
#從Java文件名轉換成class名(包含包名,a/b/c.java=>a.b.c)
ftc-name := $(subst /,.,$(basename $(JAVAFILE)))
#計算PATH環境變量中全部程序的數量。最後三行都是處理特殊狀況的,處理完後使用空格替代冒號做爲分隔符並去重,再在每一個目錄後加/*並做爲wildcard的參數,最後用words統計wildcard匹配到的程序數目。
program-nums =  $(words \
        $(wildcard  \
            $(addsuffix /*, \
                $(sort  \
                    $(subst :, ,    \
                        $(subst ::,:.:, \
                            $(patsubst :%,.:%,  \
                                $(patsubst %:,%:.,$(PATH)))))))))
流程控制

$(error text)
輸出錯誤信息,並在這以後結束make程序。

$(if condition,then-part,else-part)
與前面提到的條件指令不一樣,這裏的condition能夠是一個函數或者表達式,then-part和else-part也能夠是宏或者函數,會根據condition返回的結果是否爲空決定執行哪一部分,這裏的爲空指的是不包含任何字符(包括空格)。
下面這個例子用來判斷make是否在3.81版本下執行

$(if $(filter $(MAKE_VERSION),3.81),,$(error version not 3.8.1))

$(foreach variable,list,body)
對於list中的每一個變量variable,執行body部分的內容。每次body部分的內容會被以空格爲分隔符累計起來,最後做爲返回值。

自定義函數

其實make中定義函數的方式和定義變量的方式幾乎同樣,有單行定義和宏定義兩種方式,單行定義通常用來替代一系列內置命令組合,而對於複雜一點的過程,就要用宏來定義。
make中函數的調用方法以下:
$(call func-name[, param1...])
而在函數體中,則是用$0來表示func-name,\(1~\)n來表示傳遞的參數,這點和shell的傳值是同樣的。
下面經過一個實例來展現怎樣自定義函數,這個makefile示例如何去寫一個簡單的調試追蹤函數,樣例在命令中調用了函數b,而b又調用了a。另一個file-num則是一個單行定義的函數,返回src下指定格式的文件數。

debug_trace = 1
echo-args = $(foreach a,1 2 3,'$($a)'))
debug-enter = $(if $(debug_trace),$(warning Entering $0($(echo-args))))
debug-leave = $(if $(debug_trace),$(warning Leaving $0))
file-num = $(words $(wildcard src/*.$1))
define a
    $(debug-enter)
    @echo $1 $2 $3
    @echo $(call file-num,c)
    $(debug-leave)
endef
define b
    $(debug-enter)
    $(call a,$1,$2,hello)
    $(debug-leave)
endef

.PHONY: test
test:
    $(call b,123,$(CC))

make輸出以下,經過輸出能夠看到這兩個函數的運行過程。
`Makefile:21: Entering b('123' 'cc' '')) Makefile:21: Entering a('123' 'cc' 'hello')) Makefile:21: Leaving a Makefile:21: Leaving b 123 cc hello 3

接下來介紹eval函數,eval函數的用途是將文本直接放入make解析器,首先make會掃描eval參數中是否有變量須要進行替換,若是有的話先替換變量,接下來make會再解析文本並進行求值操做。
eval函數的理解有些困難,下面經過一個實例來講明。

.PHONY: test
test:
    @echo $(obj)

src = tt_a.c tt_b.c tt_c.c
define func
 head = $(patsubst %.c,%.h,$1)
 obj = $(head:.h=.o)
 $(obj):$(head)
endef
$(call func, $(src))

這個makefile的功能比較容易看懂,就是傳進去.c文件,分別替換後綴成.h和.o,再創建依賴關係。可是悲劇的是,這個makefile是會報錯的,緣由是make不容許在頂層將一個宏擴展成多行(只有在命令的地方能夠)。解決這個問題須要用到eval函數,將函數調用那一行改爲

$(eval $(call func,$(src)))

此次能夠經過編譯了,可是仍然悲劇的是,打印出的obj是空的,經過make –print-data-base也看不到咱們定義的規則。這是由於在eval第一遍讀取宏的時候,會對變量進行替換,這些替換隻依賴於在調用這個宏以前就已經有的變量以及函數傳遞的變量,而在調用宏以前,head是空的,從而使\((obj)和\)(head)都被替換爲空。解決的方法是使用\[來表示變量,這樣eval在第一遍讀取的時候只會將\]變成$,接下來eval會進行第二遍讀取並執行整個函數,再處理以前的變量。最後,通過改動的makefile以下

.PHONY: test
test:
    @echo $(obj)

src = tt_a.c tt_b.c tt_c.c
define func
 head = $(patsubst %.c,%.h,$1)
 obj = $$(head:.h=.o)
 $$(obj):$$(head)
endef

$(eval $(call func,$(src)))

make和make --print-data-base | grep tt_ 結果以下,能夠看到obj變量內容正確,也正確生成了依賴

```

make

tt_a.o tt_b.o tt_c.o

make --print-data-base|grep tt_

tt_a.o tt_b.o tt_c.o
head = tt_a.h tt_b.h tt_c.h
src = tt_a.c tt_b.c tt_c.c
tt_a.o: tt_a.h tt_b.h tt_c.h
tt_c.h:
tt_b.o: tt_a.h tt_b.h tt_c.h
tt_a.h:
tt_c.o: tt_a.h tt_b.h tt_c.h
tt_b.h:
...

命令

命令是每條規則的三種組成元素之一,它的實質就是一個單行的shell命令,對於大多數命令,make會將它傳給subshell去執行,對於某些不會影響make程序行爲的shell命令,make會避免去fork/exec,直接在make中執行。

make默認使用的是/bin/sh,用戶也能夠經過修改SHELL變量來更改使用的shell。爲了編寫具備可移植性的makefile,可使用/usr/bin/bash,bash是GNU/Linux採用的標準shell,能夠在大多數系統上運行。

在makefile中全部以TAB開頭的文本行都被認爲是命令,可是像註釋以及條件處理命令即便以TAB開頭,也會被make識別出來並正確處理。而空行則會被Makefile直接忽略掉。

長命令

須要注意的是,make是以行爲單位將命令輸送到shell的,若是一個命令超過一行,須要進行處理,下面舉一個簡單的例子

.PHONY: test
test:
    cd src
    ls

該makefile試圖進入src目錄並顯示src下的文件列表(固然ls src是OK的,這裏只是爲了舉例),但運行後會發現ls顯示的還只是make所在目錄下的文件,這是由於兩行命令被傳給了兩個不一樣的subshell,從而變的不具備關聯性。若是想要兩條命令在一個subshell中執行,咱們可使用反斜槓符將命令連成一行並在命令間加上分隔符。下面兩種寫法均可以實現,但其實是有所區別的,在後面的錯誤處理中會說這個問題。

# way 1
.PHONY: test
test:
    cd src &&   \
    ls
# way 2
.PHONY: test
test:
    cd src;     \
    ls

命令修飾符
@
Makefile默認會輸出命令自己,而在命令前加上@能夠禁止make的這種行爲。加上@的好處是使make輸出較容易閱讀,壞處是使命令調試變的困難。建議使用一個含@的變量,並用在命令上,經過修改這個變量就能夠決定程序的行爲

QUIET = @
test:
    @(QUIET) shell script..

-
指示make忽視改行發生的錯誤,通常當make遇到一個命令返回錯誤時,會中止make腳本的執行。但若是命令前加上了破折號,make就會忽略錯誤並繼續執行。

+
要求make執行命令,即便是–just-print或-n命令來執行make,在編寫遞歸makefile時會用到這個功能。

錯誤處理

make每執行一條命令就會返回一個狀態碼,值爲零表明命令執行成功,若是某一行命令返回非零時,make就會中止執行。這種狀況下能夠用破折號前綴或者–keep going命令讓make繼續執行下去,可是並不建議這麼作,除非能保證這一行命令的錯誤無傷大雅。

在以前長命令的例子中,分別使用&&和;做爲鏈接符,當src存在時執行結果是沒有區別的,但若是這個目錄不存在呢,咱們將src改爲srcs(不存在的目錄)後看一下輸出

#使用&&作鏈接符
cd srcs && \
    ls
/bin/sh: 1: cd: can't cd to srcs
make: *** [test] Error 2
#使用;做鏈接符
cd srcs; \
    ls
/bin/sh: 1: cd: can't cd to srcs
include  Makefile  src

能夠看到,若是&&做鏈接符,其中某一條命令的錯誤會致使整個make的中止,而使用分號做鏈接符則不會由於中間某條命令的錯誤而使make中止。這裏建議使用&&做鏈接符,在執行每一條命令以前都保證前一條命令執行成功。

另外一種避免錯誤的方式就是使用程序內置的錯誤處理機制,好比使用rm -f來替代rm,這樣在刪除不存在的文件時就不會報錯,與此類似的還有mkdir -p等。

相關文章
相關標籤/搜索