Makefile详解

1. 前言

Makefile 是用于管理项目构建过程的工具,广泛用于 C/C++ 等语言的编译。它通过定义规则和指令,自动化编译、链接等步骤,大大简化了开发者的工作。并且即使是一些其他的编译构建工具,最终也是以生成makefile为目的,比如cmake,所以了解Makefile的一些语法,对于项目构建非常重要。通过makefile定义的规则,最后通过make指令来执行。
make 是工作原理是:读取Makefile中所定义的规则,通过规则来递归遍历所有的依赖以及执行指令,最终完成整个工程的构建。
如果我们想使用并行编译,则可以在make后面加上-j的参数,来指定启动的线程数,比如make -j8表示启动8个线程进行编译,编译效率取决与你起的线程数以及cpu的内核数量。

2. Makefile 的基础语法

2.1 基础语法

Makefile的基础语法规则如下:

target: dependencies
    command
  • target(目标): 可以是一个目标文件或者一个动作名称
  • dependencies: 生成目标所依赖的文件或者其他目标
  • command: 构建目标的命令

示例:

main.o: main.cpp
	g++ -c $^ -o $@

上面用例的含义是 g++ -c main.cpp -o main.o 来生成 main.o

2.2. 伪目标

伪目标(phony targets)并不是文件,而是一种命令名称,可以用于执行一些常见的操作,如清理构建文件。它们通常定义为:

.PHONY: clean
clean:
    rm -f *.o

2.3 默认目标

当make不带目标时,表示编译生成默认目标

all: $(TARGET)

3. 变量

3.1 内置变量

上面用例其实我们使用了一些内置变量,比如$@, $^, 它是什么含义呢?

  • $@:表示目标文件
  • $^:表示所有的依赖文件
  • $<:表示第一个依赖文件
  • AR : 归档维护程序的名称,默认值为 ar,构建静态库时会使用
  • CC : C编译器的名称,默认值为 cc
  • CXX : C++ 编译器的名称,默认值为 g++

3.2 自定义变量

Makefile支持自定义自己想要的变量,可以根据自己的任务来定义变量明,比如:

SRC = main.cpp

上面的意思表示SRC变量就是main.cpp这个字符串,我们可以使用${SRC}或者$(SRC)来获取变量的值
变量还支持一些加法+, +=操作, 比如:

SRC += test.cpp

表示SRC新增一个test.cpp字符串,此时SRC = main.cpp test.cpp,也可以写成下面形式

SRC = ${SRC} test.cpp

4. 模式匹配

上面可以看到,我们一个target,只能处理这一个目标的问题,而在现实工程中,往往是一个比较大的工程,那么就涉及到很多的文件与目录,Makefile支持通配符,使用通配符来进行模式匹配就可以大大简化我们的操作。常见的通配符

%.o: %.c
	$(CC) -c $< -o $@

%.o: %.cpp
	$(CXX) -c $< -o $@

我们可以看匹配规则有:

  • %.o: %.c: %表示任意文件前缀,那么这条规则就是匹配任务后缀是.c的文件,将.c文件编译成同名的.o文件. 比如 匹配到main.c,则是表示main.o: main.cpp的编译规则
  • %.o: %.cpp: 跟上一条规则类似,是将.cpp文件编译成同名的.o文件

有时我们希望将特殊的文件编译的.o收集起来,以便于更好地管理,此时会用到以下的表达式:

LIB_SRC = library.cpp version.cpp
LIB_OBJS = $(LIB_SRC:.cpp=.o)

上面的表达式表示将LIB_SRC .cpp文件后缀名全部替换成.o, 并声明一个变量${LIB_OBJS}来表示所有替换后的.o文件

5. 函数

Makefile函数有很多,本节重点将一些常用函数

5.1 文件处理函数

1. wildcard ---- 目录遍历函数

  • $(wildcard PATTERN...)
    • 功能:获取指定目录下指定类型的文件列表
    • 参数:PATTERN 指的是某个或多个目录下的对应的某种类型的文件,如果有多个目录,一般使用空格间隔
    • 返回: 得到的若干个文件的文件列表,文件名之间使用空格间隔

示例:

SRC=${wildcard src/*.cpp}

表示变量SRC获得src目录下面的所有.cpp文件

2. dir ---- 获取目录函数

  • $(dir <names...>)
    • 功能:获取文件所在目录
    • <names…>):输出的文件名

示例:

$(dir src/foo.c sum.txt)

输出src/ ./

3. notdir ---- 获取文件名函数
dir函数正好相反,notdir获取的是非目录的文件名

  • $(notdir <names...>)
    • 功能:非目录的文件名
    • <names…>):输出的文件名

示例:

$(notdir src/foo.c sum.txt)

输出foo.c sum.txt

4. suffix ---- 获取文件后缀函数

  • $(suffix <names…>)
    • 功能:获取文件名后缀
    • <names…>:文件名,多个文件使用空格隔开

5. basename ---- 去掉文件后缀函数

  • $(basename <names…>)
    • 功能:去掉文件后缀
    • <names…>:文件名,多个文件使用空格隔开

5.2 字符串替换函数

1. subst ---- 字符串替换函数

  • $(subst <src>,<dst>,<text>)
    • 功能:将 中的字符 替换为字符

2. patsubst ---- 按格式替换函数

  • $(patsubst <pattern>,<replacement>,<text>)
    • 功能: 查找 字符串(可以是多个字符串,当多个时,以空格隔开)中所有匹配的字串,并将字串替换成
    • :匹配规则,可以包含通配符%,表示任意长度的字串
    • :替换的字符串,也支持通配符%,一般会跟联合使用,表示中的通配符所代表的字符串
    • 返回:返回替换后的字符串

3. strip ---- 去掉开头和结尾的空白字符

  • $(strip <string>)
    • 功能: 去掉开头和结尾的空白字符

4. findstring ---- 字串查找函数

  • $(findstring <dst>,<src>)
    • 功能:在字符串 中查找目标字符或者字符串
    • 返回:找到了就返回对应字符,如果没有找到返回空字符

5. filter ---- 过滤函数

  • $(filter <pattern...>,<text>)
    • 功能:过滤掉符合<pattern…>的字符串
    • 返回:返回过滤后的字符串

5. filter-out ---- 反过滤函数

  • $filter-out <pattern...>,<text>)
    • 功能:跟filter函数正好相反

5.3 其他函数

1. call ---- 调用函数

  • $(call <expression>,<parm1>,<parm2>,<parm3>,...)
    • 功能:调用自定义函数

2. shell ---- 执行命令行命令

  • shell <commands>
    • 功能:执行shell命令
    • 返回:返回执行后的输出

6. include 关键字

当 make 读取到 include 关键字的时候,会暂停读取当前的 Makefile,而是去读 include 包含的文件,读取结束后再继读取当前的 Makefile 文件。我们常用此关键字来引入一些公共的Makefile文件
include 使用的具体方式如下:

include <filenames>

使用时,通常用 -include 来代替 include 来忽略文件不存在或者是无法创建的错误提示,使用格式如下:

-include <filenames>

这两种方式之间的区别:

  • include :make 在处理程序的时候,文件列表中的任意一个文件不存在的时候或者是没有规则去创建这个文件的时候,make 程序将会提示错误并保存退出;
  • -include :当包含的文件不存在或者是没有规则去创建它的时候,make 将会继续执行程序,只有真正由于不能完成终极目标重建的时候我们的程序才会提示错误保存退出

7. 编译常用规则

  • -I(大写的i): 用来引入编译需要的头文件目录
  • -l: 编译依赖的库
  • -L:依赖库所在目录
  • --std=c++11: 配置c++支持版本,可以配置c++11c++14c++17
  • -W: 表示一些warning的处理,比如-Wall表示启用所有的编译预警, -Werror表示将warning当error处理
  • -O3: 选择编译优化器,总共有4个优化等级,-O0表示不开启编译优化,一般在debug时使用
  • -g:设置编译debug模式

8. 代码示例

我们构建一个最简单的C++项目工程

project/
├── Makefile
├── main.cpp
├── test.cpp
├── test.h

Makefile 内容如下:

# 配置编译器,也可以不配置,默认使用g++
CXX = g++

# 配置编译选项
CXXFLAGS = -Wall -std=c++17 -O3

# 遍历当前目录下所有的.cpp文件
SRCS=$(wildcard *.cpp)

# 将cpp文件后缀换成.o后缀
OBJS = $(SRCS:.cpp=.o)

# 设置编译目标名
TARGET = app

# 默认编译TARGET
all: ${TARGET}

# 编译成可执行的规则
$(TARGET): $(OBJS)
	$(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS)

# 编译.o文件的规则
%.o: %.cpp
	$(CXX) $(CXXFLAGS) -c $< -o $@

# 文件清理
clean:
	rm -f $(OBJS) $(TARGET)

# 设置伪目标,避免命名冲突
.PHONY: all clean

我们可以通过在make指令加-n来查看所有的编译指令

make -n
# 输出
>> g++ -Wall -std=c++17 -O3 -c main.cpp -o main.o
>> g++ -Wall -std=c++17 -O3 -c test.cpp -o test.o
>> g++ -Wall -std=c++17 -O3 -o app main.o test.o

我们可以看到最终会生成可执行app

七、静态模式 静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法: ;: ;: ; ; ... targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。 target-parrtern是指明了targets的模式,也就是的目标集模式。 prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。 这样描述这三个东西,可能还是没有说清楚,还是举个例子来说明一下吧。如果我们的;定义成“%.o”,意 思是我们的;集合中都是以“.o”结尾的,而如果我们的;定义成“%. c”,意思是对;所形成的目标集进行二次定义,其计算方法是,取;模式中的“%”(也就是去掉了[.o]这个结尾),并为其加上[.c]这个结尾,形成的新集合。 所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的文件名中有“%”那么你可以使用反斜杠“\”进行转义,来标明真实的“%”字符。 看一个例子: objects = foo.o bar.o all: $(objects) $(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@ 上面的例子中,指明了我们的目标从$object中获取,“%.o”表明要所有以“.o”结尾的目标,也就是“foo.o bar.o”,也就是变量$object集合的模式,而依赖模式“%.c”则取模式“%.o”的“%”,也就是“foo bar”,并为其加下“.c”的后缀,于是,我们的依赖目标就是“foo.c bar.c”。而命令中的“$<”和“$@”则是自动化变量,“$<”表示所有的依赖目标集(也就是“foo.c bar.c”),“$@”表示目标集(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则: foo.o : foo.c $(CC) -c $(CFLAGS) foo.c -o foo.o bar.o : bar.c $(CC) -c $(CFLAGS) bar.c -o bar.o 试想,如果我们的“%.o”有几百个,那种我们只要用这种很简单的“静态模式规则”就可以写完一堆规则,实在是太有效率了。“静态模式规则”的用法很灵活,如果用得好,那会一个很强大的功能。再看一个例子: files = foo.elc bar.o lose.o $(filter %.o,$(files)): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@ $(filter %.elc,$(files)): %.elc: %.el emacs -f batch-byte-compile $< $(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。其的它内容,我就不用多说了吧。这个例字展示了Makefile中更大的弹性。 八、自动生成依赖性 在Makefile中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的main.c中有一句“#include "defs.h"”,那么我们的依赖关系应该是: main.o : main.c defs.h
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值