自动化工程的构建

makefile自动化工程

在学习自动化构建工程之前,我们每要想编一个译文件,就得不厌其烦地编写一个gcc指令。自己编写指令比较麻烦还不是主要的问题,主要的问题是gcc指令写错造成的问题:

假设你有一个编写好代码的文件code.c,你用code.c文件拷贝出一份文件code1.c,当你想要编译code1.c,你应该执行的代码是:
gcc code1.c -o code1
但是,你将code1.c与code1的位置搞混,写成了:
gcc code1 -o code1.c

后果就是:不仅Bash报错,code1.c文件也丢失了。

为了解决这些问题,我们就需要学习make / makefile以构建自动化工程。

首先我们要清楚:

  • make:指令
  • Makefile / makefile:文件 ------ 描述的是如何编译当前工程

我们先来见一见自动化工程构建的过程。

1、简单演示

这里有一个我们剩下的code.c文件,里面的内容也是正常的。

创建文件makefile,vim打开makefile,写下指令:

Bash 复制代码
code:code.c
	gcc code.c -o code

然后在外面执行make。就会发现,code.c文件被编译成了code:

接着我们再在makefile中添加指令:

bash 复制代码
.PHONY:clean
clean:
	rm -rf code	

然后在外面执行make clean。就会发现,code被清除了:

我们就完成了一个简单的自动化工程,即对code.c文件进行自动化编译和清除。

创建自动化工程中,我们创建了一个文件makefile。现在我们试着理解makefile文件里到底写了些什么东西。

2、makefile内容的理解

依赖关系和依赖方法

对于指令:

bash 复制代码
code:code.c
	gcc code.c -o code

我们称:

  • code:code.c:依赖关系
  • gcc code.c -o code:依赖方法

依赖关系与依赖方法,我们可以简单举两个例子:

  • 你考入了大学,花钱上了大学,此时你与大学就建立了依赖关系。你想找工作,于是你好好学大学里的知识,然后大学老师会帮你找工作;学习知识与让老师帮你找工作,以达成你找工作的目的,就是依赖方法。
  • 父亲与儿子的父子关系是依赖关系。月底儿子打电话给父亲,叫父亲给生活费,以达成自己需要得到生活费的目的,这就是依赖方法。

对应的,makefile里记录的就是依赖关系与依赖方法的集合。

目标文件与依赖文件列表

bash 复制代码
code:code.c
# 冒号左边的是目标文件
# 冒号右边的是依赖文件列表

注意此处的目标文件,要与源文件翻译中出现的目标文件(.o文件)区分开来。

当然,我们观察与清除有关的指令:

bash 复制代码
.PHONY:clean
clean:
	rm -rf code

依赖文件列表,是可以为空的。

make指令的默认行为

我们这样做:

  • 将当前makefile的code.c编译指令与程序清除指令的顺序颠倒
  • 然后自行gcc编译code.c生成code

我们执行make:

code就被我们删掉了!

所以,默认情况下,make执行的是makefile中第一个依赖关系的依赖方法。

其实,默认情况下,make执行后生成的也只是从上到下第一个目标文件。

如果我们需要指定生成 哪个目标文件,只需在make后添加目标文件的名称。

对于mkaefile文件内容有了基本的认识,我们再来理解make / makefile到底是怎么形成可执行文件的。

3、自动化工程编译时的推导过程

我们知道,.c源文件编译的整个过程是:

  • 预处理成.i文件
  • 编译成.s文件
  • 汇编成.o文件
  • .o文件再进行链接,形成可执行程序

对于一个给定的文件源文件code.c,我们在makefile中反着设计code.c的编译过程:

bash 复制代码
code:code.o
	gcc code.o -o code
code.o:code.s
	gcc -c code.s -o code.o
code.s:code.i
	gcc -S code.i -o code.s
code.i:code.c
	gcc -E code.c -o code.i

我们惊讶地发现,code被编译出来了!而且能够执行。

看上去,make进行了逆向的推导,那么make是怎么做到逆向的推导的?

事实上,make命令内部,维护了一个栈结构。

make命令会在当前目录下寻找makefile文件并解析 ,make对makefile的解析是自上而下的。

当前makefile内容:

bash 复制代码
code:code.o                                                                                                               
	gcc code.o -o code
code.o:code.s
	gcc -c code.s -o code.o
code.s:code.i
	gcc -S code.i -o code.s
code.i:code.c
	gcc -E code.c -o code.i

对于code:code.o,make会在当前目录下寻找依赖文件code.o。而当前目录下没有code.o,依赖方法也就执行不了。此时make就会将依赖方法gcc code.o -o code入栈:

make继续向下,找到code.o:code.s,make依旧没能在当前目录下找到code.s,依赖方法也执行不了。此时make也将依赖方法gcc -c code.s -o code.o入栈:

make一直向下,如果没有在当前目录下找到依赖文件,就将依赖方法入栈。直到make能够找到依赖文件:

make就开始循环执行栈顶依赖方法,出栈...直到栈为空。

以上过程,就是make编译时的逆向推导过程。

这里有两个细节:

  • 实际上我们不会做code.c -> code.i -> code.s -> code.o -> code这么麻烦的操作,我们顶多将code.c直接翻译为code.o,再进行链接。
  • 编写makefile代码(脚本)的时候,只能用tab,不能用空格代替。

4、清除工程

工程被创建,即文件被统一编译好后,工程就可以执行;而工程不被需要的时候,我们就应该清除工程。清除工程,就是清除一些临时文件。

准备一个C语言文件code.c,建立code.c的makefile文件:

其中,清除临时文件的代码为:

bash 复制代码
.PHONY:clean
clean:
	rm -rf code
	rm -rf code.c

其中有三个细节,我们将分别进行讲解。

4.1、前两个细节

依赖文件列表可以为空

这个我们之前已经提到过。

依赖方法的种类与数量

如上面代码所示,依赖方法可以有多个,只需缩进并列在依赖关系下即可。

其实,依赖方法可以是Linux的任意指令。比如我们添加一个创建文件的指令:

bash 复制代码
.PHONY:clean
clean:
	rm -rf code
	rm -rf code.c
	touch test1.txt

4.2、.PHONY

bash 复制代码
.PHONY:clean
clean:
	rm -rf code
	rm -rf code.c

我们可以认为,.PHONY是一个修饰词。.PHONY修饰clean,就是告诉make,clean是一个伪目标。

.PHONY修饰的目标文件,目标文件对应的依赖方法,总是被执行。

总是被执行是什么意思?

4.2.1、"总是被执行",与最佳实践

对于一个完整的makefile文件:

bash 复制代码
code:code.o
	gcc -o code code.o
code.o:code.c
	gcc -c code.c -o code.o

.PHONY:clean
clean:
	rm -rf code
	rm -rf code.o 

当code.c还未被编译,或者code.c内容被修改的时候,我们执行第一次make可以成功,而接下来执行make就不成功了:

这就是不总是被执行。

而我们执行make clean,可以发现make clean每次都成功:

这就是总是被执行。

我们建议,.PHONY修饰clean,而不要修饰创建目标文件等其它依赖方法。

因为我们可以反复清理工程以保证临时文件被彻底消除,而已经编译好的文件就没必要再进行编译;只有文件还未被编译,或者文件内容被修改,才需要重新编译。

这个最佳实践本质上是为了进行有效编译,从而提高效率。

那么问题来了,make怎么知道code.c需要被重新编译?

4.2.2、code.c什么时候需要重新编译?

我们先来搞清楚文件的三个时间。

执行指令stat code.c:

我们主要关注这三个时间:

  • Access:code.c文件上一次被访问的时间
  • Modify:code.c文件上一次内容被修改的时间 ,或者code.c被创建的时间
  • Change:code.c文件上一次属性被修改的时间

我们修改文件属性,文件的Change就会改变。比如我们修改文件code.c,使之对other不具有读权限:

接着使用stat查看:

code.c的Change果然被改变了,并且code.c的Access与Modify没有被改变。

我们修改文件的内容呢?

code.c的三个时间都被改变了。我们可以做这样的解释:修改文件内容需要打开文件,Access被改变;修改文件内容那么Modify一定会被改变;修改文件内容,文件的大小被改变,Change被改变。

访问文件内容,Access有时会改变,有时不会改变。文件被访问是最频繁的事 ,理论上Access应该是三个时间中改动最频繁的那个;而文件Access的每一次改变,都必须将结果写入磁盘以保存,Access的频繁改动,就会加重系统的IO负担。所以Access并不是每次访问都更新,而是有一个更新的策略,不同的系统有不同的更新策略。

如果我们touch已有的文件,文件的三个时间也会改变:

有了上面的知识的铺垫,我们就可以解释code.c什么时候需要重新编译。

正常情况下,code.c文件完成编译,生成code可执行文件,code的Modify肯定晚于code.c:

当code.c还未被编译,或者code可执行文件被删除了,code.c需要再次编译,此时code都没有,更别谈code的时间了。

当code.c第一次编译后,code.c的内容被修改。此时code.c的Modify被修改,code.c的modify就晚于code的modify:

由此,我们就能弄清楚code.c什么时候需要重新编译:

  • 当可执行文件code存在,且code的Modify晚于code.c,code.c就不需要重新编译
  • 当可执行文件code不存在,或者code存在,但code的Modify早于code.c,code.c就需要重新编译

我们也可以顺势推导出:.PHONY的作用,就是忽略以上对于Modify时间的对比 ,使得make执行后直接生成目标文件。

4、makefile的最佳实践

我们建议:

  • 多个源文件,分别编译成.o文件
  • 所有的.o文件再与第三方库链接形成可执行文件.exe
bash 复制代码
code.exe:code.o
    gcc -o code.exe code.o
code.o:code.c
    gcc -c code.c -o code.o

.PHONY:clean
clean:
    rm -rf code.exe code.o      

5、makefile的实用语法

我们可以先给当前makefile备份,因为makefile待会儿是要被修改的,备份以防不时之需。(makefile-backup)

我们在最佳实践中写的makefile,依赖关系与依赖方法有很多重复使用的名字,太冗余了;当前makefile只能针对code.c这一个源文件进行自动化编译,应用范围太狭窄了。

所以我们来学习一些makefile的语法,使我们写的makefile更加简洁,更加通用。

首先,对于

bash 复制代码
code.o:code.c
    gcc -c code.c -o code.o

可以直接省略简写成:

bash 复制代码
code.o:code.c
    gcc -c code.c # -o code.o可省略

5.1、特殊变量

我们用@表示目标文件,用^表示所有依赖文件,用$提取。

那么,

bash 复制代码
code.exe:code.o
    gcc -o code.exe code.o

就可以改成

bash 复制代码
code.exe:code.o
    gcc -o $@ $^

其中,@与^就是特殊变量。

5.2、自定义变量

makefile里可以做到一种类似自定义变量的行为:

bash 复制代码
Bin=code.exe
Obj=code.o
Src=code.c

注意以上写法不要带空格。

所以我们就可以对一些重复使用的文件名,替换成变量名:

bash 复制代码
Bin=code.exe
Obj=code.o
Src=code.c

$(Bin):$(Obj)
    gcc -o $@ $^
$(Obj):$(Src)
    gcc -c code.c

.PHONY:clean
clean:
    rm -rf $(Bin) $(Obj)      

(copy内容到makefile上的时候,记得检查缩进,可以观察代码高亮的颜色)

其中,我们需要用$()提取变量的内容。

我们也可以认为,类似Bin=code.exe的语句,就好像C语言里的宏,其中Bin就是宏名,code.exe就是宏值。我们要想改变文件的文件名,就可以直接修改宏值。

5.3、禁止回显

为了方便调试代码,我们可能会在编译和链接的时候添加一些提示语句:

bash 复制代码
Bin=code.exe
Obj=code.o
Src=code.c

$(Bin):$(Obj)
	echo "我要开始链接了..."
	gcc -o $@ $^
$(Obj):$(Src)
	echo "我要开始编译了..."
	gcc -c code.c

.PHONY:clean
clean:
	rm -rf $(Bin) $(Obj)    

我们这样设计好提示语句后,执行make,结果却有点冗余:

echo语句都打印上来了。有没有办法不要回显 ------ 依赖方法前加上@:

bash 复制代码
Bin=code.exe
Obj=code.o
Src=code.c

$(Bin):$(Obj)
	@echo "我要开始链接了..."
	gcc -o $@ $^
$(Obj):$(Src)
	@echo "我要开始编译了..."
	gcc -c code.c

.PHONY:clean
clean:
	rm -rf $(Bin) $(Obj)    

实际上,依赖方法语句本身默认会回显在屏幕上。我们在每一个依赖方法后加@,都可以禁止其回显。

5.4、进一步优化

我们可以建立一个debug目标,以简单演示工程调试的过程,我们之后会用到:

bash 复制代码
.PHONY:debug
debug:
	@echo "Bin: $(Bin)"
	@echo "Obj: $(Obj)"
	@echo "Src: $(Src)"

由此我们可以发现:$()不仅可以在""外面提取宏值,还可以在""里面提取宏值。

工具的变量化

工具也可以变量化:

bash 复制代码
Bin=code.exe
Obj=code.o
Src=code.c

Echo=echo # 工具变量化
CC=gcc

$(Bin):$(Obj)
	@$(Echo) "我要开始链接了..." # $()提取
	$(CC) -o $@ $^
$(Obj):$(Src)
	@$(Echo) "我要开始编译了..."
	$(CC) -c code.c

.PHONY:clean
clean:
	rm -rf $(Bin) $(Obj)    

选项的变量化

选项也可以变量化:

bash 复制代码
Bin=code.exe
Obj=code.o
Src=code.c

Echo=echo
CC=gcc

Flags=-c -Wall
LD_Flags=-o

$(Bin):$(Obj)
	@$(Echo) "我要开始链接了..."
	$(CC) $(LD_Flags) $@ $^
$(Obj):$(Src)
	@$(Echo) "我要开始编译了..."
	$(CC) $(Flags) code.c

.PHONY:clean
clean:
	rm -rf $(Bin) $(Obj)    

当然,选项和工具,可以一起打包被变量化:

bash 复制代码
RM=rm -rf

.PHONY:clean
clean:
	$(RM) $(Bin) $(Obj)  

特殊变量<,与%的使用

对于

bash 复制代码
$(Obj):$(Src) # Obj: code.o   Src: code.c
	$(CC) $(Flags) code.c

我们也可以写成:

bash 复制代码
%.o:%.c
	$(CC) $(Flags) $<

对于之前的^,我们可以理解为:

  • 依赖文件列表中,所有依赖文件一起,经过依赖方法形成目标文件。

而对于<,我们可以这样理解:

  • 依赖文件列表中,每一个依赖文件,都单独经过依赖方法形成各自对应的目标文件。

至于%.o:%.c,我们可以类比通配符*,即%.c代表了所有的.c文件,%.o代表了所有将要形成的.o文件。

如何证明:每一个依赖文件,都单独经过依赖方法?

证明

这时我们就需要多文件。如下图,当我们添加好了code1.c ~ code1000.c这么1000个文件:

设置源文件变量Src,我们难道要将code.c ~ code1000.c这么1001个文件一个一个输进去吗?太麻烦了。

有两种做法:

  • Src=$(shell ls *.c)
  • Src=$(wildcard *.c)。我们推荐这种方法。

所有的源文件都要分别翻译成.o文件,那么我们也要一个一个输入?也不是的。

我们可以执行:Obj=(Src:.c=.o)

执行make debug,我们就能观察到,多个.c文件与多个.o文件都有了。

至此,我们就完成了一个当前目录下多文件编译的makefile工程。

bash 复制代码
Bin=code.exe
Obj=$(Src:.c=.o)
#Src=$(shell ls *.c)
Src=$(wildcard *.c)

Echo=echo
CC=gcc

Flags=-c -Wall
LD_Flags=-o

RM=rm -f

$(Bin):$(Obj)
	@$(Echo) "我要开始链接了... $(Obj) ---> $(Bin)"
	$(CC) $(LD_Flags) $@ $^
%.o:%.c
	@$(Echo) "我要开始编译了... $< ---> $@"
	$(CC) $(Flags) $<
                                                                                                    
.PHONY:clean
clean:
	$(RM) $(Bin) $(Obj)

.PHONY:debug
debug:
	@echo "Bin: $(Bin)"
	@echo "Obj: $(Obj)"
	@echo "Src: $(Src)"

我们执行make,也确实会看到每一个.c文件都是分别翻译为.o文件的。

编译有时间成本

我们写好上面的makefile,执行make的时候发现,多文件.c编译成.o的过程花了不少时间。所以随着文件越多越大,编译的时间是越长的。

所以,多个.c文件分开翻译成.o文件,并且要是修改了某几个文件,make对修改了的文件重新编译,而对没修改的文件不编译,再重新生成可执行文件,是能提高工程效率的。(你可以试试只修改两个文件,再重新make,就会发现编译非常快了)

相关推荐
律宏阔3 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔3 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴3 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千4 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei374 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei374 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei374 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW4 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW4 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei374 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化