原文链接
Makefiles 用于确定一个大型工程的哪些部分需要重新编译。大多数情况下,C/C++文件是需要被编译的。其他语言也有它们自己的编译工具来实现类似 Make 的功能。Make 也能被用于编译之外的情形:当你需要管理一系列的依赖文件时(比如latex)。本篇教程主要关注于 Make 用于编译 C/C++ 程序的情况。
这里有一个 Make 的依赖示意图。如果任意文件的依赖被改变了,那么这个文件就需要被重新编译:
可选的 C/C++ 有 SCons, CMake, Bazel, Ninja
。一些代码编辑器如 MVS
有它们内建的编译工具。对于 Java,其编译工具有 Ant, Maven, Gradle
。Go 和 Rust 也有他们自己的编译工具。
解释型语言,像 Python, Ruby 和 JavaScript 并不需要类似于 Makefile 的东西。Makefile 的目标是编译任何被修改而需要重新编译的文件。但是当一种解释型语言的代码被修改,没有什么需要重新编译的。当程序运行时,解释器会使用最新的文件。
Make 有不同的实现版本,但是这篇教程的大部分都适用于任何版本。然而,这篇教程是专门为 GNU Make 写的,它是 Linux 和 MacOS上的标准实现。所有的示例都能在 Make 的3版和4版上运行,这两个版本几乎等价,除了一些深奥的区别。
hello:
echo "hello, world"
注意:Makefiles 必须使用 TAB 缩进不能使用空格
这是上面的样例的运行结果:
$ make
echo "Hello, World"
Hello, World
视频中的样例:
hello: hello.o
c++ hello.o -o hello # run third
hello.o: hello.c
c++ -c hello.c -o hello.o # run second
hello.c:
echo "#include " > hello.c # run first
echo 'int main() {std::cout<< "hello, world" << std::endl; return 0;}' >> hello.c # run first
clean:
rm -rfd hello*
这是上面的样例的运行结果:
echo "#include " > hello.c # run first
echo 'int main() {std::cout<< "hello, world" << std::endl; return 0;}' >> hello.c # run first
c++ -c hello.c -o hello.o # run second
c++ hello.o -o hello # run third
一个 Makefile 文件由一系列规则组成。通常一个规则看起来像这样:
targets: prerequisites
command
command
command
hello:
echo "Hello, World"
echo "This line will always print, because the file hello does not exist."
接下来运行 make hello
。只要 hello
文件不存在,这些命令就会执行。如果 hello
文件存在,这些命令就不会执行。
必须认识到,hello
既是目标又是文件。这是因为二者是绑定在一起的。一般地,当一个目标运行起来(或者说当这个目标的命令运行起来),这些命令会创建一个与目标同名的文件。在这个案例中,hello
目标没有创建一个叫 hello
的文件。
我们创建一个更典型的 Makefile:它编译一个单独的 C 文件。我们先创建一个叫做 blah.c
的文件,包含如下内容:
//blah.c
int main() {return 0;}
然后创建 Makefile:
blah:
cc blah.c -o blah
然后运行 make
。因为没有给 make
命令提供目标参数,第一个目标将会运行。在这个案例中,只有一个目标。当你第一次运行它的时候,blah
将会被创建。当你第二次运行它的时候,你就会看到 make: 'blah' is up to date
。这是因为 blah
文件已经存在。但是这会带来一个问题:如果我们修改了 blah.c
,然后运行 make
,没有文件被重新编译。
我们通过引入依赖来解决这个问题:
blah: blah.c
cc blah.c -o blah
当我们再次运行 make
,将会发生一下事件:
blah.c
blah
中的目标。当 blah
不存在时,或 blah.c
比 blah
更新时,Make将会执行下面的 Makefile 最终会运行所有的目标。当你在终端输入 make
,它将会通过以下步骤构造一个叫做 blah
的程序:
blah
,因为第一个目标是默认目标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
下面这个示例总是会运行这2个目标,因为 some_file
依赖于 other_file
,而后者永远不会被创建
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
clean
通常作为一个目标,用于清除其他目标的输出,但它并不是 Make 中的一个关键字。你可以运行 make
和 make clean
来创建或删除 some_file
。
some_file:
touch some_file
clean:
rm -f some_file
变量只能是字符串。你最好使用 :=
,不过用 =
也行。
files := file1 file2
some_file: $(files)
echo "Look at this variable" $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file{1,2} some_file
单引号或双引号在 Make 中没有什么特别含义。它们仅仅是赋给变量的字符。引号在 shell/bash 中很有用,比如你需要在printf
命令中使用。在下面的例子中,这两个命令的输出相同:
a := one two # a is set to the string "one two"
b := 'one two' # Not recommended. b is set to the string "'one two'"
all:
printf '$a'
printf $b
引用变量既可以用 ${}
也可以用 $()
x := dude
all:
echo $(x)
echo ${x}
echo $x # Bad practice, but works
使用 all
可以一次性运行多个目标。因为它是第一条规则,如果 make
没有指定目标,它将会自动运行
all: one two there
one:
touch one
two:
touch two
there:
touch there
clean:
rm -f one two there
如果一条规则有多个目标,make 会对每一个目标都执行命令。$@
是一个自动变量,它包含了目标的名字。
all: f1.o f2.o
f1.o f2.o:
echo $@
# Equivalent to:
# f1.o:
# echo f1.o
# f2.o:
# echo f2.o
*
和 %
都是 Make 中的通配符,但是它们含义不同。*
会搜索指定目录下所有匹配的文件名。我建议你永远使用 wildcard
函数包装这个通配符来使用,否则你将会陷入下面的陷阱:
# Print out file information about every .c file
print: $(wildcard *.c)
ls -la $?
*
能够直接用于目标,依赖,或者用在 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)
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
Make 专精于 C 的编译。Make 最令人疑惑的地方在于它的“魔法/自动”规则。Make 称为 “隐式”规则:
n.o
自动地由 n.c
编译而成:$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
n.o
自动地由 n.cc
或 n.cpp
编译而成:$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
n
自动地由 n.o
链接而成:$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@
隐式规则使用的重要变量如下:
CC
:cc
CXX
:g++
CFLAGS
:给 C 编译器传递的额外标志CXXFLAGS
:给 C++ 编译器传递的额外标志CPPFLAGS
:C 预处理器的额外标志LDFLAGS
:链接器标志下面我们使用一个隐式规则来让 Make 构造一个 C 程序:
CC = gcc
CFLAGS = -g
# Implicit rule #1: hello is built via the C linker implicit rule
# Implicit rule #2: hello.o is built via the C compilation implicit rule, because hello.c exists
hello: hello.o
hello.c:
echo "int main() {return 1;}" > hello.c
clean:
rm -f hello*
静态模式规则是简化 Makefile 的另一种方式:
targets...: target-pattern: prereq-patterns ...
commands
它的本质是给定一个被 target-pattern
匹配的目标 target
(通过 %
通配符)。被匹配的内容叫做 stem
。然后stem
会被替换到 prereq-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)
$(objects): %.o : %.c
all.c:
echo "int main(){return 0;}" > all.c
%.c:
touch $@
clean:
rm -f *.o *.c all
filter
函数能够被用于静态模式规则用以匹配正确的文件。在这个例子中加入了 .raw
和 .result
扩展:
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)
你可以从2个角度来看待“模式规则”
# Define a pattern rule that compiles every .c file into a .o file
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
模式规则的目标中包含一个 %
。这个 %
匹配任意非空字符串,其他的字符匹配它们自身。在一个模式规则中,依赖中的 %
和目标中的 %
代表相同的 stem 值。
# Define a pattern rule that has no pattern in the prerequisites.
# This just creates empty .c files when needed.
%.c:
touch %.c
双冒号很少用,它允许对同一目标定义多个规则。如果只有一个冒号,将会打印一个警告并且只有第二个命令能够执行:
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`
make 的默认shell是 /bin/sh
。你可以通过设置 SHELL 变量来设置要用的 shell:
SHELL=/bin/bash
cool:
echo "Hello from bash"
如果你想在字符串中使用 $
符号,你需要使用 $$
。这在你需要引用 shell 的变量时有用。
请注意 Makefile 的变量和 Shell 的变量之间的区别:
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
选项,使得在遇到错误时 make 仍能够继续运行。当你想一次看到所有的编译错误时这个选项很有用-
能够忽略它产生的错误-i
选项,忽略所有错误,相当于在每一条命令前加上了 -
one:
# This error will be printed but ignored, and make will continue to run
-false
touch one
当你使用 ctrl + c
终止 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 运行时,它将会自动创建独立于环境变量的 Make 变量:
# 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 shell_env_var='I am an environment variable'; make
# Print out the Shell variable
echo $shell_env_var
I am an environment variable
# Print out the Make variable
echo I am an environment variable
export
指示符接收一个变量并将其设置为环境变量,使得所有的命令都可以使用:
shell_env_var=Shell env var, created inside of Make
export shell_env_var
all:
echo $(shell_env_var)
echo $$shell_env_var
如上所述,当你在 make
命令中执行 make 时,你能够使用 export
导出一个变量使得它对 sub-make 仍可见。在下面的例子中,cooly
变量被导出,使得 sub-make 能够使用它。
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
如果你需要在 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
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 支持一系列参数,比如 --dry-run
,--touch
,--old-file
你能在 Make 时指定多目标:make clean run test
有两种风格的变量定义:
=
:当变量使用时才会进行展开并赋值,而非定义时:=
:表示将变量的值立即展开,而不是在变量被使用时才展开# 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)
你能够使用 :=
在变量末尾追加内容。但是使用 =
,你会得到一个无限循环的错误:
one = hello
# one gets defined as a simply expanded variable (:=) and thus can handle appending
one := ${one} there
all:
echo $(one)
?=
仅在变量第一次赋值时起作用:
one = hello
one ?= will not be set
two ?= will be set
all:
echo $(one)
echo $(two)
行末的空格不会被省略,但是行首的空格会被省略。为了在变量中使用空格,请使用 $(nullstring)
:
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
输出结果如下:
$ make -s
hello there
start end
未定义的变量是一个空字符串:
all:
# Undefined variables are just empty strings!
echo $(nowhere)
+=
用于追加元素:
foo := start
foo += more
all:
echo $(foo)
字符串替换函数在修改变量时很有用。文本处理函数和文件名函数也很有用。
你能够使用 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
用于定义多行指令。
define
/ endef
仅仅用于创建一系列命令的变量。注意到它和使用 ;
定义多条命令是不同的,因为每一条命令都在一个独立的 shell 中运行:
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)
能够针对特定的目标设置变量:
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)
foo = ok
all:
ifeq ($(foo), ok)
echo "foo equals ok"
else
echo "nope"
endif
nullstring =
foo = $(nullstring)
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
ifndef $(bar)
echo "but bar is not"
endif
下面这个例子向你演示了如何使用 findstring
和 MAKEFLAGS
测试 make 的选项。使用 make -i
运行,观察它的输出:
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 有一系列的内建函数:
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)
进行如下操作:
将 text
以空格分隔,找到符合 pattern
的部分,并将其替换为 replacement
。这里 patern
或许包含一个 %
通配符,它能够匹配任意数目的任意字符。如果 replacement
中也含有 %
,这个 %
将会被替换为和模式中的 %
相同的内容。只有 pattern
和 replacement
中的第一个 %
会被用作替换目的,其他的 %
都不会被改变。
$(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.
there := $(foo:.o=.c)
all:
echo $(one)
echo $(two)
echo $(there)
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
检查第一个参数是否为非空。如果是,它就运行第二个参数;否则运行第三个参数。
foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)
all:
@echo $(foo)
@echo $(bar)
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,但是它将使用空格来代替新行:
all:
@echo $(shell ls -la) # Very ugly because the newlines are gone!
include filenames...
使用 vpath 来声明依赖存放的位置。格式是 vpath
能够使用 %
,它匹配0个或多个字符。你也能够使用 VPATH
来声明一个全局变量:
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
\
能够定义多行变量:
some_file:
echo This line is too long, so \
it is broken up into multiple lines
向目标加入 .PHONY
能够防止 Make 将文件名和伪目标弄混。在下面的例子中,如果存在一个叫做 clean
的文件,make clean
仍能够正确运行。
some_file:
touch some_file
touch clean
.PHONY: clean
clean:
rm -f some_file
rm -f clean
如果一条命令返回了一个非0的退出值,make 将会停止运行。
DELETE_ON_ERROR
在规则执行失败时,将会删除生成的目标。
.DELETE_ON_ERROR:
all: one two
one:
touch one
false
two:
touch two
false
下面是一个适用于中等规模的工程的 makefile 文件
# 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)