自动化工程的构建

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,就会发现编译非常快了)

相关推荐
三言老师10 小时前
秘诀-如何远程SSN访问家里的八台电脑(frp 实操方案)
linux·运维·ssh
举手12 小时前
Dispatcher模块剖析
linux·c++
allforgood13 小时前
运行容器
linux
Light_It14 小时前
Linux 内核参数 pci-stub.ids=
linux·kernel
RisunJan15 小时前
Linux命令-scriptreplay(终端会话回放)
linux·运维·chrome
spencer_tseng15 小时前
[kylin & linux] install docker
linux·docker·kylin
x²+(y-√³x²)²=116 小时前
Linux打包文件到Windows,文件/文件类型丢失
linux·运维·windows
柒号华仔16 小时前
「速通Shell」聚砖成墙,Shell函数封装之道
linux·ssh·bash
未来之窗软件服务16 小时前
企业自动化运营-电子合同预览-东方仙盟
运维·自动化·仙盟创梦ide·东方仙盟