快速入门Makefile(新手向)

举报
拓佑豪 发表于 2022/03/31 22:05:17 2022/03/31
【摘要】 新手快速入门Makefile

快速入门Makefile(新手向)

1、什么是Makefile

​ 特别是在 Unix 下的软件编译,如果你正在开发一个大型的工程,你就不能不自己写Makefile了。

​ 因为,Makefile关系到了整个代码工程的编译规则。一个工程中的源文件不计数,按类型、功能、模块分别放在若干个目录中,Makefile定义了一系列的规则来指定:哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至进行其他更加复杂的操作。Makefile就像一个 Shell 脚本一样,其中也可以执行操作系统的命令。

​ Makefile带来的好处就是——“自动化编译”,一旦你写好了Makefile,只需要一个make命令,整个工程就会完全自动编译,极大的提高了软件开发效率。

​ 在我还刚接触Makefile的时候,我常常苦恼于找不到易读好懂的Makefile教程。本篇仅仅快速描述一个简单的Makefile应该是什么样子的,介绍一些基本的指令和语法,便于快速熟悉相关的指令。

2、Makefile的一些基本规则

  • 本篇将以C语言的源码为基础,默认使用gcc编译器,需要有相关的前置知识

make 命令执行时,需要一个Makefile文件,以告诉 make 命令需要怎么样的去编译和链接程序。 文件名只能用makefile、Makefile或者GNUmakefile 。最常用的是makefileMakefile

(如果你非要使用别的名字来命名Makefile,需要使用指令make后加参数-f/--file,如 make -f your_makefile_name.md )

  • Makefile 的基本规则。
    • 如果这个工程没有编译过,那么我们的所有 C 文件都要编译并被链接。
    • 如果这个工程的某几个 C 文件被修改,那么我们只会编译被修改的 C 文件,并链接目标程序。
    • 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的 C 文件, 并链接目标程序。

3、Makefile编写

3.1 来写一个最简单的Makefile

我们来看这一段代码a.c

#include <stdio.h>

void main(){
	printf("Hello World\n");
}//a.c

非常简单。加入我们要在Linux下编译运行,应该要怎么做

是的,在shell中使用gcc编译,生成一个可执行的二进制文件。直接执行这个文件就会显示“Hello Worrld”

gcc a.c -o a

如果我要在Makefile里面编译这个a.c的代码,应该怎么写?

a:a.c
	gcc a.c -o a

如果你查阅过和Makefile相关的资料,你可能会看到这段文字

target ... : prerequisites ...  
            command  
            ...  
            ...  

你可以对照上面编译a.c的Makefile代码来看。

  • target 也就是目标文件,可以是 Object File,也可以是执行文件(比如a.c生成的 a 可执行文件)。还可以是一个标签,标签本章暂不介绍,后续的博客再做介绍。

  • prerequisites 就是,要生成那个 target 所需要的文件或是目标。 (a 可执行文件的生成需要依赖于 a.c)

  • command 也就是 make 需要执行的命令。(任意的 Shell 命令,比如调用gcc)

而介绍完基本语句,我们就得回头来看一下make的工作方式。

3.2 make的工作方式

这段文字放在这里我认为才有便于理解

在默认的方式下,也就是我们只输入 make 命令。那么:

  1. make 会在当前目录下找名字叫“Makefile”或“makefile”的文件。

  2. 如果找到,它会找文件中的第一个目标文件(target)

    比如下面这段Makefile,如果我们需要先把 *.c 文件先编译成 *.o 文件,而不是一步到位的编译成可执行文件,可以将上面gcc a.c -o a的步骤拆分成以下两句:

a:a.o
	gcc a.o -o a

a.o:a.c
	gcc -c a.c -o a.o

​ Makefile会先找到 “a” 这个目标,并把这个文件作为最终的目标文件。其余的各项依赖文件得写在后面,也就是我们的要介绍的:

  1. 如果 a 这个文件不存在,或是 a 所依赖的后面的 [*.o] 文件的文件修改时间要比 a 这个文件新,那么,他就会执行后面所定义的命令,以此生成 a 这个文件。

  2. 如果 a 所依赖的 *.o 文件也存在,那么 make 会在当前文件中找目标为 *.o 文件的依赖性,如果找到则再根据那一个规则生成 *.o 文件。(有点像一个堆栈的过程)

  3. 当然,你的 C 文件和 H 文件等等依赖文件是存在的,于是 make 会生成 *.o 文件,然后再用 *.o 文件完成make 的终极任务,也就是生成执行文件 a 了。

并且和上一次的单个语句编译不同,我们同时还能获得 a.o 的文件

3.3 多个文件编译

你现在有一个大工程代码的main代码,是一个计算器,假设你是这样编写的:

#include <stdio.h>

int add(int, int);
int sub(int, int);
int mul(int, int);

int main(){
	int a = 2, b = 1;
	printf("%d+%d=%d\n", a, b, add(a, b));
	printf("%d-%d=%d\n", a, b, sub(a, b));
	printf("%d*%d=%d\n", a, b, mul(a, b));
	return 0;
}//main.c

其中,加法,减法,乘法的函数写在其他的文件里面:

int add(int a, int b){
	return a + b;
}//add.c
int sub(int a, int b){
	return a - b;
}//sub.c
int mul(int a, int b){
	return a*b
}//mul.c

此时我们Makefile就可以这么写:

cal:main.o add.o sub.o mul.o
	gcc main.o add.o sub.o mul.o -o cal

main.o:main.c
	gcc -c main.c -o main.o
add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
mul.o:mul.c
	gcc -c mul.c -o mul.o
  • 其中 cal 文件需要main.o add.o sub.o mul.o,而我们各个 *.o 文件需要由各自的 *.c 文件编译而成。

    记住上面这段makefile的样子。我们接下来会介绍很多种方法,简化上面这段makefile。

    比如我们还可以写成这种形式,便于统一管理:

ALL:cal

main.o:main.c
	gcc -c main.c -o main.o
add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
mul.o:mul.c
	gcc -c mul.c -o mul.o

cal:main.o add.o sub.o mul.o
	gcc main.o add.o sub.o mul.o -o cal

但是如果后续我需要加入一个除法功能,或者还要加入其他计算功能,就需要不断地重写Makefile,岂不是很麻烦,于是我们引入:

3.4 变量

在makefile中,变量声明的时候需要赋初值。使用的时候在变量名前面加上$号。用小括号或大括号括起来

(如果你要使用真实的“ ”字符,那么你需要用“ ”字符,那么你需要用 “ $” 来表示)

ALL:cal

main.o:main.c
	gcc -c main.c -o main.o
add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
mul.o:mul.c
	gcc -c mul.c -o mul.o
	
objects=main.o add.o sub.o mul.o

cal:$(objects)
	gcc main.o add.o sub.o mul.o -o cal

如上面这个例子,我们把main.o add.o sub.o mul.o全部放在objects变量底下,使用的时候就可以用$(objects)把里面存的各种变量拿来编译了。变量是可以嵌套使用的,比如:

x = y 
y = z 
a := $($(x))

这里 := 是赋值的意思,另一种用变量来定义变量的方法,和 = 的区别就是,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。这里的 a 里头存的就是 z 。

有了变量这个好东西,我们就可以使用:

3.5 通配符

我们为了让Makefile自己找被我们更新过的代码,我们可以写成如下形式:

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))

ALL:cal

main.o:main.c
	gcc -c main.c -o main.o
add.o:add.c
	gcc -c add.c -o add.o
sub.o:sub.c
	gcc -c sub.c -o sub.o
mul.o:mul.c
	gcc -c mul.c -o mul.o

cal:$(obj)
	gcc $(obj) -o cal
  • wildcard,纸牌游戏中的 “百搭牌” ,计算机里称为 “通配符” 。会在当前目录自己搜索所有匹配 *.c的文件。(如果你需要使用到路径,碍于篇幅,需要自行了解notdir参数,用法为file=$(notdir $(src))
  • patsubst,模式字符串替换函数。
    • 里面的%是匹配符,假如说我们有main.c add.c sub.c mul.c这几个文件,使用%可以像使用for循环一样,挨个文件名遍历进去*.c
    • 后面$(src)表示:希望patsubst可以遍历哪些文件。我们就遍历当前目录$(src)底下的 *.c 文件

3.6 更多便捷的书写方式

如果我还想更改代码的名称,就需要自己重写makefile,未免有点太麻烦了。如果可以自己去寻找这些文件就好了。所以我们对上面的代码更新了一下,引入自动化变量,功能不变,写成如下形式:

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))

ALL:cal

main.o:main.c
	gcc -c $< -o $@
add.o:add.c
	gcc -c $< -o $@
sub.o:sub.c
	gcc -c $< -o $@
mul.o:mul.c
	gcc -c $< -o $@

cal:$(obj)
	gcc $^ -o $@

clean:
	-rm -rf $(obj) cal
符号 解释 解释
$@ 表示要生成的目标文件 main.o:main.c中的main.o
$^ 表示全部的依赖文件 cal: ( o b j ) 中的整个 (obj)中的整个 (obj)
$< 表示第一个依赖文件 main.o:main.c中第一个依赖,也就是main.c

还有很多其他的自动化变量,如 + , +, *,$?等等,不在本篇博客详细解释,感兴趣的话可以自行查阅相关资料

上面这段代码还可以进一步简化。

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))

ALL:cal

$(obj):%.o:%.c
	gcc -c $< -o $@

cal:$(obj)
	gcc $^ -o $@

clean:
	-rm -rf $(obj) cal

细心的小伙伴会发现这两段代码多出来一个目标clean,如果你希望重新make一遍工程,那就需要先把生成的各项文件删除。用make clean指令就可以自己声明清理函数

如果你在想,我们又不打算生成clean目标文件,有没有别的书写方案?答案是有的,就需要用到标签中的 “伪目标“ .PHONY

.PHONY : clean 
clean : 
	-rm -rf $(obj) cal

(rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。)

当然,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。

当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是“伪目标”。

3.7 引用文件

如果我们整个工程的头文件全都在别的文件夹,比如说在./inc目录底下,我们有add.h mul.h sub.h三个头文件,应该怎么引用进来呢?

src = $(wildcard *.c)
obj = $(patsubst %.c, %.o, $(src))

ALL:cal

$(obj):%.o:%.c
	gcc -c $< -o $@ -I inc

cal:$(obj)
	gcc $^ -o $@ -I inc

clean:
	-rm -rf $(obj) cal

如上代码,我们使用-I/或者--include-dir参数,就可以指定头文件所在位置了

事实上,不只是头文件,有其他的makefile文件,也可以用这个参数导入。make 就会在这个参数所指定的目录下去寻找。如果目录prefix/include(一般是:/usr/local/bin 或/usr/include)存在的话,make 也会去找。

如果有文件没有找到的话,make 会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成 makefile 的读取,make 会再重试这些没有找到,或是不能读取的文件,如果还是不行,make 才会出现一条致命信息。如果你想让 make 不理那些无法读取的文件,而继续执行,你可以像上面的clean目标一样,在 include 前加一个减号“-”。

文章来源:快速入门Makefile(新手向)

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。