【Makefile】 - GNU make 序&缘起

写 C++ 的在研究程序构建的时候,Python程序员已经回家抱老婆了 : )

在这里插入图片描述

什么是Makefile?

简单来说,Makefile 是一个文件,包含了一些 编译链接规则rule),当我们使用 make 命令时,make 程序会解释这些规则,并执行它们。有点类似于脚本,不过它主要用于构建项目(编译、链接)以生成目标。

为什么需要Makefile?

世间安得双全法

首先必须承认,在现代软件的开发中,集成开发环境(IDE)可以取代 make;但是 makeUniux系统 上仍有比较高的地位。

其次,虽然 IDE 帮我们实现了编译链接。但是 简单就意味着失去对细节的控制。要想实现对细节控制,就必须面对复杂性。

寻找两者之间的平衡很重要。

在这里插入图片描述
大部分情况下,我们并不需要对编译链接进行细节控制,IDE 提供默认的编译链接方案,帮我们从写 Makefile 中解放处理,这是 IDE 的价值之一。

对于 make ,考虑到其产生的背景,它帮我们摆脱重复的编译链接指令,并能控制其中细节,这是其存在的意义。

那么我们如今还需要学习写 Makefile 吗?

  • 没有 IDE 的情况下,Makefile 仍然是不错的选择

  • 阅读源码需要(一些著名的开源项目中都存在着 Makefile,想要研究它们,绕不开 Makefile

Make工具

Makefile 需要 make 去解析执行。这方面有非常多的选择:

  • Linux下的 GNU make
  • Visual C++ 的 nmake
  • ...

不同厂商的 make 各不相同,语法也不尽相同;但本质都是在文件依赖性上做文章。本文选用的是 GNU make3.8.1

此外,Makefile 出现至今,已经发展出不少工具帮助我们生成 Makefile,例如:

  • Qt 的 qmake
  • 支持跨平台的 CMake
  • Linux下的 Autoconf+Automake

GNUmake

程序编译

在没有 Makefile 之前,我们生成可执行文件,可以执行以下指令:

//编译
g++ -c main.cpp -o main.o
g++ -c foo.cpp -o foo.o
//链接
g++ main.o foo.o  -o app

每次修改程序都需要执行这几步(这还是最简单的情况,如果有10个文件构成我们的工程呢?),而且是重复的机械运动。

作为 懒惰的代表 ,思考从机械运动中解放出来怎么能叫懒惰呢?

脚本

首先想到的是:编写脚本,让它变成一个 “动作”。

例如编写一个 build.sh ,每次需要构建时执行该脚本即可。

但是这种方法引入一个问题,这个问题在大型项目中更为明显:
若仅修改了某个文件,然后执行该脚本,则有很多不必再次编译的文件会重新生成。若文件非常大…像 Ubuntu 那种级别,则编译时间非常久。但实际上,我们只需要编译改动的文件,再将它们重新链接即可。

make由来

要想解决这个问题,同时且有较好的可读性,shellbash等脚本无法满足。于是人们定义了 make,它读入Makefile(makefile、MAKEFILE亦可),Makefile中包含源文件依赖关系及编译规则;再通过这些规则生成目标程序。

只要我们的 Makefile 写得好,我们只需要一个 make指令 ,就能自动智能的根据当前文件修改情况来确定哪些文件需要重新编译,从而自动编译所需文件并链接目标文件。

# 依赖关系
app: foo.o main.o
# 规则(注意:前面是一个Tab,不要打空格)
	g++ main.o foo.o -o app
foo.o: foo.cpp
	g++ -c foo.cpp -o foo.o
main.o: main.cpp
	g++ -c main.cpp -o main.o

Makefile 中的依赖关系构成依赖树:

在这里插入图片描述

make是怎么工作的?

注意:Makefile并不是顺序执行的!

当我们执行 make 指令时:

  • make 会先找到 Makefile

  • 如果找到则默认将 第一个目标文件 作为最终目标文件(例如 app

  • 如果目标文件不存在,或后面所依赖的文件修改时间比目标文件新,则重新执行后面定义的 规则,以生成目标文件

  • 如果依赖文件已经存在,make 会查找依赖文件(例如 foo.o)的依赖。并根据上面的规则生成。最终会找到依赖树的 叶子节点

在以上过程中,如果出现错误,那么 make 会表示:我不干了! 并把错误抛给你。

PHONY依赖

按照上面的工作流程,规则是否执行取决于目标是否存在,依赖是否更新等。

但有时候,我们期待某些规则 无条件执行

那么可以定义一个永不被满足的依赖:

clean:
	rm main.o 
	rm foo.o

clean文件 永远不会被产生,规则必定执行。这种形式虽然在规则上可行,但是 make 会将 clean 当成文件,当它成为依赖树的一部分时,很容易造成误会和处理差错。

于是 Makefile 允许我们显示的把依赖目标定义为假的(Phony),这样就不用为难 make 了。

.PHONY: clean

app: foo.o main.o
	g++ main.o foo.o -o app
foo.o: foo.cpp
	g++ -c foo.cpp -o foo.o
main.o: main.cpp
	g++ -c main.cpp -o main.o
	
clean:
	rm main.o 
	rm foo.o

可以发现我们的 Makefile 中还有非常多重复内容,这些重复的内容可以通过 来替代

.PHONY: clean

CC = g++ -c
LD = g++

app: foo.o main.o
	$(LD) main.o foo.o -o app
foo.o: foo.cpp
	$(CC) foo.cpp -o foo.o
main.o: main.cpp
	$(CC) -c main.cpp -o main.o
	
clean:
	rm main.o 
	rm foo.o

重复的内容还很多,例如我们在依赖中写了 foo.o,在规则时又写了一次。

GNUmake 允许我们使用 $@ 替代 依赖对象,使用 $^ 替代 被依赖对象

于是可以改写为:

.PHONY: clean

CC = g++ -c
LD = g++

app: foo.o main.o
	$(LD) $^ -o $@
foo.o: foo.cpp
	$(CC) $^ -o $@
main.o: main.cpp
	$(CC) -c $^ -o $@
	
clean:
	rm main.o 
	rm foo.o

通配符

很明显,重复的还不少,我们将重复部分改为 通配符

.PHONY: clean

CC = g++ -c
LD = g++

app: foo.o main.o
	$(LD) $^ -o $@

%.o: %.cpp
	$(CC) $^ -o $@
	
clean:
	rm main.o 
	rm foo.o

通配符 % 的意思是:

  • 例如我们需要 foo.o 的构造规则,就在 Makefile 中寻找,然后发现了 %.o: %.cpp

  • foo.o 套入 %.o,那么 % 就是 foo了,后面的 %.cpp 就是 foo.cpp

  • OK,进行构造

隐含规则

还有更简洁的做法,即使用 GNUmake 的隐含规则:

SRC = $(wildcard *.cpp)
OBJ = $(SRC:.cpp=.o)
app: $(OBJ)
	g++ $^ -o $@

这里没有定义 foomain 的依赖,但 gnumake 默认如果 .cpp 存在,.o 就依赖对应的 .cpp ,而 .o.cpprule ,是通过 默认定义的。我们可以通过修改CCLDLIBS 这类的宏去改变默认规则。

至此,初步窥探 Makefile 一二。
在这里插入图片描述

参考鸣谢

Makefile概念入门

程皓《跟我一起写Makefile》

GNU Make(生肉)

徐海兵 译《GNU make中文手册》

Makefile中的%标记和系统通配符*的区别

猜你喜欢

转载自blog.csdn.net/weixin_40774605/article/details/106862014