Makefile 学习笔记与汇总

资料:

  1. 跟我一起写Makefile - 陈皓,学习顺序建议为先看第四节、第五节熟悉基础的语法命令

  2. 上海交通大学IPADS组 - cmake与Makefile教程 B站视频

  3. 笨方法学C - Learn C the Hard way 第28讲内容

  4. https://makefiletutorial.com/#:~:text=Hello%20from%20bash%22-,Double%20dollar%20sign,variables%20in%20this%20next%20example.

Makefile的基本规则

这部分笔记是8.11第一次学习Makefile,以及“一生一芯”项目中涉及到编写一系列Makefile规则。主要参考依据是陈皓的跟我一起写Makefile

一、基本规格和格式

1
2
3
4
target ... : prerequisites ...
recipe
...
...

上述格式中,target可以是一个object file(目标文件),也可以是一个可执行文件,还可以是一个标签(label)

prerequisites表示生成该target所依赖的文件和target,这表明了依赖关系中若有一个及以上的前驱文件比target要新的话,就要执行recipe中所定义的命令。

recipe:表示该target要执行的命令(任意的shell命令)

注意:Makefile中写的指令务必以TAB键开头

二、Makefile中的变量

Makefile中的变量也就是一个字符串,可以理解成C语言中的宏。如我们可以这样定义一个变量OBJ:

1
2
OBJ = main.o kbd.o command.o display.o \
insert.o search.o

于是,上面的变量就代表了一系列.o文件,我们可以在makefile代码中用

1
$(OBJ)

的方式来指代上述一系列文件

也即可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
OBJ = main.o kbd.o command.o display.o \
insert.o search.o

edit : $(OBJ)
cc -o edit $(OBJ)

main.o : main.c
cc -c main.c
kbd.o : kbd.c def.h command.h
cc -c kbd.c
...

Makefile在列出依赖后会自动根据依赖关系进行命令的推导,也即一般情况下我们可以不用手写cc -c… 。make会推断目标为.o的时候,用cc -c来对前驱文件进行命令的执行。

故写成

1
2
main.o : main.c
kbd.o : kbd.c def.h command.h

即可,这种利用Make自动推导命令的方法就是make的“隐式规则”。

文件的清空:

一般稳健的格式是:

1
2
3
.PHONY: clean
clean :
-rm edit $(OBJ)

.PHONY表示clean是一个“伪目标“。clean一般都放在文件的最后一面成为make的默认目标。

makefile中的注释是使用#字符的

通配符: makefile接受一般的通配符,如*

1
objects = *.o

上面这个例子,说明通配符也可以出现在变量中。但是,实际上变量中并不会直接展开,其值实际上就是可以看作字符串*.o(这和C语言的宏是类似的)。而如果我们想要通配符在变量中展开,需要这样操作:

1
objects := $(wildcard *.o)

也就是用工具wildcard检索所有.o文件并作为一个值赋给objects

自动生成依赖性:在编译器自动生成的依赖关系中,若我们不想逐一手写若干文件的依赖关系,而由编译器自动生成,则可以在编译选项中加上-M参数。也即:

1
cc -M main.c

更好的是添加-MM参数,-M参数本身可能会将一些标准库的头文件也包含进来。

具体的,我们man gcc并搜索参数-E即可查找到如下内容:

-M参数将不会输出预处理的结果,而是会输出一系列适配make的规则,来描述目标生成源文件之间的依赖关系。预处理器会输出一个make规则,这个make规则包含目标文件名、逗号和所有包含的文件。

可以利用编译器的这个功能,来实现编译规则的自动化。GNU组织建议把编译器为每一个源文件自动生成的依赖关系放到一个文件中,为每一个name.c的文件都生成一个name.d的Makefile文件,.d文件中就存放着对应.c文件的依赖关系。

于是,我们就可以写出.c文件和.d文件的依赖关系,并让make自动更新或生成.d文件,并把其包含到我们的主Makefile中,即可自动化生成每个文件之间的依赖关系了。

一般来说,我们都需要手动设定对依赖文件一系列处理(编译、链接)的参数,下面是参考gcc的手册得到的内容:

1
2
3
4
gcc的一系列选项:
-c选项,将源文件编译并且汇编,但是并不对其进行链接。最后输出的结果是对每个源文件都输出一个可重定位文件,没有最后连接成一个可执行文件。
-S选项,在编译其结束后停止,不去进行汇编(将汇编代码转换成二进制代码的过程),最后输出的结果是一个汇编代码文件。这些文件默认处理后的输出名都是将.c替换为.o等。
-E选项,在预处理结束后停止,不去运行编译的过程,最后输出的结果是预处理后的源代码文件。而不需要预处理的源文件会被忽略。

自动化变量?$<, $@

书写命令:

Makefile中每条规则的命令和操作系统Shell的命令行是一致的,make会按照顺序一条一条地执行命令,每条命令的开头必须以Tab键开头,除非,命令是紧跟在依赖规则后面的分号后的。在命令行之间的空格或是空行会被忽略,但是如果该空格或空行是以Tab开头,则make会认为其是一个空命令。

UNIX下可能会使用不同的Shell,但是make的命令默认是被/bin/sh – UNIX的标准Shell来解释执行的。除非指定一个其他的Shell。

在Makefile中,#是注释符,类比C++中的//

显示命令:

make将要执行的命令行执行前输出到屏幕上,当我们用@字符在命令行前,那么,这个命令将不被make显示出来。最具代表性的例子是,我们用这个功能来向屏幕显示一些信息,如:

1
@echo "Compiling XXX"

另外,若make执行的时候,带入make参数-n或–just-print,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile。

命令执行:

当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条地执行其后地命令。需要注意的时,如果你要让上一条命令地结果作用于下一条命令时,应该使用分号分割这两条命令。比如第一条时cd,而希望第二条命令在第一条的基础上运行,那么就不能把这两条命令写到两行上,而应该写在一行上,用分号分隔。

1
2
exec:
cd ~/Desktop; pwd

使用变量:

在Makefile中定义的变量,可以类比C/C++中的宏,代表了一个文本字串,在Makefile中执行的时候会自动展开在所使用的地方。但是与宏不同的是,我们可以在Makefile的其他部分改变其数值,在Makefile中,变量可以使用在“目标”、“依赖目标”、“命令”或是Makefile的其他部分中。

变量的名字可以包含字符、数字、下划线,但不能含有:、#、=或者空格。变量是大小写敏感的。

变量基础:

变量在声明的时候需要给予初值,而在使用时,需要给在变量名前加上$符号(类似C中对指针的解引用),但最好用小括号()或是大括号{}把变量给包括起来。如果要使用真实的$字符,那么需要$$来表示。

用变量来定义变量:

一种简单的方式如下所示,就是简单地使用等号=,左边是变量,右边是要给予它地值,左侧变量地值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,也可以用后面定义的值。

1
2
3
4
5
6
foo=$(bar)
bar=$(ugh)
ugh=Huh?

all:
echo $(foo) # 将会打印出Huh?,因为$(foo)->$(bar)->$(ugh)->Huh?

但是这种定义也会有问题,比如嵌套定义,从而引发如wildcard等程序发生不可预知的错误。

另一种更规范的定义变量的方法是使用:=操作符,如下所示:

1
2
3
4
5
6
7
x := foo
y := $(x) bar
x := later

# -----等价于-----
y := foo bar
x := later

这种方法,前面的变量不能使用后面的变量,只能使用前面已经定义好了的变量。

另一些更复杂的例子:

1
2
3
4
5
6
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami)
host-type := $(shell arch)
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif

使用条件判断

使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。

以下是一个实例,在Makefile中判断所用的C编译器是否是gcc,如果是则用GNU函数编译目标。

1
2
3
4
5
6
foo: $(objects)
ifeq ($(CC), gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif

更具体的条件判断变式在Makefile编写中用处不是很大,不多赘述。

使用函数

在Makefile中可以使用函数来处理变量,从而让我们的命令或规则更加灵活、具有智能。函数调用后,函数的返回值可以当作变量来使用。

Basic Syntax:

函数的调用语法:

1
$(<function> <arguments>)

这里,function是函数名,make支持的函数不多。arguments为函数的参数,参数间以逗号”,”分隔。而函数名和参数之间以空格分隔。函数调用以$开头,以圆括号或花括号把函数名和参数括起来。和变量的规约很像,函数中的参数可以使用变量,为了风格的同一,函数和变量的括号最好一样,如使用$(subst a,b,$(x))这样的形式,而不是$(subst a,b,${x})这样的形式。因为统一会更清楚。

1
2
3
4
5
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))

Makefile中常用的函数:

  1. subst
1
2
$(subst <from>,<to>,<text>)
$(subst ee,EE,feet on the street) #返回fEEt on the strEEt

是字符串替换函数,功能为将字串中的from字符串替换成to字符串,最后会将函数返回被替换过后的字符串。

  1. patsubst
1
2
$(patsubst <pattern>,<replacement>,<text>)
$(patsubst %.c,%.o,x.c.c bar.c) # 将bar.c和x.c.c中.c前面的内容保留,.c替换为.o,也即x.c.o bar.o

模式字符串替换函数,功能为查找text中的单词(单词用空格、Tab、回车或换行分隔)是否符合模式pattern,若匹配,则将其以replacement替换。这里pattern可以包含通配符%、任意长度的字串。若replacement里面包含%,则将会代表pattern中的哪个%所代表的字串。最后会返回被替换过后的字符串

  1. strip函数:去空格,传入一个字符串,去掉其开头和结尾的空格,返回处理后的字符串。
  2. findstring

查找字符串函数

1
$(findstring <find>,<in>)

在字串in中查找find字串,若找到则返回find,否则返回空字符串。

  1. filter
1
$(filter <pattern>,<text>)

过滤函数,以pattern为模式过滤text字符串中的单词,保留符合模式的单词,可以有多个模式。

如:

1
2
3
4
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo #注意,这一行里面%c %s是一组模式,可以用空格间隔,逗号之后的是过滤的内容源
# 筛选出来.c,.s文件,输出foo.c bar.c baz.s

注:filter-out函数与之相对应,反过滤函数,执行的效果是去除符合模式patt的值,patt可以有多个。

  1. sort

排序函数,可以给字符串中的单词排序(升序)

1
2
$(sort <list>)
$(sort foo bar lose) #返回bar foo lose

注意,sort函数会去掉list中相同的单词。

  1. word

取单词函数,取字符串text中的第n个单词。会返回字符串中的第n个单词,如果n比text中的单词数要大,那么返回空字符串。

1
$(word <n>,<text>)
  1. basename

取前缀函数,功能为从文件名序列name中取出各个文件名的前缀部分。最后会返回文件名序列的前缀序列,如果文件没有前缀,则返回空字串。

1
2
3
4
$(basename <names...>)
# 示例:
$(basename src/foo.c src-1.0/bar.c hacks)
# 返回值为:src/foo src-1.0/bar hacks
  1. addsuffix

加后缀函数,功能是讲后缀加到names中的每个单词后面,最后会返回加过后缀的文件名序列。

1
2
$(addsuffix <suffix>,<names...>)
$(addsuffix .c,foo bar) #返回值为foo.c bar.c
  1. addprefix
1
2
$(addprefix <prefix>,<names...>)
$(addprefix src/,foo bar) #返回值为src/foo src/bar
  1. dir/notdir

取目录/取文件函数

1
2
$(dir <names...>) #从文件名序列中去除目录部分,目录部分是指最后一个反斜杠/之前的部分,如果没有反斜杠,则返回./当前目录 若names传入src/foo.c hacks,返回值为src/和./
$(notdir <names...>) #取文件函数,从文件名序列中取出非目录部分,非目录部分是指最后一个反斜杠/之后的部分,传入src/foo.c hacks,返回foo.c hacks

一篇讲自动化变量的tutorial: https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html#:~:text=What%20you%20do%20is%20use,for%20the%20source%20file%20name.

Learn Makefiles with the tastiest examples

这部分笔记更新于2023.9.9,本意是在完成PA2的AM时注意到了许多Makefile的组织,尤其是自动化变量不是很明白,找到一篇英文教程继续学习。

开始

Makefiles时用来帮助组织一个大型程序,并决定这个大型程序有哪些部分需要被重新编译。在许多例子中,C或者C++的文件都会被编译,其他的语言也有如Make的类似的编译管理工具。Make也可以做许多超过编译的事情,当我们需要一系列指令来运行,且每个指令是否运行取决于文件是否被修改了。这份教程会专注于C/C++的编译使用情况。

Make的替代

许多其他C/C++的工程项目构建工具有SCons,Cmake,Bazel和Ninja。Microsoft Visual Studio也有类似的机制。其他的一些语言,如Go,Rust也有各自构建工程的工具。

解释型语言如Python,Ruby和raw Javascript是不需要类似Makefile这种工具的,Makefile的目标是为了编译任何需要编译的文件,基于那些文件被修改了。但是当文件是解释性语言编写的时候,就不需要任何重新编译的过程了。

Makefile语法:

makefile的语法如下所示:

1
2
3
targets: prerequisites
command
command
  1. 目标时由文件名组成的,文件名之间会被空格隔开,通常对于每个规则,只有一个构建目标;
  2. 命令是一系列构造目标的指令,注意makefile中的命令需要用tab分开;
  3. 前驱依赖部分也是文件名,被空格分开,这些文件需要在命令和目标运行前就存在,这些前驱依赖也被成为dependencies

make使用了文件系统的时间戳作为一个端口来检验是否存在一些改变,这是一种合理的启发式做法,因为文件系统维护的库文件时间戳只会在文件被修改的时候随之改变。但注意这里并不总是成立的,我们完全可以修改一个文件的时间戳,比如做旧于target,那么make就不会认为文件被修改了。

Makefile中出现的第一个target,是作为default target出现的。也即当我们不向make传参时会自动实现的target。

Makefile中的变量是以字符串的形式出现的,对于单引号和双引号,Make里面时没有实际意义的,他们只是赋给变量的具体的字符,而引号对于shell/bash时有用的,而且一些如printf的指令也需要他们。

引用变量是需要使用

1
2
3
4
5
6
7
8
9
$()或者${}来括住的。

x := dude
all:
echo $(x)
echo ${x}

#Bad practice, but works,尽量不要这么写变量!变量本身作为字符串可能展开会由一些问题。
echo $x

Makefile中的目标

当我们像构造多个目标并运行多个目标的时候,就可以构建一个all目标放在Makefile文件头;因为这是第一个列出来的命令,make如果不跟确定构建的对象的时候就会运行all。

当我们需要对某个规则同时构建多个目标的时候,命令需要对每个构建目标都执行一次。

1
2
3
4
all: f1.o f2.o

f1.o f2.o:
echo $@

使用自动化变量$@,代表所有的要构建的目标;上述Makefile内容与下文等价:

1
2
3
4
5
6
all: f1.o f2.o

f1.o:
echo f1.o
f2.o:
echo f2.o

Makefile中的自动化变量和Wildcards

在Makefile中,* 和 %都是通配符,但他们分别代表着完全不同的事务。* 会搜索文件系统来查找匹配的文件名,可以用在构建目标、前驱需求或者wildcard函数,不能用在变量定义上(定义在变量中的通配符是不会自动扩展的,只会以一个*字符存在)

注意,通配符除了在wildcard函数中运行,否则,在*没有匹配到合法内容的时候,会以 * .c这样的形式保留在原地。

% 是很有用的,但是有时候会因为他的多变情况而导致疑惑。

当我们在matching模式下使用,它会在同一个字符串中匹配一个或多个字符,这种匹配方式被称为stem;

当我们使用replacing模式,就会将stem中匹配的内容进行相应的替换;

Makefile中的自动化变量:

1
2
3
4
5
6
7
8
9
10
11
$@ 代表所有的目标target名称,如果目标是一个库文件对象(archieve),那么$@就是其文件名;而在匹配规则下,会有多个目标,$@就对应着所有导致规则运行的任何目标对象的名称。

$% 代表目标成员的名称,当目标对象是一个库成员时,如目标对象为foo.a(是bar.o的库文件),那么$%就是bar.o,而$@就是foo.a

$< 代表第一个前驱依赖的名称,如果目标是从隐式推导规则获得的,那么这就会成为第一个前驱依赖加上隐式规则。

$? 代表时间戳新于目标文件的所有前驱依赖,并且用空格隔开;对于还没有创建的文件,显然就代表所有的前驱依赖内容了;对于是库文件类型的前驱依赖,只有被命名的对象是会被使用的;

$^ 表示所有前驱依赖的文件对象,用空格隔开。


Make的隐含规则

  1. 编译一个C程序,可重定位目标文件n.o 通常会自动定位到其源文件n.c,并通过
1
$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@

这种隐含规则完成编译。

  1. 编译一个C++程序,可重定位目标文件n.o会被自动通过n.cc或者n.cpp来按照
1
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@

这种隐含规则完成编译。

  1. 链接一个单独的可执行目标文件n,会自动从可重定位目标文件n.o,通过:
1
$(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@

完成链接。以下是重要的,通过隐含规则执行相应功能的变量。

1
2
3
4
5
6
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

以下是一个,我们不告诉make如何直接做编译的构建例子:

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*

通常,不推荐我们用隐式规则来完成编译。

Make的静态模式规则

静态模式规则是另一种减少Make代码量的方式,以下是具体的语法:

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

精髓在于给定的target会被target-pattern相匹配(通过一个%通配符来实现)。无论什么被匹配上了,都被称作stem(主干),主干可以接着被替换到prereq-patterns里面,来生成目标的前置依赖。

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
18
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

注意,$(objects): %.o : %.c这一句的意思在于,在objects中,objects变量的内容就是target,它会被后面的%.o匹配,也即%.o中的%分别对应着foo, bar和all,然后在后面,%,c作为prereq-pattern会生成foo.c, bar.c和all.c,等价于:

1
2
3
foo.o: foo.c
bar.o: bar.c
all.o: all.c

在引入的静态模式规则的同时,可以结合filter过滤函数来使用,这样可以去匹配到对应正确的文件。

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)

这里的自动化变量,$<会随着$@具体展开的内容而变化!

Makefile的模式规则

模式规则是非常常用的规则,但是也非常令人困惑。我们可以以两种视角来理解。

一种让我们来定义自己隐含规则的方式;

一种更简单形式实现的静态模式规则;

我们从一个例子出发:

1
2
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

上面的模式规则就能够实现将每一个.c文件编译转换为一个.o文件,模式规则会在目标集合中包含一个通配符%,通配符可以匹配任何非空的字符串,而且其他的字符可以自己匹配。而在前驱依赖的中%所对应的这部分模式规则,则会代表相同的在目标中匹配的主干部分。

上面的-c选项可以让链接器不继续运行,也即以输出.o文件为最终停止的时候。这里,自动化变量$<和$@会通过make自己的推导来逐一进行匹配。最后输出所需要的.o文件。

Makefile命令与执行

在每一行的命令前面添加一个@能够使得其能够在执行的是时候不会被打印出来。在指令执行的时候,每个指令都是在一个新的shell中被运行的,或者其效果可以被这样理解。也即,如果我们要做cd ..,echo pwd这样的操作,就必须将他们放到同一行来执行。或者在行尾添加 \表示换行。

如果我们想要一个字符串有一个$号,那么只需要加两个$$即可实现。如$$sh_var在Makefile中,等价于在字符串$sh_var。如果只有一个,会被解引用为sh_var内部的值,两个$就可以理解成发生了转义。