函数与条件判断
函数负责「加工变量」,条件判断负责「按环境分支」。这一章是让 Makefile 从「能用」到「灵活」的进阶内容。
函数语法
$(函数名 参数1,参数2,...)
# 或
${函数名 参数1,参数2,...}- 函数调用发生在变量展开时,返回一个字符串
- 参数之间用逗号分隔(参数内可有空格)
- 函数可嵌套:
$(patsubst %.c,%.o,$(wildcard *.c))
常用函数速查
字符串处理
$(subst from,to,text) # 替换所有 from → to
$(patsubst pattern,replacement,text) # 按模式替换(% 通配)
$(strip string) # 去掉首尾及多余空白
$(filter pattern...,text) # 保留匹配的单词
$(filter-out pattern...,text) # 剔除匹配的单词
$(sort list) # 去重并排序
$(word n,text) # 取第 n 个单词
$(words text) # 统计单词数
$(firstword text) / $(lastword text)SRC := main.c util.c test.c
OBJ := $(patsubst %.c,%.o,$(SRC)) # main.o util.o test.o
FILES := $(filter-out test.o,$(OBJ)) # main.o util.o
文件与路径
$(wildcard pattern...) # 用 shell 通配符匹配文件名(比 shell 的 * 可靠)
$(dir names...) # 取目录部分
$(notdir names...) # 取文件名部分
$(basename names...) # 去扩展名
$(suffix names...) # 取扩展名
$(addprefix prefix,names...) # 每个单词加前缀
$(addsuffix suffix,names...) # 每个单词加后缀
$(realpath names...) # 转绝对路径
SRC := $(wildcard ./internal/**/*.go) # 收集所有 Go 文件
BIN := $(addprefix bin/,$(notdir $(wildcard ./cmd/*)))
执行与条件
$(shell command) # 执行 shell 命令并返回 stdout
$(if condition,then-part,else-part)
$(or a,b) / $(and a,b) # 短路求值
GIT_COMMIT := $(shell git rev-parse --short HEAD)
NOW := $(shell date +%Y%m%d%H%M%S)
TARGET := $(if $(filter linux,$(GOOS)),linux-bin,$(GOOS)-bin)
遍历与模板(foreach / call / eval)
# foreach:对列表每个元素执行,返回结果列表
DIRS := src lib test
BACKUPS := $(foreach d,$(DIRS),$(d)/backup)
# call:调用一个「函数」(用 define 定义的带参数模板)
define run_in_dir
cd $(1) && $(2)
endef
$(call run_in_dir,src,go test ./...)
# eval:把文本当作 Makefile 语法解析(用于动态生成规则)
define make_target
$(1):
@echo "Target $(1)"
endef
$(eval $(call make_target,foo))
# 等价于生成了规则 foo:
💡
$(wildcard)比在 recipe 里用 shell 的*更可靠——它在 Make 解析阶段展开,不依赖 shell 环境。
条件判断
条件在 Makefile 解析阶段生效(不是执行阶段),控制「哪些规则被包含」:
ifeq ($(OS),Windows_NT)
BIN := app.exe
RM := del /Q
else
BIN := app
RM := rm -f
endif四种语法:
ifeq (arg1, arg2) # arg1 == arg2
ifneq (arg1, arg2) # arg1 != arg2
ifdef VAR # VAR 已定义且非空
ifndef VAR # VAR 未定义或为空
可嵌套,也可用 else:
ifdef DEBUG
CFLAGS += -g
else
CFLAGS += -O2
endif🚨 条件是在解析时求值的:
ifeq括号里的变量用当时的值。如果后面才改这个变量,条件不会重算。所以条件通常放在 Makefile 靠前位置,并配合?=或命令行传入的变量。
include 引入其它文件
include config.mk
-include optional.mk # 前加 - 表示文件不存在也不报错
include用于拆分大 Makefile、引入外部配置-include(或sinclude)容忍文件缺失,常用于引入「可选」的环境覆盖文件
# 常见做法:允许本地覆盖公共配置
-include .env.mk模式规则与静态模式规则
模式规则(Pattern Rule)
用 % 通配,一条规则描述「一类文件」的构建方式:
%.o: %.c
gcc -c $< -o $@
# 任何 x.o 都可由 x.c 编译而来,$< 自动指向对应 .c,$* 是匹配的 x
💡
$*(stem)正是模式规则里%匹配到的部分。例如目标util.o匹配%.o,则$*=util。
静态模式规则(Static Pattern Rule)
只对「指定的一组目标」套用模式,而非全局:
OBJ := main.o util.o
$(OBJ): %.o: %.c
gcc -c $< -o $@
# 只让 main.o、util.o 匹配 %.o: %.c,不影响其它 .o 文件
| 区别 | 模式规则 %.o: %.c |
静态模式 $(OBJ): %.o: %.c |
|---|---|---|
| 作用范围 | 全局所有匹配目标 | 仅列出的 $(OBJ) |
| 适用场景 | 通用编译规则 | 只编译本项目这组文件 |
文件搜索路径(vpath / VPATH)
当源文件不在当前目录时,告诉 Make 去哪里找依赖:
# 方式一:VPATH 变量(冒号分隔的目录列表)
VPATH = src:../include
# 方式二:vpath 指令(更精细,可按模式指定)
vpath %.c src
vpath %.h includevpath %.go ./cmd ./internal ./pkg
app: main.go
go build -o $@ $^
# make 会在 vpath 指定的目录里寻找 main.go