Makefile 其实只是一个指示 make 程序(后面简称 make 或有时称之为 make 命令)如何为我们工作的命令文件,我们说 Makefile 其实是在说 make,这一点要有很清晰的认识。而对于我们的项目来说,Makefile 是指软件项目的编译环境。软件产品开发在编码阶段最常见的工作内容大致是:
上面的三个步骤是一个迭代过程,如果最终验证设计的正确性完全达到要求,那么就完成了编码阶段的开发,如果没有那还得重复这三个步骤,直到达到设计要求为止。
在上面的几步中,与 Makefile 关系最大的是第二步,那 Makefile 的好坏对于项目开发有些什么影响呢?设计得好的 Makefile,当我们重新编译时,只需编译那些上次编译成功后修改过的文件,也就是说编译的是一个 delta,而不是整个项目。反之,如果一个不好的 Makefile 环境,可能对于每一次的编译先要 clean,然后再重新编译整个项目。两种情况的差异是显然的,后者将耗费开发人员大量的时间用于编译,也就意味着低效率。对于小型项目,低效问题可能表现得并不明显,但对于规模相对大的项目,那就非常的明显了。开发人员可能一天做个十次编译(甚至更少)就没有时间用于编码和测试(调试)了。这就是为什么通常大型项目都会有一个专门维护 Makefile 的一个小团
队,来支撑产品的开发。
最为重要的是掌握二个概念,一个是目标(target),另一个就是依赖(dependency)。目标就是指要干什么,或说运行 make 后生成什么,而依赖是告诉 make 如何去做以实现目标。在 Makefile 中,目标和依赖是通过规则(rule)来表达的。我们最为熟悉的是采用 make 来进行软件产品的代码编译,但它可以被用来做很多很多的事情,后面我们会给出一些不是用 make 来进行代码编译的例子。驾驭 Makefile,最为重要的是要学会采用目标和依赖关系来思考所需解决的问题。
Makefile 是一个文本文件,其中包含一些规则和指令,用于描述如何编译和链接一个或多个源代码文件,生成可执行程序或库文件。
Makefile 的工作原理如下:
Makefile 中定义了目标文件、依赖文件和命令。目标文件通常是可执行程序或库文件,依赖文件是源代码文件、头文件或其他依赖项,命令是编译、链接和生成目标文件的操作。
当执行 make 命令时,Makefile 中的规则会被解析,根据依赖关系生成一个依赖图,确定哪些文件需要重新编译。
Make 程序根据依赖图和规则,递归地执行编译、链接和生成目标文件的操作,确保所有依赖项都被编译和链接,生成最终的目标文件。
如果某些依赖项没有改变,则不需要重新编译和链接,从而提高了编译效率。
Makefile 还支持变量、条件语句、循环语句等高级特性,可以根据不同的条件进行编译和链接,生成不同的目标文件。
使用makefile的环境要求如下:
使用步骤:
命令行输入make -v
,如果出现类似于下图的版本信息,那么说明make在你的环境中已经可用:
注意事项:
我们使用Hello World来开始Makefile规则的学习,编写一个如下的 Makefile 文件,文件的存放目录可以是任意的:
all:
echo "Hello World"
需要注意的是 echo 前面必须只有 TAB,且至少有一个 TAB,而不能用空格代替。
Makefile 中第一个很重要的概念就是目标(target),上面代码中的 all 就是我们的目标,目标放在 : 的前面,其名字可以是由字母和下划线组成。echo “Hello World”
就是生成目标的命令,这些命令可以是任何在你的环境中运行的命令以及 make 所定义的函数等等,这里的 echo 是 BASH Shell 中的一个命令,其功能是打印字符串到终端上。在这里的 all 目标是在终端上打印出“Hello World”,有时目标会是一个比较抽象的概念。all 目标的定义,其实是定义了如何生成 all 目标,这称之为规则,即上面的 Makefile 定义了一个生成 all 目标的规则。
下面的示例展示了三种不同的运行方式以及每种方式的运行结果:
make
命令,终端上就会输出两行,第一行实际上是我们在 Makefile 中所写的命令,而第二行则是运行命令的结果make all
命令,这告诉 make 工具,我要生成目标 all,其结果跟第一种方式一样make test
,指示 make 为我们生成 test 目标。由于我们根本没有定义 test 目标,所以运行结果是可想而知的,make 的确报告了不能找到 test 目标
现在对上面的 Makefile 做一点小小的改动,如下面所示,增加了 test 规则用于构建 test 目标,实现在终端上打印出“Just for test!”:
all:
echo "Hello World"
test:
echo "Just for test!"
make
命令时,我们得告诉它我们的目标是什么,即要它干什么。当没有指明具体的目标是什么时,那么 make 以 Makefile 文件中定义的第一个目标作为这次运行的目标。这第一个目标也称之为默认目标(和是不是all没有关系)对于前面的示例,当运行make
时,在终端上还打印出了 Makefile 文件中的命令。有时并不希望它这样,因为这样可能使得输出的信息看起来有些混乱。要使make
不打印出命令,只要做一点小小的修改,改过的 Makefile 如下所示,就是在命令前加了一个@
。 这一符号告诉make
,在运行时不要将这一行命令显示出来:
all:
@echo "Hello World"
test:
@echo "Just for test!
对上述代码再做一点点小改动,在 all 目标的:
后加上 test 目标,如下所示
all: test
@echo "Hello World"
test:
@echo "Just for test!"
下面讲解一下 Makefile 中的依赖关系
上面的代码中,all 目标后的 test 告诉 make,all 目标依赖 test 目标,这一依赖目标在 Makefile 中又被称之为先决条件。出现这种目标依赖关系时,make 工具会按从左到右的先后顺序先构建规则中所依赖的每一个目标。如果希望构建 all 目标,那么 make 会在构建它之前得先构建 test 目标,这就是为什么称之为先决条件。下面用类图表达了 all 目标的依赖关系:
至此,我们了解 Makefile 中规则,下面是规则的文字和 UML。一个规则是由目标(targets)、先决条件(prerequisites)以及命令(commands)所组成的。需要指出的是,目标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建目标之前,必须保证先决条件先满足(或构建);而先决条件可以是其它的目标,当先决条件是目标时,其必须先被构建出来。
targets : prerequisites
command
规则中目标可以有多个,当存在多个目标,且这一规则是 Makefile 中的第一个规则时,如果我们运行 make 命令不带任何目标,那么规则中的第一个目标将被视为是缺省目标,如下所示:
all test:
@echo "Hello World"
make 处理一个规则的活动图如下图所示,当中的构建依赖目标(build dependent target(s))这一活动(注意是活动,而不是动作)就是重复图下图所示的同样的活动,你可以看作是对下面活动图的递归调用。而运行命令构建目标(run command to build target)则是一个动作,是由命令所组成的动作。活动与动作的区别是,动作是只做一件事(但是可以有多个命令),而活动可以包括多个动作。
接下来我们试着将规则运用到程序编译当中去,下面我们假设有用于创建 simple 可执行文件的两个源程序文件,我们需要写一个用于创建simple 可执行程序的 Makefile,这个 Makefile 需要如何去写?
foo.c
#include
void foo ()
{
printf ("This is foo()\n");
}
main.c
extern void foo();
int main ()
{
foo();
return 0;
}
写一个 Makefile 文件的第一步不是一个猛子扎进去试着写一个规则,而是先用面向依赖关系的方法想清楚,所要写的 Makefile 需要表达什么样的依赖关系,这一点非常的重要。通过不断的练习,我们最终能达到很自然的运用依赖关系去思考问题。到那时,你再写 Makefile 时,头脑会非常的清楚自己在写什么,以及后面要写什么。现在抛开 Makefile,我们先看一看 simple 程序的依赖关系是什么。
第一个跃入我们脑海中的依赖关系图,其中 simple 可执行文件显然是通过 main.c 和 foo.c 最后编译并连接生成的。通过这个依赖图,其实就可以写出一个 Makefile 来了。这样的依赖关系所写出来的 Makefile,在现实中不是很可行,就是你得将所有的源程序都放在一行中让 GCC 为我们编译:
下图是 simple 程序的依赖关系更为精确的表达,其中加入了目标文件。对于 simple 可执行程序来说,下图表示的就是它的“依赖树”。接下来需要做的是将其中的每一个依赖关系,即其中的每一个带箭头的虚线,用 Makefile 中的规则来表示:
all: main.o foo.o
gcc main.o foo.o -o simple
main.o: main.c
gcc main.c -c
foo.o: foo.c
gcc foo.c -c
.PHONY:clean
clean:
rm -f main.o foo.o simple
在这个 Makefile 中,我还增加了一个伪目标用于删除生成的文件,包括目标文件和 simple 可执行程序,这在现实的项目中很常见。
如果我们在不改变代码的清况下再编译会出现什么现象呢?下图给出了结果,注意到第二次编译并没有构建目标文件的动作,但为什么有构建simple可执行程序的动作呢?
Makefile会根据文件的时间戳(即最后修改时间)来判断文件是否需要重新构建。如果某个文件的时间戳比依赖它的文件要旧,那么该文件就需要重新构建。因此,如果你多次执行make命令,即使源文件和头文件没有变化,可执行文件的时间戳也会更新,从而导致重新构建。如果想避免这种情况,可以使用make的增量构建功能,这样只会重新构建必要的文件。
下面验证一下如果对 foo.c 进行改动,是否会重新构建。对于 make 工具,一个文件是否改动不是看文件大小,而是其时间戳。Linux下只需用 touch 命令来改变文件的时间戳,这相当于模拟了对文件进行了一次编辑,而不需真正对其进行编辑,如图所示,make 发现了 foo.c 的改变,并对其进行了重新编译:
在 Makefile 中,伪目标是一种特殊的目标,它并不代表一个实际的文件,而是用于完成特定的任务或者组织其他目标的执行顺序。
假设我们有一个C语言项目,包含以下几个文件:main.c, foo.c, bar.c, foo.h, bar.h。我们需要编译这个项目生成一个可执行文件my_program
。一个简单的 Makefile 可能如下所示:
my_program: main.o foo.o bar.o
gcc -Wall -g -o my_program main.o foo.o bar.o
main.o: main.c foo.h bar.h
gcc -Wall -g -c main.c
foo.o: foo.c foo.h
gcc -Wall -g -c foo.c
bar.o: bar.c bar.h
gcc -Wall -g -c bar.c
clean:
rm -f *.o my_program
在这个 Makefile 中,我们有一个名为clean
的目标。它不依赖于其他目标,也不代表一个实际的文件。它的作用是删除所有的中间文件(.o
文件)和生成的可执行文件(my_program
),这就是一个典型的伪目标。
伪目标的主要特点和用途:
在 Makefile 中,我们可以使用`.PHONY``声明一个伪目标,以明确地告诉 make 这个目标不是一个实际的文件。例如,我们可以在上面的例子中添加如下声明:
.PHONY: clean
这样做的好处是,即使当前目录下存在一个名为clean
的文件,make 也会知道clean
是一个伪目标,而不是一个实际的文件。
当然,除了上述clean伪目标之外,还有其他常见的伪目标。以下是一些在Makefile中经常使用的伪目标:
all
:这个伪目标通常用于编译整个项目。当用户执行make或make all时,它将自动编译并生成所有需要的目标。.PHONY: all
all: my_program
install
:这个伪目标用于安装编译好的程序到系统指定的目录。通常,这需要管理员权限,因为它涉及到在系统目录中创建或修改文件。.PHONY: install
install: my_program
cp my_program /usr/local/bin
uninstall
:这个伪目标用于从系统中删除已安装的程序。和install一样,它通常也需要管理员权限。.PHONY: uninstall
uninstall:
rm -f /usr/local/bin/my_program
test
:这个伪目标用于运行项目的测试用例,确保项目的各个部分正常工作。.PHONY: test
test: my_program
./test_script.sh
help
:这个伪目标用于显示Makefile的使用说明,帮助用户了解如何使用Makefile。.PHONY: help
help:
@echo "Usage:"
@echo " make all - Compile the project"
@echo " make clean - Remove compiled files and binaries"
@echo " make install - Install the program"
@echo " make test - Run tests"