自动化构建工具——Make/Makefile
# 自动化构建工具——Make/Makefile
Makefile 是一种强大且灵活的构建工具,具有广泛的适用性,使得软件构建过程更加高效和可管理。
# make和Makefile简介
Makefile是一个配置文件,可以对程序编译进行管理,其优点是:
- 避免复杂命令行编译语句
- 减少编译所需时间
- 让编译自动运行
make是解释Makefile文件中指令的命令工具。
# Makefile的工作原理分析
+--------------+
| myprogram |
+------+-------+
|
+-----------+-----------+
| |
v v
+-------------+ +------------+
| main.o | | util.o |
+------+------+ +------------+
| |
| |
v v
+-------------+ +------------+
| main.c | | util.c |
+-------------+ +------------+
+-------------+
| main.h |
+-------------+
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这个示意图展示了myprogram
作为最终的目标,它依赖于main.o
和util.o
。main.o
和util.o
各自依赖于相应的.c
文件和main.h
头文件。
当make
命令运行时,它会检查每个文件的时间戳以确定哪些文件需要重新编译。根据文件的时间戳,它会选择性地重新编译main.c
和util.c
,然后重新链接它们以生成最终的myprogram
可执行文件,而不必重新编译所有文件。
- 结论:Makefile确保了只更新发生变化的部分,提高了构建效率。
# Makefile操作规则
Makefile的操作规则
- 如果工程没有编译过,所有的C文件都要编译并被连接。
- 如果工程的某几个文件被修改,只需要编译被修改的这几个文件,并重新链接目标程序
- 如果工程的头文件被修改了,那么所有包含此头文件的源文件都要重新编译,并重新连接目标程序
# Makefile的规则格式
target … : prerequisites …
recipe
…
…
2
3
4
target是目标文件,可以是.o中间目标文件、可执行文件或者标签。
prerequisites(前提条件)是要生成目标文件所依赖的文件。
recipe是(配方)生成规则,recipe前面要有一个Tab符号(不能用空格代替)。
# 一个简单的Makefile
这是一个简单的 makefile,描述了名为 edit 的可执行文件如何依赖于八个目标文件,而这些目标文件又依赖于八个 C 源文件和三个头文件。
在此示例中,所有 C 文件都包括 defs.h,但只有那些定义编辑命令的文件包括 command.h,并且只有更改编辑器buffer的底层文件包括 buffer.h。
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
使用此 makefile 创建名为 edit 的可执行文件
make
1Makefile中只有第一个目标是默认目标。上述例子中为'edit',当我们执行"make"命令的时候,默认最终构建'edit'可执行文件,我们可以使用
.DEFAULT_GOAL
来覆盖这种行为。使用此 makefile 从目录中删除可执行文件和所有目标文件
make clean
1目标'clean'不是一个文件,只是一个动作的名称。由于通常不希望执行此规则中的操作,'clean'不是任何目标的前提条件。因此,除非明确告诉它,否则make不会执行任何与它有关的操作。
当一个目标是一个文件时,如果它任意一个前提条件发生变化,它就需要重新编译或重新链接。此外,任何自动生成的前提条件应该首先更新。在这个示例中,'edit'依赖于八个目标文件;目标文件'main.o'依赖于源文件'main.c'和头文件'defs.h'。如果'main.c'更新,那么先更新'main.o',然后更新'edit'。
# 伪目标(PHONY Target)
伪目标是一个实际上不是文件名称的目标;它只是一个名称,用于在明确请求时执行一个配方。使用伪目标有两个原因:避免与同名文件冲突和提高性能。
如果编写了一个规则,其配方不会创建目标文件,那么每当需要重新生成目标时,该配方都将被执行。以下是一个示例:
clean:
rm *.o temp
2
因为 rm 命令不会创建名为 clean 的文件,所以可能永远不会存在这样的文件。因此,每次输入“make clean”时都会执行 rm 命令。
在上面这个示例中,如果在此目录中创建了一个名为clean的文件,clean目标将无法正常工作。由于它没有前提条件(prerequisites),clean将始终被视为最新,并且其配方不会被执行。为了避免这个问题,可以使用.PHONY
将clean声明为伪目标,如下所示:
.PHONY: clean
clean:
rm *.o temp
2
3
现在,无论是否存在名为 clean 的文件,“make clean”都将运行rm *.o temp
。
一个伪目标不应该是一个真实目标文件的前提条件。
# 使用伪目标all构建多个程序
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
2
3
4
5
6
7
8
9
10
11
当一个目录包含多个程序时,最方便的做法是在一个Makefile中描述所有程序。由于Makefile中只有第一个目标是默认目标,因此通常将其作为名为'all'的伪目标,并将所有单独的程序作为前提条件。
# 变量
Makefile中可以使用变量,且有以下特点:
- 变量类似于C语言的宏,但值可修改
- 变量名大小写敏感
- 变量名不应该包含:#=或空格
- 变量使用时用$(var)形式
# 一个例子
在makefile中定义变量的最简单方式是使用等号(=)运算符。例如,将命令gcc分配给变量CC:
CC = gcc
这也被称为递归扩展变量,它在规则中的使用如下所示:
hello: hello.c
${CC} hello.c -o hello
2
正如您可能已经猜到的那样,当这个规则传递给终端时,配方会展开如下:
gcc hello.c -o hello
${CC}
和$(CC)
都是用于调用gcc的有效引用方式。但是,如果尝试将变量重新分配给自身,这将导致无限循环。让我们验证一下:
CC = gcc
CC = ${CC}
all:
@echo ${CC}
2
3
4
5
运行make会产生以下结果:
$ make
Makefile:8: *** Recursive variable 'CC' references itself (eventually). Stop.
2
为了避免这种情况,我们可以使用:=
运算符(这也被称为简单扩展变量)。我们可以正常运行以下makefile:
CC := gcc
CC := ${CC}
all:
@echo ${CC}
2
3
4
5
# 自定义变量
赋值方式 | 示例 | 说明 |
---|---|---|
基本赋值 | VARIABLE_NAME = some_value | 基本赋值使用等号(=)分配变量值。 |
VARIABLE_NAME := new_value | 冒号等号(:=)分配变量值,覆盖之前的值。 | |
递归赋值 | VARIABLE_NAME = $(OTHER_VAR) | 递归赋值使用等号(=),支持延迟求值。 |
VARIABLE_NAME := $(OTHER_VAR) | 冒号等号(:=)分配变量值,立即求值。 | |
条件赋值 | VARIABLE_NAME ?= default_value | 条件赋值只在变量未定义时才分配值。 |
追加赋值 | VARIABLE_NAME += appended_value | 追加值到现有变量。 |
# 自动化变量
变量符号 | 描述 | 示例 |
---|---|---|
$@ | 当前目标的名称,例如:target: dependencies 中的target 。 | all: myprogram $@ 在这里代表myprogram |
$< | 第一个依赖项的名称,例如:target: dependencies 中的dependencies 中的第一个文件。 | myprogram: main.c util.c $< 在这里代表main.c |
$^ | 所有依赖项的名称,以空格分隔,例如:target: dependencies 中的所有依赖项。 | myprogram: main.c util.c other.c $^ 在这里代表main.c util.c other.c |
$? | 所有比目标文件新的依赖项的名称,以空格分隔。 | myprogram: main.c util.c other.c $? 在这里代表比myprogram 更新的依赖项 |
# 预定义变量
预定义变量 | 描述 |
---|---|
CC | C编译器的名称,默认为cc 。 |
CFLAGS | C编译器的标志,通常包括编译选项和警告标志。 |
LDFLAGS | 链接器的标志,通常包括库路径和库文件。 |
LDLIBS | 链接器的库文件列表。 |
RM | 删除文件的命令,默认为rm -f 。 |
AR | 归档工具的名称,默认为ar 。 |
ARFLAGS | 归档工具的标志。 |
MAKE | 调用make的命令,默认为make 。 |
MAKEFLAGS | 包含make命令行选项的变量。 |
SHELL | Shell的名称,默认为/bin/sh 。 |
这些预定义变量在Makefile中提供了有关编译器、链接器、命令和其他构建相关信息的访问。可以在Makefile中使用它们来自定义构建规则和行为。
# 函数
以下是一些常见的Makefile函数,包括示例,以表格形式列出:
函数 | 描述 | 示例 |
---|---|---|
$(subst from,to,text) | 在文本中将字符串from 替换为字符串to 。 | $(subst old,new,hello world) 返回 hello new world |
$(patsubst pattern,replacement,text) | 使用模式匹配替换文本中的部分字符串。 | $(patsubst %.c,%.o,main.c util.c) 返回 main.o util.o |
$(strip string) | 去掉字符串中的前导和尾随空格。 | $(strip leading and trailing spaces ) 返回 leading and trailing spaces |
$(findstring find,in) | 检查字符串find 是否存在于字符串in 中。 | $(findstring world,hello world) 返回 world |
$(filter pattern...,text) | 从文本中选择符合给定模式的部分。 | $(filter %.c %.cpp,main.c util.cpp other.o) 返回 main.c util.cpp |
$(wildcard pattern) | 返回匹配指定模式的文件列表。 | $(wildcard *.c) 返回所有以.c 为扩展名的文件列表 |
$(shell command) | 执行shell命令并返回其输出。 | $(shell date) 返回当前日期和时间 |
$(foreach var,list,text) | 对列表中的每个元素执行指定的文本操作。 | $(foreach file,a.c b.c,$(file:%.c=%.o)) 返回 a.o b.o |
$(if condition,then-part[,else-part]) | 根据条件执行不同的操作。 | $(if $(DEBUG),-g,-O2) 返回 -g 如果 DEBUG 变量已定义,否则返回 -O2 |
$(call function,args...) | 调用自定义函数并传递参数。 | 自定义函数示例:define myfunc echo $(1) $(2) endef $(call myfunc,arg1,arg2) 返回 arg1 arg2 |
上面演示了如何在Makefile中使用这些常见函数来处理文本、执行条件判断、执行外部命令以及定义和调用自定义函数。这些函数是Makefile中的强大工具,可用于自动化构建任务。
# 一个复杂一点的Makefile
# Usage:
# make # 编译所有二进制文件
# make clean # 删除所有二进制文件和对象文件
.PHONY = all clean
CC = gcc # 使用的编译器
LINKERFLAG = -lm
SRCS := $(wildcard *.c)
BINS := $(SRCS:%.c=%)
all: ${BINS}
%: %.o
@echo "Checking.."
${CC} ${LINKERFLAG} $< -o $@
%.o: %.c
@echo "Creating object.."
${CC} -c $<
clean:
@echo "Cleaning up..."
rm -rvf *.o ${BINS}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
以
#
开头的行是注释。.PHONY = all clean
定义了伪目标 all 和 clean。变量
LINKERFLAG
定义了在一个规则中与gcc一起使用的标志。SRCS :=$(wildcard *.c)
:$(wildcard pattern)
是用于文件名的一个函数。在这种情况下,所有具有 .c 扩展名的文件将被存储在变量 SRCS 中。BINS := $(SRCS:%.c=%)
:这称为替代引用。在这种情况下,如果SRCS
具有值'foo.c bar.c'
,BINS
将具有'foo bar'
。all: ${BINS}
:伪目标 all 调用${BINS}
中的值作为单独的目标。规则:
%: %.o @echo "Checking.." ${CC} ${LINKERFLAG} $<; -o $@
1
2
3让我们看一个示例来理解这个规则。假设 foo 是
${BINS}
中的一个值。那么%
将与 foo 匹配(%
可以匹配任何目标名称)。以下是该规则的展开形式:foo: foo.o @echo "Checking.." gcc -lm foo.o -o foo
1
2
3%
被替换为foo
。$<
被替换为foo.o
。$<
被模式匹配以匹配前提条件,$@
匹配目标。此规则将针对${BINS}
中的每个值调用。规则:
%.o: %.c @echo "Creating object.." ${CC} -c $<;
1
2
3上一条规则中的每个前提条件(prerequisites)都被视为此规则的目标(target)。以下是该规则的扩展形式:
foo.o: foo.c @echo "Creating object.." gcc -c foo.c
1
2
3Makefile的最后,我们移除了目标 clean 中的所有二进制文件和对象文件。
以下是上述 Makefile 的重写,假设它位于只有一个文件 foo.c 的目录中:
# Usage:
# make # 编译所有二进制文件
# make clean # 删除所有二进制文件和对象文件
.PHONY: all clean
CC = gcc # 使用的编译器
LINKERFLAG = -lm
SRCS := foo.c
BINS := foo
all: foo
foo: foo.o
@echo "Checking.."
gcc -lm foo.o -o foo
foo.o: foo.c
@echo "Creating object.."
gcc -c foo.c
clean:
@echo "Cleaning up..."
rm -rvf foo.o foo
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 参考文献
有关makefile的更多信息,请参阅 GNU Make 手册PDF版 (opens new window) 或 GNU Make 手册在线 (opens new window),其中提供了完整的参考和示例。
- 01
- Linux系统移植(五)--- 制作、烧录镜像并启动Linux02-05
- 03
- Linux系统移植(三)--- Linux kernel移植02-05