🏖️作者:@malloc不出对象
⛺专栏:Linux的学习之路
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
前言
本篇文章我们将要讲解的是项目自动化构建工具make与makefile。
一、make/makefile的背景
会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力,一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。makefile带来的好处就是——“自动化编译”,一旦写好只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
在Linux下,make命令主要用于自动化构建(build)软件项目,它能够根据程序员编写的Makefile文件中的指令,自动编译、链接、打包和安装软件。
具体而言,make完成以下工作:
1.根据Makefile文件中的规则,检查每个源文件的修改时间和依赖关系,确定哪些文件需要重新编译。
2.编译源文件,生成目标文件。
3.链接目标文件,生成可执行文件或库文件。
4.打包可执行文件或库文件,以便于分发和安装。
5.安装可执行文件或库文件到指定的目录中。
通过使用make命令,程序员可以更加方便和高效地管理项目中的代码编译、构建和部署等任务。
Makefile是一个文本文件,其中包含了一系列规则和指令,用于告诉make命令如何构建(build)软件项目。
具体而言,Makefile完成以下工作:
1.定义变量:可以定义一些变量,用于存储常量或者目录路径等信息。
2.定义规则:规则指定了如何生成一个或多个目标文件。每个规则包含了目标文件、依赖文件和生成命令。如果目标文件需要更新,make会根据规则自动执行生成命令。
3.定义伪目标:伪目标是没有实际文件对应的目标,只是一个执行动作的标签。伪目标可以用来执行清理操作、运行测试脚本等操作。
4.定义命令:命令是指make需要执行的操作。可以是编译源代码、链接目标文件、打包可执行文件等操作。
5.定义依赖关系:依赖关系指明哪些文件是构建目标文件所必需的。当依赖文件被修改时,make会根据依赖关系重新构建目标文件。
通过编写Makefile文件,程序员可以实现对项目的自动化构建,提高项目构建的效率和可靠性。
注:make是一个指令,makefile是一个文件!!
二、makefile的基本结构
makefile是一个围绕依赖关系和依赖方法构建的一个自动化编译的工具,我们想完成一件事,必须有正确的依赖关系和正确的依赖方法。
我们先来看看它的基本结构:
注意:依赖方法必须以tab(tab长度不定,我们一般tab以4字符为一个间隔)开头,这是语法规定!!
如果我们使用空格的话会出现下面的情况:
我们可以看到不符合Makefile规则是不能完成构建的。另外细心的读者也可以发现如果使用tab符合Makefile规则的话是会颜色高亮的!!
下面我们来简单使用make演示一下:
看了上图那么我有一个问题,为什么直接使用make就执行了第一个目标文件的依赖方法??而第二个目标文件clean却要显式的使用make clean?
这是因为编译器会默认从上往下扫描,找到文件中的第一个目标文件,在上面的例子中,他会找到“myfile”这个目标文件,然后根据依赖方法得到可执行程序!!而其他的目标文件则需要指令文件名才能进行编译了。
下面我们来验证一下,这次我将clean目标文件放在最前面:
我们发现此时make执行的就是第一个目标文件的依赖方法了,而myfile需要make指明构建:
回到之前的例子,我们来看看下面出现的现象:
为什么make只能进行一次,然后就提示myfile这个可执行程序已经是最新的了,不需要重新编译了,而make clean却能执行多次??
这是因为clean的依赖文件列表为空,则意味着该目标文件不依赖于任何文件。这意味着在执行该目标时,不需要检查其依赖项是否已经更新,因为没有依赖项需要检查;而myfile可执行程序它其实是需要根据依赖文件来判断是否需要重新编译的!!
Q:那么请问gcc是如何根据依赖文件来判断是否需要重新编译?
实际上gcc是根据依赖文件修改的时间来判断是否需要重新编译的,我们的源文件(依赖文件)对应一个时间戳,我们的目标文件也有一个时间戳,我们知道源文件(依赖文件)的创建时间一定比目标文件要早,这样才能形成目标文件对吧;而在源文件(依赖文件)进行修改之后它的时间戳就进行了更新,此时
源文件(依赖文件)的时间戳比目标文件新,那么Make工具就会重新构建这个目标文件。
如下图我简单的展示一下:
下面我们来进行实验一下:
修改依赖文件(源文件)之后:
Q:那么有没有办法不修改源文件也可以使目标文件能够重新构建呢?
我们可以使用touch指令将依赖文件(源文件)更新到最新的时间戳,那么此时我们的依赖文件(源文件)比目标文件的时间戳更新,所以我们就能重新进行编译了!!!
总结:如果这个目标文件不存在或者它的时间戳比依赖文件的时间戳更旧,那么 Make 工具会重新构建这个目标文件;否则,它会认为这个目标文件已经是最新的,不需要重新构建。
三、项目清理
工程是需要被清理的,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过我们可以显式要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重新编译。但是一般我们这种clean的目标文件,我们将它设置为伪目标,用 .PHONY 修饰,伪目标表示一个“虚拟目标”,它并不对应于任何实际的文件,而是表示一些需要执行的命令。当执行伪目标时,Make 不会检查它是否存在于文件系统中,而是仅仅执行对应的命令。我们记住它最重要的特性——总是执行的。
我们来看看它的使用方式:
我们来看看现象:
我们发现在clean被.PHONY修饰之后能执行多次,那之前我们写的clean依赖的文件列表为空不是也能完成这个任务吗?它们之间一样吗?
首先目标文件依赖的文件为空和.PHONY修饰的伪目标是两个不同的概念。
目标文件依赖的文件为空:
- 当一个目标文件依赖的文件为空时,make 不会检查这个依赖项是否存在或是否被更新。这通常用于创建某个目标文件,但其依赖项是通过其他方式处理或生成的情况。
.PHONY
修饰的伪目标:.PHONY
是一个特殊的目标,用于声明一些伪目标。伪目标不是实际的文件名,而是 makefile 中定义的一组操作。.PHONY
目标声明的目标通常不需要实际的文件作为依赖项。当你运行 make 时,它将忽略这些目标是否存在或是否被更新,而是直接执行定义的操作。
总的来说,目标文件依赖的文件为空适用于依赖项是通过其他方式生成的情况,而.PHONY
适用于声明一组操作而不需要实际文件作为依赖项的情况。
Q:"依赖项通过其他方式生成"是什么意思呢?
“依赖项通过其他方式生成” 的意思是在构建某个目标时,它的某个依赖项并不是一个实际的文件,而是通过其他方式生成的,比如:
1.通过命令行运行一些程序生成的结果。
2.通过其他的 Makefile 规则生成的结果。
3.通过复制或者下载远程文件得到的结果。
在这些情况下,依赖项并不是一个实际的文件,因此无法使用文件的时间戳来判断它是否需要重新构建。相反,你需要手动定义生成依赖项的规则,以确保它们能够在构建目标之前正确地生成。
我们来看看具体的例子:
上述例子我们通过clean目标文件创建了一个新的文件touch-file,我们还可以进行一些命令行的其他指令等,依赖项通过其他方式生成还有其他的情况,这里我就不一一进行讲解了,感兴趣的读者下来可以试试。
利用.PHONY修饰目标文件,我们也可以重新编译之前的目标文件了,我们一起来看看:
.PHONY修饰目标文件后忽略目标是否存在或是否被更新,直接执行定义的操作,只会更新目标文件的时间戳!!
虽然我们达到了重新编译的目的,但其实我们是非常不推荐这种行为的,因为重新编译需要消耗大量的时间(大型工程中),而且我们的文件内容并没有进行修改,重新编译也没什么必要!!
四、makefile的依赖
我们来看看下面这个例子,它很好的体现了makefile的依赖关系:
我们平时还是不建议这些写哈,因为没什么必要我们只要得到目标文件就可以了。
五、如何快速编写大型项目中的Makefile文件
我们在前面编写的Makefile文件都只涉及一个源文件的编译链接,但是如果在一个大型项目中我们有很多的源文件需要被合并链接,难道我们一个个的手动去添加吗??
既然有这种问题的出现,那么必然会有对应的解决办法。对于大型工程,我们一般采用模块化的方式组织代码,每个模块对应一个或多个源文件。为了避免将全部源文件写出来进行链接,可以使用通配符或变量来自动化处理源文件!!!
以下是一个示例 Makefile,演示如何使用通配符和变量来构建一个大型工程:
# 编译器
CC = gcc
# 列出Makefile所在目录中所有的.c文件
SOURCES := $(wildcard *.c)
# 列出SOURCES中所有.c文件对应的.o文件
OBJECTS := $(patsubst %.c,%.o,$(SOURCES))
# 最后生成的目标文件
TARGET = myfile
# 目标文件依赖OBJECTS中的所有.o文件,变量使用时需要在变量名前加上"$"符号,并且最好用小括号或者大括号把变量括起来
$(TARGET): $(OBJECTS)
$(CC) $^ -o $@ # 所有依赖目标集$^利用CC编译器,一个一个编译成目标文件集$@
%.o: %.c # 所有的.o目标文件依赖于对应的.c文件
$(CC) -c $< -o $@ # 所有的.c文件一个一个的取出来,利用CC编译器形成同名对应的.o文件
# 清理所有的.o文件以及目标文件
.PHONY:clean
clean:
rm -rf $(OBJECTS) $(TARGET)
我们的源文件如下图:
Makefile中的内容:
上述图中的CC、SOURCES、OBJECTS以及TARGET都是变量,在Makefile中变量其实也就是C/C++中的宏!!!
在OBJECTS变量中我们使用了一个patsubst
模式字符串替换函数。
使用格式:
$(patsubst \<pattern>,\<replacement>,\<text>)
。
返回:函数返回被替换过后的字符串。
功能:查找<text> 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式 <pattern> ,如果匹配的话,则以 替换。这里, <pattern> 可以包括通配符 % ,表示任意长度的字串。如果<replacement> 中也包含 % ,那么,<replacement> 中的这个 % 将是 <pattern> 中的那个 % 所代表的字串。(可以用 \ 来转义,以 % 来表示真实含义的 % 字符)
注: \$(objects:.o=.c)
和 $(patsubst \%.o,\%.c,\$(objects))
是一样的,大家下来可以试试。
另外我们上述看到的\$^
、$@
、\$<
都被叫做自动化变量!!
所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在规则的命令中。
下面是所有的自动化变量及其说明:
$@ : 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么, $@ 就是匹配于目标中模式定义的集合。
$% : 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o) ,那么, $% 就是 bar.o , $@ 就是 foo.a 。如果目标不是函数库文件(Unix下是 .a ,Windows下是 .lib ),那么,其值为空。
$< : 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
$? : 所有比目标新的依赖目标的集合。以空格分隔。
$^ : 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。
$+ : 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
$* :这个变量表示目标模式中 % 及其之前的部分。如果目标是 dir/a.foo.b ,并且目标的模式是 a.%.b ,那么, $* 的值就是 dir/foo 。这个变量对于构造有关联的文件名是比较有效。如果目标中没有模式的定义,那么 $* 也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么 $* 就是除了后缀的那一部分。例如:如果目标是 foo.c ,因为 .c 是make所能识别的后缀名,所以, $* 的值就是 foo 。这个特性是GNU make的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用 $* ,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么 $* 就是空值。
通过上述使用变量以及通配符我们已经配置好了一个简单的自动化构建的Makefile文件,下面我们来检测一下:
通过这种方式,我们就实现了自动化处理大量的源文件,避免了手动编译的繁琐。
本篇文章的Makefile就讲到这里了,它是我们开发项目中非常重要的一个工具,它非常的灵活。一个Makefile写的好不好决定了你的工作效率;另外,其实Makefile其实还有很多功能在这篇文章中并未进行展示,因为博主的精力和水平还不够就讲了大致的一部分的,如果有读者感兴趣的话可以拜读一下这位巨佬的博客分享哦orz~🙈🙈