Makefile 使用介绍

发布时间: 更新时间: 总字数:4920 阅读时间:10m 作者:IP:上海 网址

Makefile 示例

介绍

  • make 命令在当前目录依次搜索 MakefilemakefileGNUmakefile 文件,若找不到就报错
  • make 命令支持通过时间戳判断源文件和编译产物的先后关系,减少文件重新编译,提高编译速度

help

make --help ...

说明:

  • -C 指定路径
  • make -j 4 指定并行编译的任务数,可以大大缩短编译时间,-j 指定 CPU 个数
-j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.

规则

Makefile 由多条规则组成

目标: [依赖1[ 依赖2 [ 依赖3 [...]]]]
\t命令

[target] ... : [prerequisites] ...
<tab>[command]
    ...
    ...
  • 目标 一个 target 表示一条规则,也称为 伪目标(PHONY)
    • 一个 Makefile 中可以有多个目标,一般第一个为默认目标
    • .PHONY 的作用有两个
      • .PHONY跟伪目标,直接在 Makefile 中执行 伪目标 的命令。忽略 Makefile 同级目录下的同名的文件
      • 二是提高执行 makefile 时的效率
  • 依赖 是可选的,通常是多个文件名、伪目标,必须是已有的规则
    • 可以通过 shell 获取,如 $(shell find . -type f -name "*.sh")
  • 每行命令前必须有一个 <tab> 键,如果想用其他键,通过内置变量 .RECIPEPREFIX 声明
.RECIPEPREFIX = >
all:
> echo Hello, world
  • 命令 构建一个 target 的具体命令集合
    • 也可以为空,用来表示一种依赖关系
    • 命令以横杠-开头,表示忽略命令执行的状态
    • make 默认会打印每条命令,再执行,该行为被称为回声,命令前加 @ 可以禁用该打印
  • 多目标时,可以使用 % 进行匹配,如下压缩命令:
xxx-%.gz: xxx-%
    gzip --force --keep xxx-$*.exe
  • .DEFAULT_GOAL 设置默认目标,不设置默认为第一个
.DEFAULT_GOAL := default

default:...
  • ``

shell

  • shell 函数 参数是操作系统的 shell 命令,功能和使用 (`) 相同
    • 示例 ipaddress := $(shell ip a)
  • ${}$()区别
- $() 与 ``(反引号)都是用来作命令替换的
- ${}  变量替换,把变量的真实值带入
  • 每行命令在一个单独的 shell 中执行,Shell 之间没有继承关系
    • $$ 用来引用变量
# 执行时 `make test-sh`,取不到 foo 的值,两行命令在两个不同的进程执行
test-sh:
    export foo=bar
    echo "foo=[$$foo]"
  • 解决办法
# 方法一:将两行命令写在一行,中间用分号分隔
test-sh:
	export foo=bar; echo "foo=[$$foo]"

# 方法二:在换行符前加反斜杠转义
test-sh:
	export foo=bar; \
	echo "foo=[$$foo]"

list-file:
	for file in `ls /usr/local/bin/`; do\
		echo $${file};\
	done;\

# 方法三:为目标加上 .ONESHELL:
.ONESHELL:
test-sh:
	export foo=bar;
	echo "foo=[$$foo]"

include

将别的 Makefile 包含进来,这很像 C 语言的 #include

# 假设有 Makefile a.mk、b.mk、c.mk,$(bar) 包含 e.mk f.mk,则
include foo.make *.mk $(bar)
# 等价于
include foo.make a.mk b.mk c.mk e.mk f.mk

# include 当文件不存在时会报错,使用 `-` 忽略错误,也可以使用 `sinclude` 替代
-include <filename>

类似的参数:

  • make -I 或 --include-dir

条件判断

# ifeq
mode = debug
hello: hello.c
ifeq ($(mode),debug)
    @echo "debug mode"
    gcc -g -o hello hello.c
else
    @echo "release mode"
    gcc -o hello hello.c
endif

# ifneq
mode = debug
hello: hello.c
ifneq ($(mode),)
    @echo "debug mode"
    gcc -g -o hello hello.c
else
    @echo "release mode"
    gcc -o hello hello.c
endif

# ifdef
mode =
hello: hello.c
ifdef mode
    @echo "debug mode"
    gcc -g -o hello hello.c
else
    @echo "release mode"
    gcc -o hello hello.c
endif

# ifndef
mode =
hello: hello.c
ifndef mode
    @echo "debug mode"
    gcc -g -o hello hello.c
else
    @echo "release mode"
    gcc -o hello hello.c
endif

for

  • Makefile 使用 Bash 语法实现判断和循环
LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

# 等同于
all:
    for i in one two three; do \
        echo $i; \
    done

赋值运算

Makefile 一共提供了四个赋值运算符 (=:=?=+=),区别:

# 执行时扩展,允许递归扩展
VARIABLE = value

# 定义时扩展
VARIABLE := value

# 只有在该变量为空时才设置值
VARIABLE ?= value

# 将值追加到变量的尾端
VARIABLE += value

override 示例 Makefile

.PHONY: all
web = xiexianbin.cn
all:
	@echo "web = $(web)"
$ make
web = xiexianbin.cn
$ make web=www.xiexianbin.cn
web = www.xiexianbin.cn

自动变量(Automatic Variables)

Make 命令还提供一些自动变量,它们的值与当前规则有关

  • $@ 指当前目标,即 Make 命令当前构建的那个目标 比如,make foo$@ 就指 foo
a.txt b.txt:
    touch $@

# 等同于
a.txt:
    touch a.txt
b.txt:
    touch b.txt
  • $< 指第一个前置条件
    • 比如,规则为 t: p1 p2,那么 $< 就指 p1
a.txt: b.txt c.txt
    cp $< $@

# 等同于
a.txt: b.txt c.txt
    cp b.txt a.txt
  • $? 指比目标更新的所有前置条件,之间以空格分隔

    • 比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$? 就指 p2
  • $^ 指所有前置条件,之间以空格分隔

    • 比如,规则为 t: p1 p2,那么 $^ 就指 p1 p2
  • $* 指匹配符 % 匹配的部分

  • 比如 % 匹配 f1.txt 中的 f1$* 就表示 f1

  • $(@D)$(@F) 分别指向 $@ 的目录名和文件名

    • 比如,$@src/input.c,那么 $(@D) 的值为 src$(@F) 的值为 input.c
  • $(<D)$(<F) 分别指向 $< 的目录名和文件名

函数

Makefile 还可以使用函数,格式如下。

$(function arguments)
# 或
${function arguments}
  • wildcard 用来在 Makefile 中,替换 Bash 的通配符
srcfiles := $(wildcard src/*.txt)
  • subst 函数用来文本替换
$(subst from,to,text)

# 将字符串 feet on the street 替换成 fEEt on the strEEt
$(subst ee,EE,feet on the street)
  • patsubst 函数用于模式匹配的替换
$(patsubst pattern,replacement,text)

# 将文件名 x.c.c bar.c 替换成 x.c.o bar.o
$(patsubst %.c,%.o,x.c.c bar.c)
  • 替换后缀名名函数的写法是:变量名 + 冒号 + 后缀名 替换规则,本质是 patsubst 函数的一种简写形式
# 将变量OUTPUT中的后缀名 .js 全部替换成 .min.js
min: $(OUTPUT:.js=.min.js)

递归传递变量

使用 export 将变量设置为环境变量,递归传递,export 也可以在命令行中

.PHONY:all
export WEB = xiexianbin.cn
all:
    @echo "make start"
    @echo "WEB = $(WEB)"
    make -C subdir1
    make -C subdir2
    make -C subdir3
    @echo "make done"

递归执行

make -C subdir1 subdir2 subdir3 ...

# 等价于
cd subdir1 && $(MAKE)
cd subdir2 && $(MAKE)
cd subdir3 && $(MAKE)

define 定义多行变量

使用场景 搭配指令 注意事项
动态生成 Target / 规则 $(eval $(call ...)) 模板中的 Makefile 内置变量(如 $@, $<)必须写成双美元符号 $$@, $$<,防止被 $(call) 提前展开。
多行 Shell 命令包 Target 下直接 $(call ...) 每一行都会被视为一条 Shell 命令发给系统执行;如果有参数,使用 $(1), $(2) 接收。
纯文本/Help 信息 $(info ...) 或直接输出 不需要复杂的转义,所见即所得。

在 GNU Makefile 中,define 指令用于定义多行变量(通常被称为“宏”或“命令包”)。由于它可以跨越多行,因此非常适合封装复杂的逻辑、长长的 Shell 命令序列,或者结合 $(call)$(eval)动态生成 Target(目标)和规则。以下是 Makefiledefine 宏的 4 个最常用的使用场景及代码示例:

场景一:动态生成 Target 和编译规则(最强大的高级用法)

当你有很多结构类似的 Target 需要生成(例如:有十几个不同的可执行文件,它们的编译过程基本一致,只是输入输出名字不同)时,使用 define 编写模板,再配合 $(call)$(eval) 动态生成规则,可以大幅减少代码冗余。

原理:

  1. define 写一个规则模板,用 $(1), $(2) 占位。
  2. $(call) 传入参数替换占位符,生成一段符合 Makefile 语法的多行字符串。
  3. $(eval) 将这段字符串解析为真正的 Makefile 规则。

示例代码:

makefile
# 定义要生成的多个程序名称
PROGRAMS := server client monitor

# 1. 定义生成 Target 的宏模板
# 注意:规则内部的自动化变量(如 $@, $^)必须写成 $$@ 和 $$^
# 因为 $(call) 会展开一次(吃掉一个 $),$(eval) 解析时才真正作为自动化变量使用。
define GENERATE_PROG_RULE
# 生成目标 $(1)
$(1): $(1).c utils.c
	@echo "正在编译目标: $(1)"
	$(CC) $(CFLAGS) -o $$@ $$^ $(LDFLAGS)
endef

# 2. 遍历 PROGRAMS 列表,动态生成每个 target 的规则
$(foreach prog, $(PROGRAMS), $(eval $(call GENERATE_PROG_RULE,$(prog))))

all: $(PROGRAMS)

上述代码执行后,Makefile 会在内存中自动生成 server: server.c utils.cclient: client.c utils.c 等三套完整的编译规则。

场景二:定义多行命令包(Canned Recipes,最经典的用法)

如果你有一组 Shell 命令(比如安装、打包、清理等)需要在多个 Target 中重复使用,可以将它们包裹在 define 中,像调用函数一样复用。

示例代码:

makefile
# 定义一个打印并执行拷贝的多行命令包
define install_binary
	@echo "正在安装 $1 到 $2 目录..."
	mkdir -p $(2)
	cp $(1) $(2)/
	chmod 755 $(2)/$(notdir $(1))
	@echo "安装完成!"
endef

install-server:
	# 在 target 下通过 $(call ...) 调用执行
	$(call install_binary, ./build/server, /usr/local/bin)

install-client:
	$(call install_binary, ./build/client, /opt/app/bin)

这种用法使得 Target 下的配方(Recipe)非常简洁,且修改安装逻辑时只需要改一处。

场景三:封装复杂的函数逻辑

通过组合 Makefile 自带的文本处理函数(如 subst, patsubst, filter 等)和 define,你可以封装出自定义的处理函数。

示例代码:

makefile
# 自定义宏:将输入的路径列表转换为包含 -I 前缀的头文件搜索路径
define get_include_flags
$(patsubst %,-I%,$(1))
endef

# 假设有以下目录
INCLUDE_DIRS := src/include lib/utils/include vendor/api

# 调用宏生成真正的编译参数
CFLAGS += $(call get_include_flags, $(INCLUDE_DIRS))

test:
	@echo "CFLAGS 为: $(CFLAGS)"
	# 输出: CFLAGS 为: -Isrc/include -Ilib/utils/include -Ivendor/api

场景四:定义多行文本(如 Help 帮助信息或自动生成配置文件)

很多时候我们需要在终端输出格式化的多行提示信息,或者需要将大段文本写入某个文件中。如果用 echo 一行行拼写不仅麻烦而且容易出错,使用 define 会优雅得多。

示例代码(输出 Help 信息):

makefile
define HELP_TEXT
=======================================
当前项目的可用编译选项 (Make Targets):
  all      : 编译所有组件
  clean    : 清理编译产生的文件
  install  : 安装到系统目录
  test     : 运行单元测试
=======================================
endef

help:
	# 使用 $(info ...) 函数直接在解析时打印多行文本
	$(info $(HELP_TEXT))
	@# 加上 @: 是个空命令,防止 make 报错 "Nothing to be done for help"
	@:

示例代码(动态生成配置文件):

makefile
define CONFIG_FILE_CONTENT
# 自动生成的配置文件
VERSION=$(VERSION)
BUILD_DATE=$(shell date +%Y-%m-%d)
ENABLE_DEBUG=true
endef
export CONFIG_FILE_CONTENT

generate_config:
	@echo "$$CONFIG_FILE_CONTENT" > config.ini
	@echo "config.ini 生成完毕!"

常见的 GNU Make 内置函数总结表

GNU Make 提供了大量内置函数,主要分为字符串处理文件名处理系统与控制逻辑三大类。

函数名 / 语法 功能介绍 可以直接使用的示例 (含预期结果)
1. 字符串处理函数
$(subst from,to,text) 纯字符串替换:将 text 中所有的 from 替换为 to $(subst ee,EE,feet on street)
👉 结果: fEEt on strEEt
$(patsubst pat,rep,text) 模式替换:按模式匹配替换(最常用来批量改文件后缀)。 $(patsubst %.c,%.o,main.c test.c)
👉 结果: main.o test.o
$(strip string) 去空格:去掉开头和结尾的空格,并将中间的多个连续空格合并为一个。 $(strip a b c )
👉 结果: a b c
$(findstring find,text) 查找字符串:在 text 中查找 find。找到则返回 find,否则返回空。 $(findstring YES,YES NO)
👉 结果: YES
$(filter pat...,text) 正向过滤:保留 text 中符合模式 pat 的单词。 $(filter %.c %.h,a.c b.o c.h)
👉 结果: a.c c.h
$(filter-out pat...,text) 反向过滤:剔除 text 中符合模式 pat 的单词,保留剩下的。 $(filter-out main.o,main.o util.o)
👉 结果: util.o
2. 文件名处理函数
$(dir names...) 取目录名:提取文件路径中的目录部分(包含最后的 /)。 $(dir src/main.c hacks.c)
👉 结果: src/ ./
$(notdir names...) 取文件名:提取文件路径中的文件名部分(去掉目录)。 $(notdir src/main.c hacks.c)
👉 结果: main.c hacks.c
$(basename names...) 取前缀/去后缀:去掉文件名中的后缀部分(. 及其后面的内容)。 $(basename src/main.c bar.o)
👉 结果: src/main bar
$(suffix names...) 取后缀:提取文件名中的扩展名(包含 . )。 $(suffix src/main.c bar.o)
👉 结果: .c .o
$(addprefix pre,names) 加前缀:为列表中的每个单词加上前缀。 $(addprefix -I,src include)
👉 结果: -Isrc -Iinclude
$(addsuffix suf,names) 加后缀:为列表中的每个单词加上后缀。 $(addsuffix .c,main test)
👉 结果: main.c test.c
$(wildcard pattern) 展开通配符:获取当前真实存在且匹配该模式的文件列表。 $(wildcard src/*.c)
👉 结果: 自动列出 src 下的所有 .c 文件
3. 系统、循环与控制函数
$(foreach var,list,text) 循环遍历:遍历 list,将每个元素赋值给 var,并执行 text $(foreach d,src lib,-I$(d))
👉 结果: -Isrc -Ilib
$(shell command) 执行 Shell 命令:调用系统 Shell 运行命令,并将标准输出作为返回值。 $(shell date +%Y-%m-%d)
👉 结果: 2026-04-17 (当前日期)
$(if cond,then,else) 条件判断:如果 cond 不为空,则返回 then 的结果;否则返回 else $(if $(DEBUG),-g,-O2)
👉 结果: DEBUG定义了就返回 -g,否则 -O2
$(info text) 打印信息:在 Make 解析阶段在终端打印一段信息(不中断程序)。 $(info Build is starting...)
👉 结果: 终端输出此句话
$(error text) 报错并退出:打印错误信息,并立即终止 Makefile 的运行。 $(error "Missing config file")
👉 结果: 打印信息并报错退出

直接放入 Makefile 运行的综合测试用例

可以新建一个名为 Makefile 的文件,复制以下内容,然后在终端执行 make,就可以直观地看到所有函数的实际执行结果:

makefile
# 定义测试变量
SRC_FILES = src/main.c src/utils.c src/config.h test.o
DEBUG_MODE = YES

# 1. 字符串替换
OBJ_FILES = $(patsubst %.c, %.o, $(SRC_FILES))

# 2. 过滤文件
ONLY_C_FILES = $(filter %.c, $(SRC_FILES))
NO_OBJ_FILES = $(filter-out %.o, $(SRC_FILES))

# 3. 提取文件名和后缀
FILE_NAMES = $(notdir $(SRC_FILES))
SUFFIXES = $(suffix $(SRC_FILES))

# 4. 加前缀
INC_FLAGS = $(addprefix -I, include src lib)

# 5. Shell 命令获取时间
CURRENT_DIR = $(shell pwd)

# 默认目标
all:
	@echo "=== 1. patsubst 替换后缀 ==="
	@echo "原列表: $(SRC_FILES)"
	@echo "替换后: $(OBJ_FILES)"
	@echo ""
	@echo "=== 2. filter & filter-out ==="
	@echo "只保留 C文件 : $(ONLY_C_FILES)"
	@echo "剔除 O文件   : $(NO_OBJ_FILES)"
	@echo ""
	@echo "=== 3. notdir & suffix ==="
	@echo "纯文件名: $(FILE_NAMES)"
	@echo "纯后缀  : $(SUFFIXES)"
	@echo ""
	@echo "=== 4. addprefix ==="
	@echo "头文件路径: $(INC_FLAGS)"
	@echo ""
	@echo "=== 5. shell 命令 ==="
	@echo "当前路径: $(CURRENT_DIR)"

Makefile 示例

执行子命令的过程中获取 Shell 结果、赋值给变量并使用

注意 Makefile 的一个核心特性:Recipe(配方)中的每一行都在独立的 Shell 进程中运行。

get-ver1:
	@# 注意:赋值时不要有空格,使用 $$ 来引用 Shell 变量
	@VERSION=$$(uname -r); \
	echo "获取到的版本是: $$VERSION"; \
	if [ "$$VERSION" = "5.15.0-generic" ]; then \
		echo "这是一个特定的内核版本"; \
	fi

get-ver2:
	$(eval SYS_VER := $(shell uname -r))
	@echo "Makefile 变量 SYS_VER 现在是: $(SYS_VER)"
	@echo "可以在后续逻辑中直接引用: $(SYS_VER)"

说明:

  1. $$:在 Makefile 的命令区域,$VAR 会被识别为 Makefile 变量,$$VAR 才会传递给 Shell 识别为 Shell 变量。
  2. \:确保整个逻辑在同一个进程中运行,这样第一行定义的 VERSION 变量在后面几行才有效。
  • $(eval ...) 会在执行到这一行时,将结果动态注入到 Makefile 的变量表中。
  • 这种方式定义的变量在接下来的所有命令中都可以通过 $(SYS_VER) 访问。

Golang

# https://www.xiexianbin.cn/program/tools/2016-01-09-makefile/index.html
.PHONY: all test clean build build-linux build-mac build-windows

GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
BINARY_NAME=main
BINARY_LINUX=$(BINARY_NAME)-linux
BINARY_MAC=$(BINARY_NAME)-darwin
BINARY_WIN=$(BINARY_NAME)-windows

help:  ## Show this help.
	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

all: clean test build build-linux build-mac build-windows  ## Build all
test:  ## run test
	$(GOTEST) -v ./...
clean: ## run clean bin files
	$(GOCLEAN)
	rm -f bin/$(BINARY_NAME)
build:  ## build for current os
	$(GOBUILD) -o bin/$(BINARY_NAME) -v

build-linux:  ## build linux amd64
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o bin/$(BINARY_LINUX) -v
build-mac:  ## build mac amd64
	CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GOBUILD) -o bin/$(BINARY_MAC) -v
build-windows:  ## build windows amd64
	CGO_ENABLED=0 GOOS=windows GOARCH=amd64 $(GOBUILD) -o bin/$(BINARY_WIN) -v