文章目录
- 1. 环境
- 2. 规则
- 3. 原理
- 4. 伪目标
Makefile 其实只是一个指示 make 程序(后面简称 make 或有时称之为 make 命令)如何为我们工作的命令文件,我们说 Makefile 其实是在说 make,这一点要有很清晰的认识。而对于我们的项目来说,Makefile 是指软件项目的编译环境。软件产品开发在编码阶段最常见的工作内容大致是:
- 开发人员根据概要设计进行编码
- 开发人员编译所设计的源代码以生成可执行文件
- 开发人员对软件产品进行测试来验证其功能的正确性
上面的三个步骤是一个迭代过程,如果最终验证设计的正确性完全达到要求,那么就完成了编码阶段的开发,如果没有那还得重复这三个步骤,直到达到设计要求为止。
在上面的几步中,与 Makefile 关系最大的是第二步,那 Makefile 的好坏对于项目开发有些什么影响呢?设计得好的 Makefile,当我们重新编译时,只需编译那些上次编译成功后修改过的文件,也就是说编译的是一个 delta,而不是整个项目。反之,如果一个不好的 Makefile 环境,可能对于每一次的编译先要 clean,然后再重新编译整个项目。两种情况的差异是显然的,后者将耗费开发人员大量的时间用于编译,也就意味着低效率。对于小型项目,低效问题可能表现得并不明显,但对于规模相对大的项目,那就非常的明显了。开发人员可能一天做个十次编译(甚至更少)就没有时间用于编码和测试(调试)了。这就是为什么通常大型项目都会有一个专门维护 Makefile 的一个小团
队,来支撑产品的开发。
最为重要的是掌握二个概念,一个是目标(target),另一个就是依赖(dependency)。目标就是指要干什么,或说运行 make 后生成什么,而依赖是告诉 make 如何去做以实现目标。在 Makefile 中,目标和依赖是通过规则(rule)来表达的。我们最为熟悉的是采用 make 来进行软件产品的代码编译,但它可以被用来做很多很多的事情,后面我们会给出一些不是用 make 来进行代码编译的例子。驾驭 Makefile,最为重要的是要学会采用目标和依赖关系来思考所需解决的问题。
- 目标(Targets): Makefile中的目标是指需要生成的文件或者是需要执行的操作。目标可以是一个文件、一个命令或者是一个操作序列。
- 依赖(Dependencies): Makefile中的依赖是指目标所依赖的文件或者是命令。如果依赖文件发生了变化,那么目标也需要重新生成。
- 命令(Commands): Makefile中的命令是指生成目标所需要执行的操作序列。这些操作可以是编译、链接、复制、打包等等。命令必须以一个制表符或者是多个空格开头,否则会被当成注释。
Makefile 是一个文本文件,其中包含一些规则和指令,用于描述如何编译和链接一个或多个源代码文件,生成可执行程序或库文件。
Makefile 的工作原理如下:
-
Makefile 中定义了目标文件、依赖文件和命令。目标文件通常是可执行程序或库文件,依赖文件是源代码文件、头文件或其他依赖项,命令是编译、链接和生成目标文件的操作。
-
当执行 make 命令时,Makefile 中的规则会被解析,根据依赖关系生成一个依赖图,确定哪些文件需要重新编译。
-
Make 程序根据依赖图和规则,递归地执行编译、链接和生成目标文件的操作,确保所有依赖项都被编译和链接,生成最终的目标文件。
-
如果某些依赖项没有改变,则不需要重新编译和链接,从而提高了编译效率。
-
Makefile 还支持变量、条件语句、循环语句等高级特性,可以根据不同的条件进行编译和链接,生成不同的目标文件。
1. 环境
使用makefile的环境要求如下:
- 操作系统:makefile可以在大多数操作系统上使用,包括Linux、Unix、Mac OS X、Windows等。
- 编译器:makefile需要一个支持GNU make语法的编译器,例如GNU make、BSD make等。
- 目标文件:makefile需要可编译的源代码文件,例如C、C++、Java等。
- 环境变量:makefile需要一些环境变量来指定编译器、编译选项等,例如CC、CFLAGS、LDFLAGS等。
- 编辑器:makefile需要一个编辑器来编写和编辑makefile文件,例如Vim、Emacs等。
- make工具:makefile需要一个make工具来执行makefile文件,例如GNU make。
使用步骤:
- 安装GNU Make工具:Make是一个命令行工具,用于自动化构建软件的过程。可以从GNU官网下载并安装Make工具。
- 创建Makefile文件:Makefile是一个文本文件,其中包含了一系列规则和指令,用于描述如何构建软件。可以在项目根目录下创建一个名为Makefile的文件。
- 编写Makefile规则:Makefile规则由目标、依赖和命令组成。目标是指要生成的文件或者执行的操作;依赖是指生成目标的前提条件;命令是指生成目标的具体操作。
- 运行Make命令:在命令行中进入项目根目录下,输入make命令即可执行Makefile中定义的规则,生成目标文件或者执行操作。
命令行输入make -v
,如果出现类似于下图的版本信息,那么说明make在你的环境中已经可用:
注意事项:
- 在编写Makefile时,应该尽可能使用变量和函数,以便提高代码的可读性和可维护性。
- Makefile中的命令必须以Tab键开始,而不是空格键。
- Makefile中的依赖关系应该尽可能明确,以便正确地判断哪些规则需要执行。
- 在执行Make命令之前,应该确保所有依赖文件都已经存在,否则会导致构建失败。
2. 规则
我们使用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 目标的规则。
下面的示例展示了三种不同的运行方式以及每种方式的运行结果:
- 第一种方式:只要在 Makefile 所在的目录下运行
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!"
从如上输出我们可以发现:
- 一个 Makefile 中可以定义多个目标
- 调用
make
命令时,我们得告诉它我们的目标是什么,即要它干什么。当没有指明具体的目标是什么时,那么 make 以 Makefile 文件中定义的第一个目标作为这次运行的目标。这第一个目标也称之为默认目标(和是不是all没有关系) - 当 make 得到目标后,先找到定义目标的规则,然后运行规则中的命令来达到构建目标的目的。现在所示例的 Makefile 中,每一个规则中都只有一条命令,而实际的 Makefile,每一个规则可以包含很多条命令
对于前面的示例,当运行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)则是一个动作,是由命令所组成的动作。活动与动作的区别是,动作是只做一件事(但是可以有多个命令),而活动可以包括多个动作。
3. 原理
接下来我们试着将规则运用到程序编译当中去,下面我们假设有用于创建 simple 可执行文件的两个源程序文件,我们需要写一个用于创建simple 可执行程序的 Makefile,这个 Makefile 需要如何去写?
foo.c
#include <stdio.h>
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 的改变,并对其进行了重新编译:
4. 伪目标
在 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:通过伪目标,我们可以把不同的任务和操作分开,使Makefile更加清晰易读
- 强制执行:使用伪目标,我们可以强制执行某个任务,而不受文件是否存在或已经更新的影响
在 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"