Linux 第九讲 --- 工具篇(四)项目自动化构建工具 make

前言:

在上一讲中,我们深入学习了版本控制工具 git 和调试工具 gdb,掌握了如何高效管理代码版本以及定位程序问题。今天,我们将进入开发工具链的另一重要环节——项目自动化构建工具 make

在软件开发中,随着项目规模扩大,手动编译每个源文件、链接库文件会变得繁琐且容易出错。make 工具通过解析 Makefile 规则,自动化执行编译、清理、测试等任务,极大地提升了开发效率。无论你是编写小型脚本还是参与大型项目,掌握 make 的使用都能让你事半功倍。本讲将带你从基础语法到实践案例,逐步掌握这一核心工具。


一、什么是make/Makefile

make/Makefile的本质
Make是一个自动化构建工具,它根据Makefile中的指令来自动化执行构建过程。Make的主要目的是简化复杂的构建过程,减少手动操作,从而提高开发效率。

Makefile是一个文本文件,其中包含了构建项目所需的规则和指令。

一个典型的Makefile包含以下部分:

  1. 变量定义:用于定义编译器、编译选项等。
  2. 目标:需要生成的文件,如可执行文件或对象文件。
  3. 依赖关系:指定构建目标所依赖的源文件。
  4. 命令:用于生成目标的具体命令。

简单点来说make是一个命令,Makefile是当前目录下的一个文件 


二、如何使用make/Makefile

2.1 使用实例

为了方便我们下面的讲解,我们这里先带大家看一下如何使用make/Makefile来实现自动化构建的功能

首先,我们要先明白为什么要有这个自动化构建工具,在我们之前的学习中,我们在编写代码的时候会经常用到gcc和删除相关的指令,每次都需要我们重新创建并删除可执行文件,这个操作比较冗余,且当工程比较大时,这种操作就会显得非常麻烦,所以就有了自动化构建工具

下面我们来看一下如何简单的使用make/Makefile

首先,我们要先在当前目录下创建一个Makefile文件

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ ls
make.c  makefile

然后进入这个文件中,将我们的源文件和目标文件建立依赖关系,和相关的清除语句

先来看一下我们的源文件test.c中的内容

#include<iostream>
#include<cmath>
using namespace std;

int main()
{
    for(int i=0;i<10;i++) cout<<"hello world"<<endl;
    return 0;
}

在之前我们只能通过gcc来编译得到可执行文件,运行可执行文件才能得到结果,如果test.c中的内容进行了改动,就需要重新执行上面的步骤,比较繁琐,但现在我们可以通过以下操作

在Makefile文件中写入以下内容:

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ cat makefile 
test.exe:test.cpp
	g++ -o $@ $^ 
.PHONY:clean
clean:
	rm -rf test.exe

写入后保存并退出,然后执行make命令 

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ make
g++ -o test.exe test.cpp 
hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ ls
makefile  test.cpp  test.exe

执行后我们就可以发现我们执行了Makefile文件中的编译命令,生成了可执行文件,运行可执行文件后就可以得到我们想要的结果

当我们要删除我们得到的这个可执行文件时,需要下面的指令即可

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ make clean
rm -rf test.exe

 这样我们就能删除可执行程序

2.2  依赖关系和依赖方法

在上面使用make/Makefile后,我们首先应该理解各个文件之间的依赖关系以及它们之间的依赖方法。

  • 依赖关系: 文件A的变更会影响到文件B,那么就称文件B依赖于文件A。

例如,test.o文件是由test.c文件通过预处理、编译以及汇编之后生成的文件,所以test.c文件的改变会影响test.o,所以说test.o文件依赖于test.c文件。

  • 依赖方法: 如果文件B依赖于文件A,那么通过文件A得到文件B的方法,就是文件B依赖于文件A的依赖方法。

例如,test.o依赖于test.c,而test.c通过gcc -c test.c -o
test.o指令就可以得到test.o,那么test.o依赖于test.c的依赖方法就是gcc -c test.c -o test.o。


三、三个小问题

上面有几个小的知识点值得思考:

1、如果有多层依赖关系怎样处理?

这里的多层依赖关系指的是互相依赖,就比如在vim篇我们已经讲过了可执行文件是由.o文件得来,而.o文件又依赖于.s文件,.s文件依赖于.i文件,.i文件依赖于.c文件,就这样层层依赖,才得到了最终的可执行文件,如果将这几个依赖关系都写入Makefile文件中去,其实我们可以发现它会自己处理这种多层依赖关系,即使我们的顺序写的不对

这是因为,make在执行编译命令的时候,它是会自动寻找编译关系,只要在makefile文件当中存在对应的依赖关系与依赖方法,就可以。

2、为什么make命令的执行结果是编译命令?

这个其实不是一定的,make命令的功能是执行Makefile中的第一条命令,因为我们将编译的指令放在最上面,所以执行结果就是gcc编译,如果我们以下面的这种顺序写入Makefile文件:

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ cat makefile 
.PHONY:clean
clean:
	rm -rf test.exe
test.exe:test.cpp
	g++ -o $@ $^ 

此时我们再执行make命令的执行结果就是清除而不是编译

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ make
rm -rf test.exe

 所以我们的make指令究竟如何执行命令,它是执行第一条指令。

3、当源文件不变时,只能编译一次

我们来看这样一个现象:

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ make
g++ -o test.exe test.cpp 
hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ make
make: 'test.exe' is up to date.

当我们的源文件没有改动时,我们只能make编译一次,再对源文件进行编译之后发现无法再编译了。这样的原因其实是为了提高编译效率,make不允许对没有改变的同一个文件进行多次编译。那么make指令具体是怎么做的呢?这就牵扯到文件时间戳的问题了,下面我们详细讲解一下


四、make原理 

4.1 文件的三个时间戳问题

make实现高效编译的原理其实就是通过比较源文件和可执行文件的修改时间,来判断是否可以再次执行,从而避免无效的执行

具体点来说就是源文件的修改时间新于可执行文件的修改时间时,就能够再次执行make命令,生成新的可执行文件

我们可以用stat指令来查看文件的时间的相关的信息

stat 文件名
hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ stat test.cpp 
  File: test.cpp
  Size: 139       	Blocks: 8          IO Block: 4096   regular file
Device: fc03h/64515d	Inode: 655392      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/      hu)   Gid: ( 1001/      hu)
Access: 2025-05-13 08:21:02.411217775 +0800
Modify: 2025-05-13 08:20:56.867013752 +0800
Change: 2025-05-13 08:20:56.867013752 +0800
 Birth: 2025-05-13 08:20:56.867013752 +0800

在本篇我们需要关注到的就是这三个与时间相关的信息

  • Access:最近访问的时间
  • Modify:最近对文件内容做修改的时间
  • Change:最近对文件属性做修改的时间

其中我们判断是否可以再次执行比较的是源文件与可执行文件的Modify时间

就比如我们上面的test.c和可执行文件test.cpp

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ stat test.cpp
  File: test.cpp
  Size: 139       	Blocks: 8          IO Block: 4096   regular file
Device: fc03h/64515d	Inode: 655392      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/      hu)   Gid: ( 1001/      hu)
Access: 2025-05-13 08:21:02.411217775 +0800
Modify: 2025-05-13 08:20:56.867013752 +0800
Change: 2025-05-13 08:20:56.867013752 +0800
 Birth: 2025-05-13 08:20:56.867013752 +0800
hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ stat test.exe
  File: test.exe
  Size: 16528     	Blocks: 40         IO Block: 4096   regular file
Device: fc03h/64515d	Inode: 655658      Links: 1
Access: (0775/-rwxrwxr-x)  Uid: ( 1001/      hu)   Gid: ( 1001/      hu)
Access: 2025-05-13 08:56:42.929924814 +0800
Modify: 2025-05-13 08:56:42.929924814 +0800
Change: 2025-05-13 08:56:42.929924814 +0800
 Birth: 2025-05-13 08:56:42.821920844 +0800

在上图我们可以看到test.exe的Modify时间是是要比test.cpp新的。 

此时我们更新一下test.c文件中的内容

hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ cat test.cpp 
#include<iostream>
#include<stdio.h>
#include<cmath>
using namespace std;

int main()
{
    for(int i=0;i<10;i++) cout<<"hello world"<<endl;
    printf("zheshi\n");
    return 0;
}
hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ make
g++ -o test.exe test.cpp
hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ stat test.cpp 
  File: test.cpp
  Size: 181       	Blocks: 8          IO Block: 4096   regular file
Device: fc03h/64515d	Inode: 655657      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/      hu)   Gid: ( 1001/      hu)
Access: 2025-05-13 09:00:35.230463123 +0800
Modify: 2025-05-13 09:00:32.266354178 +0800
Change: 2025-05-13 09:00:32.270354325 +0800
 Birth: 2025-05-13 09:00:32.266354178 +0800
hu@iZn4a2tqizasgm5y4xgt22Z:~/linux---test/make$ stat test.exe
  File: test.exe
  Size: 16568     	Blocks: 40         IO Block: 4096   regular file
Device: fc03h/64515d	Inode: 655662      Links: 1
Access: (0775/-rwxrwxr-x)  Uid: ( 1001/      hu)   Gid: ( 1001/      hu)
Access: 2025-05-13 09:01:07.771659170 +0800
Modify: 2025-05-13 09:01:07.771659170 +0800
Change: 2025-05-13 09:01:07.771659170 +0800
 Birth: 2025-05-13 09:01:07.659655053 +0800

此时源文件的最新修改时间就晚于可执行文件的最新修改时间,所以make就可以执行

以上就是make实现高效编译的原理和时间戳介绍。

4.2 make原理

  1. make会在当前目录下找名字为“Makefile”或“makefile”的文件。
  2. 如果找到,它会找文件当中的第一个目标文件,在上面的例子中,它会找到mytest这个文件,并把这个文件作为最终的目标文件。
  3. 如果mytest文件不存在,或是mytest所依赖的后面的文件的修改时间比mytest文件新,那么它就会执行后面的依赖方法来生成mytest文件。
  4. 如果mytest所依赖的文件不存在,那么make会在Makefile文件中寻找目标文件的依赖关系,如果找到则再根据其依赖方法生成目标文件(类似于堆栈的过程)
  5. 如果编译过程是有多个目标文件,那么make会在makefile当中一个一个去找依赖关系与依赖方法,直到最终编译出第一个目标文件。
  6. 在寻找的过程中,如果出现错误,例如最后被依赖的文件找不到,那么make就会直接退出,并报错。

4.3 makefile的小技巧

Makefile文件的简写方式:

  1. $@:表示依赖关系中的目标文件(冒号左侧)。
  2. $^:表示依赖关系中的依赖文件列表(冒号右侧全部)。
  3. $<:表示依赖关系中的第一个依赖文件(冒号右侧第一个)。

makefile文件经常是把clean,声明为强制执行,格式如下

.PHONY:clean
clean:
    rm -rf 目标文件

四、使用Make的优势

  1. 简化构建过程:通过定义规则,开发者可以简化构建过程,只需执行make命令。
  2. 处理依赖关系:Make会自动处理文件之间的依赖关系,只有被修改的文件会被重新编译。
  3. 跨平台性:Makefile可以在多种Unix/Linux系统间共享,简化了跨平台开发。

总结:

至此,我们已经学习了 make 工具的核心用法,包括 Makefile 的编写规则、变量定义、自动化构建与清理等操作。希望你能通过实际项目练习,体会 make 如何将复杂的构建流程化繁为简。

下一讲,我们将开启 Linux 系统编程的核心篇章——进程概念第一讲:冯诺依曼体系结构。作为现代计算机的基石,冯诺依曼体系结构定义了计算机的五大核心组件(运算器、控制器、存储器、输入设备、输出设备),它是理解进程、内存管理、IO 操作等系统级概念的基础。准备好从底层视角重新认识计算机的工作机制吧!我们下期见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值