目录
一、概述
当谈论到 Linux 中的软件构建工具时,不得不提到 Makefile。Makefile是一种文件,它包含了一组规则,用于指导构建系统在源代码中生成可执行文件或库。它是使用GNU Make软件的标准格式。Makefile 的主要目的是描述代码文件之间的依赖关系以及如何构建以达到特定目标。通过定义规则和命令,开发人员可以轻松地管理项目的复杂构建过程。Makefile 带来的好处就是------"自动化编译",一旦写好,只需要一个 make 命令,整个工程 完全自动编译,极大的提高了软件开发的效率。make 是 一个命令工具,是一个解释 makefile 中指令的命令工具,一般来说,大多数的 IDE 都有这个命令,比如:Delphi 的 make,Visual C++ 的 nmake,Linux 下 GNU 的 make。可见,makefile 都成为了一种在工程方面的编译方法
在 Makefile 中,规则由两个主要元素组成:目标和依赖项。目标是所需的构建结果,而依赖项是生成目标所需的文件或其他目标。每个规则还包含一个或多个命令,用于告诉构建系统如何生成目标。
Makefile 还提供了一些特殊的变量和函数,用于更灵活地控制构建过程。例如,开发人员可以使用变量来定义编译器选项、源代码文件列表等。函数可以用于执行文件操作、字符串处理等。通过使用 Makefile,开发人员可以自动化构建过程,从而节省时间并确保整个项目的一致性。当代码库较大或项目结构较复杂时,Makefile 尤为重要。它能够跟踪文件的更改,并只重新构建那些受到更改影响的目标,从而提高构建的效率。
总之,Makefile 是 Linux 开发中不可或缺的一部分。它提供了一种灵活、可自动化的方式来管理构建过程,使开发人员能够更加高效地组织和构建他们的项目。通过深入理解和灵活运用 Makefile,开发人员可以更好地控制和管理他们的代码库。
二、为什么要用makefile
先来想像一个案例,假设我的可执行文件里面包含了四个源代码文件,分别是 main.c haha.c sin_value.c cos_value.c 这四个文件,这四个文件的目的是:
-
main.c :主要的目的是让使用者输入角度数据与调用其他三支副程序;
-
haha.c :输出一堆有的没有的讯息而已;
-
sin_value.c :计算使用者输入的角度(360) sin 数值;
-
cos_value.c :计算使用者输入的角度(360) cos 数值。
由于这四个文件里面包含了相关性,并且还用到数学函数在里面,所以如果你想要让这个程序可以跑, 那么就需要这样编译:
# 1. 先进行目标文件的编译,最终会有四个 *.o 的文件名出现:
[root@study ~]# gcc -c main.c
[root@study ~]# gcc -c haha.c
[root@study ~]# gcc -c sin_value.c
[root@study ~]# gcc -c cos_value.c
# 2. 再进行链接成为可执行文件,并加入 libm 的数学函数,以产生 main 可执行文件:
[root@study ~]# gcc -o main main.o haha.o sin_value.o cos_value.o -lm
# 3. 本程序的执行结果,必须输入姓名、360 度角的角度值来计算:
[root@study ~]# ./main
Please input your name: VBird <==这里先输入名字
Please enter the degree angle (ex> 90): 30 <==输入以 360 度角为主的角度
Hi, Dear VBird, nice to meet you. <==这三行为输出的结果喔!
The Sin is: 0.50
The Cos is: 0.87
编译的过程需要进行好多动作啊!而且如果要重新编译,则上述的流程得要重新来一遍,光是找出这些指令就够烦人的了! 如果可以的话,能不能一个步骤就给他完成上面所有的动作呢?那就利用 make 这个工具吧! 先试看看在这个目录下创建一个名为 makefile 的文件,内容如下:
# 1. 先编辑 makefile 这个规则档,内容只要作出 main 这个可执行文件
[root@study ~]# vim makefile
main: main.o haha.o sin_value.o cos_value.o
gcc -o main main.o haha.o sin_value.o cos_value.o -lm
# 注意:第二行的 gcc 之前是 <tab> 按键产生的空格喔!
# 2. 尝试使用 makefile 制订的规则进行编译的行为:
[root@study ~]# rm -f main *.o <==先将之前的目标文件去除
[root@study ~]# make
cc -c -o main.o main.c
cc -c -o haha.o haha.c
cc -c -o sin_value.o sin_value.c
cc -c -o cos_value.o cos_value.c
gcc -o main main.o haha.o sin_value.o cos_value.o -lm
# 此时 make 会去读取 makefile 的内容,并根据内容直接去给他编译相关的文件啰!
# 3. 在不删除任何文件的情况下,重新执行一次编译的动作:
[root@study ~]# make
make: `main' is up to date.
# 看到了吧!是否很方便呢!只会进行更新 (update) 的动作而已。
或许你会说:"如果我创建一个 shell script 来将上面的所有动作都集结在一起,不是具有同样的效果吗?"呵呵! 效果当然不一样,以上面的测试为例,我们仅写出 main 需要的目标文件,结果 make 会主动的去判断每个目标文件相关的源代码文件,并直接予以编译,最后再直接进行链接的动作! 真的是很方便啊!此外,如果我们更动过某些源代码文件,则 make 也可以主动的判断哪一个源代码与相关的目标文件文件有更新过, 并仅更新该文件,如此一来,将可大大的节省很多编译的时间呢!要知道,某些程序在进行编译的行为时,会消耗很多的 CPU 资源呢!所以说, make 有这些好处:
-
简化编译时所需要下达的指令;
-
若在编译完成之后,修改了某个源代码文件,则 make 仅会针对被修改了的文件进行编译,其他的 object file 不会被更动;
-
最后可以依照相依性来更新 (update) 可执行文件。
既然 make 有这么多的优点,那么我们当然就得好好的了解一下 make 这个令人关心的家伙啦!而 make 里面最需要注意的大概就是那个规则文件,也就是 makefile 这个文件的语法啦!所以下面我们就针对 makefile 的语法来加以介绍啰。
三、创建简单的Makefile
在 C 语言开发的大型软件中都包含很多源文件和头文件。这些文件间通常彼此依赖,且关系复杂。如果用户修改了一个其他文件所依赖的文件,则必须重新编译所有依赖它的文件。例如,拥有多个源文件,且它们都使用同一个头文件,如果用户修改了这个头文件,就必须重新编译每个源文件。 编译过程分为编译、汇编、链接等阶段。其中,编译阶段仅检查语法错误以及函数与变量的声明是否正确,链接阶段则主要完成函数链接和全局变量的链接。因此,那些没有改动的源代码根本不需要重新编译,而只要把它们重新链接就可以了。那怎么样才能只编译那些更新过的源代码文件呢? 此时可以使用 GNU 的 make 工程管理器。
make 工程管理器是一个"自动编译管理器",这里的"自动"是指它能够根据文件时间藏自动发现更新过的文件而减少编译的工作量。同时,它通过读入 makefile 文件的内容来执行大量的编译工作,只须用户编写一次简单的编译语句就可以了,它大大地提高了实际的工作效率。
make 工具提供灵活的机制来建立大型的软件项,make 工具依赖于一个特殊的、名字为 makefile 或 Makefile 的文件,这个文件描述了系统中各个模块之间的依赖关系。系统中部分文件改变时,make 根据这些关系决定一个需要重新编译的文件的最小集合。如果软件包括几十个源文件和多个可执行文件,这时 make 工具特别有用。
当一个适当的Makefile存在时,每次改变某些源文件,用简单的shell命令(make),将足以完成所有必需的重新编译。
make 命令执行时,需要一个 Makefile 文件,以告诉 make 命令需要怎么样的去编译和链接 程序。
.PHONY:mybin #修饰mybin目标文件,成为一个伪目标,总是被执行。
mybin:test.c #依赖关系 冒号左侧目标文件,右侧依赖文件列表
gcc -o mybin test.c # 依赖方法 (语法规定:此行以tab开头)
.PHONY:clean
rm mybin
在默认方式下,只输入 make 命令。make 会做如下工作:
-
make 会在当前目录下查找名字为"makefile 文件"或"Makefle 文件夹"的文件。如果找到,它会找文件中的第一个目标文件 (target) ,并把这个文件作为最终的目标文件 (target);如果依赖文件不存在,或是所依赖的后面的 .o 文件的修改时间要比这个文件新,它就会执行后面所定义的命今来生成目标文件。
-
如果目标文件所依赖的.o 文件也存在,make会在当前文件中找目标为.o 文件的依赖性,如果找到,则会根据规则生成.o 文件 (这有点像一个堆栈的过程) 。
-
当然,c 文件和h 文件如果存在,make 会生成 .o文件,然后再用.o文件生成 make的最终结果。
stat + 文件名:查看文件详细信息
对文件内容查看 access时间更新; 对文件内容修改 modify时间更新; 对文件属性修改 change时间更新;
modify时间一般和change时间一起改变,change时间会单独改变。 更改权限change时间改变,而modify时间不改变。改变内容两者都发生变化。访问时access改变,但不是每次访问都会变化(短时间内多次查看 可能不改变)。touch 文件可以改变文件的acm时间。
四、makefile的规则
target(要生成的文件): dependencies(被依赖的文件)
#命令前面用的是“tab”而非空格。
command1
command2
.
.
.
commandn
#反斜杠(\)是换行符的意思。可以使用“\”表示续行。注意,“\”之后不能有空格!
-
target 也就是一个目标文件,可以是 Object File,也可以是执行文件。还可以是一个标签 (Label),对于标签这种特性,在后续的"伪目标"章节中会有叙述。
-
dependencies就是,要生成那个 target 所需要的文件或是目标。即生成target所需的文件名列表。依赖可以为空,如"clean","clean"只有命令,没有依赖。
-
command 也就是 make 需要执行的命令。(命令可以是任何一个shell可以执行的命令。)
那个标的 (target) 就是我们想要创建的信息,而目标文件就是具有相关性的 object files ,那创建可执行文件的语法就是以 <tab> 按键开头的那一行!特别给他留意喔,"命令列必须要以 tab 按键作为开头"才行!他的规则基本上是这样的:
-
在 makefile 当中的 # 代表注解;
-
<tab> 需要在命令行 (例如 gcc 这个编译器指令) 的第一个字符;
-
标的 (target) 与相依文件(就是目标文件)之间需以":"隔开。
这是一个文件的依赖关系,也就是说,target 这一个或多个的目标文件依赖于 dependencies中的文件,其生成规则定义在 command 中。说白一点就是说,dependencies 中如果有一个以 上的文件比 target 文件要新的话,command 所定义的命令就会被执行。这就是 Makefile 的规则,也就是 Makefile 中最核心的内容。
记住,make 并不管命令是怎么工作的,他只管执行所定义的命令。 make 会比较 targets 文件和 dependencies 文件的修改日期,如果 dependencies 文件的日期要 比 targets 文件的日期要新,或者 target 不存在的话,那么,make 就会执行后续定义的命令。
make和makefile如何知道可执行程序是比较新的呢?? 这是通过对比时间比出来的,只要可执行程序的最近修改时间比所有源文件的最近修改时间新,说明它就是最新的。target通常是要生成的文件名,当使用make不带参数时,自动执行第一个target。
根据make的依赖性,make会一层层地去找文件的依赖性,直到最终第一个target文件被编译成功。整个过程类似于数据结构中栈的操作。
五、makfile的工作流程
-
make 会在当前目录下找名字叫"Makefile"或"makefile"的文件。
-
如果找到,它会找文件中的第一个目标文件(target),并把这个文件作为最终的目标文件。Makefile和make形成目标文件的时候,默认是从上往下扫描Makefile文件,默认形成的就是第一个文件。
-
如果 target 文件不存在,或是 target 所依赖的后面的 .o 文件的文件修改时间要比 target 这个文 件新,那么,他就会执行后面所定义的命令来生成 target 这个文件
-
如果 target 所依赖的.o 文件也存在,那么 make 会在当前文件中找目标为.o 文件的依赖性, 如果找到则再根据那一个规则生成.o 文件。
这就是整个make的依赖性,make会一层一层地去寻找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么 make 就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make 就不会处理。如果 make 找到了依赖关系之后,发现冒号后面的文件不存在,make 仍不工作。make 只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在, 那么对不起,我就不工作啦。
通过上述分析,我们知道,像 clean 这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要 make 执行。即命令------"make clean",以此来清除所有的目标文件,以便重编译。
于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如 file.c,那么根据我们的依赖性,我们的目标 file.o 会被重编译(也就是在这个依性关系后面所定义的命令),于是 file.o 的文件也是最新的啦,于是 file.o 的文件修改时间要比 target 要新,所以 target 也会被 重新链接了(详见 target 目标文件后定义的命令)。
六、makefile中声明变量
我们举个例子来说明:
edit:main.o kbd.o command.o insert.o
cc -o edit main.o kbd.o command.0 insert.o
我们可以看到 .o 文件部分被重复了两次,如果我们的工程需要加入一个新的 .o 文件, 那么我们需要在两个地方加(应该是三个地方,还有一个地方 在 clean 中)。当然,我们的 makefile 并不复杂,所以在两个地方加也不累,但如果 makefile 变得复杂,那么我们就有可能会忘掉一个需要加 入的地方,而导致编译失败。所以,为了 makefile 的易维护,在 makefile 中我们可以使用变量。makefile 的变量也就是一个字符串,理解成 C 语言中的宏可能会更好。
一般这样声明一个变量,我们在makefile一开始就这样定义
objects = main.o kbd.o command.o insert.o
于是,我们就可以很方便地在我们的 makefile 中以"$(objects)"的方式来使用这个变量了,于 是我们的改良版 makefile 就变成下面这个样子:
edit:$(objects)
cc -o $(objects)
于是如果有新的 .o 文件加入,我们只需简单地修改一下 objects 变量就可以了。 在 Makefile 中的定义的变量,就像是 C/C++语言中的宏一样,他代表了一个文本字串,在 Makefile 中执行的时候其会自动原模原样地展开在所 使用的地方。其与 C/C++所不同的是, 你可以在 Makefile 中改变其值。在 Makefile 中,变量可以使用在"目标","依赖目标","命令"或 是 Makefile 的其它部分中。
makefile中的常见自动变量
命令格式 | 含义 |
---|---|
$* | 不包含扩展名的目标文件名称 |
$+ | 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件 |
$< | 第一个依赖文件的名称 |
$? | 所有时间戳比目标文件晚的依赖文件,以空格分开 |
$@ | 目标文件的完整名称 |
$^ | 所有不重复的依赖文件,以空格分开 |
$% | 如果目标是归档成员,则该变量表示目标的归档成员名称 |
七、clean与伪目标
每个 Makefile 中都应该写一个清空目标文件(.o 和执行文件)的规则,这不仅便于重编译, 也很利于保持文件的清洁。一般的做法是:
.PHONY:clean
clean:
rm target
当然,clean 的规则不要放在文件的开头。不然,这就会变成 make 的默认目标,相信谁也不愿意这样。不成文的 规矩是------"clean 从来都是放在文件的最后"。
首先要明确,并不是所有的目标文件都对应于磁盘文件,有的目标文件的存在只是为了形成一条规则,从而使make完成特定的工作,并不生成新的目标文件,这样的目标称为伪目标。如上面Makefile中的clean。常用的还有all。
伪目标文件是不存在的。make不会执行任何动作,只是检查依赖文件的更新情况, 扫描剩下的几条规则并执行相应的编译命令生成可执行文件。
正像我们前面例子中的"clean"一样,即然我们生成了许多文件编译文件,我们也应该提供一个清除它们的"目标"以备完整地重编译而用。(以"make clean"来使用该目标)因为,我们并不生成"clean"这个文件。"伪目标"并不是一个文件,只是一个标签,由于"伪目标"不是 文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这 个"目标"才能让其生效。当然,"伪目标"的取名不能和文件名重名,不然其就失去了"伪目标"的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记".PHONY"来显示地指明一个目标是"伪目标",向 make说明,不管是否有这个文件,这个目标就是"伪目标"。 .PHONY:clean
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为"默认目标",只要将其放在第一个。一个示例就是,如果你的 Makefile 需要一口 气生成若干个可执行文件,但你只想简单地敲一个 make 完事,并且,所有的目标文件都写 在一个 Makefile 中,那么你可以使 用"伪目标"这个特性
all:dependence1 dependence2 dependence3
.PHONY:all
dependence1:dependence1.o
cc -o dependence1 dependence1.o
我们知道,Makefile 中的第一个目标会被作为其默认目标。我们声明了一个"all"的伪目标, 其依赖于其它三个目标。由于伪目标的特性是,总是被执行的,所以其依赖的那三个目标就总是不如"all"这个目标新。所以,其它三个目标的规则总是会被决议。也就达到了我们 一口气生成多个目标的目的。 ".PHONY : all"声明了"all"这个目标为"伪目标"。