您现在的位置是:首页 >技术交流 >makefile的简介与使用网站首页技术交流

makefile的简介与使用

LyaJpunov 2024-06-17 10:31:53
简介makefile的简介与使用

makefile用来做什么

通常一个大型程序是由多个程序模块组成的,按照其功能划分,模块文件会分布在不同的目录中,模块与模块之间也会存在依赖关系,大多数情况下,我们编写程序只是修改了部分文件,肯定不是同时更新所有文件,按理说只要重新编译那些修改过的文件,用不着编译全部文件,如果编译全部文件的话,对于那些大型工程时间成本还是非常高的,比如Linux内核这种几千万行代码的东西。

那么我们的问题就是自动针对那些有过改动的文件进行编译,这个问题分为两个小问题

(1) 目标文件依赖那些文件

(2) 依赖的文件是否更新

对于第一个问题,如果只依靠人工来维护文件间的依赖关系,当程序规模不大还好,当程序规模变得很大时,那些依赖会让人发狂。那时候我宁愿直接编译所有文件。

好在万能的Linux提供了make命令,它可以帮助我们自动找出变更的文件,并根据依赖关系,找出受变更文件影响的其他相关文件,然后对这些文件按照规则进行单独处理,此处的规则一般都是指编译,如调用gcc,但也可以是删除文件等其他行为。

上述的规则、依赖关系是定义在一个名叫makefile的文件中的,这两哥们儿的使命主要就是:发现某个文件更新后,只编译该文件和受该文件影响的相关文件,其他不受影响的文件不重新编译,从而提高了编译效率。

这里给大家强调一下,make和makefile并不是用来编译程序的,编译程序的还是gcc,他们哥俩只是找出那些文件需要更新,然后调用其他命令对这些文件进行处理,大多是情况下是调用gcc或者nasm进行编译,也有可能是rm删除文件,当然也可以在命令规则中执行其他命令,这是您自己决定的。

makefile基本语法

目标文件:依赖文件
[Tab]命令

makefile基本语法包括三部分,这三部分加在一起称为一组规则,下面解释下各部分的意义。

目标文件: 目标文件是指此规则中想要生成的文件,可以是.o结尾的目标文件,也可以是可执行文件,也可以是个伪目标,后面会介绍伪目标。

依赖文件: 依赖文件是指要生成此规则中的目标文件,需要哪些文件。通常依赖文件不是1个,所以此处是个依赖文件的列表。

命令: 命令是指此规则中要执行的动作,这些动作是指各种shell命令,命令可以有多个,但一个命令要单独占用一行,在行首必须用Tab开头,这是make规定的用法。

以上规则的意义是:要想生成目标文件,需要提前准备好依赖文件,如果依赖文件列表中任意一个文件比目标文件新,就去执行规则中的命令。

make程序如何判断文件有过更新

在Linux中,文件分为属性和数据两部分,每个文件有三种时间,分别用于记录与文件属性和文件数据相关的时间,这三个时间分别是atime、mtime、ctime。

atime,即access time,表示访问文件数据部分时间,每次读取文件数据部分时就会更新atime,强调下,是读取文件数据(内容)时改变atime,比如cat或less命令查看文件就可以更新atime,而ls命令则不会。

ctime,即change time,表示文件属性或数据的改变时间,每当文件的属性或数据被修改时,就会更新ctime,也就是说ctime同时跟踪文件属性和文件数据变化的时间。

mtime,即modify time,表示文件数据部分的修改时间,每次文件的数据被修改时就会更新mtime。在上面说过啦,ctime也跟踪数据变化时间,所以,当文件数据被修改时,mtime和ctime一同更新。

在Linux中查看这三个时间可以用 stat 命令。

lovess@Lyajpunov:~$ stat a.c
  File: a.c
  Size: 119             Blocks: 0          IO Block: 4096   regular file
Device: 2h/2d   Inode: 5629499536446330  Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/  lovess)   Gid: ( 1000/  lovess)
Access: 2023-03-20 20:59:05.709967700 +0800
Modify: 2023-03-20 20:58:41.805699100 +0800
Change: 2023-03-20 20:58:41.806695800 +0800
 Birth: -

对于文件来说,我们更加关注它的数据部分,所以只要make程序分别获取依赖文件和目标文件的mtime,对比依赖文件的mtime是否比目标文件的mtime新,就知道是否要执行规则中的命令。这里的命令并非必须是编译命令,

举个例子

1:2
	echo "OKKKKKKKKKK"

我们用touch命令先生成1和2这两个文件,然后make,就可以在终端得到字符串

echo "OKKKKKKKKKK"
OKKKKKKKKKK

但是这里把我们的命令也输出了,只要在命令前加@,就可以不输出命令

1:2
	@echo "OKKKKKKKKKK"

通过这个简单的例子,我们也能知道,make和makefile不是专门用来编译文件的,他只是执行规则中的命令。

makefile的文件名也并非固定,可以在执行make时用-f参数来指定。如果未用-f指定,默认情况下,make会先去找名为GNUmakefile的文件,若该文件不存在,再去找名为makefile的文件,若makefile也不存在,最后去找名为Makefile的文件

跳到目标处执行

make中有很多目标时,我们可以用目标名称作为make的参数,采用 “make 目标名称” 的方式,单独执行目标名称处规则。注意,这种方式只会执行目标名称处的规则,之后就退出了,后面即使存在其他的目标也不会再执行。

举个例子

t1:1
    @echo "t1"
t2:1
    @echo "t2"

执行

lovess@Lyajpunov:~$ make t1
t1
lovess@Lyajpunov:~$ make t2
t2

当make后面没有目标名称做参数时,make会在makefile中第一个出现的目标处开始执行。

一般情况下,命令能否执行是要看所依赖文件的mtime是否比目标文件要新。如果依赖文件的mtime比目标文件旧的话,说明目标文件已经是最新的,根本不需要更新,所以按理说,此种情况下规则中的命令是不会执行的,事实也正是如此。

伪目标

通过上面的例子您看到了,规则中的命令并不总是被执行,有时候我们并不关心是否产生真实的目标文件,我们只希望make不要考虑mtime,而是总能去执行一些命令。

对于这个需求还是有办法的,make规定,当规则中不存在依赖文件时,这个目标文件名就称为—伪目标。

伪目标,顾名思义,也就是不产生真实的目标文件,所以当然也就不需要依赖文件了。于是,伪目标所在的规则就变成了纯粹地执行命令,只要给make指定该伪目标名做参数,就能让伪目标规则中的命令直接执行。

举个例子

all:
	@echo "test ok"

由于makefile中仅有这一个目标all,所以如果此时执行make all或make,程序只会输出test ok。

注意,伪目标不能和真实目标文件同名,否则就失去伪目标的意义了,为了避免伪目标和真实目标文件同名的情况,可以用关键字“.PHONY”来修饰伪目标,格式为“.PHONY:伪目标名”,这样不管与伪目标同名的文件是否存在,make照样执行伪目标处的命令。

通常需要显式用.PHONY修饰伪目标的场合是删除编译过程中的.o文件,这是为了避免因旧的.o文件已存在而影响编译。如果您在Linux下有过编译源码的经验,就会了解make clean的作用了,通常clean就是伪目标,用来删除编译过程中的.o文件,如:

.PHONY:clean
clean:
    rm ./build/*.o

伪目标的命名并没有固定的规则,用户可以按照自己的意愿定义成自己喜欢的名字。不过,由于makefile已经流传很广泛了,对于伪目标的命名,业界内已经有了一些约定俗成的规则,大伙儿把类似功能的伪目标定义成了同一个名字。比如上面提到的clean,这个伪目标名称也是大伙儿公认的,它的功能通常就是清空目标文件,当然了,相应的命令部分还得是rm等清除相关的命令。这里再列举一些其他公认的伪目标名。

伪目标名称功能描述
all通常用来完成所有模块的编译工作,类似于rebuild all
clean清空编译完成所有的目标文件,一般用rm来实现
dist通常用于将打包成tar的文件再压缩
install将编译好的程序复制到安装目录下,此目录是在执行configure脚本通过–prefix参数配置的
printf打印已经发生改变的文件
tar用于将文件打包成tar文件,也就是所谓的归档
test测试makefile流程

make递推推导

在makefile中的目标,是以递归的方式逐层向上查找目标的,就好像是从迷宫的出口往回找来时的路一样,由果寻因,逐个向上推导。这一点尤其体现在多个目标相互依赖的情况下。

举个例子,两个文件有依赖关系

lovess@Lyajpunov:~$ cat -n test1.c
     1  void my_print(char *);
     2
     3  int main(){
     4      my_print("hello makefile!");
     5  }
lovess@Lyajpunov:~$ cat -n test2.c
     1  #include <stdio.h>
     2
     3  void my_print(char *str) {
     4      printf(str);
     5  }

我们试着把这两个文件编译成test.bin

test2.o:test2.c
    gcc -c -o test2.o test2.c
test1.o:test1.c
    gcc -c -o test1.o test1.c
test.bin:test1.o test2.o
    gcc -o test.bin test1.o test2.o
all:test.bin
    @echo "compile done"

执行命令

make all

就可以生成最终的执行程序 test.bin

我们关注的是这里的递推流程

(1)make未找到文件GNUmakefile,便继续找文件makefile,找到后,根据命令的参数all,从文件中找到all所在的规则。

(2)make发现all的依赖文件test.bin不存在,于是就去找以test.bin为目标文件的规则。

(3)在第5行终于找到了test.bin的规则,但make发现,test.bin的依赖文件test1.o和test2.o都不存在,于是先去找以test1.o为目标文件的规则。

(4)第3行找到了生成test1.o的规则,但它的依赖文件是test1.c,由于test1.o本身不存在,所以不用再查看test1.c的mtime,直接执行此规则的命令。

(5)生成test1.o后,执行流程返回到test.bin所在的规则,即第5行,此时make发现test2.o也不存在,于是继续递归查找目标test2.o。

(6)在第1行发现test2.o所在的规则,由于test2.o本身不存在,也不再检查其所依赖文件test2.c的mtime,直接执行规则中的编译命令

(7)生成test2.o后,此时执行流程又回到了第5行,make发现两个依赖文件test1.o和test2.o都准备齐了,于是执行本规则的命令,即第6行的gcc -o test.bin test1.o test2.o,将这两个目标文件生成可执行文件test.bin。

(8)test.bin终于生成了,此时回到了第2步目标all所在的规则,于是执行所在规则中的命令@echo “compile done”,打印字符串表示编译完成。

虽然all被当作了真实目标文件来处理,但我们给出的命令并不是为了生成它,所以它同伪目标的作用类似。

自定义变量与系统变量

makefile既然可称为编程,它必然就具备程序语言的必须的基本功能,比如,可以在makefile中定义变量。很遗憾的是makefile还没有编程语言的图灵完备性。

变量定义的格式:变量名=值(字符串),多个值之间用空格分开。make程序在处理时会用空格将值打散,然后遍历每一个值。另外,值仅支持字符串类型,即使是数字也被当作字符串来处理。

变量引用的格式:$(变量名)。这样,每次引用变量时,变量名就会被其值(字符串)替换。

注意,虽然变量的值会被当作字符串类型处理,但不能将其用双引号或单引号括起来,否则双引号或单引号也会被当作变量值的一部分。比如var = ‘file.c’,var的值并不是file1.c,而是’file.c’。当引用变量$(var) 做依赖文件时,make会去找名为’file.c’的目标,而不是file.c。

举个例子

test2.o:test2.c
    gcc -c -o test2.o test2.c
test1.o:test1.c
    gcc -c -o test1.o test1.c
objfiles = test1.o test2.o
test.bin:$(objfiles)
    gcc -o test.bin $(objfiles)
all:test.bin
    @echo "compile done"

效果和上文中定义的效果是一样的。

除了用户自定义的变量外,make还自行定义了一些系统级的变量,按其用途可分为命令相关的变量及参数相关的变量。见下表

变量名描述
AR打包程序,默认是“ar”
AS汇编语言编译器,默认是“as”
CCC语言编译器,默认是“cc”
CXXC++语言编译器,默认是“g++”
CPPC预处理器,默认是“$(CC) –E”,如gcc -E
FCFortran的编译器和预处理器,Ratfor的编译器,默认是“f77”
GET从SCCS文件中提取文件程序,默认是“get”
PCPascal语言编译器,默认是“pc”
MAKEINFO将texinfo文件转换为info文件,默认是“makeinfo”
RM删除命令,默认是“rm -f”
TEX从TeX源文件中创建TexDVI文件的程序,默认是“tex”
WEAVE将We b转换为TeX的程序,默认是“weave”
YACC处理C程序的Yacc词法分析器,默认是“yacc”
YACCR处理Ratfor程序的Yacc词法分析器,默认是“yacc -r”

隐含规则

在编写规则时,若一行写不下,可以在行尾添加反斜杠字符’’,这样下一行的内容便被认为是同一行,其实这是很多编译器和解释器都支持的功能,不仅是make才这样。

makefile中另一个必须的功能是注释,如同shell脚本一样,makefile中用#来单行注释,只要各行第一个非空字符(除空格、tab)是’#’,本行内容便被注释了。如果在行尾是反斜杠字符’’,这表示下一行也应被处理为当前行,所以,连同下一行也被注释掉。

#test2.o:test2.c
#    gcc -c -o test2.o test2.c
#test1.o:test1.c
#    gcc -c -o test1.o test1.c
objfiles = test1.o test2.o
test.bin:$(objfiles)
    gcc -o test.bin $(objfiles)
all:test.bin
    @echo "compile done"

但是我们执行这个程序

lovess@Lyajpunov:~$ make all
cc    -c -o test1.o test1.c
cc    -c -o test2.o test2.c
gcc -o test.bin test1.o test2.o
compile done

这两个文件依然被编译了,使用的是cc命令,makefile中的编译命令是gcc,此处输出的编译命令是cc,所以makefile中的gcc真的是注释掉了,这一点请大伙儿放心。另外,cc其实就是gcc的软链接,这两个是同一个程序,都是指向/usr/bin/gcc。

对于一些使用频率非常高的规则,make 把它们当成是默认的,不需要显式地写出来,当用户未在makefile中显式定义规则时,将默认使用隐含规则进行推导。

隐含规则对于不同的程序语言是不同的,是根据一般的依赖关系来自动推导,属于重建目标文件的通用方法。

针对不同的编程语言依赖关系,make程序通过除扩展名之外的文件名部分,再根据隐含规则,可以推导出最终的可执行文件。也就是说,若想通过隐含规则自动推导生成目标,存在于文件系统上的文件,除扩展名之外的文件名部分必须相同。比如x.o的C源文件必须名为x.c,这样通过隐含规则才能成功生成x.o。

下面列举一些常见语言的隐含规则

  • C程序

    “x.o”的生成依赖于“x.c”,生成x.o的命令为:

    $(CC) -c $(CPPFLAGS) $(CFLAGS)

  • C++程序

    “x.o”的生成依赖于“x.cc”或者“x.C”,生成x.o的命令为:

    $(CXX) -c $(CPPFLAGS) $(CFLAGS)

自动化变量

make还支持一种自动化变量,此变量代表一组文件名,无论是目标文件名,还是依赖文件名,此变量值的范围属于这组文件名集合,也就是说,自动化变量相当于对文件名集合循环遍历一遍。对于不同的文件名集合,有不同的自动化变量名,下面列举一些。

$@,表示规则中的目标文件名集合,如果存在多个目标文件,$@则表示其中每一个文件名

$<,表示规则中依赖文件中的第1个文件。助记,‘<’很像是集合的最左边,也就是第1个。

$,表示规则中所有依赖文件的集合,如果集合中有重复的文件,$会自动去重。助记,’^’很像从上往下罩的动作,能罩住很大的范围,所以称为集合

$?,表示规则中,所有比目标文件 mtime 更新的依赖文件集合。助记,’?’表示疑问,make最大的疑问就是依赖文件的mtime是否比目标文件的mtime要新。

举个例子

objfiles = test1.o test2.o
test.bin:$(objfiles)
    gcc -o $@ $^
all:test.bin
    @echo "compile done"

我们在第3行用$@代替了test.bin,用$^代替了所有依赖文件。

模式规则

模式,即pattern,其实就是指字符串模(mú,二声)子,正则表达式中用此概念表示字符或字符串匹配,把符合此模子的字符串找出来,make中也支持这种字符串匹配用法。

%用来匹配任意多个非空字符。比如%.o代表所有以.o为结尾的文件,g%s.o是以字符g开头的所有以.o为结尾的文件,make会拿这个字符串模式去文件系统上查找文件,默认为当前路径下。

%通常用在规则中的目标文件中,以用来匹配所有目标文件,%也可以用在规则中的依赖文件中,因为目标文件才是要生成的文件,所以当%用在依赖文件中时,其所匹配的文件名要以目标文件为准。拿%.o:%.c为例,假如用%.o匹配到了目标文件a.o和b.o,那么依赖文件中的%.c将分别匹配到a.c和b.c。

举个例子

%.o:%.c
	gcc -c -o $@ $^
objfiles = test1.o test2.o
test.bin:$(objfiles)
    gcc -o $@ $^
all:test.bin
    @echo "compile done"

但是这样会不会有别的风险呢,比如我们文件目录下有别的.c文件,会不会吧这个c文件也编译了呢,答案是不会的,因为递推是从test.bin开始的,推导到上面只会推导出 test1.ctest2.c ,不会涉及到其他的c文件了,所以其他的C文件也不会被编译。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。