Makefile干嘛的?
当我们编程做一个项目,文件众多复杂,不可能每次编译都去敲
gcc
指令编译链接工程,一则效率不高,随便修改某个文件就需要把工程内所有文件都编译一遍,二则包含项目太多稍不注意就会遗漏、缺失容易出错,Makefile
其实就是一个项目工程管理工具,只需要执行make
指令,就能做到像编程IDE
中的build
按钮那样轻轻一点自动把项目编译完成。生成目标程序,效率很高,但要写好Makefile
,写出自己的风格,并不是短时间就能做到,笔者水平有限,但我相信这是一篇针对新手来说极有效率的Makefile
入门文章
Makefile格式
目标:依赖项
(TAB) 所执行的命令
格式很简单,一个操作基本单元一般是两行,第一行是所想要生成的目标与生成该目标所需要的依赖项,两者用
:
隔开,第二行就是具体的实现方法,开头用TAB
键缩进,这是语法规定,后面跟所要执行的命令即可。
举一个简单的例子,我们编译一个main.c
文件生成run
可执行程序,一般做法是:gcc main.c -o run
,在Makefile
里面可以这样写:
run:main.c
gcc main.c -o run
保存完成后,只需要执行
make
指令,编译就会自动完成,不用每次都去敲gcc
命令,这样是不是提高效率了呢?可能一个文件的编译还是不能体现Makefile
工程管理的优越性
Makefile粗略原理:
文件编辑都会保存最后一次修改的时间戳,
Makefile
执行时会对目标和所依赖项的时间进行比较,如果目标最后修改的时间要早于依赖项内目标时间,证明依赖项被改动过,那么目标需要被重新构建,那么所在的这两行操作单元就会被执行,当然一个Makefile
会有很多这样的操作单元,Makefile
会依次比较、构建直到最终目标生成。原理大概如此,反正这样理解不会有太大的偏差。
实战
通过一个小例程一步步实现Makefile
优化,看一下我们的工程结构
root@DESKTOP-FR31BP0:/tmp# ls
src
root@DESKTOP-FR31BP0:/tmp# tree src/
src/
├── func1.c
├── func2.c
└── main.c
root@DESKTOP-FR31BP0:/tmp#
工程目录
src
里面有三个.c
文件,我们需要将它们编译成名称为run
的目标文件
普通做法:
root@DESKTOP-FR31BP0:/tmp/src# gcc *.c -o run
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c func2.c main.c run
第一版本Makefile
run:main.c func1.c func2.c
gcc main.c func1.c func2.c -o run
执行
root@DESKTOP-FR31BP0:/tmp/src# make
gcc main.c func1.c func2.c -o run
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c func2.c main.c Makefile run
执行成功没问题,生成目标文件,证明编写无误
存在的问题:执行效率太低,当我们编译一个工程文件,编译的时候就会把整个工程里面连同没改动的文件都会重新编译,这里的例子是三个文件体现不出弊端,如果工程量很大、文件数量多,编译会浪费很长的时间,针对改进执行效率,看我们推出的第二版Makefile
第二版本Makefile
run:main.o func1.o func2.o
gcc main.o func1.o func2.o -o run
main.o:main.c
gcc -c main.c
func1.o:func1.c
gcc -c func1.c
func2.o:func2.c
gcc -c func2.c
执行一下:
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c func2.c main.c Makefile
root@DESKTOP-FR31BP0:/tmp/src# make
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc main.o func1.o func2.o -o run
root@DESKTOP-FR31BP0:/tmp/src#
执行成功没问题,生成目标文件,证明编写无误
执行效率上根据Makefile
原理,make
执行时只会对改动过的文件重新编译,未改动过的内容不会重新编译,我们将过程细化,做到了可针对某个文件的编译,执行效率问题已经解决。然而它还是不完美
第二版存在问题:很多重复操作:将.c文件变为.o文件,这里只是三个文件,问题不算突出,假设工程内有成百上千的文件,这一步就要重复写成百上千次,对我们写Makfile
来说真是个灾难,这个问题同样存在第一个基本单元中,在执行命令中,把所有的依赖文件全部写出来,很烦!!!我们能不能用简洁的方法替换掉这些书写不高效的内容呢?那就要祭出变量这个工具了,第三版Makefile
就着重改进这一项吧:提高书写效率。
第三版Makefile
material=main.o func1.o func2.o
target=run
$(target):$(material)
gcc $^ -o $@
%.o:%.c
gcc $< -c
执行效果:
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c func2.c main.c Makefile
root@DESKTOP-FR31BP0:/tmp/src# make
gcc main.c -c
gcc func1.c -c
gcc func2.c -c
gcc main.o func1.o func2.o -o run
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c func1.o func2.c func2.o main.c main.o Makefile run
执行成功书写没有问题,书写上是不是比第二版简洁很多,但多出来很多奇怪的字符,这些都是什么呢?
这些是Makefile
变量,在Makefile
里变量分类很多
- 自定义变量
变量名
(小写)=变量
- 取变量值
$(变量名)
makefile
自带的变量:要大写
cppFLAGS
CC
- 自动变量
$@
:规则中的目标$<
:规则中的第一个依赖$^
:规则中所有的依赖- 自动变量只能在规则的命令中使用,不能用在目标与依赖里面
%
:通配符,根据需要用来填充相关内容
虽然我们只写了一次%.o:%.c
,但这个操作会一直进行,直到没有新的依赖要转换,%
根据实际情况会被替换,例如main.o:main.c
,执行时%
会被相应替换。只要替换未完成就会一直执行
第三版写到这儿对于一个工程来说已经算可以了,书写效率、执行效率也都还好,但是如果我们想让它更加完美,下一个突破点在哪呢?那得看我们还有哪些需求,通常公司对工程目录结构都有规定,这样每个员工写出的代码,工程目录结构都差不多,便于管理,这就意味着每个工程的Makefile
书写具有很大的相似性,我们能不能搞一个通用的Makefile
,让它适应任何相似工程结构的工程呢?答案是肯定的,我们现在存在通用性的阻碍主要是存在特定声明,Makefile
变量里的.o
文件只存在这个工程,另外一个工程并没有,进行移植时,我们需要逐一修改变量里的.o文件,如果文件数量多,那么书写工作也不轻松,那我们能不能让Makefile
自己去寻找我们所需要的东西,自动填充,而不需要我们自己去书写呢?这样对与工程目录相似的工程的通用性问题便解决了,只要工程目录相似,Makefile
都可以直接拿来使用。这个就是第四版Makefile
所解决的问题了,思路就是利用Makefile
函数
第四版Makefile
写之前先介绍两个Makefile
函数
Makefile
函数都是有返回值的,参数表不用括号括起来- 查找指定目录下指定类型的文件
返回值 wildcard 参数
- 参数:
目录+文件类型
(注意是一个参数) - 返回值:查找出来的指定的文件列表
- 参数:
- 匹配替换函数
返回值 patsubst 参数1,参数2,参数3
- 参数1:原来的类型
- 参数2:替换的类型
- 参数3:指定替换文件
- 返回值:替换后的文件列表
具体用法见下面:
material=$(patsubst %.c,%.o,$(wildcard ./*.c))
target=run
$(target):$(material)
gcc $^ -o $@
%.o:%.c
gcc $< -c
运行情况:
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c func2.c main.c Makefile
root@DESKTOP-FR31BP0:/tmp/src# make
gcc func1.c -c
gcc main.c -c
gcc func2.c -c
gcc func1.o main.o func2.o -o run
root@DESKTOP-FR31BP0:/tmp/src# ls
func1.c func1.o func2.c func2.o main.c main.o Makefile run
可以正常运行
注意:
patsubst
函数的参数一定要用%
号。不要用*
否则替换会失败
通用性问题解决了,书写效率问题解决了,运行效率问题解决了,想一想还有什么需要做的?那就是最后一步工程清理工作了,为了运行效率我们会生成很多中间文件.o
等等。当我们不需要这些中间文件时可以使用一个伪命令去删除这些文件,为什么叫做伪命令,因为这些目标并不真实存在,流行做法如下:
clean:
-rm *.o run -f
clean
为Makefile
语法中的目标,但它没有任何依赖,执行make
它会被忽略,除非我们在终端指定它强制执行make clean
,它才会被执行,这个命令一般放在Makefile
尾部,执行Makefile
时并不会妨碍我们正常编译工作。这不是标准写法,绝大多数情况下不会出问题,但是当工程里面确实存在clean
这个文件时,执行make clean
便不能正常执行,因为clean
真实存在,执行时又不需要任何依赖,会被认定永远是最新的,会把clean
的命令忽略掉不去执行,碰到这种情况怎么办呢?我们还是写标准一点吧。用Makefile
关键字`.PHONY
去声明伪指令
material=$(patsubst %.c,%.o,$(wildcard ./*.c))
target=run
$(target):$(material)
gcc $^ -o $@
%.o:%.c
gcc $< -c
.PHONY:clean
clean:
-rm *.o run -f
解释:在删除命令前加
-
,表示如果执行这个命令遇阻会继续执行下面命令(我们的命令比较多的时候),不会影响其他命令的执行,加-f
命令表示强制删除,不需要任何提示。
当然一个工程还需要链接头文件库文件等等,这里没有体现,可以自己去尝试,大同小异,写好Makefile
就是需要不断练习,多看其他人的写法,不断丰富自己,本人能力有限,希望能给大家有所帮助