Make和Makefile快速入门

Posted Naisu Xu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Make和Makefile快速入门相关的知识,希望对你有一定的参考价值。

文章目录

目的

我们可以在终端中通过命令来使用GCC编译代码生成可执行文件,对于实际的项目因为涉及的文件通常会比较多一些,情况也会复杂些。如果每次修改代码后都在终端中一行行输入命令来操作就比较麻烦了,这个时候可以把这些命名都写入一个shell脚本文件中,每次执行这个脚本就可以完成相应的工作。

当然实际操作中有比shell脚本更加合适运用到这个场景中的东西,那就是Make和Makefile。Make是一个程序,最常用的是 GNU Make ,Makefile是用Make来解析执行的脚本文件的默认文件名。这篇文章将对相关的内容做个说明。

官方页面:https://www.gnu.org/software/make/

GNU Make is a tool which controls the generation of executables and other non-source files of a program from the program’s source files.


Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the non-source files and how to compute it from other files. When you write a program, you should write a makefile for it, so that it is possible to use Make to build and install the program.

基础入门

下面是一个Make和Makefile使用的基础演示:

Makefile文件内容是由一条一条的规则组成的,规则的语法如下:

target: dependencies ...
[tab]   commands

target为想要得到的目标;dependencies为要得到该目标需要的依赖,可以有零个或者多个;commands为为实现目标需要执行的命令,commands前 必须使用tab键 缩进。 target和dependencies都可以有多个,使用空格分隔。不加特殊修饰的情况下make工具会首先将target和dependencies字段当作文件名来看待。

上面演示中Makefile文件中有两条规则,当在终端中不带参数使用make命令时按照下面顺序进行了操作:

  • 寻找当前目录下找名字叫Makefile的脚本文件;
  • 执行Makefile文件中第一条规则,即目标为all的规则;
  • 目标all依赖于main.o,没有该文件时则会去寻找目标为main.o的规则去执行命令生成;
  • 当生成main.o之后有又返回去执行目标为all的规则中的命令;

make有一个重要的特性是在执行规则时会去比较target和dependencies的修改时间,只有在 target不存在 或是 dependencies比target新 的时候才会执行规则中的命令。 请看下面演示:

make也可以指定特定target的规则来执行,比如下面演示:

上面演示中有一个目标为clean的规则,该规则执行时用来删除一些文件。上面演示中出现了 .PHONY: clean 这一行语句, .PHONY 是一个内置目标,它的作用是将后面的依赖声明为伪目标,这样就算目录下有和伪目标同名的文件,伪目标的规则还是会执行。

到上面为止make和Makefile基本的一些功能与使用已经介绍的差不多了。接下来详细的内容就下面两块,对于make和Makefile的更多使用需要结合这些内容进行:

  • Makefile作为脚本来说有一些自己的语法规则,像是变量、函数等;
  • make命令的选项和使用;

Makefile语法

基础杂项

Makefile语法和Shell有部分相似的地方,也有差异的地方,这里对一些小的语法点进行说明:

  • 注释
    使用 # 开头行为注释;
  • 换行
    使用 \\ 对长行进行换行;
  • 通配符
    在commands中可以 * 作为通配符来表示任意字符串,用它去匹配文件会得到单个文件名;在Makefile中还可以使用 % 来表示任意字符串,它用于匹配列表中符合条件文件;
  • 命令执行
    make在执行commands时,每一行都会在独立的子shell进程中执行,如果需要多个命令在同一进程中执行可以写在同一行使用 ; 分隔,太长的话可以使用换行符;
  • 隐藏命令
    默认情况下make在执行commands时会打印具体的命令,可以在命令前加 @ 来隐藏命令;
  • 忽略命令出错
    在make执行时如果某个规则中有命令出错了,那么这个规则就会终止执行,可以在命令前加上 - 来忽略命令出错;
  • 文件搜索路径
    在make执行时默认情况下都是在当前目录下搜索检查相关文件的,可以使用 VPATH 变量来增加文件搜索的路径,比如 VPATH := src:include 就是增加了src和include两个目录;

变量

Makefile中变量的定义与赋值主要有四种方式:

  • VAR := value 将值赋给变量VAR;
  • VAR = value 在使用VAR时才将值赋给它;
  • VAR ?= value 如果VAR没有被定义过,则将值赋给VAR,否则忽略该句;
  • VAR += value VAR的值用空格隔开后追加新值;

Makefile中使用 $(VAR) 方式来使用变量。

Makefile中变量也可以通过使用make时进行赋值:

Makefile中还有一些自动化变量,常用的有下面这些:

自动化变量说明
$@规则的目标
$<规则的第一个依赖
$?所有比目标新的依赖的列表
$^所有的依赖的列表,会自动去重

条件选择

Makefile中也有条件选择的语法。主要的可以分为两类,分别看下面演示:

上面是一类演示,用于比较两个参数。 ifeq 比较时参数相等为真 ,ifneq 比较时参数不相等为真。两者后面都可以使用 (arg1, arg2) 或是 "arg1" "arg2" 来表示要比较的参数。后续关键词中 else 是可选的, endif 是必选的。


上面是一类演示,用于判断是否有值。 ifdef 后参数存在或者有值时为真 ,ifndef 后参数不存在或者没有值时为真。后续关键词中 else 是可选的, endif 是必选的。这类方式使用时需要特别注意一种情况:

函数

Makefile中有很多内置的函数可以使用,使用方式如下:
$(<function> <arguments>)
其中 function 是函数名, arguments 是输入参数,有多的参数的话参数间用 , 分隔。

常用的一些函数如下:

  • $(wildcard <pattern>)
    列出当前目录下所有符合模式 pattern 的文件名;
  • $(patsubst <pattern>,<replacement>,<text>)
    查找 text 中的单词是否符合模式 pattern,如果匹配的话,则用 replacement 替换;

    这个函数这种替换的字段的功能经常还可以看到一种类似的方式 $(VAR:<pattern>=<replacement> )
  • $(filter <pattern>,<text>)
    过滤出 text 中符合模式 pattern 的字符串;
  • $(filter-out <pattern>,<text>)
    去除 text 中符合模式 pattern 的字符串;
  • $(foreach <var>,<list>,<text>)
    把 list 中的词条逐一取出,放到名为 var 的变量中,然后执行 text 表达式;
  • $(shell command)
    调用shell执行命令;

文件引用

Makefile中可以使用 include 关键词引入文件,被引入的文件中的内容将在引入的位置展开。下面是个简单的演示:

嵌套执行

很多大型的工程中通常包含很多模块,每个模块有各自都有各自的Makefile文件,这个时候经常也会涉及到嵌套执行,主要方式有两种,比如下面演示:

在嵌套执行时可以使用 export 来修饰变量,这样这些变量会传递到下级Makefile中;如果用 unexport 来修饰变量,可以让变量不传递到下级Makefile中。

make使用

对于make而言,除了前面的使用方式,更进一步的就是配合它的各个选项的使用,常用的一些选项如下:

  • -b-m 忽略和其它版本make的兼容性;
  • -B 强制重新编译;
  • -C 指定读取Makefile文件的目录;
  • -f 指定需要执行的脚本文件名称;
  • -i 执行时忽略所有错误;
  • -I 指定执行时搜索文件的路径;
  • -j 指定同时运行命令的个数,用来加快编译速度,比如常见的 -j4 -j8 这些,选择时主要视CPU线程数而定;
  • -n 打印输出过程但不真正执行编译操作,用于调试;

使用进阶

有了前面的内容之后就可以对前面 基础入门 里的Makefile文件进行改造了,首先准备下面几个文件用于测试:


上面的Makefile中每个 .o 文件的生成都是手动编写命令处理的,如果文件很多的话这就会很麻烦,其实make很聪明的,如果要生成一个 xxx.o 的文件它会自己推导这个文件是由 xxx.c 等文件得到的(甚至你不用写 gcc -c xxx.c 这条命令也行,make也会自动推导,这点在后面会用到)。这个叫做 make的隐含规则(Implicit Rules) 。下面首先对这部分进行改造:

上面演示中第一条规则中直接写了依赖的文件,其实我们可以把它存储到一个变量中;另外现在默认生成的可执行文件名称不太好,我们也可以改一改:

上面变量赋值时一定要注意行末的空格,不然可能运行出错。经过上面的改造我们使用时只要改变前两行的变量值就可以调整需要编译的文件和编译生成的程序名称了。不过目前情况下还有个BUG,现在的Makefile的各个规则的依赖中并没有体现 .h 文件,而我们有些内容是定义在 .h 文件中的,如果修改这个文件那么再次执行make时依据make的规则其实是不会重新编译的,所以我们需要在依赖条件中加上 .h 文件:

通常上面这样在依赖中手动添加用到的 .h 文件其实是不现实的,因为每个 .c 文件中包含的 .h 文件其实是不确定的,有的可能还非常多,这个时候就要用到 gcc 的一些选项了 -M -MM ,这两个选项可以给出 .c 文件生成对应 .o 文件的依赖规则:

可以看到这些信息的格式就是Makefile规则的格式,比如 hello.o: hello.c hello.h ,只要把这个内容加入到Makefile文档中,再加上make隐含规则,它会自动推导生成该 .o 文件的命令。

这里我们先对Makefile进行些调整:

然后再加上生成依赖规则相关内容,把依赖规则保存到 .d 文件中,再使用 include 将文件内容引入到Makefile进行展开,这样就可以实现自动添加依赖规则的效果了:


到此为止实现了一个可以将当前目录下所有 .c 文件编译生成指定名称的应用程序的脚本,上面脚本中其实稍稍还有点小问题(当连续使用 make clean 时会重复生成 .d 文件再进行删除)。下面代码是修复并添加注释后的代码:

# 该脚本可以获取当前目录下所有的 .c 文件,然后编译生成名称由 TARGER 指定的程序文件

SRCS := $(wildcard *.c) # 使用 wildcard 函数获取所有 .c 文件
OBJS := $(patsubst %.c, %.o, $(SRCS)) # OBJS为生成 TARGER 的依赖
TARGER := sayhello # 这里是最终生成的程序文件名

# 生成最终程序的规则
$(TARGER): $(OBJS)
	gcc -o $@ $^

DEPS := $(patsubst %.c, %.d, $(SRCS)) # 每个 .c 文件对应一个 .d 的依赖规则文件

# MAKECMDGOALS 是 make 的一个环境变量,保存了当前操作的目录列表
# 只执行 make 时 MAKECMDGOALS 中没有 clean ,所以下面的 include 会被执行
# 执行 make clean 时,下面的 include 不会被执行
ifneq ($(MAKECMDGOALS), clean)
  -include $(DEPS) # 引用所有 .d 文件内容到这里展开
endif

# 获取依赖规则重定向保存到 .d 文件中
%.d: %.c
	gcc -MM $< > $@

# 生成 .o 文件的规则
%.o: %.c
	gcc -c $<

.PHONY: clean
# 清除编译生成的文件
clean: 
	rm -f $(shell find -name "*.o")
	rm -f $(shell find -name "*.d")
	rm -f $(shell find -name $(TARGER))

总结

make和Makefile基础的一些内容主要就是上面这些,更多内容可以参考可以参考下面链接:
https://seisman.github.io/how-to-write-makefile/index.html
http://c.biancheng.net/makefile/

以上是关于Make和Makefile快速入门的主要内容,如果未能解决你的问题,请参考以下文章

GNU开发工具——CMake快速入门

Makefile入门(八):make运行

Makefile入门: 用最美味的例子

conan入门(二十六):使用make编译erpc/erpcgen(makefile)

conan入门(二十六):使用make编译erpc/erpcgen(makefile)

Makefile入门