Learn Makefiles
Makefiles笔记 整理自己学习的一些知识点 并且加入自己的理解
Getting Started
Makefiles的作用
make是以中项目构建工具,Makefile是其输入。make按照Makefile里的步骤一次构建项目。
在软件开发项目中,Makefile用来决定哪些文件需要编译,文件之间的依赖关系,所要执行的命令以及并按照依赖关系依次执行。Makefile在管理依赖关系时,是以文件为单位。make命令可以解析Makefile并按照定义的依赖关系依次执行相应的shell命令,实现源文件的编译、链接,以及其他常规操作。make还可以根据文件是否发生更改(通过比较文件的modify time),仅执行受到这些文件影响的操作。make命令本身不能解析文件依赖,需要开发人员在Makefile里编辑依赖关系。make本身也不负责编译等操作,而是通过调用gcc、g++等命令进行源文件编译。
Makefile有自己的语法,并支持通配符、模式匹配、内嵌函数,从而提高其灵活性及编写效率。
如何通过Makefile管理下图的依赖?
对比
Makefile主要用于c、c++项目的构建,同样用于c、c++项目构建的还有SCons, CMake, Bazel,and Ninja。类似于以及java中的maven,gradle。
版本
通过make --version查看make的版本
machi@machiunbuntu:~$ make --version
GNU Make 4.2.1
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Makefile语法
Makefile有诺干个rule(规则)组成,一个rule形式如下:
targets目标(一个或多个): prerequisites前提(一个或多个)
命令
命令
命令
-
targets是文件名,空格分割。通常一个rule只有一个。
-
命令是为了生成target而需要执行的一系列步骤。制表符开头。
prerequisites也是文件名,空格分隔。为了运行对应rule下的命令,这些文件必须提前存在。prerequisites也被称之为依赖。
开始示例
Makefile文件如下:
some_file: other_file
echo "This will run second, because it depends on other_file"
touch some_file
other_file:
echo "This will run first"
touch other_file
clean:
rm -f some_file other_file
直接执行make命令,默认执行第一条rule:some_file,因为other_file不存在,所以会先执行other_file。other_file不存在依赖,执行完成后,执行some_file的命令。
也可以通过make some_file或other_file从制定的rule开始。
clean用于删除生成的文件,通常无依赖。
变量
变量只能是字符串。变量可以通过$Var, $(Var), ${Var}三种方式引用,但是第一种不建议使用,可能存在识别错误的情况。推荐使用第二种和第三种。此外,为了于便于与函数区分,推荐使用第三种。
下面的files看上去像数组,本质上是空格分隔的字符串。
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
all目标 The all target
为了实现某个target而存在多个target,将最终target设置成 all
target.
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
多个目标 Multiple targets
一个rule有多个target,那么每个target会分别执行对应rule的前提判断及命令。
可以理解成,遍历多个target并依次从左往右执行规则,Makefile对这种遍历使用 $@
代表每次执行rule输入的target名。
$@
详情见自动变量。
all: f1.o f2.o
f1.o f2.o:
echo $@
# Equivalent to:
# f1.o
# echo $@
# f2.o
# echo $@
自动变量和通配符 Automatic Variables and Wildcards
* Wildcard
*
和 %
在Makefile中都称之为通配符,但是用于完全不同的场景。*
用于搜索文件系统下匹配的文件名。建议只在 wildcard
函数下使用 *
, 否则可能存在匹配失败直接返回通配符字符串的问题。
用下面的例子说明直接使用 *
存在的风险。
wrong := *.o
target: ${wrong}
echo $^
当文件夹下存在a.o、b.o这种可以被匹配到的文件时,返回a.o b.o作为prerequisite,可以正常执行。但是当不存在匹配文件时,${wrong}会直接返回 *.o
作为target的prerequisite,报错。
right := $(wildcard *.c)
target: ${wrong}
echo $^
当使用 wildcard
函数时,匹配失败直接返回空。
如何打印当前目录下所有匹配的文件,如下:
编辑print target,因为不会生成文件,因此每次make print都会执行。将匹配的文件作为prerequisite打印出来。
print: $(wildcard *.c)
ls -la $<
% Wildcard
%
适用于多种场景,有时候会让人感到困惑。
- 用于模式匹配,可以匹配字符串中的一个或多个字符。匹配的部分叫做主干。
- 用于替换模式,可以提取字符串中匹配上的主干并替换。
-
%
常用于rule定义和一些特定函数。
自动变量 Automatic Variables
输入 make hello world
hello world: one two
# Outputs "hey", since this is the first target
echo $@
# Outputs all prerequisites newer than the target
echo $?
# Outputs all prerequisites
echo $^
# Outputs first prerequisites
echo $<
touch $@
one:
touch one
two:
touch two
clean:
rm -f hello world one two
花式规则 Fancy Rules
静态模式匹配 Static Pattern Rules
targets ...: target-pattern: prereq-patterns ...
commands
静态模式匹配可以分解成一下几个步骤理解:
targets本身是一个target列表,满足某种target-pattern;
遍历targets,按照target-pattern提取出匹配的主干;
将主干填充到对应的prereq-patterns,由此为每一个target生成对应的prerequisite。
如果通过静态模式匹配,为每一个 .c
文件创建rule生成对应的 .o
文件
objects = foo.o bar.o all.o
all: $(objects)
# 等同于
# foo.o: foo.c
# bar.o: bar.c
# all.o: all.c
$(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
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
# 通过filter函数对obj_files src_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)
隐式规则 Implicit Rules
对于一些常用的rule,make会自动为我们添加,而不需要我们编写。例如:
Perhaps the most confusing part of make is the magic rules and variables that are made. Here’s a list of implicit rules:
- 编译C程序:
n.o
会自动通过命令$(CC) -c $(CPPFLAGS) $(CFLAGS)
从n.c
编译生成,而无需编写相应的规则 - 编译C程序:
n.o
会自动通过命令$(CXX) -c $(CPPFLAGS) $(CFLAGS)
从n.cc
或c.cxx
编译生成,而无需编写相应的规则 - 链接单个目标文件:
n
会自动通过命令$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)
链接生成 ,而无需编写相应的规则
隐式rule使用的重要变量:
-
CC
: Program for compiling C programs; default cc -
CXX
: Program for compiling C++ programs; default G++ -
CFLAGS
: Extra flags to give to the C compiler -
CXXFLAGS
: Extra flags to give to the C++ compiler -
CPPFLAGS
: Extra flags to give to the C preprocessor -
LDFLAGS
: Extra flags to give to compilers when they are supposed to invoke the linker
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*
模式规则 Pattern Rules
模式规则常用的两种方式:
- 自定义隐式规则
- 简化静态模式规则
# 编译任意 .c 文件到 .o 文件的模式规则
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
# 前提中没有如何模式的模式规则,仅仅只创建空的 .c 文件
%.c:
touch $@
双冒号规则 Double-Colon Rules
允许为同一个目标创建多个规则,如果是但冒号,同一个目标下后创建的规则会覆盖前一个。
all: blah
blah::
echo "hello"
blah::
echo "hello again"
命令和执行 Commands and execution
回声/静默 Command Echoing/Silencing
默认情况下,make执行command会打印当前执行的command,通过前置@符取消打印当前command。
all:
@echo "This make line will not be printed"
echo "But this will"
命令执行 Command Execution
同一行命令都会在同一个shell里执行。不同行目录会开启一个新的shell。
all:
cd ..
echo `pwd`
cd ..;echo `pwd`
cd ..; \
echo `pwd`
默认shell Default Shell
默认shell是 /bin/sh
。可以通过如何方式修改。
SHELL=/bin/bash
cool:
echo "Hello from bash"
异常处理 Error handling with -k
, -i
, and -
Add -k
make命令参数,遇到错误时继续运行,便于一次查看所有的错误。
Add a -
添加在命令前,不捕获这一行命令异常
Add -i
make命令参数,遇到错误时继续运行,即使前面的依赖失败,后面的作业会假装之前的依赖全部成功。
all: fail success something_wrong
all success:
@echo $@
fail:
exit 1
@echo $@
something_wrong:
-false
@echo $@
中断或杀死 make Interrupting or killing make
ctrl+c
杀死make会删除新生成的目标文件。
递归调用make Recursive use of make
递归调用makefile,使用特定的 $(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中使用输出 Use export for recursive make
export输出一个变量,使得sub-make的命令可以使用这个变量。
注意:export只是形式上和shell的export一样,其实两者没有关系。
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中运行时,也需要通过export输出。
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
Arguments to make
https://www.gnu.org/software/make/manual/make.html
变量 2 Variables Pt. 2
风格和修改
变量有两种风格:
- 递归式(
=
) - 只在变量被调用时,才查找变量的值,而不是在变量定义的时候。 - 简单扩充式 (
:=
) - 和通常的编程语言一样 - 只对已经定义了的变量进行扩充。
# 递归式变量 会打印"later"
one = one ${later_variable}
# 简单扩充 不会打印"later"
two := two ${later_variable}
later_variable = later
all:
echo $(one)
echo $(two)
简单扩充 (:=
)允许拼接变量。递归定义会报无限循环错误。
one = hello
# 通过 (:=) 定义 one 为简单扩充式,从而可以处理自拼接
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 # "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)
字符串替代也是一个常用而且高效的方式修改变量。查看 Text Functions 和 Filename Functions。
命令行参数和覆盖
通过 override
覆盖命令行传入的变量。运行make命令 make option_one=hi option_two=you
# 覆盖命令行参数
override option_one = did_override
# 不覆盖命令行参数
option_two = not_override
all:
echo $(option_one)
echo $(option_two)
命令列表和定义
"定义"实际上是一个命令列表。与方程无关。注意这和以分号分隔的一系列命令不同,因为每一个命令都分别在一个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)
条件语句 Makefiles
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)
This example shows you how to test make flags with findstring
and MAKEFLAGS
. Run this example with make -i
to see it print out the echo statement.
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}
的形式调用函数。你可以通过call内置函数编写自己的函数。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:
# 输出 ", a , b , c". 注意添加了空格
@echo $(bar)
字符串替换
$(patsubst pattern,replacement,text)
:
“在文本中查找符合模式的空格分隔的单词,并用替换字符替换。模式可能包含通配符‘%’,匹配单词中任意多个字符。如果替换字符中也包含‘%’, ‘%’会被替换成被匹配字符串对应的部分。只有模式中第一个‘%’会被匹配并按照这个规则替换;后续的‘%’不会改变。”(GNU docs)
简写 $(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 var,list,text)
. 将一系列空格分割的单词转换成另一个。 var
被设置成list里面的单词, text
扩充每一个单词。
下面的例子为每一个单词追加感叹号。
foo := who are you
# 对于foo中的每一个 "word" , 在word后添加感叹号并输出。
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:
# 输出 "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) # 换行被替换成空格
Other Features
Include Makefiles
The include directive tells make to read one or more other makefiles. It’s a line in the makefile makefile that looks like this:
include filenames...
This is particularly useful when you use compiler flags like -M
that create Makefiles based on the source. For example, if some c files includes a header, that header will be added to a Makefile that’s written by gcc. I talk about this more in the Makefile Cookbook
The vpath Directive
Use vpath to specify where some set of prerequisites exist. The format is vpath <pattern> <directories, space/colon separated>
<pattern>
can have a %
, which matches any zero or more characters.
You can also do this globallyish with the variable 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
Multiline
The backslash ("") character gives us the ability to use multiple lines when the commands are too long
some_file:
echo This line is too long, so \
it is broken up into multiple lines
.phony
Adding .PHONY
to a target will prevent make from confusing the phony target with a file name. In this example, if the file clean
is created, make clean will still be run. .PHONY
is great to use, but I’ll skip it in the rest of the examples for simplicity.
some_file:
touch some_file
touch clean
.PHONY: clean
clean:
rm -f some_file
rm -f clean
.delete_on_error
The make tool will stop running a rule (and will propogate back to prerequisites) if a command returns a nonzero exit status.DELETE_ON_ERROR
will delete the target of a rule if the rule fails in this manner. This will happen for all targets, not just the one it is before like PHONY. It’s a good idea to always use this, even though make does not for historical reasons.
.DELETE_ON_ERROR:
all: one two
one:
touch one
false
two:
touch two
false
Makefile Cookbook
Let’s go through a really juicy Make example that works well for medium sized projects.
The neat thing about this makefile is it automatically determines dependencies for you. All you have to do is put your C/C++ files in the src/
folder.
# 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)