彻底掌握Makefile(一)

介绍

makefile就是一个可以被make命令解析的文件,他定义了一系列编译的规则,帮助我们更加方便、简洁的去完成编译的过程。在一个大工程当中我们会有各种各样的文件,我们可能会分模块去存放各种文件,可能有些文件还依赖其他的文件,因此我们在编译的时候需要先将被依赖的文件先编译,其他文件后编译,而我们使用makefile就可以更好的去完成这件事儿。

Makefile基础

在很多情况下我们在C/C++的项目当中使用makefile,其实在其他语言的项目也可以使用make和makefile,它不局限于语言的,可以是C也可以是Java。现在写一个基本的例子:

demo: demo.c
gcc demo.c -o demo clean:
rm demo

上面是一个makefile的例子,我们简要说明一下makefile的书写规则:

编译目标:依赖文件
编译命令

然后我们使用make命令去解释执行makefile,我们可以使用make 编译目标去执行特定的语句,比如在上面的例子当中我们执行make demo的话就会执行gcc demo.c -o demo命令。

其实上面我们也可以直接使用make命令,不需要指定编译目标,因为make会自己寻找第一个目标作为被执行的目标。

在上面的代码当中我们当我们执行make的时候寻找到makefile文件的第一个目标demo,但是因为我没有改动demo.c这个文件,而且这个文件已经编译过了,因此我们没有必要再去编译这个文件,这也是make给我们提供的一个非常好的特性,我们不需要重新编译已经编译好的文件,这在一个大型项目当中是非常有用的,当我们的项目当中有成千上万的文件的时候,如果我们重新编译每一个文件的话,那么编译的时间消耗是非常大的。因此我们在执行make执行执行clean编译目标先删除demo这个编译结果,然后在执行make这次它再找到demo目标,而此时demo已经被删除了,因此会重新编译。

Make命令的工作流程

当我们在命令行当中输入make的时候他的执行流程如下:

  • make命令首先会在当前目录下面寻找makefile或者Makefile文件。
  • 寻找到makefile文件之后,他会在文件当中寻找到一个编译目标,比如在上面的makefile文件当中他会找到demo这个编译目标,而不是clean这个目标,因为clean是第二个编译目标。
  • 然后make会解析编译目标的依赖,如果这个依赖是其他的编译目标A的话,那么make会先完成它依赖的编译目标A的命令,如果它依赖的编译目标A也存在依赖B的话,make就会去执行依赖的B的编译命令,如此的递归下去,知道有所得依赖目标都存在了,才会完成第一个编译目标的编译,这个也很好理解,只有依赖文件都存在了我们才能够完成正确的编译过程。

make编译的过程为寻找编译目标,依赖关系,如果依赖的文件还存在依赖那么make会一层一层的寻找下去,只要所有的依赖都被成功解析了,才会最终执行第一个编译目标的编译命令。但是在makefile当中不被第一个编译目标的目标的编译命令是不会被执行的,比如上面我们执行make的时候会执行demo编译目标,但是不会执行clean编译目标。

下面我们写一个例子了解make解析依赖的过程:

main: demo.o myprint.o
gcc demo.o myprint.o -o out
echo make 解析编译完成 demo.o: demo.c
gcc -c demo.c -o demo.o myprint.o: myprint.c
gcc -c myprint.c -o myprint.o clean:
rm myprint.o demo.o out

执行make之后的结果如下图所示:

因为我们没有指定编译的目标,因此make会寻找一个编译目标,也就是main,但是在main当中依赖两个文件demo.omyprint.o,因此make会再去执行demo.omyprint.o两个编译目标,当这两个编译目标被完成之后才能执行main的编译,根据make的输出结果我们可以得到这一个结果的印证。

Makefilex小技巧

Makefile当中的变量

在makefile当中我们可以定义一些我们的变量这样就可以避免反复输入了。比如我们经常会变编译文件的时候增加一些编译选项,比如像下面这样:

cflags=-c
main: demo.o myprint.o
gcc demo.o myprint.o -o out demo.o: demo.c
gcc $(cflags) demo.c -o demo.o myprint.o: myprint.c
gcc $(cflags) myprint.c -o myprint.o clean:
rm myprint.o demo.o out

在上面的makefile当中我们定义了一个变量cflags并且在编译命令当中使用,我们定义变量的方法其实和shell差不多,我们直接使用=可以定义变量,然后使用$(变量名)可以使用变量,因为上面的例子当中cflag=-c比较短,比较简单,但是如果当我们的编译参数很多很长的时候使用变量就非常有效了,而且如果在一个项目当中如果有成千上万个文件我们像统一改变编译时候的参数的话,我们一个一个改是很麻烦的,但是如果我们使用变量就可以做到一改全改。

Makefile当中的include命令

在makefile当中我们也可以使用include命令去包含其他的makefile文件,比如我们将上面的makefile文件分成两个部分makefilesubmakefile:

makefile:

include submakefile

demo.o: demo.c
gcc $(cflags) demo.c -o demo.o myprint.o: myprint.c
gcc $(cflags) myprint.c -o myprint.o clean:
rm myprint.o demo.o out

submakefile:

cflags=-c
main: demo.o myprint.o
gcc demo.o myprint.o -o out

然后在目录下执行make命令, 得到的效果和前文当中提到的makefile结果是一样的,这就相当于将submakefile的内容放到include语句的位置。

Makefile中的PHONY

在上面谈到的makefile当中有一个clean的编译目标用于清除我们编译的结果文件,现在我们在当前的目录下面增加一个文件clean在执行make命令,我们的makefile文件如下:

cflags=-c
main: demo.o myprint.o
gcc demo.o myprint.o -o main demo.o: demo.c
gcc $(cflags) demo.c -o demo.o myprint.o: myprint.c
gcc $(cflags) myprint.c -o myprint.o clean:
rm myprint.o demo.o main

然后执行下面的命令(touch命令是新增一个文件,touch clean 就是往目录中增加一个名字为clean的文件):

我们可以看到当目录下面增加一个clean文件之后,我们使用make clean命令的时候,make给我们的提示信息为clean文件是最新的了。这是因为当执行make 编译目标,make首先会检查当前目录下面是否存在clean文件如果不存在则执行编译目标clean的命令。如果存在clean文件的话,make会检查编译目标clean的依赖文件是否发生更改,如果发生更改了那么就会执行clean对应的命令,但是在上面的makefile当中clean没有依赖文件,因此相当于他的依赖文件没有发生改变,make不会执行编译目标clean对应的命令了。

但是我们的需求是仍然希望执行clean之后的命令,这个时候我们就可以使用PHONY了,他可以保证即使存在clean文件的时候,make命令依然会执行编译目标clean对应的命令。

cflags=-c
main: demo.o myprint.o
gcc demo.o myprint.o -o main demo.o: demo.c
gcc $(cflags) demo.c -o demo.o myprint.o: myprint.c
gcc $(cflags) myprint.c -o myprint.o clean:
rm myprint.o demo.o main
.PHONY: clean # 增加这一行

执行结果如下图所示:



我们现在来测试一下当我们命令生成的文件和编译目标不同名的时候,make是如何解释执行makefile的,makefile的内容如下:

cflags=-c
main: demo.o myprint.o
gcc demo.o myprint.o -o out # 这里的输出文件是 out 而不是 main 输出文件名字和目标不同名 demo.o: demo.c
gcc $(cflags) demo.c -o demo.o myprint.o: myprint.c
gcc $(cflags) myprint.c -o myprint.o clean:
rm myprint.o demo.o main
.PHONY: clean

执行结果:

从上面的结果我们可以发现,当我们编译的结果文件(out)和编译目标(main)不同名的时候make每次都回去执行这个目标,因为make在进行检测的时候没有发现main这个文件,因此每次执行make命令的时候都会去执行这个编译目标。

Makefile的通配符

我们现在修改前面的makefile,修改的结果如下(当前目录下面有两个文件demo.c和myprint.c):

cflags=-c

main: demo.o myprint.o
gcc demo.o myprint.o -o main %.o: %.c
gcc $(cflags) $<
clean:
rm myprint.o demo.o main
.PHONY: clean

上面的makefile当中有一个通配符%,其中%.c表示当前目录下面的所有的以.c结尾的文件。上面的makefile与下面的makefile的效果是一样的:

cflags=-c

main: demo.o myprint.o
gcc demo.o myprint.o -o main demo.o:demo.c
gcc $(cflags) demo.c myprint.o:myprint.c
gcc $(cflags) myprint.c clean:
rm myprint.o demo.o main
.PHONY: clean

在上面的makefile当中 $< 表示第一个依赖文件,上面的%就是一个通配符,%.c可以匹配任何以.c结尾的文件,你可能会有疑问%.c匹配不是所有的.c文件吗?那么等价的结果不应该是:

cflags=-c

main: demo.o myprint.o
gcc demo.o myprint.o -o main demo.o myprint.o:demo.c myprint.c
gcc $(cflags) demo.c clean:
rm myprint.o demo.o main
.PHONY: clean

事实上你可以认为make会将通配符匹配的文件一一展开,有几个文件就将产生对应数目的编译目标,而不是将他们都放在一起。

Makefile文件自动搜索

在一个工程项目当中我们可以会有许多的目录以及源文件,而make命令只会自动搜索当前目录下的文件,如果当前目录下没有那么make命令就会产生错误。因此make也给我们提供了一种文件搜索的功能。

在makefile当中,我们可以使用VAPTH指定make去搜索文件的路径。我们先来测试一下当我们没有使用VPATH的时候指定其他目录下的文件会出现什么情况,我们的文件目录结构如下图所示:

我们的makefile内容如下(先把VPATH的那一行注释掉):

cflags=-c

# VPATH=./files

main: demo.o myprint.o a.o b.o
gcc demo.o myprint.o a.o b.o -o main demo.o:demo.c
gcc $(cflags) demo.c myprint.o:myprint.c
gcc $(cflags) myprint.c
a.o: a.c
gcc $(cflags) a.c
b.o: b.c
gcc $(cflags) b.c clean:
rm myprint.o demo.o main
.PHONY: clean

执行结果如下图所示:

我们现在修改我们的makefile文件如下:

cflags=-c

VPATH=./files

main: demo.o myprint.o a.o b.o
gcc demo.o myprint.o a.o b.o -o main demo.o:demo.c
gcc $(cflags) demo.c myprint.o:myprint.c
gcc $(cflags) myprint.c a.o: a.c
gcc $(cflags) $<
b.o: b.c
gcc $(cflags) $< clean:
rm myprint.o demo.o main
.PHONY: clean

再次执行make命令,执行结果如下图所示:

我们可以看到仍然出错了,这是因为虽然make可以找到a.c和b.c的位置,但是在执行编译命令的时候我们执行的依然是gcc -c a.c,这条命令是会在当前目录下去寻找a.c的,而不会在其他路径下去寻找a.c,在这里我们可以使用$<去解决这个问题,$<表示第一个依赖的文件。

修改之后的makefile文件如下所示:

cflags=-c

VPATH=./files

main: demo.o myprint.o a.o b.o
gcc demo.o myprint.o a.o b.o -o main demo.o:demo.c
gcc $(cflags) demo.c myprint.o:myprint.c
gcc $(cflags) myprint.c a.o: a.c
gcc $(cflags) $<
b.o: b.c
gcc $(cflags) $< clean:
rm myprint.o demo.o main a.o b.o
.PHONY: clean

执行结果如下图所示:

如果你想设置多条目录的话,设置的规则为VPATH=dir1:dir2:dir3:dir4

总结

在本篇文章当中主要介绍了一些makefile的基础用法,和一些常用的小技巧,虽然不是很难,但是掌握了可以很大的跳高我们的工作效率,也可以方便我们阅读别人写的makefile文件。


以上就是本篇文章的所有内容了,我是LeHung,我们下期再见!!!更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

彻底掌握Makefile(一)的更多相关文章

  1. 说说Makefile那些事儿

    说说Makefile那些事儿 |扬说|透过现象看本质 工作至今,一直对Makefile半知半解.突然某天幡然醒悟,觉得此举极为不妥,只得洗心革面从头学来,以前许多不明觉厉之处顿时茅塞顿开,想想好记性不 ...

  2. 编写一个通用的Makefile文件

    1.1在这之前,我们需要了解程序的编译过程 a.预处理:检查语法错误,展开宏,包含头文件等 b.编译:*.c-->*.S c.汇编:*.S-->*.o d.链接:.o +库文件=*.exe ...

  3. 编写简单的Makefile文件

    makefile中的编写内容如下: www:hello.c x.h gcc hello.c -o hello clean: rm hello www:hello.c  x.h 表示生成www这个文件需 ...

  4. 简单编写Makefile

    相信很多朋友都有过这样的经历,看着开源项目中好几页的makefile文件,不知所云.在日常学习和工作中,也有意无意的去回避makefile,能改就不写,能用ide就用ide.其实makefile并没有 ...

  5. [转]Linux中configure/makefile

    本文教你如何使用autoconf.automake等来制作一个以源代码形式(.tar.gz)发布的软件.并可在执行configure时使用自定义参数. 一.概述和基础知识 在Linux下得到一个以源代 ...

  6. Linux内核配置、编译及Makefile简述

    Hi,大家好!我是CrazyCatJack.最近在学习Linux内核的配置.编译及Makefile文件.今天总结一下学习成果,分享给大家^_^ 1.解压缩打补丁 首先是解压缩你获取到的Linux内核. ...

  7. make 查找的文件名顺序为:“GNUmakefile”、“makefile”、“Makefile”

    默认的情况下,make会在工作目录(执行make的目录)下按照文件名顺序寻找makefile文件读取并执行,查找的文件名顺序为:“GNUmakefile”.“makefile”.“Makefile”. ...

  8. 实例:对2个Makefile的备注

    实例1:Makefile编译链接简单.c函数 example.c Makefile exe: example.c gcc example.c -o exe clean: rm exe 执行效果: 实例 ...

  9. Linux中C程序调试、makefile

    gcc基本语法格式:gcc [-选项] 源文件 [-选项] 目标文件,GCC编译C程序的过程: 预处理:gcc -E hello.c hello.i.-E指定执行到预处理结束,下面类似. 编译:gcc ...

  10. Linux工具入门:make工具与Makefile文件

    1. make工具 利用make工具可以自动完成编译工作,这些工作包括: 如果修改了某几个源文件,则只重新编译这几个源文件 如果某个头文件被修改了,则重新编译所有包含该头文件的源文件 利用这种自动编译 ...

随机推荐

  1. RASP | 远程Java应用的RASP调试教程

    远程Java应用的RASP调试教程 介绍 Java RASP是基于Java Agent技术实现的,而Java Agent代码无法独立启动,必须依赖于一个Java运行时程序才能运行. 如何调试一个Jav ...

  2. 强化学习-学习笔记7 | Sarsa算法原理与推导

    Sarsa算法 是 TD算法的一种,之前没有严谨推导过 TD 算法,这一篇就来从数学的角度推导一下 Sarsa 算法.注意,这部分属于 TD算法的延申. 7. Sarsa算法 7.1 推导 TD ta ...

  3. Pytorch分布式训练

    用单机单卡训练模型的时代已经过去,单机多卡已经成为主流配置.如何最大化发挥多卡的作用呢?本文介绍Pytorch中的DistributedDataParallel方法. 1. DataParallel ...

  4. Page和list的区别 mybatis

    先看一段代码 点击查看代码 @Override public Result findUserPage(PageParam pageParam) { Page<SysUser> page = ...

  5. java------JRE和JDK

    JDK(Java Development kit):Java开发工具包 包括 JVM(Java Virtual Machine):java虚拟机,真正运行java程序的地方(Java语言在运行时并不是 ...

  6. 基于WPF重复造轮子,写一款数据库文档管理工具(一)

    项目背景 公司业务历史悠久且复杂,数据库的表更是多而繁杂,每次基于老业务做功能开发都需要去翻以前的表和业务代码.需要理解旧的表的用途以及包含的字段的含义,表少还好说,但是表一多这就很浪费时间,而且留下 ...

  7. mysql 跨库事务XA

    前一段时间在工作中遇到了跨库事务问题,后来在网上查询了一下,现在做一下整理和总结. 1.首先要确保mysql开启XA事务支持 SHOW VARIABLES LIKE '%XA%' 如果innodb_s ...

  8. 用 Antlr 重构脚本解释器

    前言 在上一个版本实现的脚本解释器 GScript 中实现了基本的四则运算以及 AST 的生成. 当我准备再新增一个 % 取模的运算符时,会发现工作很繁琐而且几乎都是重复的:主要是两步: 需要在词法解 ...

  9. Java中list集合自定义排序-2022新项目

    一.业务场景 为了加快首页数据查询的效率,因此将首页查询的数据大多数都放在了缓存中,包括各种list集合数据.对这些 从缓存中获取的数据做了一个兜底处理,如果从缓存中没有获取到数据,则直接从数据库中去 ...

  10. Luogu1064 金明的预算方案 (有依赖的背包)

    枚举多个状态 #include <iostream> #include <cstdio> #include <cstring> #include <algor ...