Makefile:把一堆长命令收成一个入口

项目里总有那么几条命令:一长串参数、顺序还不能错。与其写在 README 里每次复制,不如做成 make deploy

一、最小结构

makefile
# Makefile
deploy:
	sync -av --exclude ".git" ./ user@host:/var/www/baize/
bash
make deploy

规则很简单:

makefile
目标: 依赖
	命令(必须是 Tab 缩进,不能用空格!)

必须用 Tab 缩进,这是 Makefile 最著名的坑。用空格会报 missing separator

二、假目标 .PHONY

如果目录下恰好有个同名文件(比如 deploy),make 会认为「目标已存在」而拒绝执行。加 .PHONY 声明:

makefile
.PHONY: deploy backup test clean

deploy:
	sync -av ./ user@host:/var/www/baize/

backup:
	tar czf backup-$(shell date +%F).tar.gz /var/www/baize

test:
	nginx -t

clean:
	rm -rf dist/

三、变量

makefile
HOST  ?= user@example.com
PORT  ?= 2222
DIR   ?= /var/www/baize
STAMP := $(shell date +%F)

deploy:
	rsync -avz --delete -e "ssh -p $(PORT)" ./ $(HOST):$(DIR)/

backup:
	ssh -p $(PORT) $(HOST) "tar czf /backup/baize-$(STAMP).tar.gz $(DIR)"
赋值含义
=延迟展开(用到时才求值)
:=立即展开(推荐)
?=未定义时才赋值(可被命令行覆盖
+=追加
bash
make deploy HOST=root@1.2.3.4 PORT=22   # 命令行覆盖变量

四、依赖触发

多个步骤按顺序执行:

makefile
.PHONY: all build test deploy

all: test build deploy

test:
	npm run lint

build:
	npm run build

deploy: build
	rsync -av dist/ server:/var/www/

make all 会依次跑 test → build → deploy。

真正的文件依赖(目标文件比依赖旧才重建):

makefile
dist/app.js: src/app.js src/util.js
	npx esbuild src/app.js --bundle --outfile=dist/app.js

# 源文件没变时,第二次执行会提示 up to date,直接跳过

五、静默与容错

makefile
# 不回显命令本身(只显示输出)
quiet:
	@echo "只在终端看到这行"

# 忽略某条命令的错误继续执行
try:
	-command-that-may-fail

# 让 make 不因为错误中断(谨慎)
.IGNORE: clean

默认 make 会回显每条命令再执行。加 @ 只显示结果,输出更干净。

六、默认目标与帮助

第一个目标是默认目标。常见做法是放一个 help

makefile
.DEFAULT_GOAL := help

.PHONY: help deploy backup logs

help: ## 显示帮助
	@grep -E "^[a-zA-Z_-]+:.*?## .*$$" $(MAKEFILE_LIST) \
		| awk "BEGIN {FS = ":.*?## "}; {printf "  \033[36m%-12s\033[0m %s\n", $$1, $$2}"

deploy: ## 同步到生产服务器
	rsync -avz --delete ./ $(HOST):$(DIR)/

backup: ## 打包备份站点目录
	tar czf baize-$(STAMP).tar.gz $(DIR)

logs: ## 实时看服务端错误日志
	ssh $(HOST) "tail -f /var/log/nginx/error.log"
bash
$ make
  help        显示帮助
  deploy      同步到生产服务器
  backup      打包备份站点目录
  logs        实时看服务端错误日志

七、本站的 Makefile

makefile
.DEFAULT_GOAL := help
BASE ?= https://www.bzii.cn

.PHONY: help serve feeds check zip

help: ## 显示可用命令
	@grep -E "^[a-zA-Z_-]+:.*?## .*$$" $(MAKEFILE_LIST) \
		| awk "BEGIN {FS = ":.*?## "}; {printf "  %-10s %s\n", $$1, $$2}"

serve: ## 本地预览(http://localhost:8000)
	python3 -m http.server 8000

feeds: ## 重建 feed.xml / sitemap.xml / robots.txt
	node tools/build-feeds.js --base $(BASE)

check: ## 校验所有文章能否正确渲染
	node tools/check-posts.js

zip: ## 打包源码
	zip -r baize-network.zip . -x "*.git*"

八、make vs shell 脚本

维度Makefileshell 脚本
入口多个具名目标一个(或参数分派)
依赖内建,未变则跳过要自己写判断
参数变量覆盖 make x K=v位置参数 $1
补全make + Tab 能列出目标
复杂逻辑

判断标准:

  • 一组独立命令(部署、备份、测试、清理)→ Makefile
  • 有复杂判断、循环、需要传多参数 → shell 脚本;
  • 两者可以结合:make deploy 内部调用 scripts/deploy.sh

九、常见错误

报错原因
missing separator用了空格缩进,必须是 Tab
command not found: deploy目标名拼错,或没加 .PHONY
变量取不到值用了 = 延迟展开,改用 :=
$ 被 make 吃掉shell 变量要写 $$
makefile
# 错误:make 会把 $HOME 当成自己的变量(空)
backup:
	echo $HOME

# 正确:shell 变量用双美元
backup:
	echo $$HOME

Makefile 的价值不在「自动化」,而在把命令变成一个可发现、可补全、有文档的入口