自学内容网 自学内容网

Linux项目自动化构建工具make/Makefile

前言

身处 linux 平台环境开发中的伙伴们都知道 gcc/g++ 编译器以及编译指令,但是不难想象在以后的生活或者工作中,肯定是有多文件编译的需求,少则数10个,多则上百也不是不可能。

那么我们难道就直接 gcc -o test t1 t2 t3 ..... t99 吗??显然是费力不讨好,毕竟还有一个 rm t1 t2 t3 ...... t99 等着你呢!

所以针对上述场景及其需求,该篇文章主要介绍的是在linux系统中项目自动化构建工具make以及其配置文件Makefile的相关内容。



我们先抛开一切原理及其设计理念,先见一见所谓的make以及Makefile,所谓 “没吃过猪肉,咱也得见一见猪跑吧~~”

# Makefile
test:test.cpp
g++ -o test test.cpp
clean:
    rm -f test

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/eb256ed177f14a4e89da9d1bcc4d3cb9.png

有了 Makefile 之后,我们只需要make 即可完成编译生成可执行程序,make clean 即可完成删除可执行程序。

1. Makefile 文件的基本构成

# Makefile
test:test.cpp
   g++ -o test test.cpp
clean:
   rm -f test

其中的 test:test.cpp 和 clean 我们称之为依赖关系,test 和 clean 下面带的指令我们称为依赖方法

这么一听似乎有点抽象。什么是依赖关系?什么是依赖方法?

打个比方,月底到了,作为大学生的张三生活费以及见底了,于是乎,他需要打电话给他的老爸,而接通电话的那一刻,张三即需要表面身份,即与他电话沟通的人的关系,即所谓的依赖关系。而张三表面完关系之后,需要表面其来电目的,即依赖方法。如果张三不表明依赖关系,他爸凭什么给他生活费?换言之,假设今天张三拨号给中国银行,让中国银行给予其生活费,中国银行会同意吗??道理很简单,因为张三与中国银行之间不存在依赖关系,因此,没有依赖关系的基础上,无法执行依赖方法(也即为张三来电的目的)。同理,假设张三拨号给他老爸,开头一句:”爸!“,然后马上把电话挂了,他爸知道他要干嘛吗??是不是显然不知道!因此,仅有依赖关系也不够,还需要有与该依赖关系对应匹配的依赖方法!

2. makefile的依赖关系的自动化推导

我们把 Makefile 文件改稍微复杂一点,如下:

test:test.o   想要生成 test 可执行程序,需要先有 test.o 文件
g++ test.o -o test    
test.o:test.s想要生成 test.o 文件,需要先有 test.s 文件
g++ -c test.s -o test.o
test.s:test.i想要生成 test.s 文件,需要先有 test.i 文件
g++ -S test.i -o test.s
test.i:test.cpp直到最后找到 test.cpp,然后预处理之后生成 test.i 文件
    g++ -E test.cpp -o test.i
clean:
    rm -f test.i test.s test.o test

在这里插入图片描述

我们可以看到,将makefile 改为四步编译之后,执行make依旧可以顺利的进行编译,并且其编译过程是按照四步编译的顺序进行编译的(与Makefile 文件里面的依赖关系与其匹配的依赖方法的位置顺序无关!!!)。这就说明在 make 执行的过程中,make会自动推导 makefile 中的依赖关系。

并且我们这么一看,想要 test,需要先有 test.o, 想要 test.o,需要先有 test.s, 想要 test.s,需要先有 test.i, 想要 test.i,需要先有 test.cpp,然后 .i 文件有了,返回给 .s 的依赖关系。。。依次进行返回。这个过程不就是类似于递归的过程吗?!而所谓的 test.cpp 就类似于递归出口。 我们都知道,递归就需要借助栈帧或者栈结构来完成。那么我们就将 这种类似于递归过程,栈这样的结构,称之为makefile的依赖关系的自动化推导!


但是,为什么 clean 的时候,我们需要在前面加上一个make,而编译的时候不用呢??
我们修改一下makefile文件:

clean:
    rm -f test.i test.s test.o test
    
test:test.o   
g++ test.o -o test    
test.o:test.s
g++ -c test.s -o test.o
test.s:test.i
g++ -S test.i -o test.s
test.i:test.cpp
    g++ -E test.cpp -o test.i

在这里插入图片描述

在我们调换 可执行程序 和 clean 的位置之后,我们发现,make 变成 clean了!! 而编译我们需要 make + 目标文件名 才能够完成。因此我们不难得出结论:make会自顶向下扫面 makefile 文件,把你要形成的第一个目标文件,当作make的默认动作!而 make + 目标文件即为:指定名称的执行 该依赖关系 与其匹配的 依赖方法


3. make执行过程中的一些现象及其原理

在这里插入图片描述

可以看到,当我们make编译完该文件之后,系统就不让我们继续make了!这是为什么呢??

我们知道的是,不管在 windows 还是 linux, 文件 = 文件内容 + 文件属性,并且该等式恒成立。那么我们现在先猜测,可能是因为源文件没有进行任何修改,因此 make 会根据源文件和目标文件的新旧,判定是否需要重新执行依赖关系进行编译!

那么现在的问题是:为什么 make 要这么做??原因其实也很好理解,因为在学习阶段,源代码本身的编译时间几乎忽略不计,但是等到了工作和研发当中,几十万,几百万的源代码,编译时间可是少说一个小时起步的,那么 make 这样做的原因无非就是在不影响程序的前提下,提高效率!

到这里,又有另一个问题产生,make 是如何完成对源文件和目标文件的新旧进行判定的??
在回答这个问题之前,我们要有一个基本的认知:“一定是先有的源文件,才有的可执行程序,而一般而言,源文件的最近修改时间 比 可执行文件要早!!” 。那么这样一来,我们就不难猜测出,该操作只需要 对 可执行程序的最近修改时间 (.out)与 源文件的最近修改时间 (.cpp)进行比较即可判断是否需要对该源文件进行编译。如果 .out < .cpp 那么即说明在可执行程序生成之后,源文件有过变动,因此需要重新编译;反之, .out > .cpp,即说明源文件生成可执行程序之后,并没有过修改动作(不管是文件内容还是文件属性)。

讲到这里,上述的一切也只不过是我们的猜测结论而已!!那么接下来,我们将对上述的猜测进行一定的证明:

3.1 证明该现象原理


[outlier@localhost makefile]$ stat test.cpp 
  File: ‘test.cpp’
  Size: 153       Blocks: 8          IO Block: 4096   regular file
Device: fd00h/64768dInode: 1351420     Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/ outlier)   Gid: ( 1001/ outlier)
Access: 2024-07-06 22:13:05.007175733 +0800
Modify: 2024-07-06 21:55:05.962028145 +0800
Change: 2024-07-06 21:55:05.963028143 +0800
 Birth: -

以上是关于 test.cpp 文件的三个时间属性,分别是:
Access(最近访问时间,对文件增删查改都属于访问的范畴)
Modify(文件内容的最近修改时间),
Change(文件属性的最近修改时间)

先是对这三个时间属性进行分析,虽然我们不太能对访问的范畴有一个很好的标准,但是对于文件内容和文件属性的修改可以做一波推论 ==》 Modify 修改,极大可能的会导致 Change 的修改,最常见的就比如文件的大小发生改变。而Change 的修改,不一定会导致 Modify 的修改,比如改变文件的权限属性,那么这种情况下,文件内容是不变的。


接下来我们对文件内容进行修改,并且观察修改前后的 Modify 时间,以此来判断 make 是不是因为最近修改时间作为判断依据。

在这里插入图片描述

设 源文件的最近修改时间 = Tcpp,可执行程序的最近修改时间 = Tout

这次的测试证明了我们上述的猜想和推论是正确的,修改了源文件,Tcpp 得到改变之后,Tcpp > Tout,因此 make 会重新执行依赖关系进行编译源文件。

而对文件内容进行修改之后,Change 也进行了更新,说明文件内容的修改,往往极大可能伴随的是,文件属性的修改!一般 Change 也会随之改变。

————————————————————————————————————————————————————

3.2 关于 stat 时间属性的拓展


上面说了, Access 是文件访问的最近时间,可是现在又有一个现象,当我访问了文件,却不见得 Access 得到更新,这是为什么呢??

在这里插入图片描述

Access 并没有随着我们的查看而进行改变,其中的原因可能涉及到 IO 方面的设计。

我们应该都需要清楚的是, Access 毫无疑问是三大时间属性当中,修改频率最高的一个,而文件是存储在磁盘当中,修改一次Access ,等价于修改一次文件属性,意味着需要做一次持久化。问题就在于磁盘属于外设,其读写速度远远低于内存,更别提cpu了,因此,高频率的对外设进行读写,是一个非常大的代价,也是效率极低的一个行为,该行为不利于系统整机的效率!

因此,Access 采用了类似“缓存“的更新策略,比如根据 Modify 和 Change 的修改次数 或者 文件被访问到一定次数 之后,才对 Access 进行新的持久化。

但是假如此刻我就是想要在访问文件之后,Access 就立刻、马上得到更新!那有办法吗??
这就需要我们重新来认识一下 touch 这个指令了

touch 文件
a. 文件不存在时,创建文件
b. 文件存在时,更新文件的时间(三个时间属性都会更新)

touch -a  
touch -m
touch -c 
也可以只根据某个时间属性进行更新

在这里插入图片描述
在这里插入图片描述


到这里,已经验证了我们上述的所有猜想以及推论,make 是否会重新执行依赖关系进行编译,取决于 可执行程序的最近修改时间 是否早于 或者晚于 源文件的最近修改时间。而 touch 文件 时,文件已经存在,则可以更新文件的时间属性,而一般情况下,不管是 touch -a 还是 touch -m ,Change 也会随着这两个的更新而更新,而 touch 不带选项是更新整个文件的时间


现在,我就是不想要使用 touch, 但是我还想要可以让 依赖关系 总是被执行!还有其它办法吗??

.PHNOY:test
test:test.cpp
   g++ -o test test.cpp
clean:
   rm -f test

其中的 .PHNOY 后面修饰的文件符号,我们称之为 伪目标
.PHNOY:test 代表的就是:test 的依赖关系 与其对应的 依赖方法 总是被执行!
就相当于告诉 make,你别管 Tcpp 和 Tout 的关系了!当用户执行 make 指令的时候,你就执行一遍依赖关系进行编译就行了!

在这里插入图片描述

按照 make 原本的规则,这 Tccp < Tout,其依赖关系与其对应的依赖方法是不被执行的啊!但是,我们在 Makefile 添加了 test 这个依赖关系的伪目标,因此 make 在执行的时候就可以忽略时间上的关系。


————————————————————————————————————————————————————

好了,回到我们这篇文章的主题:make

上面讲了那么多样的任性,我们可以那么任性,但是我们一般不那么做,因为出于各种考虑,给 make 加上时间上的限制也不是什么坏事。

所以我们的 makefile 文件一般会把伪目标设置成 clean,因为在以后,clean 的依赖方法可能不仅仅是 rm 这么简单的操作!

test:test.cpp
g++ -o $@ $^
.PHNOY:clean
clean:
    rm -f test

其中的 $@ 就是目标文件,即冒号以左的部分,$^ 就是源文件,寂即冒号以右的部分,这样写的好处是,当依赖关系中的源文件很多的时候,依赖方法就不需要你重新在写一遍了,只需要 $@ $^ 即可。

而当我们在执行 make 或者 make clean 的时候,不希望执行的依赖关系对应的依赖方法回显在屏幕上,我们可以这样写

test:test.cpp
@g++ -o $@ $^
.PHNOY:clean
clean:
    @rm -f test

OK,关于 make/Makefile 我们就讲这么多,该篇文章可以说是比较完整,系统的讲述了 make 和 Makefile 的方方面面,包括各种想象,以及其原因。

如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!


原文地址:https://blog.csdn.net/Crazy_Duck_Weng/article/details/140236298

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!