参考链接:

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. 赋值优先级

执行顺序(从高到低优先级):

  1. 命令行参数:make VAR=value​

  2. Makefile 中的赋值:VAR = value​

  3. 环境变量

  4. 条件赋值: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>​:需要进行替换操作的字符串。

函数返回值是替换后的新字符串。注意事项:

  1. ​subst​ 函数是直接替换,不支持通配符或正则表达式。如果需要更复杂的模式匹配,可以使用 patsubst​ 函数。

  2. 替换操作是大小写敏感的,确保输入的 <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​。

详细解释

  1. ​条件判断​:if 函数的第一个参数 是一个条件表达式。如果这个表达式展开后为非空字符串,则条件为真。

  2. ​执行部分​:如果条件为真,则执行第二个参数 ​​;如果条件为假,则执行第三个参数 ​​。如果没有定义 ​​,则返回空字符串。

  3. ​返回值​: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"​ 参数。