Makefile
使用 Makefile 比直接执行 gcc 命令编译程序的优点主要体现在以下几个方面:
- 自动判断依赖关系
Makefile 可以根据目标文件和依赖文件之间的依赖关系,自动判断哪些目标需要重新编译,只重新编译必要的文件,提高编译效率。
- 简化编译过程
编写 Makefile 后,编译过程可以简化为在命令行执行 make 命令,而不需要每次都输入复杂的 gcc/g++ 命令。同时 Makefile 也很容易配置、修改和维护。
- 指定编译规则
可以用 Makefile 中的规则精确控制文件编译的顺序、使用的编译器和编译参数等,避免重复工作。
- 支持增量编译
只需要重新编译修改过的文件,省去重复编译的时间。
- 支持并行编译
Makefile 支持同时利用多个 CPU 进行编译,缩短编译时间。
- 生成库文件等
可以通过 Makefile 自动生成项目所需的库文件等,简化整个构建流程。
- 规范化编译过程
使用 Makefile 可以将编译过程规范化,方便团队协作和程序维护。
总之,使用 Makefile 可以大大简化和规范化编译过程,提高编译效率,所以大型 C/C++ 项目基本上都会使用 Makefile 进行编译。
Makefile的执行是依赖于时间戳的新旧,当创建了一个大体量的文件后,我需要的是快速编译,但是每一次修改之后重新去实现编译花费的时间非常多,Makefile的目的就是在抛弃了小内存空间后用内存换取时间效率,通过在编译的时候,保存.o文件,这样在修改了某一个.c文件之后,只需要把那某一个.c文件编译为.o文件之后,同一链接。可以大幅度的降低消耗的时间
基本格式/规则
目标:依赖
命令
Makefile是一个用于构建项目的配置文件,主要用于C/C++项目,但也可用于其他语言。它由一系列的规则(rules)组成,每个规则定义了如何生成一个或多个目标文件(targets)。
#规则的格式
# 目标文件:[依赖对象]
# [<tab>命令1]
# [<tab>命令2]
# [<tab>...]
all:m
m:main.c
gcc -o m main.c
这里的all是一个伪目标,并没有实际的意义,作为makefile的一个开头和进入
一个规则包含目标,依赖,命令可以没有依赖和命令,但是不能没有目标
目标可以理解为你最终需要实现的文件或者其他指令 依赖可以理解为这个目标的实现需要这个“依赖”才能完成
all:m
# 第二个规则,有目标(m),依赖(main.c),也有命令(gcc -o m main.c)
m:main.c
gcc -o m main.c
# 第三个规则,有目标(clean),没有依赖,有命令(rm -rf m)
# 没有依赖的规则,会直接执行命令
clean:
rm -rf m
# 目标和依赖都可以包含多项,项与项之间用空格分隔
aa:bb cc # 依赖中包含两项
bb cc: # 目标中包含两项,每个项会执行一次命令
echo $@ # $@ 代表目标
规则的顺序
规则的顺序不太重要,make 会自动依据依赖关系查找规则执行
但入口规则(即第一个规则)是默认首先执行的规则,
除非在执行 make 时指定了目标,否则就会先执行入口规则
all:m
m:main.o
gcc -o m main.o
main.o:main.c
gcc -c -o main.o main.c
个人觉得其实这个顺序的关系就是一层层的往下找。首先从all进入,发现依赖于m,想实现m,则需要main.o;main.o的实现是由main.c决定,则实现的是gcc -c -o main.o
这样就是先实现main.c转换为main.o的工作;实现了main.o接下来就是找到上一层,会执行gcc -o m main.o
的命令,生成的就是一个名为m的可执行文件
补充知识:
- gcc <*.c>的文件的时候就是执行的是从.c文件直接产生了一个可执行文件a.out
- gcc -c执行的是从.c文件到.o文件,只进行编译,但是不执行链接
- gcc -o后面加上文件名执行的是指定输出的文件名
- gcc -c -o mian.o mian.c 和gcc -c mian.c -o main.o是一样的,主要看那个-o后面跟的是什么
时间戳
Makefile 是根据每一次文件更新的时间所判断的,如果一个.c文件的时间比.o文件产生的时间更新,那就知道这个文件进行了更改,需要将这个文件重新编译
但是问题就来了,当发生在时间又差值的地区,例如东半球和西半球,收到同样的文件,但是时区不一样,Makefile就没法实现更新,无法执行编译的任务
问题的解决就是touch,touch的目的就是触摸一次时间戳,更新,不要小看touch的能力,不是只用来产生文件的,当文件已经产生了之后,就需要去一遍一遍的更新时间戳
伪目标
我们在前面的时候,举例的时候,用的是all作为文件的进入,这其中all就是一个伪目标,因为它不是最终要完成的目的/目标
伪目标是指并不是为了创建那个目标的目标,而是为了执行一些命令。
常见的伪目标有 all, install, uninstall, clean 等。
当我们在创建文件的时候,可能在某些时候,会恰好创建一些文件的名称为all,install,unintall,clean,那么这个时候是make,我就没法判断是否是Makefile中的目标,还是我书写的文件
解决办法就是在文件头加上这样一句话:
.PHONY:all clean install unstall
phony(虚伪的),意思就是在.PHONY:
后面,就是伪目标,不是一个文件的名称
make的引用
make 命令默认按以下顺序查找 makefile 文件来执行
GNUmakefile, makefile, Makefile
也可以不使用以上三个名称,而另外指定 makefile 的文件名执行,
带 "-f makefile文件名" 选项执行即可,如
make -f GNUmakefile1
如果要执行指定目录下的 makefile 文件,使用 "-C 目录" 选项
-f 和 -C 选项可以同时使用,如:
make -C src -f Makefile
执行指定的目标
make all
make clean
当然,可以同时指定多个目标编译
make main.o test.o
all:test
@echo "This is all"
test:
@echo "This is test"
clean:
@echo "This is clean"
install:
@echo "This is install"
这里的@
的作用是不会执行echo "This is all"
只会执行This is all
自定义变量
makefile 中的变量有 :自定义变量,预定义变量,自动变量
分别类似于 shell 中的:自定义变量,环境变量,系统变量
自定义变量方式
-
递归展开
VAR = value
value 中的变量会递归展开,特别是如果 value 中又包含 VAR 时,会造成无穷循环展开,最终展开失败
-
简单拓展方式
VAR := value
value 中的值会展开一次,如果value 中又包含 VAR 时,则只会展开一次,不会无穷展开
-
变量为空时赋值
VAR ?= value
VAR 的值为空才赋值 value
-
追加赋值
VAR += value
在 VAR 原值的基础上追加 value 值,不是算术累加运算
AAA = $(BBB)
BBB = CCC # 递归展开时不论定义的顺序
#CCC = $(CCC) DDD # Makefile:39: *** Recursive variable 'CCC' references itself (eventually). Stop.
EEE := 123
FFF := $(EEE) # 非递归展开,引用的变量必须事先定义
GGG := $(HHH) # 只展开一次,在后续定义的值不会用于当前变量
HHH := 456
JJJ ?= 789 # 此时 JJJ 为空,因此赋值成功
JJJ ?= abc # 此时 JJJ 非空,因此不赋值
JJJ += abc # 追加赋值
all:
@echo AAA = $(AAA)
@echo CCC = $(CCC)
@echo FFF = $(FFF)
@echo GGG = $(GGG)
@echo JJJ = $(JJJ)
$
的作用是用来引用变量的,定义的时候不用加$
,在变量调用的时候需要加上$
预定义变量
常见的预定义变量,有的有默认值,有的没有:
AR 归档维护程序的名称,默认值为 ar。 ARFLAGS 归档维护程序的选项,默认值 rv。 CC C 编译器的名称,默认值为 cc。 CFLAGS C 编译器的选项,无默认值。 LDFLAGS C 链接器的选项,无默认值。 RM 文件删除程序名称,默认值为 rm –f。 CXX C++ 编译器的名称,默认值为 g++。 CXXFLAGS C++ 编译器的选项,无默认值。
可以重新定义预定义变量,即将其变为自定义变量。
使用 export 将 CFLAGS 导出为全局预定义变量,可以用于子 Makefile 中
export CFLAGS := -Wall
-Wall
是一个GCC/G++常用的编译参数,代表“启用所有编译器警告”。export
表示将后续的变量导出,使其在Makefile规则中可见。
自动变量
在 Makefile 规则中,make 维护了一些自动变量,常用的有:
$<
- 第一个依赖文件的名称$@
- 目标的完整名称$^
- 所有的依赖文件,以空格分开$*
- 不包含扩展名的目标名称$?
- 比目标新编译的依赖文件
使用 Markdown 语言描述如下:
Makefile自动变量
在 Makefile 每条规则中,make 提供以下自动变量:
$<
- 第一个依赖文件的名称$@
- 目标的完整名称$^
- 所有的依赖文件,以空格分开$*
- 不包含扩展名的目标名称$?
- 比目标新编译的依赖文件
例如:
target: dep1.o dep2.o
gcc $< $@ -o $^
这里 $<
将被替换为 dep1.o
,$@
将被替换为 target
,$^
将被替换为 dep1.o dep2.o
。
最常用的自动变量有 $<
、$@
和 $^
。
变量替换
OBJS := main.o fac.o array.o hello.o
SRCS1 := $(OBJS:o=c)
SRCS2 := $(OBJS:.o=.c)
SRCS3 := $(OBJS:%.o=%.c)
all:
@echo OBJS = $(OBJS)
@echo SRCS1 = $(SRCS1)
@echo SRCS2 = $(SRCS2)
@echo SRCS3 = $(SRCS3)
这个 Makefile 中展示了三种变量替换的用法:
-
$(OBJS:o=c)
这种格式是把变量OBJS中的所有单词里匹配到的"o"替换成"c"。
例如把 "main.o" 替换成 "main.c"。
-
$(OBJS:.o=.c)
这种是把变量OBJS中所有单词里匹配到的".o"替换成".c"。
效果同上,把 "main.o" 替换成 "main.c"。
-
$(OBJS:%.o=%.c)
这种形式使用了模式匹配,
%
代表匹配任意字符。它匹配 OBJS 中所有符合
%.o
形式的单词,并替换为%.c
。同样把 "main.o" 替换成 "main.c"。
注意,这并不是直接改变了文件的类型,而是改变的是保存的变量的名称罢了,实际的类型不变
以这个Makefile为例:
OBJS := main.o fac.o
SRCS := $(OBJS:.o=.c)
执行后,OBJS的值为main.o fac.o,SRCS的值为main.c fac.c。
但这仅仅是改变了Makefile变量SRCS的值,硬盘上实际的文件main.o和fac.o是不会被改名为main.c和fac.c的。
变量替换只在Makefile执行的时候临时生效,不会对实际文件系统产生影响。
这主要利用的是Makefile的变量替换功能,使得可以方便地在规则中使用修改后的变量值,例如:
SRCS := $(OBJS:.o=.c)
%.o: %.c
$(CC) -c -o $@ $<
这里可以先通过变量替换生成一个源代码文件的变量SRCS,然后在规则中使用%匹配来编译这些源文件。
所以,Makefile变量替换是一个隔离于实际文件系统的功能,它只会在Makefile内部生效,不会对外部产生副作用。我们可以利用这一点来方便地定义转换规则。
隐式规则
与 C 语言相关的 3 条隐式规则:
C语言编译隐式规则
-
编译C程序
"%.o" 自动由 "%.c" 生成,执行命令为:
$(CC) -c $(CPPFLAGS) $(CFLAGS)
-
汇编和需要预处理的汇编程序
"%.s" 是不需要预处理的汇编源文件,"%.S" 是需要预处理的汇编源文件。汇编器为 "as"。
"%.o" 可自动由 "%.s" 生成,执行命令是:
$(AS) $(ASFLAGS)
"%.s" 可由 "%.S" 生成,C预编译器 "cpp",执行命令是:
$(CPP) $(CPPFLAGS)
-
链接单一的object文件
"%" 自动由 "%.o" 生成,通过C编译器使用链接器(GNU ld),执行命令是:
$(CC) $(LDFLAGS) %.o $(LOADLIBES) $(LDLIBS) -o %
此规则仅适用于一个源文件直接生成executable的情况。如果有多个源文件,需在Makefile中增加隐式规则的依赖文件。
以Markdown格式写出来更清晰直观一点。让我知道如果还有什么问题。
# Makefile
SRC = main.c foo.c bar.c
OBJ = $(SRC:.c=.o)
EXECUTABLE = prog
all: $(EXECUTABLE)
$(EXECUTABLE): $(OBJ)
$(CC) -o $@ $^
clean:
rm -f $(EXECUTABLE) $(OBJ)
.PHONY: all clean
这里仔细看,首先将.c文件赋给了一个变量SRC,接着将.c文件名变成了.o文件名保存给了OBJ,只是保存了变量名,但是保存的是更改了.c为.o的文件名
接着,这个目标中没有使用%.o:%.c的操作,而是直接由%.o转换为了可执行文件,越过了.c到.o的操作
这就是一种隐式规则,是系统自带的方法,检测到了.c文件后,就会编译为.o文件
模式规则
模式规则(Pattern Rules)是Makefile中的一种特殊语法,它用来定义对一类文件进行批量处理的规则。
模式规则的基本语法如下:
targets : target-pattern : prereq-patterns
commands
其中:
- targets:规则的目标文件,可以指定多个目标。
- target-pattern:目标文件模式,如
%.o
。 - prereq-patterns:依赖文件模式,如
%.c
。 - commands:生成目标的命令。
模式规则通过%
通配符匹配某一类文件。例如:
# 将所有的.c文件编译成.o文件
%.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
这个模式规则表示,所有%.o
后缀的文件依赖于%.c
文件,运行命令时$<
和$@
会被替换为相应的目标和依赖文件。
优点:
- 避免重复输入命令
- 简化语法
- 自动推导依赖
问:上一个部分不是说了可以由隐式规则来完成.c文件转变为.o文件吗?为什么需要模式规则?
对于编译.c文件成.o文件的这个标准过程,Makefile中确实存在一个隐式规则可以自动完成:
%.o : %.c
$(CC) -c $(CFLAGS) -o $@ $<
但是,有时候我们可能还是要显式写出这个模式规则,主要有以下几个原因:
- 覆盖隐式规则中的变量值
隐式规则使用的变量可能和我们实际设置的变量值不同,通过显式覆盖可以使变量一致。
- 添加更复杂的命令
实际编译命令可能更复杂,不只是简单的-c
参数。
- 提高可读性
尽管隐式规则可以自动推导,但直接写出模式规则可以增加Makefile的可读性。
- 与其他规则结合使用
配合静态模式规则可以实现更复杂的功能。
- 移植考虑
不同Makefile实现的隐式规则可能有细微不同,写明可以增加可移植性。
所以,即使有了隐式规则,我们有时候仍然会选择显示写出模式规则,这在实际的大型Makefile中是很常见的做法。
需要根据具体情况进行权衡,决定是否要利用隐式规则的自动推导,或者显示写出模式规则。
缺省规则
具体解释一下Makefile中的缺省规则(Default Rules)。
假设我们有以下的Makefile:
target: dep1 dep2
command to build target
dep1:
command to build dep1
在执行make时,情况分为两种:
- dep2文件已存在,直接make
这时会按正常流程构建target。
- dep2文件不存在
由于Makefile中没有指定如何构建dep2,makefile会报错并停止。
这时就需要缺省规则:
%:
touch $@
这表示对于任何没有指定构建规则的目标文件,执行touch命令生成一个空文件。
那么在make的时候,遇到不存在的dep2,就会执行缺省规则,生成一个空的dep2文件。这样make可以继续执行下去,最终生成target。
缺省规则的主要作用就是在makefile中某些依赖不存在时,提供一个备用机制,使得makefile不会直接报错停止。
它为那些没有构建规则的文件生成空文件,使得整个make过程可以继续进行。
在调试makefile时就很有用。
总结一下,缺省规则是makefile的一种补救机制,当缺少规则时执行默认命令,保证makefile可以继续运行。
引用其他的Makefile
在Makefile中可以使用include语句来引用其他 Makefile 文件,从而将一个大的 Makefile 拆分成多个小的文件进行组织。
include的语法为:
include filename
其中filename是要引用的Makefile文件名。
例如:
include config.mk
将引入config.mk文件。
include也支持通配符,例如:
include *.mk
将按字母顺序依次引入所有后缀为.mk的文件。
引用其他Makefile文件的主要优点:
- 拆分复杂的Makefile,按功能分解为多个小文件,更易维护
- 多个项目可以共用一份公共规则文件
- 避免重复复制相同内容的Makefile文件
- 不同人可以维护不同部分的Makefile
需要注意:
- include是递归引入,可以多层嵌套
- 被引入的文件与主Makefile目录相同
- 如果同名,后引入的变量会覆盖之前的变量
#主文件中
include m1.mk m2.mk
all:m
m:main.o
$(CC) $(CFLAGS) -o $@ $^
#m1.mk文件中
%.o:%.c
$(CC) $(CFLAGS) -c -o $@ $<
#m2.mk文件中
export CC = gcc
export CFLAGS = -Wall
.PHONY: all clean install uninstall
三个文件,分别实现的是
- 在主文件中,引用了其他的两个makefile文件,并且用all作伪目标,将.o文件链接为可执行文件
- 在m1.mk文件中,实现的是将.c文件编译为.o文件
- 在m2.mk文件中,定义了两个变量,并用export导入到其他makefile文件中,.PHONY对其目标声明
条件控制
- ifeq 相当于if()语句,其后可以使用(),也可以不使用,可以单/双引号
ifeq ($(AAA),ok)
使用 (),没有引号 ifeq "$(AAA)" "ok"
不使用 (),要去掉中间的逗号,使用双引号 ifeq ('$(AAA)','ok')
使用单引号
但是这个没有多个else if
最后需要添加一个endif
结束判断条件
条件控制的作用大多数时候用来定义变量,也可以包含规则
但是不能在命令规则中进行条件判断
target:dep1 dep2
ifeq($(AAA),ok)
command to build target
endif
上述文本就是典型的错误,这种错误很明显,不可以粗略的认为和函数一样,不可以在规则内部进行判断
Makefile内置函数(origin)
总结一下Makefile中origin内置函数的用法:
origin函数用于显示一个变量的来源,调用格式为:
$(origin VARIABLE)
它会返回如下几种可能的值:
- undefined - 变量未定义
- default - 默认内置变量
- environment - 环境变量
- environment override - 使用-e选项的环境变量
- file - 在Makefile中定义
- command line - 在命令行中定义
- override - 在Makefile中用override定义
- automatic - 自动变量如$@
例如:
$(origin CC)
会显示CC变量的来源。
origin函数常用于调试Makefile,判断变量的值从何而来。
主要用法:
- 查看变量是否被定义
- 判断变量来源优先级
- 调试多层次Makefile中的变量值
总之,origin函数可以让我们追踪变量的来源,从而更好地理解和调试Makefile的行为。
VPATH和vpath
Makefile中VPATH和vpath都用于定义搜索文件路径,区别和相同点如下:
相同点:
- 都是用于指定make在当前路径找不到依赖文件时搜索的附加路径
- 路径可以使用空格或冒号分隔多个路径
- 都可以使makefile构建跨目录的项目
区别:
- VPATH是make的一个预定义全局变量,适用于所有文件
- vpath是make提供的一个命令,可以为不同文件指定不同的搜索路径,更灵活
- VPATH只定义了一个搜索路径
- vpath可以通过通配符为不同类型的文件定义不同的搜索路径
- VPATH对所有文件生效
- vpath需为特定文件指定匹配模式
例如:
# VPATH
VPATH = src
# vpath
vpath %.c src
vpath %.h inc
可以用VPATH查找,但是就和环境变量一样,需要事先添加其路径,不然没有办法准确的查找到文件
相较而言,vapath就显得更高级,可以支持自由路径,并不限于当前makefile所在的目录。
vpath的搜索路径可以是相对路径,也可以是绝对路径,所以可以引用makefile目录外的任意位置。
综合实例
接下来是一个实例对于mian..c,fac.c,array.c,fac.h,array.h进行编写makefile脚本实战
由于繁琐,我将把每一部分写在一个代码块中
#在主文件中
export CC = gcc
export CFLAGS = -Wall
export RM += -r
BIN = main
SUBDIRS = fac array
INCLUDES = -I./inc
vpath %.o $(SUBDIRS)
# 查找所有子目录下的 .c 文件,并且去掉目录,只保留文件名
SUBSRCS = $(foreach dir,$(SUBDIRS),$(notdir $(wildcard $(dir)/*.c)))
# 根据 .c 文件得到 .o 文件
OBJS = $(SUBSRCS:.c=.o)
OBJS += main.o
.PHONY: all clean $(SUBDIRS)
ifndef OK
export OK = TRUE
all:$(SUBDIRS)
make
$(SUBDIRS):
make -C $@
else
all:$(BIN)
$(BIN):$(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o:%.c
$(CC) $(CFLAGS) $(INCLUDES) -c -o $@ $<
endif
clean:
# 循环地从 $(SUBDIRS) 中取一个参数赋值给 dir,然后执行 do 和 done 之间的合作
# 因为 dir 不是 Makefile 变量,而是 shell 变量,因此引用时需要两个 $$dir
for dir in $(SUBDIRS); do $(RM) $$dir/*.o; done # for 是 shell 的循环
$(RM) $(BIN) *.o
#在m1.mk中
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
all:$(OBJS)
echo $(OBJS)
%.o:%.c
$(CC) $(CFLAGS) -c -o $@ $<
#在fac文件夹中的makefile文件
include ../m1.mk
#在fac文件夹中的makefile文件
include ../m1.mk
可以将上述分为三类:
主文件夹,m1.mk补充文件夹,.c文件夹
主文件夹:
- 首先定义的是三个变量,CC,CFLAGS,RM这三个变量可以在所有的文件中使用,可以理解为全局变量
- 接下来是定义了BIN,SUBDIRS,INCLUDES三个变量,分别用来指代可执行文件,.c文件夹,其中INCLUDES表示设置包含头文件的搜索路径为当前makefile目录下的inc子目录
- vpath寻找.o文件,在SUBDIRS这个变量中保存的文件夹名称
$(foreach dir,$(SUBDIRS),$(notdir $(wildcard $(dir)/*.c)))
这个要分开看,首先看这里 的foreach表示一个循环,dir表示目录,目录的地址在SUBDIRS中,wiledcard表示找到目录下的所有.c文件,所以$ (wildcard $(dir)/*.c)
就代表的是fac/fac.c和array/array.c但是我们不想要fac/和array/所以我们用notdir把.c前面的目录删除掉了,只保留了几个.c的文件名并赋值给SUBSRCS- 接下来把SUBSRCS的.c文件名,改为.o文件名,字符串名赋值给OBJS
- OBJS再加上了main.o文件,其实现在OBJS中就保存的是三个文件名,fac.o,array.o,main.o
- 使用条件判断语句,因为这里有SUBDIRS和BIN都是编译的过程,但是SUBDIRS和BIN有先后顺序的,应该需要先执行SUBDIRS,跳转直至m1.mk文件中,将.c文件编译为.o文件
- 当执行了一次之后,就会再一次执行,make,m1.mk 中的编译规则可能只有
$(CC) $(CFLAGS)
,但 Makefile 可能需要增加$(INCLUDES)等参数。所以需要再次执行