Makefile简易教程

YiQi 管理员

下图是一个典型的依赖关系,Makefile 可以用来构建这样的依赖关系

最简单的一个实例,使用 Makefile 在终端输出 Hello World!

1
2
hello:
echo "Hello World!"

Makefile 语法

1
2
3
4
targets: prerequisites
command
command
command
  • targets 是目标文件,可以是 object file,也可以是可执行文件,还可以是一个标签
  • prerequisites 是生成 target 所需要的文件或者目标,可以有多个,中间用空格分开
  • commandmake 执行的命令,可以有多行,每行必须以 tab 开头

比如,我们可以创建下面这样一个C文件:

1
2
3
4
// blah.c
int main() {
return 0;
}

然后使用 Makefile 来编译这个文件:

1
2
blah: blah.c
cc blah.c -o blah

第一次运行时,将会创建 blah 文件。第二次运行时,您将看到 make: 'blah' is up to date 的提示。这是因为 blah 文件已经存在。但是存在一个问题:如果我们修改了 blah.c 文件然后运行 make,没有任何内容被重新编译。通过添加一个 prequisites,我们可以解决这个问题:

1
2
blah: blah.c blah.h
cc blah.c -o blah

Make 决定是否运行 blah 目标。只有在 blah 不存在,或者 blah.c 的时间戳较新时,它才会运行。在文件被修改保存后,时间戳一般会更新,即修改日期会变成最新的。

更复杂的例子:

1
2
3
4
5
blah: blah.o
cc blah.o -o blah # Runs third

blah.o: blah.c
cc -c blah.c -o blah.o # Runs second
  • blah 依赖于 blah.o,因此 make 搜索 blah.o 目标。
  • blah.o 依赖于 blah.c,因此 make 搜索 blah.c 目标。
  • 然后运行 cc -c 命令,因为所有 blah.o 的依赖项都已完成。
  • 最后运行顶层的 cc 命令,因为所有的 blah 依赖项都已完成。
  • 这就是结果:blah 是一个已编译的C程序。

Make clean

clean 通常被用作一个目标,用于删除其他目标的输出:

1
2
3
4
some_file:
touch some_file
clean:
rm -f some_file

变量

变量只能是字符串。通常你会想使用 :=,但 = 也可以使用。

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

Targets

多个 targets

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

自动变量和通配符

自动变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
hey: one two
# Outputs "hey", since this is the target name
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

* 通配符

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

# Same as rule three
four: $(wildcard *.o)

% 通配符

  • 当以“匹配”模式使用时,它匹配字符串中的一个或多个字符。这个匹配称为“词干”。
  • 当以“替换”模式使用时,它取得匹配到的词干并替换字符串中的内容。
  • % 最常用于规则定义和某些特定函数中。

Fancy Rules

隐性规则

  • 编译 C 程序:n.o 会自动从 n.c 生成,使用的命令形式为 $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
  • 编译 C++ 程序:n.o 会自动从 n.ccn.cpp 生成,使用的命令形式为 $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
  • 链接单个目标文件:n 会自动从 n.o 生成,使用的命令形式为 $(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@

隐式规则使用的重要变量包括:

  • CC:用于编译 C 程序的程序;默认为 cc
  • CXX:用于编译 C++ 程序的程序;默认为 g++
  • CFLAGS:给 C 编译器的额外标志
  • CXXFLAGS:给 C++ 编译器的额外标志
  • CPPFLAGS:给 C 预处理器的额外标志
  • LDFLAGS:在需要调用链接器时给编译器的额外标志
1
2
3
4
5
6
7
8
9
10
11
12
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
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

对于

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

可以使用静态模式规则来简化:

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
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:
touch $@

clean:
rm -f $(src_files)

Pattern Rules

  • 定义自己的隐式规则的方法
  • 一种更简单的静态模式规则形式
    1
    2
    3
    # Define a pattern rule that compiles every .c file into a .o file
    %.o : %.c
    $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
    在模式规则的前提条件中,符号 % 代表与目标中的 % 匹配的相同部分。
    1
    2
    3
    4
    # Define a pattern rule that has no pattern in the prerequisites.
    # This just creates empty .c files when needed.
    %.c:
    touch $@

Double-Colon Rules

双冒号规则很少被使用,但它允许为同一目标定义多个规则。如果这些规则是单冒号,会打印一个警告,并且只有第二组命令会运行。

1
2
3
4
5
6
7
all: blah

blah::
echo "hello"

blah::
echo "hello again"

Commands and execution

在命令前加上 @ 符号可以阻止其打印输出。你也可以在运行 make 命令时使用 -s 选项选择静默模式,相当于在每行前加上@符号。

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

每个命令等效于在一个新的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`

如果你想让一个字符串含有美元符号,你可以使用 $$。这是在 bashsh 中使用 shell 变量的方法。请注意下面的示例中 Makefile 变量和 Shell变量之间的区别。

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)
  • 在运行 make 时添加 -k 参数,即使出现错误也可以继续运行。如果您想一次查看所有 make 的错误信息,这将非常有帮助。
  • 在命令前添加 - 可以抑制错误。
  • 添加 -i 参数到 make 命令中,使得每个命令都会发生这种情况。
1
2
3
4
one:
# This error will be printed but ignored, and make will continue to run
-false
touch one

make 递归调用

要递归调用一个 Makefile,请使用 $(MAKE) 而不是 make,因为 $(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

Makefile 中,&& 用于连接多个命令,并且只有前一个命令成功执行(返回退出码为0)时,才会执行下一个命令。这种方式可以用于确保依赖关系的正确执行顺序。

Export, environments, and recursive make

1
2
3
4
5
6
7
# Run this with "export shell_env_var='I am an environment variable'; make"
all:
# Print out the Shell variable
echo $$shell_env_var

# Print out the Make variable
echo $(shell_env_var)

当使用 export 命令导出一个变量时,该变量将在 Makefile 中的后续命令中可见,并且还可以在通过 $(MAKE) 调用的子进程中使用。这样可以确保变量的值在整个构建过程中始终可用。

unexport cooly 将会撤销之前通过 export cooly 导出的变量。

.EXPORT_ALL_VARIABLES exports all variables for you.

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

There’s a nice list of options that can be run from make.

Variables Pt. 2

  • 递归(使用 =)- 仅在命令被使用时才查找变量,而不是在其定义时查找。
  • 简单扩展(使用 :=)- 类似于常规的命令式编程 - 仅展开到目前为止定义的变量。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 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

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

使用 := 进行简单扩展允许您向变量追加内容。递归定义会导致无限循环错误。

1
2
3
one = hello
# one gets defined as a simply expanded variable (:=) and thus can handle appending
one := ${one} there

?= 仅在变量尚未设置时设置变量

1
2
3
one = hello
one ?= will not be set
two ?= will be set

+= 用于向变量追加内容。如果变量尚未设置,则等效于 :=

1
2
one = hello
one += there

行末的空格不会被去除,但是行首的空格会被去除。要创建一个只包含一个空格的变量,请使用 $(nullstring)

1
2
3
4
5
6
7
8
9
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 start"$(space)"end

您可以使用 override 来覆盖来自命令行的变量。在这里,我们使用了 make option_one=hi 命令运行了 make

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)

define/endef 简单地创建一个变量,该变量被设置为一系列命令。需要注意的是,这与在命令之间使用分号有些不同,因为每个命令都在单独的 shell 中运行

1
2
3
4
5
6
7
8
9
10
11
12
one = export blah="I was set!"; echo $$blah

define two
export blah="I was set!"
echo $$blah
endef

all:
@echo "This prints 'I was set'"
@$(one)
@echo "This does not print 'I was set' because each command runs in a separate shell"
@$(two)

变量可以针对特定 targets 进行设置

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

all:
echo one is defined: $(one)

other:
echo one is nothing: $(one)

你可以为特定的 targets 模式设置变量

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

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

other:
echo one is nothing: $(one)

条件判断

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

ifdef

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 -i 运行此示例,以查看它打印出 echo 语句。

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

函数

函数主要用于文本处理。通过 $(fn, arguments)${fn, arguments} 来调用函数。Make 具有相当数量的内置函数

文本替换

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)

patsubst

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)

foreach

foreach 函数的使用方式如下:$(foreach var,list,text)。它将一个由空格分隔的单词列表转换为另一个列表。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 检查第一个参数是否非空

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)

Make 支持创建基本函数。通过创建变量来定义函数,但使用 $(0)$(1) 等调用参数。然后,使用特殊的内建函数来调用函数。语法是 $(call 变量,参数,参数)$(0) 是变量(函数名),而 $(1)$(2) 等是参数

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)

其他特性

include

include 用于包含其他 Makefile。如果 Makefile 不存在,make 将会继续执行。如果 Makefile 存在,make 将会执行它。

1
include some_file

vpath

使用 vpath 指定一组prerequisites存在的位置。格式为 vpath <pattern> <directories, space/colon separated>。模式可以包含 %,表示匹配任意零个或多个字符。您还可以在全局范围内使用变量 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

.PHONY

.PHONY 目标用于声明一些常见的操作,例如 clean(清理生成的文件)、all(构建所有目标)、install(安装程序)等。通过将这些操作声明为伪目标,可以确保它们在每次运行 Make 时都会被正确执行,而不需要检查文件的更新状态

1
2
3
4
5
6
7
8
9
10
11
12
.PHONY: clean all install

all: program

program: main.o foo.o bar.o
gcc $^ -o $@

clean:
rm -f *.o program

install:
cp program /usr/local/bin/

.DELETE_ON_ERROR

.DELETE_ON_ERROR 是一个特殊的 Makefile 目标,用于在出现错误时删除生成的目标文件

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

myapp: main.o util.o
gcc main.o util.o -o myapp

main.o: main.c
gcc -c main.c -o main.o

util.o: util.c
gcc -c util.c -o util.o

如果在编译 main.o 时发生错误,Make 会立即停止编译,并删除已经生成的目标文件 main.outil.o。这样可以确保下次构建时从干净的状态开始,避免残留文件导致的问题。

Makefile Cookbook

让我们来看一个非常精彩的 Make 示例,适用于中等规模的项目。

这个 Makefile 的好处是它可以自动为您确定依赖关系。您只需要将您的 C/C++ 文件放在 src/ 文件夹中即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Thanks to Job Vranish (https://spin.atomicobject.com/2016/08/26/makefile-c-projects/)
TARGET_EXEC := final_program

BUILD_DIR := ./build
SRC_DIRS := ./src

# Find all the C and C++ files we want to compile
# Note the single quotes around the * expressions. The shell will incorrectly expand these otherwise, but we want to send the * directly to the find command.
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')

# Prepends BUILD_DIR and appends .o to every src file
# As an example, ./your_dir/hello.cpp turns into ./build/./your_dir/hello.cpp.o
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)

# String substitution (suffix version without %).
# As an example, ./build/hello.cpp.o turns into ./build/hello.cpp.d
DEPS := $(OBJS:.o=.d)

# Every folder in ./src will need to be passed to GCC so that it can find header files
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
# Add a prefix to INC_DIRS. So moduleA would become -ImoduleA. GCC understands this -I flag
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

# The -MMD and -MP flags together generate Makefiles for us!
# These files will have .d instead of .o as the output.
CPPFLAGS := $(INC_FLAGS) -MMD -MP

# The final build step.
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
$(CXX) $(OBJS) -o $@ $(LDFLAGS)

# Build step for C source
$(BUILD_DIR)/%.c.o: %.c
mkdir -p $(dir $@)
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# Build step for C++ source
$(BUILD_DIR)/%.cpp.o: %.cpp
mkdir -p $(dir $@)
$(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@


.PHONY: clean
clean:
rm -r $(BUILD_DIR)

# Include the .d makefiles. The - at the front suppresses the errors of missing
# Makefiles. Initially, all the .d files will be missing, and we don't want those
# errors to show up.
-include $(DEPS)