一文学会Makefile

系列文章推荐

Linux文件系统目录结构
Linux必备基础
Linux构建一个deb软件安装包

前言

本文主要来自正点原子、野火Linux教程及本人理解,若有侵权请及时联系本人删除。如果本篇对您有帮助的话希望能一键三连,万分感谢。

Makefile简介

在我们的项目工程中,势必会有越来越多的 C 文件和 H 头文件。当一个工程中有很多 C 源文件和 H 头文件时,直接使用编译器指令非常麻烦,而且哪怕你只是修改一个文件,也需要重新编译所有的文件,白白浪费了很多开发时间。要解决这个问题,最好的方式就是把工程的编译规则写下来,让编译器自动加载该规则进行编译。解决方法就是使用 make 和 Makefile,这两个工具是搭配使用的

  • make 工具:它可以帮助我们找出项目里面修改变更过的文件,并根据依赖关系,找出受修改影响的其他相关文件,然后对这些文件按照规则进行单独的编译,这样一来,就能避免重新编译项目的所有的文件。
  • Makefile 文件:上面提到的规则、依赖关系主要是定义在这个 Makefile 文件中的,我们在其中合理地定义好文件的依赖关系之后,make 工具就能精准地进行编译工作。

它们的关系如下图所示:
在这里插入图片描述
从我们上面的介绍可以知道,我们管理一个项目工程,实质上就是管理项目文件间的依赖
关系。所以我们在学习和使用 Makefile 的时候,一定要牢牢抓住它这种面向依赖的思想,这里再多介绍一下,当工程复杂度再上一个台阶的时候,会觉得手写 Makefile 也很麻烦,那个时候可以用 CMake、autotools 等工具来帮忙生成 Makefile。实际上Windows 系统下很多 IDE 工具内部也是使用类似 Makefile 的方式组织工程文件的,只不过被封装成图形界面,对用户不可见而已。

Makefile 概览

在这里插入图片描述
在这里插入图片描述
图里面的知识点不少,这里先不深入学习具体语法,我们从左上角开始讲起:
1、基础语法–描述目标和依赖的特定格式,Makefile 的核心。
2、变量–记录特定的信息,避免重复输入原始信息。尤其是手动输入原始信息很长时,特别好用。
3、分支判断–灵活控制多个不同的编译过程,方便兼容不同属性。
4、头文件依赖–监控头文件的变化,头文件也是程序的关键内容。
5、隐含规则–利用 Makefile 的一些默认规则,可以减少编写 Makefile 的工作量。
6、自动化变量–利用 Makefile 的默认的自动化变量,可以减少编写 Makefile 的工作量。
7、模式规则–灵活使用正则表达式,可以减少编写 Makefile 的工作量。
8、函数–使用 Makefile 的各种函数,可以更方便地实现 Makefile 的功能。

了解完 Makefile 的知识点,从上面的分析可以知道,Makefile 的核心在于基础语法,用来描述目标和依赖的关系。其他语法的目的,是为了减少我们编写 Makefile 工作量,让我们能够以更加优雅、更加简洁、更好维护的方式来实现 Makefile 的功能。这跟我们程序开发是很相似的,不止要实现功能,还要兼顾程序的可读性、拓展性、可维护性等等。

使用 Makefile 控制编译

Makefile小实验

为了直观演示Makefile的作用,我们使用一个示例进行讲解,首先使用编辑器创建一个名为“Makefile”的文件,输入如下代码并保存,其中使用“#”开头的行是注释,自己做实验时可以不输入,另外要注意在“ls -lh”、”touchtest.txt”等命令前要使用 Tab 键,不能使用空格代替。

#Makefile 格式
#目标: 依赖的文件或其它目标
#Tab 命令 1
#Tab 命令 2
#第一个目标,是最终目标及 make 的默认目标
#目标 a,依赖于目标 targetc 和 targetb
#目标要执行的 shell 命令 ls -lh,列出目录下的内容
targeta: targetc targetb
ls -lh

#目标 b,无依赖
#目标要执行的 shell 命令,使用 touch 创建 test.txt 文件
targetb:
touch test.txt

#目标 c,无依赖
#目标要执行的 shell 命令,pwd 显示当前路径
targetc:
pwd

#目标 d,无依赖
#由于 abc 目标都不依赖于目标 d,所以直接 make 时目标 d 不会被执行
#可以使用 make targetd 命令执行
targetd:
rm -f test.txt

这个 Makefile 文件主要是定义了四个目标操作,先大致了解它们的关系:

  • targeta:这是 Makefile 中的第一个目标代号,在符号“:”后面的内容表示它依赖于 targetc 和targetb 目标,它自身的命令为“ls -lh”,列出当前目录下的内容。
  • targetb:这个目标没有依赖其它内容,它要执行的命令为“touch test.txt”,即创建一个 test.txt文件。
  • targetc:这个目标同样也没有依赖其它内容,它要执行的命令为“pwd”,就是简单地显示当前的路径。
  • targetd:这个目标无依赖其它内容,它要执行的命令为“rm -f test.txt”,删除目录下的 test.txt文件。与 targetb、c 不同的是,没有任何其它目标依赖于 targetd,而且它不是默认目标。

下面使用这个 Makefile 执行各种 make 命令,对比不同 make 命令的输出,可以清楚地了解 Makefile的机制。在主机 Makefile 所在的目录执行如下命令:

# 在主机上 Makefile 所在的目录执行如下命令
# 查看当前目录的内容
ls
# 执行 make 命令,make 会在当前目录下搜索“Makefile”或“makefile”,并执行
make
# 可看到 make 命令后的输出,它执行了 Makefile 中编写的命令
# 查看执行 make 命令后的目录内容,多了 test.txt 文件
ls
# 执行 Makefile 的 targetd 目标,并查看,少了 test.txt 文件
make targetd
ls
# 执行 Makefile 的 targetb 目标,并查看,又生成了 test.txt 文件
make targetb
ls
# 执行 Makefile 的 targetc 目标
make targetc

在这里插入图片描述
上图中包含的原理说明如下:
make 命令:

  • 在终端上执行 make 命令时,make 会在当前目录下搜索名为“Makefile”或“makefile”的文件,然后根据该文件的规则解析执行。如果要指定其它文件作为输入规则,可以通过“-f”参数指定输入文件,如“make -f 文件名”。
  • 此处 make 命令读取我们的 Makefile 文件后,发现 targeta 是 Makefile 的第一个目标,它会被当成默认目标执行。
  • 又由于 targeta 依赖于 targetc 和 targetb 目标,所以在执行 targeta 自身的命令之前,会先去完成 targetc 和 targetb。
  • targetc 的命令为 pwd,显示了当前的路径。
  • targetb 的命令为 touch test.txt ,创建了 test.txt 文件。
  • 最后执行 targeta 自身的命令 ls -lh ,列出当前目录的内容,可看到多了一个 test.txt 文件。make targetd 、make targetb、make targetc 命令:
  • 由于 targetd 不是默认目标,且不被其它任何目标依赖,所以直接 make 的时候 targetd 并没有被执行,想要单独执行 Makefile 中的某个目标,可以使用”make 目标名“的语法,例如上图中分别执行了”make targetd“、”make targetb“和”make targetc“指令,在执行”make targetd”目标时,可看到它的命令 rm -f test.txt 被执行,test.txt 文件被删除。

从这个过程,可了解到 make 程序会根据 Makefile 中描述的目标与依赖关系,执行达成目标需要的 shell 命令。简单来说,Makefile 就是用来指导 make 程序如何干某些事情的清单。

使用 Makefile 编译程序

使用 GCC 编译多个文件

接着我们使用 Makefile 来控制程序的编译,为方便说明,先把前面的 hello.c 程序分开成三个文件来写,分别为 hello_main.c 主文件,hello_func.c 函数文件,hello_func.h 头文件,其内容如下
hello_main.c

#include "hello_func.h"
int main()
{
hello_func();
return 0;
}

hello_func.c

#include <stdio.h>
#include "hello_func.h"
void hello_func(void)
{
printf("hello, world! This is a C program.\n");
for (int i=0; i<10; i++ ) {
printf("output i=%d\n",i);
}
}

hello_func.h

void hello_func(void);

也就是说 hello_main.c 的 main 主函数调用了 hello_func.c 文件的打印函数,而打印函数在hello_func.h 文件中声明,在复杂的工程中这是常见的程序结构。如果我们直接使用 GCC 进行编译,需要使用如下命令:

# 在主机上示例代码目录执行如下命令
# 注意最后的"-I ." 包含名点"."
gcc -o hello_main hello_main.c hello_func.c -I .
# 运行生成的 hello_main 程序
./hello_main

在这里插入图片描述
相对于基础的hello.c编译命令,此处主要是增加了输入的文件数量, 如“hello_main.c”、“hello_func.c”,另外新增的“-I .”是告诉编译器头文件路径,让它在编译时可以在“.”(当前目录)寻找头文件,其实不加”-I .”选项也是能正常编译通过的,此处只是为了后面演示 Makefile 的相关变量。

使用 Makefile 编译

可以想像到,只要把 gcc 的编译命令按格式写入到 Makefile,就能直接使用 make 编译,而不需要每次手动直接敲 gcc 编译命令。操作如下使用编辑器在 hello_main.c 所在的目录新建一个名为“Makefile”的文件,并输入如下内容并保存。

#Makefile 格式
#目标: 依赖
#Tab 命令 1
#Tab 命令 2
#默认目标
#hello_main 依赖于 hello_main.c 和 hello_func.c 文件
hello_main: hello_main.c hello_func.c
gcc -o hello_main hello_main.c hello_func.c -I .

#clean 目标,用来删除编译生成的文件
clean:
rm -f *.o hello_main

该文件定义了默认目标 hello_main 用于编译程序,clean 目标用于删除编译生成的文件。特别地,其中 hello_main 目标名与 gcc 编译生成的文件名”gcc -o hello_main”设置成一致了,也就是说,此处的目标 hello_main 在Makefile 看来,已经是一个目标文件 hello_main。

这样的好处是 make 每次执行的时候,会检查 hello_main 文件和依赖文件 hello_main.c、hello_func.c的修改日期,如果依赖文件的修改日期比 hello_main 文件的日期新,那么 make 会执行目标其下的 Shell 命令更新 hello_main 文件,否则不会执行。

请运行如下命令进行实验:

# 在主机上 Makefile 所在的目录执行如下命令
# 若之前有编译生成 hello_main 程序,先删除

rm hello_main
ls

# 使用 make 根据 Makefile 编译程序
make
ls

# 执行生成的 hello_main 程序
./hello_main

# 再次 make,会提示 hello_main 文件已是最新
make

# 使用 touch 命令更新一下 hello_func.c 的时间
touch hello_func.c

# 再次 make,由于 hello_func.c 比 hello_main 新,所以会再编译
make
ls

在这里插入图片描述
如上图所示,有了 Makefile 后,我们实际上只需要执行一下 make 命令就可以完成整个编译流程。图中还演示了 make 会对目标文件和依赖进行更新检查,当依赖文件有改动时,才会再次执行命令更新目标文件。

目标与依赖

下面我们再总结一下 Makefile 中跟目标相关的语法:
[目标 1]:[依赖]
[命令 1]
[命令 2]
[目标 2]:[依赖]
[命令 1]
[命令 2]

  • 目标:指 make 要做的事情,可以是一个简单的代号,也可以是目标文件,需要顶格书写,前面不能有空格或 Tab。一个 Makefile 可以有多个目标,写在最前面的第一个目标,会被Make 程序确立为“默认目标”,例如前面的 targeta、hello_main。
  • 依赖:要达成目标需要依赖的某些文件或其它目标。例如前面的 targeta 依赖于 targetb 和targetc,又如在编译的例子中,hello_main 依赖于hello_main.c、hello_func.c 源文件,若这些文件更新了会重新进行编译。
  • 命令 1,命令 2…命令 n:make 达成目标所需要的命令。只有当目标不存在或依赖文件的修改时间比目标文件还要新时,才会执行命令。要特别注意命令的开头要用“Tab”键,不能使用空格代替,有的编辑器会把 Tab 键自动转换成空格导致出错,若出现这种情况请检查自己的编辑器配置。

伪目标

前面我们在 Makefile 中编写的目标,在 make 看来其实都是目标文件,例如 make 在执行的时候由于在目录找不到 targeta 文件,所以每次 make targeta 的时候,它都会去执行 targeta 的命令,期待执行后能得到名为 targeta 的同名文件。如果目录下真的有 targeta、targetb、targetc 的文件,即假
如目标文件和依赖文件都存在且是最新的,那么 make targeta 就不会被正常执行了,这会引起误会。

为了避免这种情况,Makefile 使用“.PHONY”前缀来区分目标代号和目标文件,并且这种目标代号被称为“伪目标”,phony 单词翻译过来本身就是假的意思。

也就是说,只要我们不期待生成目标文件,就应该把它定义成伪目标,前面的演示代码修改如下。

#使用.PHONY 表示 targeta 是个伪目标
.PHONY:targeta
#目标 a,依赖于目标 targetc 和 targetb
#目标要执行的 shell 命令 ls -lh,列出目录下的内容
targeta: targetc targetb
ls -lh
#使用.PHONY 表示 targetb 是个伪目标
.PHONY:targetb

#目标 b,无依赖
#目标要执行的 shell 命令,使用 touch 创建 test.txt 文件
targetb:
touch test.txt

#使用.PHONY 表示 targetc 是个伪目标
.PHONY:targetc

#目标 c,无依赖
#目标要执行的 shell 命令,pwd 显示当前路径
targetc:
pwd

#使用.PHONY 表示 targetd 是个伪目标
.PHONY:targetd

#目标 d,无依赖
#由于 abc 目标都不依赖于目标 d,所以直接 make 时目标 d 不会被执行
#可以使用 make targetd 命令执行
targetd:
rm -f test.txt

#默认目标
#hello_main 依赖于 hello_main.c 和 hello_func.c 文件
hello_main: hello_main.c hello_func.c
gcc -o hello_main hello_main.c hello_func.c -I .
#clean 伪目标,用来删除编译生成的文件
.PHONY:clean
clean:
rm -f *.o hello_main

GNU 组织发布的软件工程代码的 Makefile,常常会有类似以上代码中定义的 clean 伪目标,用于清除编译的输出文件。常见的还有“all”、“install”、“print”、“tar”等分别用于编译所有内容、安装已编译好的程序、列出被修改的文件及打包成 tar 文件。虽然并没有固定的要求伪目标必须用这些名字,但可以参考这些习惯来编写自己的 Makefile。

如果以上代码中不写“.PHONY:clean”语句,并且在目录下创建一个名为 clean 的文件,那么当执行“make clean”时,clean 的命令并不会被执行,感兴趣的可以亲自尝试一下。

默认规则

在前面《GCC 编译过程》章节中提到整个编译过程包含如下图中的步骤,make 在执行时也是使用同样的流程,不过在 Makefile 的实际应用中,通常会把编译和最终的链接过程分开。
在这里插入图片描述
也就是说,我们的 hello_main 目标文件本质上并不是依赖 hello_main.c 和 hello_func.c 文件,而是依赖于 hello_main.o 和 hello_func.o,把这两个文件链接起来就能得到我们最终想要的 hello_main目标文件。另外,由于 make有一条默认规则,当找不到 xxx. o 文件时,会查找目录下的同名xxx.c 文件进行编译。根据这样的规则,我们可把 Makefile 修改如下。

1 #Makefile 格式
2 #目标文件: 依赖的文件
3 #Tab 命令 1
4 #Tab 命令 2
5 hello_main: hello_main.o hello_func.o
6 gcc -o hello_main hello_main.o hello_func.o
7 #以下是 make 的默认规则,下面两行可以不写
8 #hello_main.o: hello_main.c
9 # gcc -c hello_main.c
10
11 #以下是 make 的默认规则,下面两行可以不写
12 #hello_func.o: hello_func.c
13 # gcc -c hello_func.c

以上代码的第 5~6 行把依赖文件由 C 文件改成了.o 文件,gcc 编译命令也做了相应的修改。第8~13 行分别是 hello_main.o 文件和 hello_func.o 文件的依赖和编译命令,不过由于 C 编译成同名的.o 文件是 make 的默认规则,所以这部分内容通常不会写上去。

使用修改后的 Makefile 编译结果如下图所示。
在这里插入图片描述
从 make 的输出可看到,它先执行了两条额外的“cc”编译命令,这是由 make 默认规则执行的,它们把 C 代码编译生成了同名的.o 文件,然后 make 根据 Makefile 的命令链接这两个文件得到最终目标文件 hello_main。

使用变量

使用 C 自动编译成 *.o 的默认规则有个缺陷,由于没有显式地表示 *.o 依赖于.h 头文件,假如我们修改了头文件的内容,那么 *.o 并不会更新,这是不可接受的。并且默认规则使用固定的“cc”进行编译,假如我们想使用 ARM-GCC 进行交叉编译,那么系统默认的“cc”会导致编译错误。要解决这些问题并且让 Makefile 变得更加通用,需要引入变量和分支进行处理。

基本语法

在 Makefile 中的变量,有点像 C 语言的宏定义,在引用变量的地方使用变量值进行替换。变量的命名可以包含字符、数字、下划线,区分大小写,定义变量的方式有以下四种:

  • “=”:延时赋值,该变量只有在调用的时候,才会被赋值
  • “:=”:直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。
  • “?=”:若变量的值为空,则进行赋值,通常用于设置默认值。
  • “+=”:追加赋值,可以往变量后面增加新的内容。

当我们想使用变量时,其语法如下:

$(变量名)

下面通过一个实验来讲解这四种定义方式,对于后两种赋值方式比较简单,主要思考延时赋值和直接赋值的差异,实验代码如下所示。

VAR_A = FILEA
VAR_B = $(VAR_A)
VAR_C := $(VAR_A)
VAR_A += FILEB
VAR_D ?= FILED
.PHONY:check
check:
@echo "VAR_A:"$(VAR_A)
@echo "VAR_B:"$(VAR_B)
@echo "VAR_C:"$(VAR_C)
@echo "VAR_D:"$(VAR_D)

这里主要关心 VAR_B 和 VAR_C 的赋值方式,实验结果如下图所示。执行完 make 命令后,只有 VAR_C 是 FILEA。这是因为 VAR_B 采用的延时赋值,只有当调用时,才会进行赋值。当调用 VAR_B 时,VAR_A 的值已经被修改为 FILEA FILEB,因此 VAR_B 的变量值也就等于 FILEA FILEB。
在这里插入图片描述

改造默认规则

接下来使用变量对前面 hello_main 的 Makefile 进行大改造,如下所示。

1 # 定义变量
2 CC=gcc
3 CFLAGS=-I.
4 DEPS = hello_func.h
5
6 # 目标文件
7 hello_main: hello_main.o hello_func.o
8 $(CC) -o hello_main hello_main.o hello_func.o
9
10 #*.o 文件的生成规则
11 %.o: %.c $(DEPS)
12 $(CC) -c -o $@ $< $(CFLAGS)
13
14 # 伪目标
15 .PHONY: clean
16 clean:
17 rm -f *.o hello_main
  • 代码的 1~4 行:分别定义了 CC、CFLAGS、DEPS 变量,变量的值就是等号右侧的内容,定义好的变量可通过”$(变量名)”的形式引用,如后面的” $(CC)”、 ” $( CFLAGS)”、” $(DEPS)”等价于定义时赋予的变量值”gcc”、”-I.”和”hello_func.h”。
  • 代码的第 8 行:使用 $(CC) 替代了 gcc,这样编写的 Makefile 非常容易更换不同的编译器,如要进行交叉编译,只要把开头的编译器名字修改掉即可。
  • 代码的第 11 行:”%”是一个通配符,功能类似”*”,如”%.o”表示所有以”.o”结尾的文件。所以”%.o:%.c”在本例子中等价于”hello_main.o: hello_main.c”、”hello_func.o: hello_func.c”,即等价于 o 文件依赖于 c 文件的默认规则。不过这行代码后面的”$ (DEPS)”表示它除了依赖 c 文件,还依赖于变量”$(DEPS)”表示的头文件,所以当头文件修改的话,o 文件也会被重新编译。
  • 代码的第 12 行:这行代码出现了特殊的变量”$ @”,”$ <”,可理解为 Makefile 文件保留的关键字,是系统保留的自动化变量,”$ @”代表了目标文件,”$ <”代表了第一个依赖文件。即”$ @”表示” %.o”,”$ <”表示”%.c”,所以,当第 11 行的”%”匹配的字符为”hello_func”的话,第12行代码等价于:
1 # 当"%" 匹配的字符为"hello_func" 的话:
2 $(CC) -c -o $@ $< $(CFLAGS)
3 # 等价于:
4 gcc -c -o hello_func.o func_func.c -I .

也就是说 makefile 可以利用变量及自动化变量,来重写.o 文件的默认生成规则,以及增加头文件的依赖。

改造链接规则

与 *.o 文件的默认规则类似,我们也可以使用变量来修改生成最终目标文件的链接规则,具体参考如下代码。

1 # 定义变量
2 TARGET = hello_main
3 CC = gcc
4 CFLAGS = -I.
5 DEPS = hello_func.h
6 OBJS = hello_main.o hello_func.o
7
8 # 目标文件
9 $(TARGET): $(OBJS)
10 $(CC) -o $@ $^ $(CFLAGS)
11
12 #*.o 文件的生成规则
13 %.o: %.c $(DEPS)
14 $(CC) -c -o $@ $< $(CFLAGS)
15
16 # 伪目标
17 .PHONY: clean
18 clean:
19 rm -f *.o hello_main

这部分说明如下:

  • 代码的第 2 行:定义了 TARGET 变量,它的值为目标文件名 hello_main。
  • 代码的第 6 行:定义了 OBJS 变量,它的值为依赖的各个 o 文件,如hello_main.o、hello_func.o文件。
  • 代码的第 9 行:使用 TARGET 和 OBJS 变量替换原来固定的内容。
  • 代码的第 10 行:使用自动化变量“$ @”表示目标文件“$ (TARGET)”,使用自动化变量“$ ^”表示所有的依赖文件即“$ (OBJS)”。

也就是说以上代码中的 Makefile 把编译及链接的过程都通过变量表示出来了,非常通用。使用这样的 Makefile 可以针对不同的工程直接修改变量的内容就可以使用。

其它自动化变量

Makefile 中还有其它自动化变量,此处仅列出方便以后使用到的时候进行查阅,见下表。表自动化变量
在这里插入图片描述

使用分支

为方便直接切换 GCC 编译器,我们还可以使用条件分支增加切换编译器的功能。在 Makefile 中的条件分支语法如下:

ifeq(arg1, arg2)
分支 1
else
分支 2
endif

分支会比较括号内的参数“arg1”和“arg2”的值是否相同,如果相同,则为真,执行分支 1 的内容,否则的话,执行分支 2 的内容,参数 arg1 和 arg2 可以是变量或者是常量。使用分支切换 GCC 编译器的 Makefile 如下所示。

1 # 定义变量
2 #ARCH 默认为 x86,使用 gcc 编译器,
3 # 否则使用 arm 编译器
4 ARCH ?= x86
5 TARGET = hello_main
6 CFLAGS = -I.
7 DEPS = hello_func.h
8 OBJS = hello_main.o hello_func.o
9
10 # 根据输入的 ARCH 变量来选择编译器
11 #ARCH=x86,使用 gcc
12 #ARCH=arm,使用 arm-gcc
13 ifeq ($(ARCH),x86)
14 CC = gcc
15 else
16 CC = arm-linux-gnueabihf-gcc
17 endif
18
19 # 目标文件
20 $(TARGET): $(OBJS)
21 $(CC) -o $@ $^ $(CFLAGS)
22
23 #*.o 文件的生成规则
24 %.o: %.c $(DEPS)
25 $(CC) -c -o $@ $< $(CFLAGS)
26
27 # 伪目标
28 .PHONY: clean
29 clean:
30 rm -f *.o hello_main

Makefile 主要是增加了 ARCH 变量用于选择目标平台,第 4 行代码中使用“?=”给 ARCH 赋予默认值 x86,然后在代码 11~18 行增加了根据 ARCH 变量值的内容对 CC 变量赋予不同的编译器名。

在执行 make 命令的时候,通过给 ARCH 赋予不同的变量值切换不同的编译器平台:

1 # 清除编译输出,确保不受之前的编译输出影响
2 make clean
3 # 使用 ARM 平台
4 make ARCH=arm
5 # 清除编译输出
6 make clean
7 # 默认是 x86 平台
8 make

在这里插入图片描述

使用函数

在更复杂的工程中,头文件、源文件可能会放在二级目录,编译生成的 * .o 或可执行文件也放到专门的编译输出目录方便整理,如下图所示。示例中 * .h 头文件放在 includes 目录下,* .c 文件放在 sources 目录下,不同平台的编译输出分别存放在 build_x86 和 build_arm 中。

实现这些复杂的操作通常需要使用 Makefile 的函数。
在这里插入图片描述

函数格式及示例

在 Makefile 中调用函数的方法跟变量的使用类似,以“ ( ) ” 或 “ ()”或“ (){}”符号包含函数名和参数,具体语法如下:

$(函数名 参数)
# 或者使用花括号
${函数名 参数}

下面以常用的 notdir、patsubst、wildcard、foreach 函数为例进行讲解,并且示例中都是我们后面 Makefile 中使用到的内容。

notdir 函数
notdir 函数用于去除文件路径中的目录部分。它的格式如下:

$(notdir 文件名)

例如输入参数“./sources/hello_func.c”,函数执行后的输出为“hell_func.c”,也就是说它会把输入中的“./sources/”路径部分去掉,保留文件名。使用范例如下:

# 以下是范例
$(notdir ./sources/hello_func.c)

上面的函数执行后会把路径中的“./sources/”部分去掉,输出为:hello_func.c

wildcard 函数
wildcard 函数用于获取文件列表,并使用空格分隔开。它的格式如下:
$(wildcard 匹配规则)

例如函数调用“$(wildcard *.c)”,函数执行后会把当前目录的所有 c 文件列出。假设我们在上图中的 Makefile 目录下执行该函数,使用范例如下:

# 在 sources 目录下有 hello_func.c、hello_main.c、test.c 文件
# 执行如下函数
$(wildcard sources/*.c)
# 函数的输出为:
sources/hello_func.c sources/hello_main.c sources/test.c

patsubst 函数
patsubst 函数功能为模式字符串替换。它的格式如下:

$(patsubst 匹配规则, 替换规则, 输入的字符串)

当输入的字符串符合匹配规则,那么使用替换规则来替换字符串,当匹配规则中有“%”号时,替换规则也可以例程“%”号来提取“%”匹配的内容加入到最后替换的字符串中。有点抽象,请直接阅读以下示例:

执行如下函数

$(patsubst %.c, build_dir/%.o, hello_main.c )
# 函数的输出为:
build_dir/hello_main.o
# 执行如下函数
$(patsubst %.c, build_dir/%.o, hello_main.xxx )
# 由于 hello_main.xxx 不符合匹配规则"%.c",所以函数没有输出

第一个函数调用中,由于“hello_main.c”符合“%.c”的匹配规则(% 在 Makefile 中的类似于 * 通配符),而且“%”从“hello_main.c”中提取出了“hello_main”字符,把这部分内容放到替换规则“build_dir/%.o”的“%”号中,所以最终的输出为”build_di r/hello_main.o”。

第二个函数调用中,由于由于“hello_main.xxx”不符合“%.c”的匹配规则,“.xxx”与“.c”对不上,所以不会进行替换,函数直接返回空的内容。

foreach 函数
foreach 函数不同于其他函数。它是一个循环函数,类似于Linux的shell中的for语句,它的格式如下:

$(foreach VAR,LIST,TEXT)

这个函数的工作过程是这样的:如果需要(存在变量或者函数的引用),首先展开变量“VAR”和“LIST”的引用;而表达式“TEXT”中的变量引用不展开。执行时把“LIST”中使用空格分割的单词依次取出赋值给变量“VAR”,然后执行“TEXT”表达式,重复直到“LIST”的最后一个单词(为空时结束)。“TEXT”中的变量或者函数引用在执行时被展开,因此如果在“TEXT”中存在对“VAR”的引用,那么“VAR”的值在每一次展开式将会得到不同的值。

示例:定义变量“files”,它的值为四个目录(变量“dirs”代表的a、b、c、d四个目录)下的文件列表

dirs:=a b c d
files:=$(foreach dir,$(dirs),$(wildcard $(dir)/*))

此函数和下面等价

files:=$ (wildcard a/* b/* c/* d/* )

多级结构工程的 Makefile

接下来我们使用上面三个函数修改我们的 Makefile,以适应包含多级目录的工程,修改后的内容如下所示。

1 #定义变量
2 #ARCH 默认为 x86,使用 gcc 编译器,
3 #否则使用 arm 编译器
4 ARCH ?= x86
5 TARGET = hello_main
6
7
8 #存放中间文件的路径
9 BUILD_DIR = build_$(ARCH)
10 #存放源文件的文件夹
11 SRC_DIR = sources
12 #存放头文件的文件夹
13 INC_DIR = includes .
14
15 #源文件
16 SRCS = $(wildcard $(SRC_DIR)/*.c)
17 # 目标文件(*.o)
18 OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS)))
19 # 头文件
20 DEPS = $(wildcard $(INC_DIR)/*.h)
21
22 # 指定头文件的路径
23 CFLAGS = $(patsubst %, -I%, $(INC_DIR))
24
25 # 根据输入的 ARCH 变量来选择编译器
26 #ARCH=x86,使用 gcc
27 #ARCH=arm,使用 arm-gcc
28 ifeq ($(ARCH),x86)
29 CC = gcc
30 else
31 CC = arm-linux-gnueabihf-gcc
32 endif
33
34 # 目标文件
35 $(BUILD_DIR)/$(TARGET): $(OBJS)
36 $(CC) -o $@ $^ $(CFLAGS)
37
38 #*.o 文件的生成规则
39 $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(DEPS)
40 # 创建一个编译目录,用于存放过程文件
41 # 命令前带“@”, 表示不在终端上输出
42 @mkdir -p $(BUILD_DIR)
43 $(CC) -c -o $@ $< $(CFLAGS)
44
45 # 伪目标
46 .PHONY: clean cleanall
47 # 按架构删除
48 clean:
49 rm -rf $(BUILD_DIR)
50
51 # 全部删除
52 cleanall:
53 rm -rf build_x86 build_arm

注意这个 Makefile 文件需要配合前面上图中的工程结构,否则即使 Makefile 写对了编译也会错误,因为目录对不上。具体可以直接参考我们示例代码“step5”中的内容。修改后的 Makefile 文件分析如下:

  • 代码的 8~12 行:定义了变量 BULID_DIR、SRC_DIR、INC_DIR 分别赋值为工程的编译输出路径 build_$(ARCH)、源文件路径 sources 以及头文件路径 includes 和当前目录“.”。其中编译输出路径包含了架构 $(ARCH) 的内容,ARCH=x86 时编译输出路径为 build _x86,ARCH=arm 时编译输出路径为 build_arm,方便区分不同的编译输出。
  • 代码的第 15 行:定义了变量 SRCS 用于存储所有需要编译的源文件,它的值为 wildcard 函数的输出,本例子中该函数的输出为“sources/hello_func.c sources/hello_main.c sources/test.c”。
  • 代码的第 17 行:定义了 OBJS 变量用于存储所有要生成的的.o 文件,它的值为 patsubst 函数的输出,本例子中该函数是把所有 c 文件名替换为同名的.o 文件,并添加 build 目录,即函数的输出为”build/hello_func.o build /hello_main.o build /test.o”。
  • 代码的第 19 行:与 SRCS 变量类似,定义一个 DEPS 变量存储所有依赖的头文件,它的值为 wildcard 函数的输出,本例子中该函数的输出为“includes/hello_func.h ”。
  • 代码的第 22 行:定义了 CFLAGS 变量,用于存储包含的头文件路径,它的值为 patsubst 函数的输出,本例子中该函数是把 includes 目录添加到“-I”后面,函数的输出为“-Iincludes”。
  • 代码的第 34 行:相对于之前的 Makefile,我们在 $(TARGET) 前增加了 $(BUILD_DIR) 路径,使得最终的可执行程序放在 build 目录下。
  • 代码的第 38 行:与上面类似,给.o 目标文件添加 $(BUILD_DIR) 路径。
  • 代码的第 41 行:在执行编译前先创建 build 目录,以存放后面的.o 文件,命令前的“@”表示执行该命令时不在终端上输出。
  • 代码的第 48 行:rm 删除命令也被修改成直接删除编译目录$(BUILD_DIR)。
  • 代码的 51~52 行:增加了删除所有架构编译目录的伪目标 cleanall。使用该 Makefile 时,直接在 Makefile 的目录执行 make 即可:

使用该 Makefile 时,直接在 Makefile 的目录执行 make 即可:

# 使用 tree 命令查看目录结构
# 若提示找不到命令,使用 sudo apt install tree 安装
tree

# 编译
make

如下图:
在这里插入图片描述
本示例中的 Makefile 目前只支持使用一个源文件目录,如果有多个源文件目录还需要改进,关于这些,我们在以后的学习中继续积累。

Makefile解决头文件依赖

1、写一个头文件,并把头文件添加到编译器的头文件路径中。如图中2所示

gcc -I +"头文件"

2、实时检查头文件的更新情况,一旦头文件发生变化,应该要重新编译所有相关文件。

gcc -MM 

在这里插入图片描述
第1步INC_DIR用来存放所有头文件所在文件夹,第2步CFLAGS变量为GCC编译的一个选项,将所有 “头文件” 替换为 “-I头文件”,然后赋值给CFLAGS变量,以此来把头文件添加到编译器的头文件路径中,第3步INCLUDES变量用来存放所有的头文件,第4步让.o文件也依赖所有头文件

常见错误

在这里插入图片描述
在这里插入图片描述
两种报错均是因为不能使用空格,应该使用tab
在这里插入图片描述

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只嵌入式爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值