Bootstrap

第十五节 使用Makefile 控制编译

Makefile 小实验

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

列表1: Makefile 小实验

#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 所在的目录执行如下命令:

#1、查看当前目录的内容
ls

#2、执行make 命令,make 会在当前目录下搜索“Makefile”或“makefile”,并执行
make

#3、查看执行make 命令后的目录内容,多了test.txt 文件
ls

#4、执行Makefile 的targetd 目标,并查看,发现少了test.txt 文件
make targetd
ls

#5、执行Makefile 的targetb 目标,并查看,又生成了test.txt 文件
make targetb
ls

#6、执行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“指令,在执行”maket argetd”目标时,可看到它的命令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 头文件,其内容如下代码所示,这些文件可在示例的step1 目录下找到。

列表2: hello_main.c 文件

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

列表3: 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);
	}
}

列表4: 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”的文件,并输入如下内容并保存。

列表5: Makefile 示例文件1

#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 单词翻译过来本身就是假的意思。

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

列表6: 使用.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

列表7: 使用.PHONY 定义伪目标

#默认目标
#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 改修改如下。

列表8: Makefile 文件

#Makefile 格式
#目标文件: 依赖的文件
#Tab 命令1
#Tab 命令2
hello_main: hello_main.o hello_func.o
	gcc -o hello_main hello_main.o hello_func.o
#以下是make 的默认规则,下面两行可以不写
#hello_main.o: hello_main.c
# gcc -c hello_main.c

#以下是make 的默认规则,下面两行可以不写
#hello_func.o: hello_func.c
# 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 语言的宏定义,在引用变量的地方使用变量值进行替换。变量的命名可以包含字符、数字、下划线,区分大小写,定义变量的方式有以下四种:

  • “=”:延时赋值,该变量只有在调用的时候,才会被赋值

  • “:=”:直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。

  • “?=”:若变量的值为空,则进行赋值,通常用于设置默认值。

  • “+=”:追加赋值,可以往变量后面增加新的内容。

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

$(变量名)

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

列表9: Makefile_test 变量实验

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 的变量值也就等于FILEAFILEB。

在这里插入图片描述

改造默认规则

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

列表10: 使用变量修改默认规则

# 定义变量
CC=gcc
CFLAGS=-I.
DEPS = hello_func.h

# 目标文件
hello_main: hello_main.o hello_func.o
	$(CC) -o hello_main hello_main.o hello_func.o

#*.o 文件的生成规则
%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

# 伪目标
.PHONY: clean
clean:
	rm -f *.o hello_main
  • 代码的1~4 行:分别定义了CC、CFLAGS、DEPS变量,变量的值就是等号右侧的内容,定义好的变量可通过”$ (变量名)”的形式引用,如后面的” ( C C ) ”、” (CC)”、” (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”的话,第1 2 行代码等价于:

# 当"%" 匹配的字符为"hello_func" 的话:
$(CC) -c -o $@ $< $(CFLAGS)
# 等价于:
gcc -c -o hello_func.o func_func.c -I .

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

改造链接规则

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

列表11: 使用变量修改链接规则

# 定义变量
TARGET = hello_main
CC = gcc
CFLAGS = -I.
DEPS = hello_func.h
OBJS = hello_main.o hello_func.o

# 目标文件
$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

#*.o 文件的生成规则
%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

# 伪目标
.PHONY: clean
clean:
	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 如下所示。

列表12: 给Makefile 增加编译器的选择切换

# 定义变量
#ARCH 默认为x86,使用gcc 编译器,
# 否则使用arm 编译器
ARCH ?= x86
TARGET = hello_main
CFLAGS = -I.
DEPS = hello_func.h
OBJS = hello_main.o hello_func.o

# 根据输入的ARCH 变量来选择编译器
#ARCH=x86,使用gcc
#ARCH=arm,使用arm-gcc
ifeq ($(ARCH),x86)
CC = gcc
else
CC = arm-linux-gnueabihf-gcc
endif

# 目标文件
$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

#*.o 文件的生成规则
%.o: %.c $(DEPS)
	$(CC) -c -o $@ $< $(CFLAGS)

# 伪目标
.PHONY: clean
clean:
	rm -f *.o hello_main

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

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

# 清除编译输出,确保不受之前的编译输出影响
make clean
# 使用ARM 平台
make ARCH=arm
# 清除编译输出
make clean
# 默认是x86 平台
make

在这里插入图片描述

使用函数

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

实现这些复杂的操作通常需要使用Makefile 的函数。

在这里插入图片描述

函数格式及示例

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

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

下面以常用的notdir、patsubst、wildcard 函数为例进行讲解,并且示例中都是我们后面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_dir/hello_main.o”。

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

多级结构工程的Makefile

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

列表13: 使用函数处理多级结构的工程

#定义变量
#ARCH 默认为x86,使用gcc 编译器,
#否则使用arm 编译器
ARCH ?= x86
TARGET = hello_main



#存放中间文件的路径
BUILD_DIR = build_$(ARCH)
#存放源文件的文件夹
SRC_DIR = sources
#存放头文件的文件夹
INC_DIR = includes .

#源文件
SRCS = $(wildcard $(SRC_DIR)/*.c)
# 目标文件(*.o)
OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(notdir $(SRCS)))
# 头文件
DEPS = $(wildcard $(INC_DIR)/*.h)


# 指定头文件的路径
CFLAGS = $(patsubst %, -I%, $(INC_DIR))

# 根据输入的ARCH 变量来选择编译器
#ARCH=x86,使用gcc
#ARCH=arm,使用arm-gcc

ifeq ($(ARCH),x86)
CC = gcc
else
CC = arm-linux-gnueabihf-gcc
endif

# 目标文件
$(BUILD_DIR)/$(TARGET): $(OBJS)
	$(CC) -o $@ $^ $(CFLAGS)

#*.o 文件的生成规则
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(DEPS)
# 创建一个编译目录,用于存放过程文件
# 命令前带“@”, 表示不在终端上输出
@mkdir -p $(BUILD_DIR)
$(CC) -c -o $@ $< $(CFLAGS)

# 伪目标
.PHONY: clean cleanall
# 按架构删除
clean:
	rm -rf $(BUILD_DIR)

# 全部删除
cleanall:
	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 即可:

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

# 编译
make

如下图:

在这里插入图片描述

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


参考资料:Linux 基础与应用开发实战指南——基于STM32MP1 系列

;