工程目录
假如我们有以下目录结构:
.
├── inc
│ ├── add.h
│ └── sub.h
├── main.c
└── src
├── add.c
└── sub.c
文件中的内容如下:
//main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main()
{
int x = 9;
printf("x = %d\n", add_one(x));
printf("x = %d\n", sub_one(x));
return 0;
}
//add.h
int add_one(int x);
//add.c
int add_one(int x)
{
return x + 1;
}
//sub.h
int sub_one(int x);
//sub.c
int sub_one(int x)
{
return x - 1;
}
对于上述这样的多.c文件,又不在同一个目录下的大型工程中,借助makefile可以来减轻工作任务
(上述是一个很小很小的工程)
准备工作
在使用gcc 将 源文件 main.c编译成 可执行目标程序 总共需要4步:
平常在编译项目时,预处理与编译器这两步会省略,是先将源文件 .c 编译成 .o 文件,然后再链接 .o 文件
gcc -c main.c -o main.o
gcc main.o -o main.exe/main.out
编写Makefile
接下来会一步一步的编写一个Makefile文件,这个文件可以适配于大部分C/C++工程,让我们开始吧!
1. 定义可执行文件名、GCC类型
先定义一个最终可执行文件名的变量:
TARGET = main
变量值可以随意定义。
gcc分为很多种,常见的有:gcc、arm-linux-gcc、arm-none-eabi-gcc等等,所以为了Makefile适配更多的C/C++项目,可以将编译器定义一个变量,这后续更改起来很方便。我这里使用的gcc:
CC = gcc
2. 中间文件的路径的变量
由前文可知,在编译过程中会编译出很多的 .o 文件,一般将这些编译过程中产生的文件单独放到一个文件夹下,文件夹的名字大多叫做 build ,定义一个变量 BUILD_DIR 该变量的值就是build,用来存放中间产物,在后续编译过程中会用到:
BUILD_DIR = build
3、.c 源文件的路径
事先需要将工程中所用到的源文件 .c 的路径,这样在后续中就可直接得到 .c 文件,定义一个变量 SRC_DIR 来存放源文件 .c 的路径
SRC_DIR = \
./ \
./src
4、 头文件的路径
接着得到所有用到的头文件路径:
INC_DIR = \
./inc
这gcc选项中有这个参数 -I 是告诉编译器头文件的路径,在后续中会使用Makefile的一个函数为每个头文件路径添加 -I。
5、为头文件路径添加 -I
当所引用的头文件与源文件不在同一级目录下时需要添加 -I 选项指定头文件路径,在第四步中已经获取到头文件的路径,下面借助一个Makefile中的一个函数在每个头文件前面添加 -I
首先看一下函数 patsubst 的介绍。
$(patsubst <pattern>,<replacement>,<text>)
- 名称:模式字符串替换函数。
-
功能:查找 <text> 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式 <pattern> ,如果匹配的话,则以 <replacement> 替换。这里, <pattern> 可以包括通配符 % ,表示任意长度的字串。如果 <replacement> 中也包含 % ,那么, <replacement> 中的这个 % 将是 <pattern> 中的那个 % 所代表的字串。(可以用 \ 来转义,以 \% 来表示真实含义的 % 字符)
-
返回:函数返回被替换过后的字符串。
-
示例:
$(patsubst %.c, %.o, x.c.c bar.c)
把字串 x.c.c、bar.c 符合模式 %.c 的单词替换成 %.o ,返回结果是 x.c.o bar.o-
定义一个变量 INCLUDE
INCLUDE = $(patsubst %, -I %, $(INC_DIR))
这样就会在每个头文件路径前加入 -I 了。
6、得到带路径的源文件
在第三步中,我们得到了 .c 文件的存放路径,这一步我们得到带有路径的 .c 文件,简单来说就是,假如在src目录下有一个foo.c的文件,在第3步中只得到了 src 这个目录,这一步得到的是 src/foo.c 。
得到目录下的 .c 文件需要用到Makefile中的两个函数,foreach函数 、wildcard 函数
1、wildcard 函数
$(wildcard PATTERN...)
在Makefile中,它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表
2、foreach函数
$(foreach <var>,<list>,<text>)
这个函数的意思是,把参数 <list> 中的单词逐一取出放到参数 <var> 所指定的变量中,然后再执行 <text> 所包含的表达式。每一次 <text> 会返回一个字符串,循环过程中, <text> 的所返回的每个字符串会以空格分隔,最后当整个循环结束时, <text> 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。
所以, <var> 最好是一个变量名,\ 可以是一个表达式,而 <text> 中一般会使用 <var> 这个参数来依次枚举 <list> 中的单词。
举个例子:
names := a b c d
files := $(foreach n,$(names),$(n).o)
上面的例子中, $(name) 中的单词会被挨个取出,并存到变量 n 中,$(n).o 每次根据 $(n) 计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以, $(files) 的值是 a.o b.o c.o d.o 。
使用这两个函数得到带路径的 .c 文件
CFILES := $(foreach dir, $(SRC_DIR), $(wildcard $(dir)/*.c))
7. 得到不带路径的 .c 文件
在上一步中我们得到了带路径的 .c 文件,这步借助Makefile中的函数 notdir 将路劲去除,得到 “真正的.c”
notdir 函数
$(notdir <names...>)
-
名称:取文件函数——notdir。
-
功能:从文件名序列 <names> 中取出非目录部分。非目录部分是指最后一个反斜杠( / )之后的部分。
-
返回:返回文件名序列 <names> 的非目录部分。
-
示例:
$(notdir src/foo.c hacks)
返回值是 foo.c hacks 。
定义一个变量 CFILENDIR 来存放不带路径的 .c 文件:
CFILENDIR := $(notdir $(CFILES))
8. 将工程中的.c 文件替换成 ./build 目录下对应的目标文件 .o
这一步只是简单的字符串进行替换,对原文件不进行任何操作。我们可以先写一个伪目标,打印一下变量 CFILENDIR 的内容
# 打印结束后可以删除
print:
@echo $(CFILENDIR)
使用 make 查看一下输出结果,会得到字符串:main.c add.c sub.c
在前面讲过编译时会在 build 目录下得到.o文件,这个.o 文件就是由.c文件生成的,因此 main.c add.c sub.c 会对应于 build 目录下的 main.o add.o sub.o
由于现在不是编译阶段,我们只对字符串进行个简单的替换操作,定义一个变量 COBJS
用来存放目录 build 下的 .o 文件
COBJS = $(patsubst %, ./$(BUILD_DIR)/%, $(patsubst %.c, %.o, $(CFILENDIR)))
此时变量 COBJS 的值就是:./build/main.o ./build/add.o ./build/sub.o
到目前为止已经得到了工程中的源文件 CFILENDIR
、可重定位目标文件 COBJS
以及带有 -I 前缀的头文件路径 INCLUDE
,注意,到目前为止我们操作的只是字符串而已,还未对源文件做任何操作。
9、搜索源文件
在我们这个工程中,有两个目录下存放着 .c 文件,当make需要去找寻文件的依赖关系时,可以使用变量 VPATH
让make在自动在这两个目录中去找依赖文件。
VPATH = $(SRC_DIR)
10、生成可重定位目标文件(编译阶段)
$(COBJS) : $(BUILD_DIR)/%.o : %.c
@mkdir -p $(BUILD_DIR)
$(CC) $(INCLUDE) -c -o $@ $<
会将源文件 .c 编译成可重定位目标文件 .o
11、链接 .o 文件
此步骤是最后一步,将所有的 .o 文件链接成可执行程序
可执行文件可以生成到指定的目录下,我这里生成到了 build 目录下
$(BUILD_DIR)/$(TARGET).exe : $(COBJS)
$(CC) -o $@ $^
此时,Makefile 已经编写完成。
当执行make 时,发现并不是预期的目标,只执行了一句指令:
gcc -I ./inc -c -o build/main.o main.c
这是因为make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件,如是依赖存在编译成功后就会退出执行,若是没有找到依赖,则会报错并退出。
当想达到预期的目标,共有两种办法:
第一种:将目标$(BUILD_DIR)/$(TARGET).exe 写在目标 $(COBJS) 的前面,这样就可以达到预期的结果了。
第二种:使用关键字 all ,写在关键字 all 后面的目标都会执行一次,直到所有目标执行完成,或者某个目标不成立。
此时,再执行make,就能得到预期的结果了
12、清理目标
make编译之后会在工程中多出很多目标文件*.o,可以写一个目标 clean 用来删除工程中的目标文件
clean:
rm -rf $(BUILD_DIR)
Makefile全部内容:
# 可执行文件名
TARGET = main
# gcc类型
CC = gcc
# 存放中间文件的路径
BUILD_DIR = build
#存放.c 源文件的文件夹
SRC_DIR = \
./ \
./src
# 存放头文件的文件夹
INC_DIR = \
./inc
# 在头文件路径前面加入-I
INCLUDE = $(patsubst %, -I %, $(INC_DIR))
# 得到带路径的 .c 文件
CFILES := $(foreach dir, $(SRC_DIR), $(wildcard $(dir)/*.c))
# 得到不带路径的 .c 文件
CFILENDIR := $(notdir $(CFILES))
# 将工程中的.c 文件替换成 ./build 目录下对应的目标文件 .o
COBJS = $(patsubst %, ./$(BUILD_DIR)/%, $(patsubst %.c, %.o, $(CFILENDIR)))
# make 自动在源文件目录下搜索 .c 文件
VPATH = $(SRC_DIR)
$(BUILD_DIR)/$(TARGET).exe : $(COBJS)
$(CC) -o $@ $^
$(COBJS) : $(BUILD_DIR)/%.o : %.c
@mkdir -p $(BUILD_DIR)
$(CC) $(INCLUDE) -c -o $@ $<
clean:
rm -rf $(BUILD_DIR)
至此,Makefile通用模板已经编写完成,文章中若有错误的地方请在评论区指出~~