Makefile 教程

预计时间: 25分钟

make 命令会在当前目录寻找一个名为 Makefile 的文件,该文件指导 make 工具如何生成项目,例如自动编译链接生成一个可执行文件。

为什么会存在 Makefile?

用编译语言生成二进制文件通常涉及很多步骤,手工执行这些步骤令人生厌,虽然可以通过写shell脚本来执行,但这样有一些缺点,如不能很方便的决定大型程序的哪些部分需要重新编译,哪些部分不需要重新编译。

Makefile 用于指导 make 在构建大型程序时,分析出哪些部分需要重新编译。 Makefile 通常用于 C 或 C++ 语言项目。其他语言通常有自己的工具,其用途与 Make 相似。

对于 Java,有AntMavenGradle。Go 和 Rust 等其他语言有自己的构建工具

Python、Ruby 和 Javascript 等解释型语言不需要类似于 Makefile。Makefiles 的目标是根据已更改的文件来编译需要编译的任何文件。但是当解释语言中的文件发生变化时,不需要重新编译任何东西。当程序运行时,将使用该文件的最新版本。

Make 有哪些替代方案?

流行的 C/C++ 替代构建系统是SConsCMakeBazelNinja

一些代码编辑器(如Microsoft Visual Studio)有自己的内置构建工具。

Make 的版本和类型

Make 有多种实现方式,它们的表现几乎相同。大部分情况下,本文内容都适用于你正在使用的任何 make 工具。

本教程内容主要参考 GNU Make 的文档,它是 Linux 上的标准实现。

Makefile 语法规则

一个 Makefile 由一组规则组成。规则通常如下所示:

targets: prerequisites
	command
	command
	command
  • 目标 targets 是文件名,以空格分隔。通常,每条规则只有一个。
  • 命令 command 是生成目标的一系列步骤。以制表符开头,不能用空格开头。
  • 先决条件 prerequisites 是文件名(也称为依赖项),以空格分隔。这些文件需要在执行命令之前存在。

示例

blah: blah.o
	cc blah.o -o blah # 第三步

blah.o: blah.c
	cc -c blah.c -o blah.o # 第二步

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

通过下面的练习,理解目标、先决条件和命令的关系。

  • 初次执行 make 则执行第一、二、三步。
  • 再次执行则提示 make: blah is up to date.
  • 将 blah.c 中的 return 0 改为 return 1 再次执行 make 则只执行第二步和第三步。

通过下面的练习理解目标文件

another:
	echo "hello"

练习一:执行 make ,会输出 hello

练习二:手工创建 another 文件,执行 make,不会执行 echo 命令。

touch another
make

练习三:手工删除 another 文件,再次执行 make,会输出 hello。

rm another
make

练习四:修改 echo 前面的 tab 制表符为空格,再次执行 make ,会报错。

all

Makefile 中定义了多个目标,通常定义一个 all 目标来生成所有目标。

all: one two three

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

clean:
	rm -f one two three

多个目标

Makefile 注释

# 开始的内容为注释

Makefile 变量

变量的定义和引用

变量是把一个名字和任意长的字符串关联起来。基本语法如下

MY_VAR=A text string

使用${}$()来引用变量。

示例:

MY_VAR=file1.c file2.c

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

变量的求值时机

recursive 和 simply expanded

  • = recursive 仅在使用命名时解析变量值。
  • := simply expanded 在定义时立即解析变量值。

示例

# 在下面的echo命令执行时再求值,输出 "later"
one = one ${later_variable}

# 简单的扩展变量,由于 later_variable 未定义,下面不会输出 "later"
two := two ${later_variable}

later_variable = later

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

递归定义变量将产生无限循环错误,示例

one = hello
# 简单的扩展变量,若将 := 改为 = 则产生无限循环错误。
one := ${one} there

all: 
	echo $(one)

变量是否覆盖

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

one = hello
one ?= will not be set
two ?= will be set

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

变量中的前后空格

行尾的空格不会被删除,但开头的空格会被删除。

with_spaces = hello   # with_spaces 变量是 hello 末尾有三个空格
after = $(with_spaces)there

nullstring =
space = $(nullstring) # 创建只有一个空格的变量

all: 
	echo "$(after)"
	echo start"$(space)"end

未定义的变量实际上是一个空字符串!

all: 
	# 未定义的变量实际上是一个空字符串
	echo $(nowhere)

变量的追加

+= 用于追加

foo := start
foo += more

all: 
	echo $(foo)

Makefile 自动变量

$@ 是一个包含目标名称的自动变量。

all: f1.o f2.o

f1.o f2.o:
	echo $@
# 相当于:
# f1.o:
#	 echo f1.o
# f2.o:
#	 echo f2.o

这些变量具有根据规则的目标和先决条件为每个执行的规则重新计算的值。

  • $@ 规则目标的文件名。
  • $% 目标成员名称,当目标是存档成员时。
  • $< 第一个先决条件的名称。
  • $? 比目标新的所有先决条件的名称,它们之间有空格。
  • $^ 所有先决条件的名称,它们之间有空格。
  • $+ 这就像$^,但不止一次列出的先决条件会按照它们在 makefile 中列出的顺序重复。
  • $| 所有仅命令先决条件的名称,它们之间有空格。
  • $* 隐式规则匹配的词干。

更多变量请参考: 自动变量

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

通配符

* 通配符

*% 在 Make 中都称为通配符,但它们的含义完全不同。

`* 在您的文件系统中搜索匹配的文件名。我建议您始终将其包装在wildcard函数中,否则您可能会陷入下面描述的常见陷阱。

# Print out file information about every .c file
print: $(wildcard *.c)
	ls -la  $?
  • * 可以在目标、先决条件或 wildcard 函数中使用。
  • * 不能在变量定义中直接使用
  • * 没有匹配到文件时,保持原样(除非在 wildcard 函数中运行)
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)

% 通配符

% 确实很有用,但是由于可以使用的情况多种多样,因此有些混乱。

  • 在“匹配”模式下使用时,它匹配字符串中的一个或多个字符。这种匹配称为 stem。
  • 在“替换”模式下使用时,它采用匹配的词干并替换字符串中的词干。
  • % 最常用于规则定义和某些特定功能中。

隐式规则

以下是隐式规则列表:

  • 编译 C 程序:使用以下形式的命令n.o自动生成n.c$(CC) -c $(CPPFLAGS) $(CFLAGS)
  • 编译 C++ 程序:n.o由n.cc或n.cpp使用以下形式的命令自动生成$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)
  • 链接单个目标文件:通过运行命令n自动生成n.o$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)

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

  • CC:用于编译 C 程序的程序;默认 cc
  • CXX: 用于编译 C++ 程序的程序;默认 g++
  • CFLAGS: 提供给 C 编译器的额外标志
  • CXXFLAGS: 提供给 C++ 编译器的额外标志
  • CPPFLAGS: 给 C 预处理器的额外标志
  • LDFLAGS: 当编译器应该调用链接器时提供额外的标志

让我们看看我们现在如何构建一个 C 程序,而无需明确告诉 Make 如何进行编译:

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*

静态模式规则

静态模式规则的语法:

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

匹配 target-pattern 生成 targets, 匹配 prereq-pattern 生成 target-pattern

一个典型的用例是编译.c文件到.o文件。这是手动方式

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

静态模式规则和过滤器

filter函数可用于静态模式规则以匹配正确的文件。在这个例子中,我组成了.raw.result扩展。

obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

.PHONY: all
all: $(obj_files)

$(filter %.o,$(obj_files)): %.o: %.c
	echo "target: $@ prereq: $<"
$(filter %.result,$(obj_files)): %.result: %.raw
	echo "target: $@ prereq: $<" 

%.c %.raw:
	touch $@

clean:
	rm -f $(src_files)

模式规则

模式规则可以有两种方式:

  • 一种定义自己的隐式规则的方法
  • 一种更简单的静态模式规则

让我们先从一个例子开始:

# Define a pattern rule that compiles every .c file into a .o file
%.o : %.c
		$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

模式规则在目标中包含一个“%”。这个 '%' 匹配任何非空字符串,其他字符匹配它们自己。模式规则的先决条件中的“%”代表与目标中的“%”匹配的相同词干。

这是另一个例子:

# Define a pattern rule that has no pattern in the prerequisites.
# This just creates empty .c files when needed.
%.c:
   touch $@

双冒号规则

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

all: blah

blah::
	echo "hello"

blah::
	echo "hello again"

命令和执行

命令回显/静音

在命令之前添加一个@以阻止它被打印。

您也可以运行 make 时使用 -s 参数, 这将为每一行命令添加一个@

all: 
	@echo "This make line will not be printed"
	echo "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`

默认 shell

默认 shell 是/bin/sh. 您可以通过更改变量 SHELL 来更改它:

SHELL=/bin/bash

cool:
	echo "Hello from bash"

使用-k-i- 进行错误处理

在运行 make 时指定参数

  • -k 即使遇到错误也能继续运行。如果您想一次查看 Make 的所有错误,这很有帮助。
  • -i 使每个命令都发生这种情况。

在命令前添加一个-以抑制错误

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

打断或终止 make

如果用 ctrl+c 终止 make,它将删除它刚刚生成的较新目标。

递归使用make

要递归调用 makefile,请使用$(MAKE)变量代替make。它会为您传递 make 标志并且本身不会受到它们的影响。

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

使用导出进行递归make

export 指令接受一个变量,并使其可供 sub-make 命令访问。在这个例子中,cooly被导出以便 subdir 中的 makefile 可以使用它。

注意:export 与 sh 语法相同,但它们不相关(虽然功能相似)

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

您还需要导出变量以使它们在 shell 中运行。

one=this will only work locally
export two=we can run subcommands with this

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

.EXPORT_ALL_VARIABLES为您导出所有变量。

.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

make 命令参数

make 执行时可选的参数列表:make 选项列表

看看--dry-run, --touch, --old-file.

您可以创建多个目标,即make clean run test运行clean目标,然后run,然后test

命令行参数和覆盖

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

# 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 实际上只是一个命令列表。它与成为一个函数无关。 请注意,这与在命令之间使用分号有点不同,因为每个命令都在单独的 shell 中运行,正如预期的那样。

one = export blah="I was set!"; echo $$blah

define two
export blah=set
echo $$blah
endef

# One and two are different.

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)

特定目标变量

可以为特定目标分配变量

all: one = cool

all: 
	echo one is defined: $(one)

other:
	echo one is nothing: $(one)

特定于模式的变量

您可以为特定目标模式分配变量

%.c: one = cool

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

other:
	echo one is nothing: $(one)

Makefile 的条件部分

if/else

foo = ok

all:
ifeq ($(foo), ok)
	echo "foo equals 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

检查是否定义了变量

ifdef 不扩展变量引用;它只是查看是否定义了某些东西

bar =
foo = $(bar)

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

$(makeflags)

此示例向您展示如何使用findstringMAKEFLAGS测试 make 标志。运行此示例make -i以查看它打印出 echo 语句。

bar =
foo = $(bar)

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}调用函数。

您可以在自定义函数中调用 调用 内置函数

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

如果要替换空格或逗号,请使用变量

comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))

all: 
	@echo $(bar)

不要在第一个参数之后包含空格。这将被视为字符串的一部分。

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)执行以下操作:

“在文本中查找与模式匹配的空格分隔的单词并用替换替换它们。 这里的模式可能包含一个充当通配符的'%',匹配一个单词中任意数量的任何字符。 如果替换还包含一个'%', '%' 被替换为与模式中的 '%' 匹配的文本。 只有模式中的第一个 '%' 和替换会以这种方式处理;任何后续的 '%' 都不会改变。

GNU make manual Text-Functions

替换引用$(text:pattern=replacement)是对此的简写。

还有另一种只替换后缀的速记:$(text:suffix=replacement). 这里没有%使用通配符。

注意:不要为这个速记添加额外的空格。它将被视为搜索词或替换词。

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针对每个单词进行扩展。 这会在每个单词后附加一个感叹号:

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 函数

if检查第一个参数是否为非空。如果是,则运行第二个参数,否则运行第三个。

foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)

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

call 函数

Make 支持创建基本函数。您只需通过创建变量来“定义”该函数,但使用参数$(0)$(1)等。然后您可以使用特殊函数调用该call函数。语法是$(call variable,param,param). $(0)是变量,而$(1),$(2)等是参数。

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)

shell 函数

shell - 这调用了 shell,但它用空格替换了换行符!

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

其他特性

包含 Makefile

include 指令告诉 make 读取一个或多个其他 makefile。它是 makefile makefile 中的一行,如下所示:

include filenames...

-M当您使用编译器标志(如基于源创建 Makefile )时,这特别有用。例如,如果某些 c 文件包含头文件,则该头文件将被添加到由 gcc 编写的 Makefile 中。

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

多行

当命令太长时,反斜杠(“\”)字符使我们能够使用多行

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

.phony

添加.PHONY到目标将防止 make 将虚假目标与文件名混淆。在此示例中,如果clean创建了文件,make clean 仍将运行。.PHONY很好用,但为了简单起见,我将在其余示例中跳过它。

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 出于历史原因没有这样做。

.DELETE_ON_ERROR:
all: one two

one:
	touch one
	false

two:
	touch two
	false

Makefile 示例

让我们看一个非常有趣的例子,这个例子适用于中型项目。

这个 makefile 的巧妙之处在于它会自动为您确定依赖关系。您所要做的就是将您的 C/C++ 文件放入该src/文件夹中。

# 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. Make will incorrectly expand these otherwise.
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')

# String substitution for every C/C++ file.
# As an example, hello.cpp turns into ./build/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)
	$(CC) $(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)
更新于2022年04月11日