参考链接:
1. 概述
1.1. 什么是 Makefile?
Makefile 是 make 工具的配置文件,用于自动化代码编译、测试和部署过程。它定义了一系列规则来指定文件之间的依赖关系以及如何更新文件。
1.2. 为什么使用 Makefile?
自动化构建:一键执行复杂编译流程
增量编译:只重新编译修改过的文件
跨平台支持:兼容 Unix / Linux / macOS / Windows
减少重复工作:封装复杂命令
项目管理:清晰定义项目结构
1.3. Makefile 基本结构
1.3.1. 基本规则语法
target: dependencies
command1
command2
...
target:要生成的文件或执行的操作名
dependencies:生成目标所需的文件及 target 列表,如果是 target,会先执行对应的 target,再执行当前 target
command:生成目标的 shell 命令(必须以 Tab 开头)
make 默认执行 Makefile 中的第一个 target
在 command 中可以使用 @ 开头来抑制 make 输出执行的命令
1.3.2. 简单示例
# 编译go程序
BUILD_DIR := build
build:
# 下面这个就不会输出执行的命令 go build -o ./test
@go build -o ./test
clean:
# 下面这个就会输出执行的命令 rm -rf test
rm -rf test
2. Makefile 核心使用详解
2.1. 变量定义与使用
变量定义方式有如下几种:
2.1.1. = 递归赋值
使用时才会根据定义去展开
# 示例:
x = foo
y = ${x} bar
test:
@echo ${y}
x = new
上面这种情况中,如果在 x = new 之后输出 y 的值,是输出 new bar,因为 = 赋值是使用时才展开;
另外需要特别注意的是,makefile 的执行逻辑是先全部跑完所有的变量定义等,才会执行 target,也就是说,哪怕 target 像上面这样定义,最后的输出也会是 new bar。所以为了避免误解,所有的变量定义最好还是放在最上面
2.1.2. := 简单赋值
简单赋值并直接展开
x := foo
y := ${x} bar
x := new
test:
@echo ${y}
这种情况输出的是 foo bar
2.1.3. ?= 条件赋值
当变量没有值时才会被赋值,如果变量已经有值,则不会改变现有的值
# 示例1:基本使用
x ?= default
y ?= default
x = custom # 覆盖之前的值
test:
@echo "x = $(x)" # 输出: x = custom
@echo "y = $(y)" # 输出: y = default
执行 make 输出的是 x = custom y = default,如果执行 y=33 make,输出则是 x = custom y = 33
主要用于赋予一个默认值,后续需要使用命令行优先覆盖的场景
2.1.4. += 为追加赋值
在原有变量基础上添加新的内容,展开实际跟随源变量
# 示例1:基本使用
CFLAGS = -Wall
CFLAGS += -O2
CFLAGS += -I./include
test:
@echo "CFLAGS = $(CFLAGS)" # 输出: -Wall -O2 -I./include
执行 make 输出 CFLAGS = -Wall -O2 -I./include
# 示例2:混合使用
x = foo
x += bar
x += $(y) # 如果 y 在后面定义
y := baz
test:
@echo "x = $(x)" # 输出: x = foo bar baz
再来一个示例
# 示例2:混合使用
x := foo
x += bar
x += $(y) # 如果 y 在后面定义
y := baz
test:
@echo "x = $(x)" # 输出: x = foo bar
2.1.5. != shell 赋值
主要用于执行后续的 shell 命令,并获取结果
# 示例1:获取当前目录
CURRENT_DIR != pwd
DATE != date +%Y-%m-%d
FILES != find . -name "*.go"
test:
@echo "当前目录: $(CURRENT_DIR)"
@echo "今天日期: $(DATE)"
@echo "找到 $(words $(FILES)) 个 .go 文件"
# 执行make输出如下:
当前目录: /data/git/home/jiaoben/test/makefile
今天日期: 2026-01-23
找到 1 个 .go 文
2.1.6. 赋值优先级
执行顺序(从高到低优先级):
命令行参数:make VAR=value
Makefile 中的赋值:VAR = value
环境变量
条件赋值:VAR ?= default
示例:
# Makefile
ENV_VAR ?= from_makefile # 如果环境变量已设置,这个不会执行
test:
@echo "ENV_VAR = $(ENV_VAR)"
执行:
/data/git/home/jiaoben/test/makefile master ❯ make
ENV_VAR = from_makefile
/data/git/home/jiaoben/test/makefile master ❯ export ENV_VAR=from_env
/data/git/home/jiaoben/test/makefile master ❯ make
ENV_VAR = from_env
/data/git/home/jiaoben/test/makefile master ❯ make ENV_VAR=from_cmd
ENV_VAR = from_cmd
2.2. 自动变量
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
2.3. 通配符与模式规则
# 获取所有.c文件
SRCS = $(wildcard *.c)
# 将.c替换为.o
OBJS = $(patsubst %.c,%.o,$(SRCS))
# 模式规则:如何从.c生成.o
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
2.4. 函数使用
# 获取目录下所有.c文件
SRCS = $(wildcard src/*.c)
# 替换文件后缀
OBJS = $(patsubst %.c,%.o,$(SRCS))
# 添加前缀
INCLUDES = $(addprefix -I, include1 include2)
# 过滤
SRC_FILES := file1.c file2.cpp file3.c file4.h
C_FILES := $(filter %.c %.cpp, $(SRC_FILES))
使用示例:
# 获取目录下所有.go文件
GO_FILES := $(wildcard ./*.go)
# 替换文件后缀
OBJS := $(patsubst %.go,%.c,$(GO_FILES))
# 添加前缀
INCLUDES = $(addprefix -I , $(GO_FILES))
# 过滤以 .c 和 .cpp 结尾的
SRC_FILES := file1.c file2.cpp file3.c file4.h
C_FILES := $(filter %.c %.cpp, $(SRC_FILES))
.PHONY: test
test:
@echo "GO_FILES:\n $(GO_FILES)"
@echo "OBJS:\n $(OBJS)"
@echo "INCLUDES:\n $(INCLUDES)"
@echo "C_FILES:\n$(C_FILES)"
执行输出:

2.4.1. subst
subst 函数的基本语法如下:
$(subst <from>,<to>,<text>)
<from>:需要被替换的子串。
<to>:替换后的子串。
<text>:需要进行替换操作的字符串。
函数返回值是替换后的新字符串。注意事项:
subst 函数是直接替换,不支持通配符或正则表达式。如果需要更复杂的模式匹配,可以使用 patsubst 函数。
替换操作是大小写敏感的,确保输入的 <from> 和 <text> 匹配。
示例:
# 替换 ee 为 EE
RESULT := $(subst ee,EE,feet on the street)
# 将变量 foo 中的空格替换为逗号:
comma := ,
# 定义真正的空格 space := $(empty) $(empty)
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))
# 将文件名中的 name 替换 为 fame
S1 := name.cpp
NEW_S1 := $(subst name,fame,$(S1))
.PHONY: test
test:
@echo "RESULT:\n $(RESULT)"
@echo "bar:\n $(bar)"
@echo "NEW_S1:\n $(NEW_S1)"
执行输出:

2.4.2. foreach
foreach 是 GNU Make 提供的一个循环函数,用于遍历列表并对每个元素执行指定操作,返回所有结果的拼接字符串。它的语法为:
$(foreach var, list, text)
var:临时变量名
list:以空格分隔的元素列表
text:对 var 进行操作的表达式,返回值会用空格连接
示例:
target := test base build arg amd arm64 cmd shell
.PHONY: test
test:
@echo "$(foreach t,$(target),$(t).go)"
执行输出:

2.4.3. if
在 Makefile 中,if 函数提供了条件判断的功能。它的语法如下:
$(if <condition>, <then-part>, <else-part>)
其中,<condition> 是一个表达式,如果其返回非空字符串,则表示条件为真,执行 <then-part>;否则,执行 <else-part>。如果没有定义 <else-part>,则返回空字符串。
示例
以下是一个使用 if 函数的示例:
SRC_DIR := src
# 如果变量 SRC_DIR 的值不为空,则将 SRC_DIR 指定的目录作为 SUBDIR 子目录;否则将 /home/src 作为子目录
SUBDIR += $(if $(SRC_DIR), $(SRC_DIR), /home/src)
all:
@echo $(SUBDIR)
在这个示例中,如果 SRC_DIR 变量不为空,则 SUBDIR 将被设置为 src;否则,SUBDIR 将被设置为 /home/src。
详细解释
条件判断:if 函数的第一个参数 是一个条件表达式。如果这个表达式展开后为非空字符串,则条件为真。
执行部分:如果条件为真,则执行第二个参数 ;如果条件为假,则执行第三个参数 。如果没有定义 ,则返回空字符串。
返回值:if 函数的返回值是 或 的计算结果,具体取决于条件的真假。
注意事项
if 函数的条件表达式会先展开变量或函数引用,然后进行判断。
如果条件表达式为空且没有定义 ,则函数返回空字符串。
通过使用 if 函数,可以在 Makefile 中实现灵活的条件判断,从而根据不同的条件执行不同的操作。
示例 2:
target := test base build arg amd arm64 cmd shell
targets := $(if $(target), $(target), base)
.PHONY: test
test:
@echo "$(foreach t,$(targets),$(t).go)"
执行输出(第一个是当 target 为空时的结果):

2.4.4. eval 和 call
在 Makefile 中,eval 和 call 是两个非常有用的函数,用于动态生成规则或变量。call 用于调用自定义函数并传递参数,而 eval 会将展开的结果作为 Makefile 的一部分进行解析。
使用示例
2.4.4.1. call 函数
call 函数允许你定义一个模板,并通过参数化调用来生成不同的内容。
define test
foo := $(1)
endef
$(warning $(call test,apple))
all:
@echo foo = $(foo)
输出:
foo := apple
foo =
在这里,$(call test,apple) 将参数 apple 传递给模板 test,但由于没有进一步解析,变量 foo 的值仍为空。
2.4.4.2. eval 函数
eval 函数会对参数进行两次展开,并将结果作为 Makefile 的一部分解析。
apple_tree := 3
define test
foo := $($(1)_tree)
endef
$(eval $(call test,apple))
all:
@echo foo = $(foo)
输出:
foo = 3
在此示例中,$(call test,apple) 返回的 foo := $(apple_tree),然后通过 eval 将其解析为 Makefile 的一部分,使得变量 foo 被赋值为 3。
注意事项
双重展开:使用 eval 时需要注意变量的双重展开,因此需要用双美元符号(如 $$1)来避免提前解析。
返回值:call 返回的是展开后的字符串,而 eval 没有返回值,其作用是将结果直接嵌入 Makefile 中。
通过结合使用这两个函数,可以实现动态生成复杂规则或变量的需求,从而提高 Makefile 的灵活性和可维护性。
2.4.4.3. 综合应用
define test
.PHONY: $(1)
$(1):
@echo "$(1) 执行了"
endef
$(eval $(call test,$(target)))
all: $(target)
@echo "all 执行了"
上面这个示例在执行 make all 时,由于没有定义 target,所以 all 也没有要执行的依赖项,所以最终只是输出 all 执行
而当执行 make target=base all 时,从上到下渲染,通过 $(eval $(call test,$(target))) 将上面的 define 的内容双重展开定义为 makefile 的一部分,这时的 makefile 类似如下:
.PHONY: base
base:
@echo "base 执行了"
all: base
@echo "all 执行了"
所以最终执行的输出结果为:
base 执行了
all 执行了
2.5. 条件判断
DEBUG = 1
ifeq ($(DEBUG),1)
CFLAGS += -g
else
CFLAGS += -O2
endif
test:
@echo "CFLAGS = ${CFLAGS}"
执行:
/data/git/home/jiaoben/test/makefile master ❯ make
CFLAGS = -g
/data/git/home/jiaoben/test/makefile master ❯ make DEBUG=0
CFLAGS = -O2
2.6. 伪目标
可以使用 .PHONY 声明不生成实际文件的目标。默认情况下,makefile 会优先查找当前目录下同名的文件,如果有,则更新这个文件,如果没有,才会去执行 target 对应的命令,看如下实例:
# Makefile
ENV_VAR ?= from_makefile # 如果环境变量已设置,这个不会执行
ENV_VAR = from_makefile
test:
@echo "ENV_VAR = $(ENV_VAR)"
我的目录结构如下:

执行 make test:

可以看出,并没有执行我们定义好的 echo 命令,同样的再定义如下 makefile:
# Makefile
ENV_VAR ?= from_makefile # 如果环境变量已设置,这个不会执行
ENV_VAR = from_makefile
# 添加这一行
.PHONY: test
test:
@echo "ENV_VAR = $(ENV_VAR)"
再执行:

可以看到正确执行了我们定义的命令
2.7. 嵌套执行 Makefile
在一些大工程中,会把不同模块或不同功能的源文件放在不同的目录中,我们可以在每个目录中都写一个该目录的 Makefile 这有利于让我们的 Makefile 变的更加简洁,不至于把所有东西全部写在一个 Makefile 中。
列如在子目录 subdir 目录下有个 Makefile 文件,来指明这个目录下文件的编译规则。外部总 Makefile 可以这样写
subsystem:
cd subdir && $(MAKE)
# 其等价于:
subsystem:
$(MAKE) -C subdir
定义 $(MAKE) 宏变量的意思是,也许我们的 make 需要一些参数,所以定义成一个变量比较有利于维护。两个例子意思都是先进入 "subdir" 目录,然后执行 make 命令
我们把这个 Makefile 叫做总控 Makefile,总控 Makefile 的变量可以传递到下级的 Makefile 中,但是不会覆盖下层 Makefile 中所定义的变量,除非指定了 "-e" 参数。