
(題圖來自 appstorm.net)
<a target="_blank"></a>
make這個詞,英語的意思是"制作"。make指令直接用了這個意思,就是要做出某個檔案。比如,要做出檔案a.txt,就可以執行下面的指令。
$ make a.txt
但是,如果你真的輸入這條指令,它并不會起作用。因為make指令本身并不知道,如何做出a.txt,需要有人告訴它,如何調用其他指令完成這個目标。
比如,假設檔案 a.txt 依賴于 b.txt 和 c.txt ,是後面兩個檔案連接配接(cat指令)的産物。那麼,make 需要知道下面的規則。
a.txt: b.txt c.txt
cat b.txt c.txt > a.txt
也就是說,make a.txt 這條指令的背後,實際上分成兩步:第一步,确認 b.txt 和 c.txt 必須已經存在,第二步使用 cat 指令 将這個兩個檔案合并,輸出為新檔案。
像這樣的規則,都寫在一個叫做makefile的檔案中,make指令依賴這個檔案進行建構。makefile檔案也可以寫為makefile, 或者用指令行參數指定為其他檔案名。
$ make -f rules.txt
# 或者
$ make --file=rules.txt
上面代碼指定make指令依據rules.txt檔案中的規則,進行建構。
總之,make隻是一個根據指定的shell指令進行建構的工具。它的規則很簡單,你規定要建構哪個檔案、它依賴哪些源檔案,當那些檔案有變動時,如何重新建構它。
建構規則都寫在makefile檔案裡面,要學會如何make指令,就必須學會如何編寫makefile檔案。
makefile檔案由一系列規則(rules)構成。每條規則的形式如下。
<target> : <prerequisites>
[tab] <commands>
上面第一行冒号前面的部分,叫做"目标"(target),冒号後面的部分叫做"前置條件"(prerequisites);第二行必須由一個tab鍵起首,後面跟着"指令"(commands)。
"目标"是必需的,不可省略;"前置條件"和"指令"都是可選的,但是兩者之中必須至少存在一個。
每條規則就明确兩件事:建構目标的前置條件是什麼,以及如何建構。下面就詳細講解,每條規則的這三個組成部分。
一個目标(target)就構成一條規則。目标通常是檔案名,指明make指令所要建構的對象,比如上文的 a.txt 。目标可以是一個檔案名,也可以是多個檔案名,之間用空格分隔。
除了檔案名,目标還可以是某個操作的名字,這稱為"僞目标"(phony target)。
clean:
rm *.o
上面代碼的目标是clean,它不是檔案名,而是一個操作的名字,屬于"僞目标 ",作用是删除對象檔案。
$ make clean
但是,如果目前目錄中,正好有一個檔案叫做clean,那麼這個指令不會執行。因為make發現clean檔案已經存在,就認為沒有必要重新建構了,就不會執行指定的rm指令。
為了避免這種情況,可以明确聲明clean是"僞目标",寫法如下。
.phony: clean
rm *.o temp
如果make指令運作時沒有指定目标,預設會執行makefile檔案的第一個目标。
$ make
上面代碼執行makefile檔案的第一個目标。
前置條件通常是一組檔案名,之間用空格分隔。它指定了"目标"是否重新建構的判斷标準:隻要有一個前置檔案不存在,或者有過更新(前置檔案的last-modification時間戳比目标的時間戳新),"目标"就需要重新建構。
result.txt: source.txt
cp source.txt result.txt
上面代碼中,建構 result.txt 的前置條件是 source.txt 。如果目前目錄中,source.txt 已經存在,那麼make result.txt可以正常運作,否則必須再寫一條規則,來生成 source.txt 。
source.txt:
echo "this is the source" > source.txt
上面代碼中,source.txt後面沒有前置條件,就意味着它跟其他檔案都無關,隻要這個檔案還不存在,每次調用make source.txt,它都會生成。
$ make result.txt
上面指令連續執行兩次make result.txt。第一次執行會先建立 source.txt,然後再建立 result.txt。第二次執行,make發現 source.txt 沒有變動(時間戳晚于 result.txt),就不會執行任何操作,result.txt 也不會重新生成。
如果需要生成多個檔案,往往采用下面的寫法。
source: file1 file2 file3
上面代碼中,source 是一個僞目标,隻有三個前置檔案,沒有任何對應的指令。
$ make source
執行make source指令後,就會一次性生成 file1,file2,file3 三個檔案。這比下面的寫法要友善很多。
$ make file1
$ make file2
$ make file3
指令(commands)表示如何更新目标檔案,由一行或多行的shell指令組成。它是建構"目标"的具體指令,它的運作結果通常就是生成目标檔案。
每行指令之前必須有一個tab鍵。如果想用其他鍵,可以用内置變量.recipeprefix聲明。
.recipeprefix = >
all:
> echo hello, world
上面代碼用.recipeprefix指定,大于号(>)替代tab鍵。是以,每一行指令的起首變成了大于号,而不是tab鍵。
需要注意的是,每行指令在一個單獨的shell中執行。這些shell之間沒有繼承關系。
var-lost:
export foo=bar
echo "foo=[$$foo]"
上面代碼執行後(make var-lost),取不到foo的值。因為兩行指令在兩個不同的程序執行。一個解決辦法是将兩行指令寫在一行,中間用分号分隔。
var-kept:
export foo=bar; echo "foo=[$$foo]"
另一個解決辦法是在換行符前加反斜杠轉義。
export foo=bar; \
最後一個方法是加上.oneshell:指令。
.oneshell:
export foo=bar;
井号(#)在makefile中表示注釋。
# 這是注釋
cp source.txt result.txt # 這也是注釋
正常情況下,make會列印每條指令,然後再執行,這就叫做回聲(echoing)。
test:
# 這是測試
執行上面的規則,會得到下面的結果。
$ make test
在指令的前面加上@,就可以關閉回聲。
@# 這是測試
現在再執行make test,就不會有任何輸出。
由于在建構過程中,需要了解目前在執行哪條指令,是以通常隻在注釋和純顯示的echo指令前面加上@。
@echo todo
通配符(wildcard)用來指定一組符合條件的檔案名。makefile 的通配符與 bash 一緻,主要有星号(*)、問号(?)和 [...] 。比如, *.o 表示所有字尾名為o的檔案。
rm -f *.o
make指令允許對檔案名,進行類似正則運算的比對,主要用到的比對符是%。比如,假定目前目錄下有 f1.c 和 f2.c 兩個源碼檔案,需要将它們編譯為對應的對象檔案。
%.o: %.c
等同于下面的寫法。
f1.o: f1.c
f2.o: f2.c
使用比對符%,可以将大量同類型的檔案,隻用一條規則就完成建構。
makefile 允許使用等号自定義變量。
txt = hello world
@echo $(txt)
上面代碼中,變量 txt 等于 hello world。調用時,變量需要放在 $( ) 之中。
調用shell變量,需要在美元符号前,再加一個美元符号,這是因為make指令會對美元符号轉義。
@echo $$home
有時,變量的值可能指向另一個變量。
v1 = $(v2)
上面代碼中,變量 v1 的值是另一個變量 v2。這時會産生一個問題,v1 的值到底在定義時擴充(靜态擴充),還是在運作時擴充(動态擴充)?如果 v2 的值是動态的,這兩種擴充方式的結果可能會差異很大。
variable = value
# 在執行時擴充,允許遞歸擴充。
variable := value
# 在定義時擴充。
variable ?= value
# 隻有在該變量為空時才設定值。
variable += value
# 将值追加到變量的尾端。
output:
$(cc) -o output input.c
make指令還提供一些自動變量,它們的值與目前規則有關。主要有以下幾個。
(1)$@
$@指代目前目标,就是make指令目前建構的那個目标。比如,make foo的 $@ 就指代foo。
a.txt b.txt:
touch $@
a.txt:
touch a.txt
b.txt:
touch b.txt
(2)$<
$< 指代第一個前置條件。比如,規則為 t: p1 p2,那麼$< 就指代p1。
cp $< $@
cp b.txt a.txt
(3)$?
$? 指代比目标更新的所有前置條件,之間以空格分隔。比如,規則為 t: p1 p2,其中 p2 的時間戳比 t 新,$?就指代p2。
(4)$^
$^ 指代所有前置條件,之間以空格分隔。比如,規則為 t: p1 p2,那麼 $^ 就指代 p1 p2 。
(5)$*
$* 指代比對符 % 比對的部分, 比如% 比對 f1.txt 中的f1 ,$* 就表示 f1。
(6)$(@d) 和 $(@f)
$(@d) 和 $(@f) 分别指向 $@ 的目錄名和檔案名。比如,$@是 src/input.c,那麼$(@d) 的值為 src ,$(@f) 的值為 input.c。
(7)$(<d) 和 $(<f)
$(<d) 和 $(<f) 分别指向 $< 的目錄名和檔案名。
dest/%.txt: src/%.txt
@[ -d dest ] || mkdir dest
上面代碼将 src 目錄下的 txt 檔案,拷貝到 dest 目錄下。首先判斷 dest 目錄是否存在,如果不存在就建立,然後,$< 指代前置檔案(src/%.txt), $@ 指代目标檔案(dest/%.txt)。
makefile使用 bash 文法,完成判斷和循環。
ifeq ($(cc),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
上面代碼判斷目前編譯器是否 gcc ,然後指定不同的庫檔案。
list = one two three
for i in $(list); do \
echo $$i; \
done
# 等同于
for i in one two three; do \
echo $i; \
上面代碼的運作結果。
one
two
three
makefile 還可以使用函數,格式如下。
$(function arguments)
${function arguments}
(1)shell 函數
shell 函數用來執行 shell 指令
srcfiles := $(shell echo src/{00..99}.txt)
(2)wildcard 函數
wildcard 函數用來在 makefile 中,替換 bash 的通配符。
srcfiles := $(wildcard src/*.txt)
(3)替換函數
替換函數的寫法是:變量名 + 冒号 + 替換規則。
min: $(output:.js=.min.js)
上面代碼的意思是,将變量output中的 .js 全部替換成 .min.js 。
(1)執行多個目标
.phony: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
cleandiff :
rm *.diff
上面代碼可以調用不同目标,删除不同字尾名的檔案,也可以調用一個目标(cleanall),删除所有指定類型的檔案。
(2)編譯c語言項目
edit : main.o kbd.o command.o display.o
cc -o edit main.o kbd.o command.o display.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h
cc -c display.c
clean :
rm edit main.o kbd.o command.o display.o
.phony: edit clean
今天,make指令的介紹就到這裡。下一篇文章我會介紹,如何用 make 來建構 node.js 項目。
----------------------------------------------------------------------------------------------------------------------------
原文釋出時間:2015-02-24
本文來自雲栖合作夥伴“linux中國”