当项目比较小的时候,我们可以手写gcc编译命令编译源文件,但是当工程稍微大一点,就有点晕了,所以Makefile闪亮登场,用来完成大型工程的编译工作。Makefile中定义了一系列规则,哪些文件先编译,哪些文件后编译,哪些文件需要重新编译,甚至更加复杂的操作,Makefile就像一个shell脚本,包含了一系列的操作,比如编译命令,也可以执行操作系统的命令,Makefile的好处就是自动化编译,一旦写好,只需要一个make命令,整个工程自动编译。
示例
假设有三个.c文件,两个.h文件,如下所示
/* main.c */
#include <stdio.h>
#include "file1.h"
#include "file2.h"
int main(void)
{
printf("This is main function in main file.\n");
print1();
print2();
return 0;
}
/* file1.c */
#include <stdio.h>
void print1(void)
{
printf("This is print1 function in file1 file.\n");
}
/* file1.h */
#ifndef _FILE_1_H
#define _FILE_1_H
void print1(void);
#endif
/* file2.c */
#include <stdio.h>
void print2(void)
{
printf("This is print2 function in file2 file.\n");
}
/* file2.h */
#ifndef _FILE_2_H
#define _FILE_2_H
void print2(void);
#endif
为了自动编译上述文件,Makefile应该如下所示
main: main.o file1.o file2.o
gcc -o main main.c file1.c file2.c
main.o: main.c file1.h file2.h
gcc -c main.c
file1.o: file1.c file1.h
gcc -c file1.c
file2.o: file2.c file2.h
gcc -c file2.c
clean:
@echo "cleanning project"
rm main *.o
@echo "clean completed"
通过Makefile,只需要在terminal中输入make,就会实现自动编译,生成目标文件以及可执行文件,下面就开始解释下规则
Makefile 规则
target ... : prerequisites ...
command
规则由三部分组成,target(目标)、prerequisites(先决条件)、command(命令)
- target: 个目标文件,可以是 Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续叙述
- prerequisites: 就是,要生成那个 target 所需要的文件或是目标。
- command: make 需要执行的命令。(任意的 Shell 命令)
也就是说,这是一个文件的依赖关系,target中的目标文件依赖于prerequisites中的问题件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
Makefile是怎样工作的
默认情况下,我们输入make命令
- make 会在当前目录下找名字叫“Makefile”或“makefile”的文件.
- 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到main这个文件,并把这个文件作为最终的目标文件,第一个目标为缺省目标。
- 如果 main 文件不存在,或是 main 所依赖的后面的 .o文件的文件修改时间要比 main这个文件新,那么,他就会执行后面所定义的命令来生成 main 这个文件。
- 如果 main 所依赖的.o文件也不存在,那么 make 会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
- 当然源文件.c及.h文件都存在,故会生成.o文件,然后生成目标文件即可执行文件main
Makefile就是通过文件的依赖性,完成自动编译的工作,如果在找寻过程中,出现错误,make就会直接退出并报错,而像clean这种目标,没有被第一个目标文件所关联,那么将不会执行,不过可以显示执行,比如输入命令make clean,完成清除工作。
Makefile中使用变量
在Makefile中可以使用变量简化文件,比如Makefile中
main: main.o file1.o file2.o
gcc -o main main.c file1.c file2.c
我们可以使用一个变量带表示obj文件
objects = main.o file1.o file2.o
则上述规则就变为
objects = main.o file1.o file2.o
main: $(objects)
gcc -o main $(objects)
make自动推导
GNU 的 make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的 make会自动识别,并自己推导命令。
只要 make 看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果 make找到一个 whatever.o,那么 whatever.c,就会是 whatever.o 的依赖文件。并且 cc -c whatever.c也会被推导出来,于是,我们的 makefile 再也不用写得这么复杂。我们的是新的 makefile 又出炉了。
objects = main.o file1.o file2.o
main: $(objects)
gcc -o main $(objects)
main.o: file1.h file2.h
file1.o: file1.h
file2.o: file2.h
.PHONY: clean
clean:
@echo "cleanning project"
-rm main *.o
@echo "clean completed"
这种方法,就是make的隐晦规则。
上面的.PHONY表示clean是个伪目标, 伪目标不是一个文件,只是一个标签,所以make无法生成它的依赖关系和决定它是否执行,当然,为了避免和文件重名,使用特殊标记.PHONY显示指明一个目标是伪目标。
命令rm main *.o前面的-表示执行命令的过程中可能某些文件出现问题,但是忽略它,继续做后面的事。
Makefile 组成
- 显示规则:说明了如何生成一个或多个的目标文件,由书写者明显指出,要生成的文件,文件的依赖,生成的命令
- 隐晦规则:make有自动推导的功能,所以隐晦规则可以使我们比较粗略的书写Makefile
- 变量定义:在Makefile中可以定义一系列变量,变量一般都是字符串,有点类似于C语言的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上
- 文件指示:包括三部分
- 在一个Makefile中引用另一个Makefile,类似于C语言的include
- 根据某些情况指定Makefile中的有效部分,类似于C语言的条件编译
- 定义一个多行的命令
- Makefile只有行注释,#字符,类似于C/C++的//,如果文件中使用#,可以使用转义符#,Makefile中的命令必须以[Tab]键开始
make的工作方式
- 读入所有的 Makefile。
- 读入被 include 的其它 Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
自动生成依赖性
大型工程中可能无法准确得知一个目标的所有依赖文件,并且修改源文件的时候,也要小心的修改Makefile,幸好C/C++编译器支持- M选项,即自动找寻源文件包含的头文件,并生成一个依赖关系。 比如
gcc -M main.c # output: main.o: main.c file1.h file2.h
这样就不必在手动书写若干文件的依赖关系,而是由编译器自动生成,但是如果使用GNU的C/C++编译器,要使用-MM参数,否则会将标准头文件也包含进来
通配符
通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号()、问号(?)和 […] 。比如, .o 表示所有后缀名为o的文件。
使用变量
变量基础
变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 符号,但最好用小括号()或是大括号把变量给包括起来。如果你要使用真实的 符 号 , 但 最 好 用 小 括 号 ( ) 或 是 大 括 号 把 变 量 给 包 括 起 来 。 如 果 你 要 使 用 真 实 的 字符,那么你需要用$$来表示
objects = program.o foo.o utils.o
program : $(objects)
变量中的变量
Makefile中有两种方式用变量定义变量的值
= 方法
使用=符号,符号左侧是变量,右侧是变量的值,且右侧变量的值可以定义在文件的任何一处,即可以使用后面定义的值
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
执行make all命令,会打印变量$(foo)的值为Huh?,可见变量可以使用后面的变量定义
但有一个权问题就是可能会导致无限循环的变量展开,当然make有能力检测这样的定义,并会报错
:=方法
使用:=操作符,但是前面的变量定义不能使用后面的变量,只能使用签名已经定义好了的变量
?=操作符
FOO ?= bar # 表示如果FOO未定义,则为bar,否则什么也不错
+=操作符
objects = main.o foo.o bar.o utils.o
objects += another.o # objects值为 main.o foo.o bar.o utils.o another.o
条件判断
条件表达式的语法为
<conditional-directive>
<text-if-true>
<else>
<text-if-false>
endif
其中表示条件关键字,有四个
- ifeq(, ): 表示值同为真,值异为假
- ifneq (, ): 表示值异为真
- ifdef : 变量值非空,为真
- ifndef : 与上相反
使用函数
函数调用语法
$(<function> <arguments>) 或者${<function> <arguments>}
字符串处理函数
subst
$(subst <from>,<to>,<text>)
字符串替换函数,把字串中的字符串替换成, 返回被替换后的字符串
比如$(subst ee,EE,feet on the street) ==> fEEt on the strEEt
patsubst 函数
$(patsubst <pattern>,<replacement>,<text>)
模式字符串替换函数, 查找
shell 函数
Shell函数用来执行shell命令,并将系统命令后的输出作为函数返回。
contents := $(shell cat foo)
注意,这个函数会新生成一个shell程序来执行命令,所以要注意运行性能,不要使用太过复杂的规则
隐含规则
隐含规则是一种惯例,迎来运行我们没有明确的规则,隐含规则会使用一些系统变量,我们可以改变系统变量的值定制隐含规则的运行时参数。
隐晦规则命令变量
变量 | 含义 |
---|---|
AR | 函数库打开包程序。默认命令是“ar” |
AS | 汇编语言编译程序。默认命令是“as” |
CC | C语言编译程序。默认命令是“cc” |
CXX | C++语言编译程序。默认命令是“g++” |
CO | 从RCS文件中扩展文件程序。默认命令是“co” |
CPP | C程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) -E” |
FC | Fortran和Ratfor的编译器和预处理程序。默认命令是”f77” |
GET | 从SCCS文件扩展文件的程序。默认命令是“get” |
LEX | Lex方法分析器程序(针对于C或Ratfor)。默认命令是”lex” |
PC | Pascal语言编译程序。默认命令是”pc” |
YACC | Yacc文法分析器(针对C程序)。默认命令是“yacc” |
YACCR | Yacc文法分析器(针对Ratfor程序)。默认命令是“yacc -r” |
MAKEINFO | 转换Texinfo源文件(.texi)到info文件程序。默认命令是“makeinfo” |
TEX | 从TeX源文件创建TeX DVI文件的程序。默认命令是“tex” |
WEAVE | 转化Web到TeX的程序。默认命令是“weave” |
TEXI2DVI | 从Texinfo源文件创建TeX DVI文件的程序。默认命令是“texi2dvi” |
CWEAVE | 转化C Web到TeX的程序。默认命令是“cweave” |
TANGLE | 转换Web到Pascal语言的程序,默认命令是”tangle“ |
CTANGLE | 转换C Web到C。默认命令是”ctangle“ |
RM | 删除文件命令。默认命令是”rm -f“ |
隐晦规则命令参数变量
下面的这些变量都是相关上面命令的参数
变量 | 含义 |
---|---|
ARFLAGS | 函数库打包程序AR命令的参数。默认值是“rv” |
ASFLAGS | 汇编语言编译参数(当明显地调用”.s”或”.S”文件时) |
CFLAGS | C语言编译器参数 |
CXXFLAGS | C++语言编译器参数 |
COFLAGS | RCS命令参数 |
CPPFLAGS | C预处理器参数(C和Fortran编译器也会用到) |
FFLAGS | Fortran语言编译器参数 |
GFLAGS | SCCS ”get“程序参数 |
LDFLAGS | 连接器参数(如“ld”) |
LFLAGS | Lex文法分析器参数 |
PFLAGS | Pascal语法编译器参数 |
RFLAGS | Ratfor程序的Fortran编译器参数 |
YFLAGS | Yacc文法分析器参数 |
模式规则
模式规则用来定义一个隐含规则,在规则中,需要有%字符,表示一个或多个任意字符,在依赖目标中同样可以使用%,只是依赖目标中的%取值,取决于其目标
%的展开发生在变量和函数的展开之后,变量和函数的展开发生在make载入Makefile时,而模式规则的%发生在运行时
模式规则示例
%.o: %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
该例表示把所有的[.c]文件都编译成[.o]文件,其中, @表示所有的目标的挨个值, @ 表 示 所 有 的 目 标 的 挨 个 值 , <表示了所有依赖目标的挨个值。这些奇怪的变量我们叫”自动化变量”,
自动化变量
把模式中所定义的一系列的文件自动的逐个取出,直至所有的符合模式的文件都取完了。
变量 | 含义 |
---|---|
$@ | 表示规则中的所有目标文件的集合。在模式规则中如果有多个目标,“$@”就是匹配于目标中模式定义的集合 |
$% | 仅当目标是函数库文件时,表示规则中的目标成员名,如果目标不是函数库文件(UNIX下是.a,Windows是.lib),其值为空。 |
$< | 依赖目标中的第一个目标名字,如果依赖目标是以模式(即”%“)定义的,则”$<”是符合模式的一系列的文件集 |
$? | 所有比目标新的依赖目标的集合,以空格分隔 |
$^ | 所有依赖目标的集合,以空格分隔。如如果在依赖目标中有多个重复的,则自动去除重复的依赖目标,只保留一份 |
$+ | 同”$^”,也是所有依赖目标的集合,只是它不去除重复的依赖目标。 |
$* | 目标模式中“%”及其之前的部分 |
$(@D) | “@”的目录部分(不以斜杠作为结尾),如果”@”中没有包含斜杠,其值为“.”(当前目录) |
$(@F) | “@”的文件部分,相当于函数”(notdir $@)” |
$(*D) | 同”$(@D)”,取文件的目录部分 |
$(*F) | 同”$(@F)”,取文件部分,但不取后缀名 |
$(%D) | 函数包文件成员的目录部分 |
$(%F) | 函数包文件成员的文件名部分 |
$( | 依赖目标中的第一个目标的目录部分 |
$( | 依赖目标中的第一个目标的文件名部分 |
$(^D) | 所有依赖目标文件中目录部分(无相同的) |
$(^F) | 所有依赖目标文件中文件名部分(无相同的) |
$(+D) | 所有依赖目标文件中的目录部分(可以有相同的) |
$(+F) | 所有依赖目标文件中的文件名部分(可以有相同的) |
$(?D) | 所有被更新文件的目录部分 |
$(?F) | 所有被更新文件的文件名部分 |
Tips
- 文件名最好使用Makefile或者makefile
- 伪目标可以避免和同名文件冲突,并且已知’.PHONY为伪目标,非时间文件,make会跳过隐含规则搜索,会改善性能
- 面试经常问到赋值操作符=和:=的区别
- = :可以先使用后定义,这就导致makefile 在全部展开后才能决定变量的值,有可能出现循环递归,无法展开的问题
- :=:必须先定义后使用,在当前的位置就可以决定变量的值
由于才疏学浅,可能会有些许遗漏和插座,后续会更正修补