一文學完makefile語法

zhangyazhou發表於2021-05-24

一、開始

1.Hello World

新建一個makefile檔案,寫入如下內容,

hello:
	echo "Hello World"
clean:
	echo "clean all"

執行make命令,輸出結果為

echo "Hello World"
Hello World

2.makfile 語法

targets:prerequisites
    command
    ...
  • target 可以是一個 object file(目標檔案),也可以是一個執行檔案,還可以是一個標籤(label)。
  • prerequisites 生成該 target 所依賴的檔案和/或 target。
  • command 該 target 要執行的命令(任意的 shell 命令)。
    這是一個檔案的依賴關係,也就是說,target 這一個或多個的目標檔案依賴於 prerequisites 中的檔案,其生成規則定義在 command 中。說白一點就是說:prerequisites 中如果有一個以上的檔案比 target 檔案要新的話,command 所定義的命令就會被執行。

3.簡單的例子

hello:hello.o
	gcc hello.o -o hello # Runs third
hello.o:hello.c 
	gcc -c hello.c -o hello.o # Runs second
hello.c:
	echo "int main() { return 0; }" > hello.c # Runs first

執行makemake hello,結果如下

echo "int main() { return 0; }" > hello.c # Runs first
gcc -c hello.c -o hello.o # Runs second
gcc hello.o -o hello # Runs third

可以看到執行的順序與makefile內容是相反的,當執行make hello時,由於hello是由hello.o生成的,需要找到hello.o,但是hello.o是由hello.c生成的,那麼需要先生成hello.c,以此類推,通過這個例子可以清晰地看到makefile是通過依賴關係自動推導的。

4.變數

files = file1 file2
some_file:$(files)
	echo "look at this variable:" $(files)
file1:
	touch file1
file2:
	touch file2
clean:
	rm -f file1 file2

makefile檔案的變數不僅可以用$(),也可以用${}

二、目標檔案

all: one two three
one:
	touch one
two:
	touch two
three:
	touch three
clean:
	rm -f one two three

這樣寫的話比較繁瑣,可以使用$@,當一條規則有多個目標時,將為每個目標執行這些命令,$@是一個自動變數,包含目標名稱。makefile可以重新寫為

all: one two three
one two three:
	echo $@
clean:
	rm -r one two three

三、自動推導變數和萬用字元

1. 萬用字元 *

thing_wrong := *.o # Don't do this! '*' will not get expanded
thing_right := $(wildcard *.o)

all: one two three four

# Fails, because $(thing_wrong) is the string "*.o"
one: $(thing_wrong)

# Stays as *.o if there are no files that match this pattern :(
two: *.o 

# Works as you would expect! In this case, it does nothing.
three: $(thing_right)

# Same as rule three
four: $(wildcard *.o)
  • *能夠被用在targetprerequisites以及wildcard函式裡。
  • *直接用在變數中比較危險,不會被認為是萬用字元,而是被看做字元*
  • *沒有被匹配到的時候,它就保持原樣,比如像上面two中的*.o能夠匹配到字尾為.o的檔案,但是如果沒有匹配到的話,會被看做是普通的字串*.o,這樣是比較危險的。

2. 萬用字元 %

  • 當在 "匹配 "模式下使用時,它匹配字串中的一個或多個字元。這種匹配被稱為stem。
  • 當在 "替換 "模式下使用時,它獲取被匹配的stem並將其替換到一個字串中。%最常被用於規則定義和一些特定的功能中。在下文中會被重點說明。

3. 自動推導變數

  • $@:代表目標檔案(target)
  • $^:代表所有的依賴檔案(prerequisites)
  • $<:代表第一個依賴檔案(prerequisites中最左邊的那個)。
  • $?:代表示比目標還要新的依賴檔案列表。以空格分隔。
  • $%:僅當目標是函式庫檔案中,表示規則中的目標成員名。
hey: one two
    # Outputs "hey", since this is the first target
    echo $@
    # Outputs all prerequisites newer than the target
    echo $?
    # Outputs all prerequisites
    echo $^
    touch hey
one:
    touch one
two:
    touch two
clean:
    rm -f hey one two

第一個echo $@輸出的是hey,即目標檔案。echo $?echo $^的輸出看起來沒啥區別,但是調換一下次序,如

hey: one two
	echo $^
	echo "---------------"
	echo $?
	echo "---------------"
	touch hey
one:
	touch one
two:
	touch two
clean:
	rm -f hey one two

輸出

touch one
touch two
echo one two
one two
echo "---------------"
---------------
echo one two
one two
echo "---------------"
---------------
touch hey

可以明顯地看到echo $?沒有呼叫touch onetouch two,原因是echo $^已經呼叫並生成最新檔案,不需要再次呼叫。

四、高效技能

1. 靜態模式規則

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

其本質是,給定的目標被目標模式(通過%萬用字元)匹配。被匹配的內容被稱為stem。然後,該stem被替換到前置條件模式中,以生成目標的前置條件。

objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
foo.o: foo.c
bar.o: bar.c
all.o: all.c
all.c:
    echo "int main() { return 0; }" > all.c
%.c:
    touch $@
clean:
    rm -f *.c *.o all

更有效地方式可以這樣寫

objects = foo.o bar.o all.o
all: $(objects)
# These files compile via implicit rules
# Syntax - targets ...: target-pattern: prereq-patterns ...
# In the case of the first target, foo.o, the target-pattern matches foo.o and sets the "stem" to be "foo".
# It then replaces the '%' in prereq-patterns with that stem
$(objects): %.o: %.c
all.c:
    echo "int main() { return 0; }" > all.c
%.c:
    touch $@
clean:
    rm -f *.c *.o all

2. 靜態模式規則和過濾器

obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
# 只會讓.o的檔案執行
$(filter %.o,$(obj_files)): %.o: %.c
    echo "target: $@ prereq: $<"
# 只會讓.result的檔案執行
$(filter %.result,$(obj_files)): %.result: %.raw
    echo "target: $@ prereq: $<" 
%.c %.raw:
    touch $@
clean:
    rm -f $(src_files)

3. 隱式規則

CC = gcc # Flag for implicit rules
CFLAGS = -g # Flag for implicit rules. Turn on debug info
# Implicit rule #1: blah is built via the C linker implicit rule
# Implicit rule #2: blah.o is built via the C compilation implicit rule, because blah.c exists
# Define a pattern rule that compiles every .c file into a .o file
blah:blah.o 
%.o : %.c
	$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
blah.c:
	echo "int main() { return 0; }" > blah.c
clean:
	rm -f blah*

4. 雙引號規則

雙冒號規則很少使用,但允許為同一目標定義多個規則。如果這些是單冒號,就會列印出一個警告,而且只有第二組命令會執行。

all: blah
blah::
	echo "hello"
blah::
	echo "hello again"

五、命令與執行

在命令前新增@以阻止其被列印出來,也可以在執行make -s,效果好比在每一行前新增一個@

all: 
	@echo "This make line will not be printed"
	echo "But this will"

執行make輸出

This make line will not be printed
echo "But this will"
But this will

執行make -s時,輸出

This make line will not be printed
But this will

每條命令都在一個新的shell中執行(或者至少效果是這樣)。

all: 
    cd ..
    # The cd above does not affect this line, because each command is effectively run in a new shell
    echo `pwd`

    # This cd command affects the next because they are on the same line
    cd ..;echo `pwd`

    # Same as above
    cd ..; \
    echo `pwd`

在執行make時,增加引數-k,以便在出現錯誤時繼續執行。如果你想一次性看到make的所有錯誤,這很有幫助。
在命令前加一個-來抑制錯誤的發生,如果在make中新增-i引數,可以使每條命令都出現這種情況。

one:
    # This error will be printed but ignored, and make will continue to run
    false
    touch one

執行make,會出現出現終止程式錯誤

false
make: *** [one] Error 1

如果在false前面加上-,輸出

false
make: [one] Error 1 (ignored)
touch one

注意:如果你在make過程中ctrl+c,會刪除剛剛製作的較新目標檔案。

要遞迴呼叫一個makefile,請使用特殊的$(MAKE)而不是make

all:
	touch two
	cd subdir && $(MAKE)
clean:
	rm -rf subdir two

在當前資料夾內新建一個名為subdir的子資料夾,makefile的內容為

hello:
	touch one

export指令獲取一個變數,並使其能夠被呼叫的make命令訪問。在這個例子中,corey被export,這樣subdir中的makefile就可以使用它。

new_contents = "hello:\n\\techo \$$(cooly)"
all:
	mkdir -p subdir
	echo $(new_contents) | sed -e 's/^ //' > subdir/makefile
	@echo "---MAKEFILE CONTENTS---"
	@cd subdir && cat makefile
	@echo "---END MAKEFILE CONTENTS---"
	cd subdir && $(MAKE)
# Note that variables and exports. They are set/affected globally.
cooly = "The subdirectory can see me!"
export cooly
# This would nullify the line above: unexport cooly
clean:
	rm -rf subdir

export定義的變數也可以在本makefile中使用

one=this will only work locally
export two=we can run subcommands with this
all: 
	@echo $(one)
	@echo $$one  # 沒有輸出
	@echo $(two)
	@echo $$two # 等效於$(two)

輸出

this will only work locally

we can run subcommands with this
we can run subcommands with this

makefile中的變數前輸入.EXPORT_ALL_VARIABLES:,可以等效與在變數前輸入export

.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"
cooly = "The subdirectory can see me!"
# This would nullify the line above: unexport cooly
all:
	mkdir -p subdir
	echo $(new_contents) | sed -e 's/^ //' > subdir/makefile
	@echo "---MAKEFILE CONTENTS---"
	@cd subdir && cat makefile
	@echo "---END MAKEFILE CONTENTS---"
	cd subdir && $(MAKE)
clean:
	rm -rf subdir

六、再談變數

1. 變數符號類別

當使用=時,該變數使用的時候會全域性搜尋,不需要提前定義。
當使用:=是,需要提前定義,否者找不到,用空字串代替。

# Recursive variable. This will print "later" below
one = one ${later_variable}
# Simply expanded variable. This will not print "later" below
two := two ${later_variable}
later_variable = later
three := three ${later_variable}
all: 
	echo $(one)
	echo $(two)
	echo $(three)

輸出

echo one later
one later
echo two 
two
echo three later
three later

?=表示如果先前沒有對該變數賦值,那麼會賦值成功,否者沒有效果。

one = hello
one ?= will not be set
two ?= will be set
all:
	echo $(one)
	echo $(two)

輸出

echo hello
hello
echo will be set
will be set

行末的空格不會被忽略,但行首的空格會被忽略。要使一個變數帶有一個空格,可以使用空字串變數

with_spaces = hello   # with_spaces has many spaces after "hello"
after = $(with_spaces)there
nullstring =
space = $(nullstring) # Make a variable with a single space.
all: 
	echo "$(after)"
	echo "$(space)"start"$(space)"end

輸出

echo "hello   there"
hello   there
echo " "start" "end
 start end

未定義的變數被認為是空字串

+=表示追加(append)

foo := start
foo += more
all:
	echo $(foo)

輸出

echo start more
start more

make有兩個比較特殊的變數:SHELL”和“ MAKEFLAGS”,這兩個變數除非使用“ unexport”宣告,否則的話在整個 make的執行過程中,它們的值始終自動的傳遞給子make。

2. 命令列過載

用於指示makefile中定義的變數不能被覆蓋,變數的定義和賦值都需要使用override關鍵字。

override one = did_override
one = hello
two = world
all:
	echo $(one)
	echo $(two)

輸出為

echo did_override
did_override
echo world
world

3. 巨集定義

define...endef包起來,

define one
echo hello world
endef
all:
	@$(one)

輸出

hello world

但是在define...endef裡的每一行都是單獨執行的,

define one
export str=hello
echo $$str
endef
all:
	@$(one)

這樣的話會輸出空字串,因為他們沒有聯絡。makefile允許多目標,當執行多目標的時候,會按照順序全部執行。

all: one = cool
all:
	echo $(one)
other:
	echo $(one)

make all有輸出coolmake other沒有輸出。
你可以為特定的目標模式分配變數

%.c: one = cool
blah.c: 
    echo one is defined: $(one)
other:
    echo one is nothing: $(one)

只有執行make blah.c時,才會匹配到第一行。

七、條件

if/else語句

foo = ok
all:
ifeq ($(foo),ok)
	echo "foo equal ok"
else
	echo "nope"
endif

判斷一個變數是否為空

nullstring =
foo = $(nullstring) # end of line; there is a space here
all:
ifeq ($(strip $(foo)),)
    echo "foo is empty after being stripped"
endif
ifeq ($(nullstring),)
    echo "nullstring doesn't even have spaces"
endif

判斷變數是否被定義(define),ifdef只檢視是否有定義的東西。

bar =
foo = $(bar)
all:
ifdef foo
	echo "foo is defined"
endif
ifdef bar
	echo "but bar is not"
endif

輸出為

echo "foo is defined"
foo is defined

八、函式

1. 替換函式

函式的形式有${fn,argument}$(fn,argument)兩種,先看一個字串替換函式

bar := ${subst not,totally,"I am not superman"}
all:
	@echo $(bar)

輸出

I am totally superman
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))
all: 
    @echo $(bar)

輸出

a,b,c

2. 字串置換

foo := a.o b.o l.a c.o
one := $(subst %.o,%.c,$(foo))
# This is a shorthand for the above
two := $(foo:%.o=%.c)
# This is the suffix-only shorthand, and is also equivalent to the above.
three := $(foo:.o=.c)
all:
	echo $(one)
	echo $(two)
	echo $(three)

輸出

echo a.c b.c l.a c.c
a.c b.c l.a c.c
echo a.c b.c l.a c.c
a.c b.c l.a c.c
echo a.c b.c l.a c.c
a.c b.c l.a c.c

3. foreach 函式

foo := who are you
# For each "word" in foo, output that same word with an exclamation after
bar := $(foreach wrd,$(foo),$(wrd)!)
all:
    # Output is "who! are! you!"
    @echo $(bar)

4. if 函式

foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)
all:
	@echo $(foo) # Output then!
	@echo $(bar) # Output else!

5. call 函式

sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)
all:
    # Outputs "Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:"
    @echo $(call sweet_new_fn, go, tigers)

6. shell 函式

all: 
    @echo $(shell ls -la) # Very ugly because the newlines are gone!

九、 其他特性

1. include

include指令告訴make讀取一個或多個其他makefile檔案。它是makefile檔案中的一行,看起來像這樣。

include filenames...

2. vpath 指令

使用vpath來指定某些先決條件的存在位置。格式是vpath <pattern> <directories, space/colon separated><pattern>可以有一個%,它可以匹配任何零或更多的字元。你也可以用變數VPATH在全域性範圍內這樣做。

vpath %.h ./headers ./other-directory
some_binary: ./headers blah.h
	touch some_binary
./headers:
	mkdir ./headers
blah.h:
	touch ./headers/blah.h
clean:
	rm -rf ./headers
	rm -f some_binary

3. 多行

可以用\進行分割多行

some_file: 
    echo This line is too long, so \
        it is broken up into multiple lines

4. .phony

在目標上新增.PHONY可以防止make將虛假的目標與檔名混淆。在這個例子中,如果檔案clean被建立,make clean仍然會被執行。

some_file:
    touch some_file
    touch clean

.PHONY: clean
clean:
    rm -f some_file
    rm -f clean

5. .delete_on_error

如果一個命令返回非零的退出狀態,make工具將停止執行一個規則(並將傳播回先決條件)。
如果規則以這種方式失敗,DELETE_ON_ERROR將刪除該規則的目標。這將發生在所有的目標上,而不是像PHONY那樣只發生在它之前的那個目標。始終使用這個是個好主意,即使make由於歷史原因沒有這樣做。

.DELETE_ON_ERROR:
all: one two
one:
    touch one
    false
two:
    touch two
    false

輸出

touch one
false
make: *** [makefile:21:one] 錯誤 1
make: *** 正在刪除檔案“one”

相關文章