Makefile 语法

标签:工程实践首次发布:2026-08-19最近修改:2026-08-19

入门基础

Make 是什么

make 是一个自动化构建工具。它最初常用于 C/C++ 项目编译,但现在也广泛用于:

  • Go 项目构建
  • 前端项目打包
  • Python 环境初始化
  • Docker 镜像构建
  • 代码生成
  • 单元测试
  • lint 检查
  • 本地开发环境启动
  • CI/CD 中的任务编排

它的核心能力是:

  • 根据规则执行命令
  • 根据文件依赖决定是否重新执行
  • 把复杂命令封装成简单命令,比如 make build
  • 为项目提供统一的开发入口

你可以把 Makefile 理解成一个项目级别的“命令说明书”。

最小 Makefile

最简单的 Makefile 文件长这样:

bash
hello:    echo "Hello, Makefile"

在当前目录执行:make hello

输出:

bash
echo "Hello, Makefile"Hello, Makefile

这里:

  • hello 是目标,英文叫 target
  • echo "Hello, Makefile" 是命令,英文叫 recipe
  • 命令行前面必须是 Tab,不是空格

结构如下:

bash
目标:    命令

或者更完整地写:

bash
target: prerequisites    recipe

其中:

  • target:要生成的目标,或者要执行的任务名
  • prerequisites:依赖项
  • recipe:实际执行的 shell 命令

Makefile 的基本语法

目标

bash
build:    go build -o app

执行:

bash
make build

含义是:执行 build 目标下的命令。

多条命令

bash
build:    echo "start build"    go build -o app    echo "build done"

执行顺序是从上到下。

默认目标

Makefile 的第一个目标是默认目标。

例如:

bash
build:    go build -o apptest:    go test ./...

直接执行:

bash
make

等价于:

bash
make build

很多项目默认要做的事不止一件:先构建,再测试,甚至再生成文档。
如果直接把 build 放在第一个,那 make 默认就只构建,不测试。

所以我们想要一个“总指挥”——通常叫 all,让它去调用别的任务:

bash
.PHONY: allall: build testbuild:    go build -o apptest:    go test ./...
  • all 是第一个目标,所以 make 默认跑它。
  • all 后面写了 build test,表示它依赖这两个目标,Make 会先去完成它们。
  • 结果:make → 默认跑 all → 依次执行 buildtest

这样想加什么默认步骤,只要加到 all 的依赖里就行,非常灵活。

目标与文件的关系

Make 最早是为“文件构建”设计的。比如:

bash
hello: hello.c    gcc hello.c -o hello

含义是:

  • hello 是目标文件
  • hello.c 是依赖文件
  • 如果 hello 不存在,执行命令生成它
  • 如果 hello.chello 更新,重新执行命令
  • 如果 hello 已经是最新的,就不执行

假设目录中有:hello.c,执行:

bash
make hello

会生成:hello 这个可执行文件,再执行一次:

bash
make hello

可能输出:make: ‘hello’ is up to date.

这就是 Make 的增量构建能力。

原理:时间戳比较规则,make 会做两步检查:

  1. 目标文件 hello 是否存在?
    • 不存在 → 一定“过时”,执行命令生成。
    • 存在 → 进入下一步。
  2. 依赖文件 hello.c 的修改时间(mtime),是否比目标文件 hello 的修改时间更新?
    • 是 → hello.c 被改过,hello 过时,重新编译。
    • 否 → hello 已经是最新的,跳过命令。

伪目标 .PHONY

Make 的“本能”:所有目标都优先当作文件

Make 的设计初衷是生成文件,所以当你写:

bash
clean:    rm -rf build

Make 的思路是:

  1. 寻找一个叫 clean 的文件。
  2. 如果这个文件存在,而且它的所有依赖都比它旧,就不执行命令——因为“文件已经是最新的”。
  3. 如果文件不存在,就运行命令来“生成”它。

所以,如果你当前目录下恰好有一个文件名叫 clean(哪怕是个空文件),执行 make clean 时,Make 会发现:

  • 文件 clean 存在
  • 它没有依赖(自然没有依赖比它新)
  • 于是判定:clean 已经是最新,不需要执行命令

于是就输出 make: 'clean' is up to date.,你的清理任务就被跳过了,这显然是错的。所以应该声明伪目标:

bash
.PHONY: cleanclean:    rm -rf build

.PHONY 的意思是:这些目标不是实际文件,每次都执行。

常见写法:

bash
.PHONY: all build test clean install runall: buildbuild:    go build -o apptest:    go test ./...clean:    rm -rf app

当前 Makefile 第一行是:

bash
.PHONY: all build test clean install run

意思是这些目标都不是文件,而是任务名。

变量体系

变量

基础变量

bash
APP = myappbuild:    go build -o $(APP)

执行 make build 时,$(APP) 会展开为 myapp

Make 里变量引用常见两种形式:

bash
$(APP)${APP}

推荐使用 $(APP)。这两种写法在大多数现代 Make 工具(GNU Make、BSD Make)里功能完全一样,但推荐 $(APP) 主要是出于历史、可移植性和防混淆的考虑。

标准与兼容性:$( ) 是“嫡系”,${ } 是“旁支”

Make 最初的设计者 Stuart Feldman 选择了 $(VAR),这是 Makefile 的“正统”语法。
${VAR} 是后来一些实现(尤其是 GNU Make)为了兼容 shell / Perl 等语言的习惯而加入的扩展。但严格的 POSIX 标准只定义了 $(...),并不是所有 Make 变体都支持 ${...}(比如一些老旧的 Unix 系统或嵌入式环境里的简化版 make)。

如果你希望 Makefile 在陌生系统上也可靠运行,$(APP) 是唯一保险的写法。


与 Shell 的冲突风险

Make 里大量工作是通过 Shell 执行的。Shell 本身也使用 ${},而且用法更复杂。

虽然 Make 在展开变量时会先处理完再交给 Shell,但人的大脑容易混淆。用 $( ) 可以一眼区分这是 Make 变量,不是 Shell 变量,减少误解。同时,如果 Makefile 中不小心写错了引用,错误信息也更容易定位。

变量覆盖(优先级问题)

Makefile:

bash
APP = myappbuild:    go build -o $(APP)

命令行覆盖:

bash
make build APP=server

实际执行:

bash
go build -o server

这在工程里很常用,比如:

bash
make build ENV=prodmake docker IMAGE_TAG=v1.2.3

默认优先级:命令行 > Makefile 赋值 > 环境变量

在 不加任何特殊选项 的情况下,Make 按这个顺序决定变量的最终值:

  1. 命令行传入的变量(如 make APP=server)优先级最高,它会覆盖其他同名变量。
  2. Makefile 里用 =:= 直接赋值的变量次之。
  3. 来自 Shell 环境变量的值优先级最低,除非 Makefile 里完全没有定义这个变量,它才会作为默认值出现。

两个“逆转”优先级的特殊手段

  1. -e 选项(让环境变量反超 Makefile 赋值):执行 make -e build,会强制环境变量覆盖 Makefile 里的同名赋值(但命令行依然最高)。这很少用,因为容易让构建行为变得不可预期。
  2. override 指令(锁定 Makefile 的值):如果你在 Makefile 里写:override APP = locked_value。那么无论在命令行怎么传 APP=...,Makefile 都会保持 locked_value,不会被命令行覆盖。通常用于保护一些关键路径名,比如 override SHELL := /bin/bash

递归展开变量 =

bash
A = $(B)B = helloprint:    echo $(A)

执行:

bash
make print

输出:

plain
hello

= 是递归展开,变量在使用时才展开。

简单展开变量 :=

bash
B = helloA := $(B)B = worldprint:    echo $(A)

输出:

plain
hello

:= 是立即展开,定义时就确定值。

追加变量 +=

bash
FLAGS = -WallFLAGS += -O2build:    gcc $(FLAGS) main.c -o app

最后 FLAGS 是:

plain
-Wall -O2

条件赋值 ?=

bash
ENV ?= dev    # 只在 ENV 未被定义时,才赋值为 devrun:    echo "env=$(ENV)"
  • make runenv=dev
    (未通过命令行或环境变量定义 ENV,使用默认值)
  • make run ENV=prodenv=prod
    (命令行定义 ENV 会覆盖默认值)
  • 若 Shell 中已 export ENV=stagingmake runenv=staging
    (环境变量也视为“已定义”,?= 不生效)

?= 表示:只在变量尚未被赋值(无论来自命令行、环境变量还是 Makefile 内部)时,才赋默认值。

自动变量

Make 提供了一组动态变化的特殊变量,它们的值会根据当前规则的目标和依赖自动填充。这些变量能让你写出更通用、复用性更强的规则。

$@

含义:当前规则中目标文件的名字。

bash
app: main.go    go build -o $@

这里 $@app

它让你不需要重复目标名,改动目标名时命令会自动同步。

$<

含义:当前规则的第一个依赖文件。

bash
main.o: main.c    gcc -c $< -o $@

这里:

  • $<main.c
  • $@main.o

典型场景:编译单个源文件时,$< 常代表“主源文件”,头文件作为后续依赖不会干扰命令。

$^

表示所有依赖。

bash
app: main.o util.o    gcc $^ -o $@

这里 $^ 是:main.o util.o

典型场景:当目标需要链接多个对象文件时,$^ 是最自然的写法。

$?

表示比目标更新的依赖。

bash
backup.zip: a.txt b.txt c.txt    zip $@ $?

只有发生变化的文件会放进 $?

典型场景:增量打包、库更新

常用自动变量总结

命令执行

Shell 与 Make 的关系

Makefile 里的命令默认由 /bin/sh 执行。

每一行命令默认是一个 shell

bash
test:    cd app    pwd

很多人以为 pwd 会在 app 目录下执行,但实际上不是。因为每一行都是单独 shell,等价于:

bash
sh -c 'cd app'sh -c 'pwd'

所以 pwd 仍然是原目录。

正确写法:

bash
test:    cd app && pwd

或者:

bash
test:    cd app; pwd

更推荐 &&,前一步失败则后一步不执行。

使用反斜杠续行

bash
install:    for pkg in a b c; do \        echo $$pkg; \    done

这里有两个重点:

  • 行尾的 \ 表示 shell 续行
  • $$pkg 才是 shell 变量 $pkg

为什么要写两个 $

因为 Make 会先处理变量,如果写 $pkg,Make 会把 $p 当成 Make 变量。要把 $ 传给 shell,必须写 $$

当前 Makefile 里有:

bash
install-tools:    @for tool in $(TOOLS) ; do \        go install $$tool; \    done

这里:

  • $(TOOLS) 是 Make 变量
  • $$tool 是 shell 循环变量

命令前缀

Makefile 命令前面可以加特殊符号。

@ 不打印命令本身

bash
hello:    @echo "hello"

不加 @

bash
echo "hello"hello

@

bash
hello

- 忽略错误

bash
clean:    -rm app

如果 app 不存在,rm app 报错,但 Make 不会停止。常见写法:

bash
clean:    rm -f app

-rm app 更清晰。

+ 强制执行递归 make

bash
sub:    +$(MAKE) -C subdir build

这个在递归 Make 或并行构建中有用。

内置能力

Make 的内置变量

Make 提供了一些预定义变量,可以直接在 Makefile 中使用,无需自己声明。它们让操作目录、调用编译器、控制 Shell 等变得非常方便。

变量 含义 默认值
CURDIR make 启动时的绝对路径 (执行路径)
MAKE 当前 make 命令名 make
MAKECMDGOALS 命令行中指定的目标 空或 all
MAKEFILE_LIST 所有已加载 Makefile 路径列表 依 include 而定
SHELL 执行命令的 Shell /bin/sh
CC C 编译器 cc
CXX C++ 编译器 g++
CFLAGS C 编译参数
CXXFLAGS C++ 编译参数
LDFLAGS 链接器参数
VPATH 依赖文件的搜索路径

比如:CWD := $(CURDIR)就是获取make命令启动时所在目录的绝对路径。

函数

Makefile 里有函数,格式是:

bash
$(函数名 参数1,参数2,...)# 或者${函数名 参数1,参数2,...}
  • 函数名与参数之间必须有空格(这是与变量引用 $(VAR) 的唯一区别)。
  • 参数之间用逗号分隔。

shell

执行 shell 命令并拿到输出。

bash
DATE := $(shell date +%Y%m%d)print:    echo $(DATE)

当前 Makefile 里有:

bash
$(shell go list -f '{{.Dir}}/' -m)

意思是执行 go list,拿到所有 Go module 的目录。

wildcard

通配符匹配文件。

bash
SRCS := $(wildcard *.c)

比如目录中有:

plain
main.c util.c

SRCS 是:

plain
main.c util.c

patsubst

模式替换。将 $(SRCS) 中所有符合 %.c 模式的字符串替换成 %.o% 表示匹配任意非空字符串:

bash
SRCS := main.c util.cOBJS := $(patsubst %.c,%.o,$(SRCS))

OBJS 是:

plain
main.o util.o

subst

字符串替换,参数顺序:$(subst 查找字符串,替换字符串,原始文本)

bash
TEXT := a-b-cOUT := $(subst -, ,$(TEXT))    *#  '-' 换成空格*

OUT 是:

plain
a b c

filter

保留匹配项。

bash
FILES := a.go b.py c.goGOFILES := $(filter %.go,$(FILES))

GOFILES 是:

plain
a.go c.go

filter-out

去掉匹配项。

bash
FILES := a.go b_test.go c.goFILES := $(filter-out %_test.go,$(FILES))

dir notdir

拆分路径的目录部分和文件名部分

bash
PATHS := src/main.go pkg/util.goDIRS := $(dir $(PATHS))FILES := $(notdir $(PATHS))

结果:

plain
DIRS = src/ pkg/FILES = main.go util.go

条件判断

Makefile 支持条件语句。

基本语法形式

bash
# 单分支ifeq (参数1, 参数2)    语句块endif# 双分支ifeq (参数1, 参数2)    语句块1else    语句块2endif# 多分支(GNU Make 3.81+)ifeq (参数1, 参数2)    语句块1else ifeq (参数3, 参数4)    语句块2else    语句块3endif

关键规则:

  • 条件必须顶格写,前面不能有 Tab 缩进(ifeq/ifneq/ifdef/ifndef 在行首)。
  • 语句块内的 Make 变量赋值或规则可以正常缩进,但 else/endif 也必须顶格。
  • 条件判断发生在 Makefile 解析阶段(不是执行阶段),因此不能放在规则命令内部。

ifeq — 相等判断

用途:判断两个参数是否完全相同(展开后进行字符串比较)。

bash
ENV ?= devifeq ($(ENV),prod)    FLAGS = -O2else    FLAGS = -gendif
  • ($(ENV),prod) 括号内逗号两边可以有空格(推荐添加以提升可读性),也可以省略成 '$(ENV)' 'prod' 或用双引号包裹:"$(ENV)" "prod"
  • 如果 ENV=prod,则 FLAGS = -O2,否则 FLAGS = -g

ifneq — 不等判断

用途:与 ifeq 相反,两个参数不相同时条件为真。

bash
ifneq ($(ENV),prod)    FLAGS = -gendif

等价于 ifeq + else 的另一个分支。可以用于“非生产环境才加调试信息”等场景。

ifdef — 变量已定义

用途:判断一个变量是否已被定义(即使其值为空,也算已定义)。

bash
ifdef DEBUG    FLAGS += -gendif
  • 执行 make build DEBUG=1DEBUG 被定义为 1,条件为真。
  • 执行 make build DEBUG=DEBUG 的值是空字符串,但 ifdef DEBUG 仍然为真。
  • 若从未在命令行、环境变量或 Makefile 中声明过 DEBUG,则 ifdef DEBUG 为假。

易混淆点:检查变量是否非空,更稳妥的方式是用 ifeq ($(VAR),) 测试空值,而不是依赖 ifdef

bash
# 只在 DEBUG 非空时添加 -gifneq ($(DEBUG),)    FLAGS += -gendif

ifndef — 变量未定义

用途:变量未定义时条件为真。常用来设置默认值。

bash
ifndef ENV    ENV = devendif
  • ENV 从未被赋值(包括命令行未传入、环境变量不存在、Makefile 中之前没有对其赋值),则执行赋值。
  • 这和我们之前学的 ?= 功能很相似,但 ?= 更简洁;ifndef 的优势在于可以在条件块内执行更复杂的操作,比如:
bash
ifndef ENV    $(info Setting ENV to default)    ENV = devendif

模式规则

模式规则是 Makefile 非常重要的能力。

基础模式规则

bash
%.o: %.c    gcc -c $< -o $@

含义是:

  • 任意 .o 文件依赖对应的 .c 文件
  • 比如 main.o 依赖 main.c
  • 比如 util.o 依赖 util.c

完整例子:

bash
app: main.o util.o    gcc $^ -o $@%.o: %.c    gcc -c $< -o $@

执行:

bash
make app

Make 会自动知道:

plain
main.o <- main.cutil.o <- util.capp <- main.o util.o

静态模式规则

bash
OBJS = main.o util.o$(OBJS): %.o: %.c    gcc -c $< -o $@

它由三部分组成:

plain
目标列表 : 目标模式 : 依赖模式    命令
  • 目标列表:你需要应用规则的具体文件列表,这里 $(OBJS) 就是 main.o util.o
  • 目标模式:%.o —— 从目标列表中提取“匹配部分”的模板。
  • 依赖模式:%.c —— 根据提取出的匹配部分,生成对应的依赖。

它是怎么工作的?

Make 逐一取出 $(OBJS) 中的每个文件,用目标模式 %.o 去匹配,提取出 % 对应的茎(stem),然后把依赖模式中的 % 替换成这个茎,得到依赖文件。

例如:

  • 对于 main.o% 匹配到 main,所以依赖是 main.c
  • 对于 util.o% 匹配到 util,所以依赖是 util.c

效果上看起来和普通模式规则一样,但范围限制在了 $(OBJS) 列表里。

依赖关系

Makefile 最大的价值之一是表达依赖。

bash
build: generate    go build -o appgenerate:    go generate ./...

执行:

bash
make build

会先执行:

bash
make generate

再执行:

bash
go build -o app

多个依赖:

bash
release: clean test build package

执行顺序大体是:

plain
clean -> test -> build -> package -> release

但注意:如果开了并行构建 make -j,没有依赖关系的目标可能并行执行。

include 拆分 Makefile

大项目中 Makefile 可能拆成多个文件。

bash
include scripts/common.mkinclude scripts/docker.mk

如果文件不存在会报错。可以用 -include 忽略不存在:

bash
-include .env.mk

常见用法:

bash
-include local.mk

让每个人可以有自己的本地配置。

工程化实践

递归 Make

如果项目有多个子目录:

bash
build-common:    $(MAKE) -C common buildbuild-server:    $(MAKE) -C server build

-C 表示进入目录执行 make。例如:

bash
make -C predict build

等价于:

bash
cd predict && make build

推荐用 $(MAKE) 而不是直接写 make,因为它会继承 make 的参数,比如 -j

并行构建

执行:

bash
make -j4

表示最多同时执行 4 个任务。例如:

bash
all: a b ca:    sleep 1; echo ab:    sleep 1; echo bc:    sleep 1; echo c

执行:

bash
make -j3 all

a b c 可能同时执行。

如果你希望某些任务必须顺序执行,要明确写依赖:

bash
all: cc: b    echo cb: a    echo ba:    echo a

错误处理

默认行为

命令返回非 0,Make 停止。

bash
test:    false    echo "never run"

执行时 echo 不会执行。

忽略错误

bash
test:    -false    echo "still run"

使用 set -e

如果一行中有复杂 shell:

bash
build:    set -e; \    cd app; \    go build ./...

如果中间失败,会停止。

推荐用 &&

bash
build:    cd app && go build ./...

Makefile 与环境变量

Make 变量和 shell 环境变量会互相影响。

Makefile 变量传给 shell

bash
ENV = devrun:    echo $$ENV

这样并不会输出 dev,因为 ENV 是 Make 变量,不一定是 shell 环境变量。

要传给 shell:

bash
export ENV = devrun:    echo $$ENV

或者:

bash
run:    ENV=dev go run main.go

命令行传变量

bash
make run ENV=prod

Makefile:

bash
run:    echo $(ENV)

输出:

plain
prod

export 所有变量

bash
exportENV = dev

不太推荐大范围使用,容易污染环境。

常见工程目标设计

一个规范的 Makefile 通常会有这些目标:

bash
.PHONY: all init install-tools build test lint fmt clean run tidy generateall: buildinit:    @echo "init project"install-tools:    @echo "install tools"build:    @echo "build project"test:    @echo "run tests"lint:    @echo "run lint"fmt:    @echo "format code"clean:    @echo "clean output"run:    @echo "run app"tidy:    @echo "tidy deps"generate:    @echo "generate code"

对于 Go 项目,常见写法:

bash
APP ?= appPKGS := ./....PHONY: build test lint fmt tidy clean runbuild:    go build -o bin/$(APP) .test:    go test $(PKGS)lint:    golangci-lint runfmt:    gofmt -w .tidy:    go mod tidyclean:    rm -rf binrun: build    ./bin/$(APP)

常见坑

Tab 和空格

命令必须用 Tab。

Make 变量和 Shell 变量混淆

Make 变量:$(NAME)

Shell 变量:$$name

每行一个 shell

错误:

bash
run:    cd output    sh predict_server.sh

正确:

bash
run:    cd output && sh predict_server.sh

忘记 .PHONY

建议所有任务型 target 都加 .PHONY

变量展开时机

bash
A = $(shell date)B := $(shell date)
  • A 每次使用时展开
  • B 定义时展开

路径中有空格

Makefile 对空格非常敏感。工程路径尽量不要包含空格。

make 默认用 /bin/sh

如果你写了 Bash 特性,最好指定:

bash
SHELL := /bin/bash

编写高质量 Makefile 的建议

提供帮助命令

常见写法:

bash
.PHONY: helphelp:    @echo "Available targets:"    @echo "  make build        Build project"    @echo "  make test         Run tests"    @echo "  make lint         Run linter"    @echo "  make clean        Clean output"

更高级一点:

bash
.PHONY: helphelp:    @grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \    awk 'BEGIN {FS = ":.*?## "}; {printf "%-20s %s\n", $$1, $$2}'build: ## Build project    go build -o apptest: ## Run tests    go test ./...

执行:

bash
make help

输出:

plain
build                Build projecttest                 Run tests

可覆盖变量

bash
APP ?= appENV ?= devGOFLAGS ?=build:    go build $(GOFLAGS) -o $(APP)

这样用户可以:

bash
make build APP=server GOFLAGS="-race"

明确失败行为

复杂 shell 目标推荐:

bash
deploy:    set -euo pipefail; \    echo "deploy"; \    ./deploy.sh

注意 pipefail 是 Bash 特性,如果用它,要设置:

bash
SHELL := /bin/bash

不要把太复杂的逻辑塞进 Makefile

Makefile 适合做任务编排,不适合写复杂业务逻辑。

推荐:

bash
build:    ./script/build.sh

不推荐在 Makefile 里塞几百行 shell。

Go 项目模板

Go 项目 Makefile 模板

下面是一个比较实用的 Go 项目模板:

bash
SHELL := /bin/bashAPP ?= appPKGS := ./...GOFLAGS ?=LDFLAGS ?=.PHONY: help install-tools fmt lint test build run clean tidyhelp:    @echo "Available targets:"    @echo "  install-tools  Install development tools"    @echo "  fmt            Format code"    @echo "  lint           Run linter"    @echo "  test           Run tests"    @echo "  build          Build binary"    @echo "  run            Run binary"    @echo "  clean          Remove build outputs"    @echo "  tidy           Tidy go modules"install-tools:    go install golang.org/x/tools/cmd/goimports@latest    go install github.com/golang/mock/mockgen@latestfmt:    gofmt -w .    goimports -w .lint:    golangci-lint run ./...test:    go test -count=1 $(PKGS)build:    mkdir -p bin    go build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o bin/$(APP) .run: build    ./bin/$(APP)clean:    rm -rf bintidy:    go mod tidy

使用:

bash
make buildmake testmake runmake build APP=servermake build LDFLAGS="-s -w"

多模块 Go Workspace 模板

当项目是多模块 workspace,可以参考这种结构:

bash
MODULE_DIRS := $(shell go list -f '{{.Dir}}' -m).PHONY: tidy test synctidy:    @for mod in $(MODULE_DIRS); do \        echo "tidy $$mod"; \        (cd $$mod && go mod tidy); \    done    @go work synctest:    @for mod in $(MODULE_DIRS); do \        echo "test $$mod/..."; \        go test $$mod/...; \    donesync:    go work sync

注意:

bash
(cd $$mod && go mod tidy)

比:

bash
cd $$mod && go mod tidy

更安全一些,因为它在子 shell 里切目录,不影响后续命令。

调试与学习路径

Makefile 调试技巧

打印变量

bash
print:    @echo "APP=$(APP)"    @echo "CURDIR=$(CURDIR)"

或者通用写法:

bash
print-%:    @echo '$*=$($*)'

使用:

bash
make print-APPmake print-CURDIR

只打印不执行

bash
make -n build

会打印将要执行的命令,但不执行。

显示详细调试信息

bash
make --debug=b build

更详细:

bash
make --debug=v build

打印数据库

bash
make -p

会输出 Make 解析后的所有变量、规则,非常多。

禁用内置规则

bash
make -r

大项目中有时会用:

bash
MAKEFLAGS += --no-builtin-rules

Makefile 与 CI/CD

很多项目会在 CI 里直接使用 Makefile:

yaml
steps:  - run: make install-tools  - run: make lint  - run: make test  - run: make build

好处是:

  • 本地和 CI 使用同一套入口
  • 新人只要学会 make testmake build
  • CI 配置更简洁

但当前项目比较特殊:CI 并不完全走 make install-tools,而是直接在 CI 配置里下载工具。这也是你之前遇到本地工具链和 CI 行为不一致的原因。

Comments

评论区将在滚动到这里时加载。