makefile教學

Makefiles用途

用來決定大型程式需要被重新編譯的部分。

第一個範例

首先安裝make,並且將下面程式放到名稱為Makefile的檔案裡面。注意Makefile必須要用TAB來縮排而不是用空白鍵。

1
2
hello:
echo "Hello, World"

接下來在Makefile所在的資料夾位置下make指令

1
2
3
$ make
echo "Hello, World"
Hello, World

Makefile語法

Makefile是由許多的規則組合而成,每一條則看起來如下

1
2
3
4
targets: prerequisites
command
command
command
  • targets是檔案名稱,用空白建作為分隔。通常每個rule只有一個target
  • command是產生targets的一系列的步驟。command以Tab作為開頭,而不是空白鍵。
  • prerequisites也是檔案名稱,以空白鍵作為分隔,這些是指令開始製作target前必須存在的檔案,因此這些檔案也可以稱為dependencies

Make基礎元素

1
2
3
hello:
echo "Hello, World"
echo "This line will always print, because the file hello does not exist."

以這個範例來說,

  • 有一個名為hello的target
  • 這個target有兩個command
  • 這個target沒有prerequisites
    接下來我們執行make hello,由於hello檔不存在,所以下面的指令會被執行。如果hello檔存在,make就不會做任何事。
    特別注意在這裡hello同時代表target以及檔案,因為通常來說下面的command執行的目的就是要生成target。
    下面舉一個編譯c語言的例子,首先我們製作一個blah.c檔。
    1
    2
    // blah.c
    int main() { return 0; }
    接下來製作另一個Makefile
    1
    2
    blah:
    cc blah.c -o blah
    接下來執行make,因為我們每有在第一個參數指定目標target,所以第一個target會被執行。第一次執行的時blah檔會被生成,如果再執行一次就會出現make: 'blah' is up to date的訊息,因為blah檔已經存在了。但是這有一個問題,如果我們更動blah.c檔案,make並不會重新編譯!!
    要解決這個問題就必須要增加prerequisite。
    1
    2
    blah: blah.c
    cc blah.c -o blah
    我們增加了blah的prerequisite,這時候make就除了檢查blah有沒有存在以外,還會另外去檢查blah.c是不是比blah還新。這裡我們可以看到make是利用系統的時間戳來判定blah.c有沒有被修改過,因此如果修改blah.c之後又把blah.c時間戳改回修改前的時間,那make就會以為blah.c沒有被修改過。

Make clean

clean常用來清除make產生的檔案,但是他並不是make的關鍵字。可以用make clean清除make產生的檔案。我們可以在makefile中編寫clean要清除的檔案。注意clean這個target除非你用make clean指令,不然他不會被執行。

1
2
3
4
5
some_file: 
touch some_file

clean:
rm -f some_file

Makefile使用相對路徑

1
rootdir = $(realpath .)

https://stackoverflow.com/a/3342259

Variables

變數Variables只能夠是字串,並且用:=賦值。對make來說單引號跟雙引號並意義,make會把他當成字元來處理。因此賦值的時候不需要加引號。
下面範例使用Variables

1
2
3
4
5
6
7
8
9
10
11
12
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file

file1:
touch file1
file2:
touch file2

clean:
rm -f file1 file2 some_file

要引用變數可以用${}或是$()

1
2
3
4
5
6
7
8
x := dude

all:
echo $(x)
echo ${x}

# Bad practice, but works
echo $x

Targets

all target

可以用來依次產生所有需要的target,通常會放在第一個target的位置,如此一來只要下make指令就可以生成所有target。

1
2
3
4
5
6
7
8
9
10
11
all: one two three

one:
touch one
two:
touch two
three:
touch three

clean:
rm -f one two three

多個target

當一個rule有多個target的時候,底下的command就會針對每一個target都跑一次
$@就是一個有target名稱的automatic variable,下面範例就可以用$@看看現在command正在執行的是哪一個target

1
2
3
4
5
6
7
8
9
all: f1.o f2.o

f1.o f2.o:
echo $@
# Equivalent to:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o

Automatic Variables and Wildcards

* 萬用字元

在cmake中*%在cmake中都是萬用字元,但是它們代表的意義不一樣。*最好要包裝在wildcard 萬用字符函式中。否則可能會常陷入下面常見的陷阱。

  1. 陷阱: *不能直接在變量定義中使用
  2. 陷阱: 當*未匹配任何文件時,它會保持不變(除非在萬用字符函數(wildcard)中運行)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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)

% 萬用字元

%非常有用,但由於它可以在各種情況下使用,因此有些令人困惑。

  1. 在“匹配”模式下使用時,它會在字符串中匹配一個或多個字符。此匹配稱為stem。
  2. 在“替換”模式下使用時,它會取出匹配的stem,並將其替換為一個字符串。
  3. 在規則定義和某些特定函數中最常用。

Automatic Variables

這裡有完整的Automatic Variables表,下面只介紹常用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
hey: one two
# Outputs "hey", since this is the target name
# 印出目前的target
echo $@

# Outputs all prerequisites newer than the target
# 印出所有比target新的prerequisite
echo $?

# Outputs all prerequisites
# 印出所有的prerequisite
echo $^

touch hey

one:
touch one

two:
touch two

clean:
rm -f hey one two

Rules

Make的隱藏規則

CC CXX CFLAGS CXXFLAGS LDFLAGS LDLIBS這些變數是make的隱藏規則。

  • CC : 編譯C語言的編譯器; 預設是 cc
  • CXX : 編譯C++語言的編譯器; 預設是 g++
  • CFLAGS : 給C語言編譯器的額外參數
  • CXXFLAGS : 給C++語言編譯器的額外參數
  • CPPFLAGS : 給C/C++編譯器的額外參數
  • LDFLAGS : 給連結器的額外參數

下面範例使用隱藏規則

1
2
3
4
5
6
7
8
9
10
11
12
CC = gcc # 使用gcc來編譯C語言
CFLAGS = -g # 給gcc的額外參數,開啟debug模式

# 隱藏規則 #1: blah會由C連接器產生(即使我們的command沒有呼叫C連接器)
# 隱藏規則 #2: blah.o 會由c編譯器產生,因為blah.c存在(即使我們的command沒有呼叫c編譯器)
blah: blah.o

blah.c:
echo "int main() { return 0; }" > blah.c

clean:
rm -f blah*

Static Pattern Rules

他的語法如下

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

這個語法的意思是,如果有一個target符合target-pattern(利用% wildcard),且它的所有prerequisite都符合prereq-patterns,那麼就會執行commands
例如我們可以改寫下面makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

改寫後如下,可以看到,我們把foo.o bar.o all.o的規則都合併成一個規則$(objects): %.o: %.c。首先foo.o符合%.o,且它的所有prerequisite都符合%.c,因此會執行%.o: %.c的規則。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

Static Pattern Rules and Filter

此外,我們可以使用filter函式來過濾掉不需要的檔案,後面會再講到函式這裡只是先展示如何跟函式搭配使用,在下面的範例我們使用了 .raw 和 .result 這兩個擴展名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

all: $(obj_files)
# Note: PHONY is important here. Without it, implicit rules will try to build the executable "all", since the prereqs are ".o" files.
.PHONY: all

# Ex 1: .o files depend on .c files. Though we don't actually make the .o file.
$(filter %.o,$(obj_files)): %.o: %.c
@echo "target: $@ prereq: $<"

# Ex 2: .result files depend on .raw files. Though we don't actually make the .result file.
$(filter %.result,$(obj_files)): %.result: %.raw
@echo "target: $@ prereq: $<"

%.c %.raw:
@echo "touch $@ prereq: $<"
touch $@

clean:
rm -f $(src_files)

執行的結果如下,首先執行第一條規則all: $(obj_files)產生第一個target foo.result,並由$(filter %.result,$(obj_files)): %.result: %.raw產生foo.resultfoo.result的prerequisitfoo.raw%.c %.raw:產生。
可以看到,我們把foo.result bar.o lose.o的規則都合併成一個規則$(filter %.result,$(obj_files)): %.result: %.raw。首先foo.result符合%.result,且它的所有prerequisite都符合%.raw,因此會執行%.result: %.raw的規則。

1
2
3
4
5
6
touch foo.raw
target: foo.result prereq: foo.raw
touch bar.c
target: bar.o prereq: bar.c
touch lose.c
target: lose.o prereq: lose.c

Pattern Rules

我們可以將Pattern Rules視為兩種用法。

  • 自定義的implicit rules

    1
    2
    3
    # 自訂一個 pattern rule 將每一個.c檔編譯成.o檔
    %.o : %.c
    $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
  • 簡化版的static pattern rules

    1
    2
    3
    4
    # 定義一個沒有 prerequisites 的 pattern rule
    # 他將會在需要的時候產出一個空白的.c檔
    %.c:
    touch $@

    在這裡%代表任意非空白的字串。

Double-Colon Rules

Double-Colon Rules 很少被用到,他允許對同一個target定義多個規則,且這些規則可以是不同的commands。如以下範例

1
2
3
4
5
6
7
all: blah

blah::
@echo "hello"

blah::
@echo "hello again"

他的輸出是

1
2
hello
hello again

Commands and execution

指令回顯/禁用

在預設情況下,make會回顯每一個command,如果你不想要回顯,可以在command前面加上@,如下

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

命令執行

每一行命令都會在一個新的shell中執行,因此如果你想要在同一個shell中執行,可以使用分號;來連接命令,如下

1
2
3
4
5
6
7
8
9
10
11
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`

預設shell

預設情況下,make會使用/bin/sh來執行命令,如果你想要使用其他的shell,可以使用.SHELL來指定,如下

1
2
3
4
SHELL=/bin/bash

cool:
echo "Hello from bash"

$$符號

在Makefile中,$$代表一個$符號,如此一來,我們就可以在Makefile中使用bash或是sh的shell variable。在下面這個例子中特別注一一下 Makefile variables 和 Shell variables

1
2
3
4
5
6
7
make_var = I am a make variable
all:
# Same as running "sh_var='I am a shell variable'; echo $sh_var" in the shell
sh_var='I am a shell variable'; echo $$sh_var

# Same as running "echo I am a make variable" in the shell
echo $(make_var)

-k-i-進行錯誤處理

執行make的時候使用-k參數可以讓make繼續執行,即使其中一個target失敗了。執行make的時候使用-i參數可以讓make忽略所有的錯誤。

在command前面加上-可以讓make忽略該command的錯誤,如下

1
2
3
4
one:
# This error will be printed but ignored, and make will continue to run
-false
touch one

打斷或是結束make

ctrl + c可以打斷或是結束make,他將會刪掉剛生成的target

遞迴使用 make

為了遞迴調用 Makefile,使用特殊的 $(MAKE) 代替 make,因為它將為您傳遞 make 標誌,並且不會受到這些標誌的影響。

當使用 $(MAKE) 來遞迴調用 Makefile 時,它將傳遞先前用於調用 make 的所有標誌和選項,以及在 Makefile 中定義的任何其他變量。這有助於確保在整個項目中使用相同的編譯選項和變量。同時,$(MAKE) 不會受到當前 make 的影響,這可以避免不必要的錯誤和不一致性。

1
2
3
4
5
6
7
8
new_contents = "hello:\n\ttouch inside_file"
all:
mkdir -p subdir
printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
cd subdir && $(MAKE)

clean:
rm -rf subdir

Export, environments, and recursive make

當make執行的時候,他會先把所有環境變數轉換成make的變數,例如下面範例假如我們先在shell設定環境變數shell_env_var

  1. 設定環境並且執行make
    1
    export shell_env_var='I am an environment variable'; make
  2. 執行下面的makefile
    1
    2
    3
    4
    5
    6
    all:
    # Print out the Shell variable
    echo $$shell_env_var

    # Print out the Make variable
    echo $(shell_env_var)
  • make的export指令可以把make的變數直接轉換成環境變數
    1
    2
    3
    4
    5
    shell_env_var=Shell env var, created inside of Make
    export shell_env_var
    all:
    echo $(shell_env_var)
    echo $$shell_env_var

如此一來當我們在make command呼叫make的時候,就可以利用export指令將變數傳遞給子make程式。下面範例中cooly變數會被傳遞到子資料夾內所執行的make的makefile裡面。這裡可以注意到,cooly變數在all target之前被定義,但是他還是可以被all target使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new_contents = "hello:\n\techo \$$(cooly)"

all:
mkdir -p subdir
printf $(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_ALL_VARIABLES
    .EXPORT_ALL_VARIABLES可以直接把所有的make變數都轉換成環境變數
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    .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
    printf $(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

make的命令列選項

這裡有make的命令列選項,可以注意--dry-run, --touch, --old-file這幾個。
另外make可以一次接受多個target,例如make clean run test就會先執行clean,接著run和test。

變數part2

兩種賦值方法

  • 延遲賦值(lazy evaluation)=:

    1
    2
    3
    4
    5
    6
    7
    VAR = foo
    VAR2 = $(VAR)
    VAR = bar

    all:
    # 在此處 VAR2 的值將是 "bar",因為VAR2直到被真正使用展開
    echo $(VAR2)

    ?=這個符號可以為沒被設定過的店數設定值

    1
    2
    3
    4
    5
    6
    7
    one = hello
    one ?= will not be set #one被設定過了,所以沒作用
    two ?= will be set #two還沒被設定做,將設定值

    all:
    echo $(one)
    echo $(two)

    輸出如下

    1
    2
    3
    4
    echo hello
    hello
    echo will be set
    will be set
  • 立即賦值(immediate assignment):=:

    1
    2
    3
    4
    5
    6
    7
    VAR := foo
    VAR2 := $(VAR)
    VAR := bar

    all:
    # 在此處 VAR2 的值將是 "foo",因為VAR2在:=的時候就展開了
    echo $(VAR2)

    因此:=可以append variable,如果是=就會齣戲無窮迴圈錯誤

    1
    2
    3
    4
    5
    6
    7
    one = hello
    # 這段程式可以運行
    # one gets defined as a simply expanded variable (:=) and thus can handle appending
    one := ${one} there

    all:
    echo $(one)

    下面這段程式會出現無窮迴圈錯誤。

    1
    2
    3
    4
    5
    6
    one = hello
    # 注意這裡用的是 = ,會造成無窮迴圈錯誤
    one = ${one} there

    all:
    echo $(one)

空白

一行字起頭的空白會被make忽略掉,但是尾巴空白的不會,要在起頭加空白可以用$(nullstring),更精確地說其實未定義的變數都是empty string

1
2
3
4
5
6
7
8
9
with_spaces =   hello   # with_spaces has many spaces after "hello"
after = $(with_spaces)there

start_space = $(nullstring) hello

all:
echo "$(after)"
echo "$(start_space)"
echo $(nowhere) #這也會輸出empty string

append

+=可以用來append variable

1
2
3
4
5
foo := start
foo += more

all:
echo $(foo)

覆寫make命列列參數

override可以用來覆寫make命令列參數,例如下面這個例子,分別用make option_one=himake option_two=hi去執行,可以發現只有option_one會被覆寫

1
2
3
4
5
6
7
# Overrides command line arguments
override option_one = did_override
# Does not override command line arguments
option_two = not_override
all:
echo $(option_one)
echo $(option_two)

針對target設定變數

變數可以只設定給指定的target,例如下面例子,one只定義給all target

1
2
3
4
5
6
7
all: one = cool

all:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

Pattern-specific variables

變數也可以指定義給特定的target patterns,例如下面例子,只有符合%.cpattern的target會被定義one

1
2
3
4
5
6
7
%.c: one = cool

blah.c:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

Makefile判斷式

if/else

1
2
3
4
5
6
7
8
foo = ok

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

檢查變數是否為空

1
2
3
4
5
6
7
8
9
10
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

檢查變數是否被定義

1
2
3
4
5
6
7
8
9
10
bar =
foo = $(bar)

all:
ifdef foo
echo "foo is defined"
endif
ifndef bar
echo "but bar is not"
endif

$(MAKEFLAGS)

下面範例展示如何使用 findstringMAKEFLAGS 測試 make flag。分別用make指令和make -i指令執行下面makefile

1
2
3
4
5
all:
# Search for the "-i" flag. MAKEFLAGS is just a list of single characters, one per flag. So look for "i" in this case.
ifneq (,$(findstring i, $(MAKEFLAGS)))
echo "i was passed to MAKEFLAGS"
endif

Functions

First Functions

函式主要用來處理文字。呼叫函式的方法有$(fn, arguments)${fn, arguments},而make也內建許多函式。例如subst替換掉文字。

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

而如果你相替換掉空白或是逗號,可以利用變數。

1
2
3
4
5
6
7
8
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))

all:
@echo $(bar)

特別注意到不要在逗號和下一個參數之間留空白,因為它會被視為文字。

1
2
3
4
5
6
7
8
9
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))

all:
# Output is ", a , b , c". Notice the spaces introduced
@echo $(bar)

字串替換

函式$(patsubst pattern,replacement,text)的功能如下。

1
2
3
4
5
6
7
8
9
10
11
foo := a.o b.o l.a c.o
one := $(patsubst %.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)

The foreach function

foreach函式的用法為$(foreach var,list,text),foreach會把以空白間區隔文字的list一個一個賦值給var,而text會累加前面的結果,範例如下

1
2
3
4
5
6
7
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)

if function

用法如下

1
2
3
4
5
6
7
foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)

all:
@echo $(foo)
@echo $(bar)

The call function

make可以用call來呼叫自定義函式

1
2
3
4
5
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)

The shell function

make也可以呼叫shell函式,但是會把輸出的換行符號改成空白鍵

其他功能

Include Makefiles

使用Include可以讓makefile裡面呼叫其他makefile

vpath 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vpath %.h ../headers ../other-directory

# Note: vpath allows blah.h to be found even though blah.h is never in the current directory
some_binary: ../headers blah.h
touch some_binary

../headers:
mkdir ../headers

# We call the target blah.h instead of ../headers/blah.h, because that's the prereq that some_binary is looking for
# Typically, blah.h would already exist and you wouldn't need this.
blah.h:
touch ../headers/blah.h

clean:
rm -rf ../headers
rm -f some_binary

換行

指令太長可以利用\換行

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

.phony

在目標中添加”.PHONY”將防止Make將虛擬目標與文件名混淆。在這個例子中,如果創建了名為”clean”的文件,”make clean”仍然會運行。從技術上講,我應該在每個帶有”all”或”clean”的例子中都使用它,但為了保持例子的清晰,我沒有這樣做。此外,”phony”目標通常具有很少用作文件名的名稱,在實踐中許多人都會跳過這一步。

1
2
3
4
5
6
7
8
some_file:
touch some_file
touch clean

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

.delete_on_error

如果命令返回非零的退出狀態,make工具將停止運行規則(並將向前傳播到前置要求)。
DELETE_ON_ERROR將在規則以這種方式失敗時刪除該規則的目標。這將對所有目標發生,不僅僅是像PHONY這樣的目標。儘管由於歷史原因,make工具沒有使用這個選項,但始終使用它是一個好主意。

1
2
3
4
5
6
7
8
9
10
.DELETE_ON_ERROR:
all: one two

one:
touch one
false

two:
touch two
false
Author

Steven

Posted on

2023-02-05

Updated on

2025-01-14

Licensed under

Comments