学习 Makefile

原文链接

开始

为什么需要 Makefile

Makefiles 用于确定一个大型工程的哪些部分需要重新编译。大多数情况下,C/C++文件是需要被编译的。其他语言也有它们自己的编译工具来实现类似 Make 的功能。Make 也能被用于编译之外的情形:当你需要管理一系列的依赖文件时(比如latex)。本篇教程主要关注于 Make 用于编译 C/C++ 程序的情况。

这里有一个 Make 的依赖示意图。如果任意文件的依赖被改变了,那么这个文件就需要被重新编译:

学习 Makefile_第1张图片

Make的替代选项

可选的 C/C++ 有 SCons, CMake, Bazel, Ninja。一些代码编辑器如 MVS 有它们内建的编译工具。对于 Java,其编译工具有 Ant, Maven, Gradle。Go 和 Rust 也有他们自己的编译工具。

解释型语言,像 Python, Ruby 和 JavaScript 并不需要类似于 Makefile 的东西。Makefile 的目标是编译任何被修改而需要重新编译的文件。但是当一种解释型语言的代码被修改,没有什么需要重新编译的。当程序运行时,解释器会使用最新的文件。

Make的版本和类别

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 语法

一个 Makefile 文件由一系列规则组成。通常一个规则看起来像这样:

targets: prerequisites
	command
	command
	command
  • targets 是空格分隔的文件名。一般每条规则只有一个
  • commands 是生成 target 的一系列命令。它们需要以 tab 开头,而非空格
  • prerequisites 也是空格分隔的文件名。这些文件需要在执行命令之前就已存在。它们也被称为依赖

Make 的本质

hello:
	echo "Hello, World"
	echo "This line will always print, because the file hello does not exist."
  • 有一个目标叫做 hello
  • 这个目标有2个命令
  • 这个目标没有依赖

接下来运行 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
  • Make 决定它是否运行 blah 中的目标。当 blah 不存在时,或 blah.cblah 更新时,Make将会执行

更多的示例

下面的 Makefile 最终会运行所有的目标。当你在终端输入 make,它将会通过以下步骤构造一个叫做 blah 的程序:

  • Make 选定目标 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"

Make clean

clean 通常作为一个目标,用于清除其他目标的输出,但它并不是 Make 中的一个关键字。你可以运行 makemake 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目标

使用 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 称为 “隐式”规则:

  • 编译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 $@

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

  • CCcc
  • CXXg++
  • 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`

默认Shell

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

终止make

当你使用 ctrl + c 终止 make 时,它将会删除所有刚创建的新的目标文件

递归调用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 的参数

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)

Makefile 中的条件判断

if/else 条件判断

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

$(MAKEFLAGS)

下面这个例子向你演示了如何使用 findstringMAKEFLAGS 测试 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 中也含有 %,这个 % 将会被替换为和模式中的 % 相同的内容。只有 patternreplacement 中的第一个 % 会被用作替换目的,其他的 % 都不会被改变。

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

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!

其他特性

Include Makefiles

include filenames...

vpath

使用 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

.delete_on_error

如果一条命令返回了一个非0的退出值,make 将会停止运行。

DELETE_ON_ERROR 在规则执行失败时,将会删除生成的目标。

.DELETE_ON_ERROR:
all: one two

one:
	touch one
	false

two:
	touch two
	false

Makefile Cookbook

下面是一个适用于中等规模的工程的 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)

你可能感兴趣的:(Linux,学习,linux,c++,软件构建)