【Makefile】通用模板

电子说

1.3w人已加入

描述

工程目录

假如我们有以下目录结构:

.
├── inc
│   ├── add.h
│   └── sub.h
├── main.c
└── src
    ├── add.c
    └── sub.c

文件中的内容如下:

//main.c
#include 
#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步:

make

平常在编译项目时,预处理与编译器这两步会省略,是先将源文件 .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 ,,)
  • 名称:模式字符串替换函数。

  • 功能:查找 中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式 ,如果匹配的话,则以 替换。这里, 可以包括通配符 % ,表示任意长度的字串。如果 中也包含 % ,那么, 中的这个 % 将是 中的那个 % 所代表的字串。(可以用 **** 来转义,以 % 来表示真实含义的 % 字符)

  • 返回:函数返回被替换过后的字符串。

  • 示例:

    $(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 >)

这个函数的意思是,把参数 中的单词逐一取出放到参数 所指定的变量中,然后再执行 所包含的表达式。每一次 会返回一个字符串,循环过程中, 的所返回的每个字符串会以空格分隔,最后当整个循环结束时, 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

所以, 最好是一个变量名,\\ 可以是一个表达式,而 中一般会使用 这个参数来依次枚举 中的单词。

举个例子:

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
  • 名称:取文件函数——notdir。
  • 功能:从文件名序列 中取出非目录部分。非目录部分是指最後一个反斜杠( / )之后的部分。
  • 返回:返回文件名序列 的非目录部分。
  • 示例:
    $(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,就能得到预期的结果了

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通用模板已经编写完成,文章中若有错误的地方请在评论区指出。

审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分