Makefile:把一堆长命令收成一个入口
项目里总有那么几条命令:一长串参数、顺序还不能错。与其写在 README 里每次复制,不如做成 make deploy。
一、最小结构
# Makefile
deploy:
sync -av --exclude ".git" ./ user@host:/var/www/baize/make deploy规则很简单:
目标: 依赖
命令(必须是 Tab 缩进,不能用空格!)必须用 Tab 缩进,这是 Makefile 最著名的坑。用空格会报
missing separator。
二、假目标 .PHONY
如果目录下恰好有个同名文件(比如 deploy),make 会认为「目标已存在」而拒绝执行。加 .PHONY 声明:
.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/三、变量
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)"| 赋值 | 含义 |
|---|---|
= | 延迟展开(用到时才求值) |
:= | 立即展开(推荐) |
?= | 未定义时才赋值(可被命令行覆盖) |
+= | 追加 |
make deploy HOST=root@1.2.3.4 PORT=22 # 命令行覆盖变量四、依赖触发
多个步骤按顺序执行:
.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。
真正的文件依赖(目标文件比依赖旧才重建):
dist/app.js: src/app.js src/util.js
npx esbuild src/app.js --bundle --outfile=dist/app.js
# 源文件没变时,第二次执行会提示 up to date,直接跳过五、静默与容错
# 不回显命令本身(只显示输出)
quiet:
@echo "只在终端看到这行"
# 忽略某条命令的错误继续执行
try:
-command-that-may-fail
# 让 make 不因为错误中断(谨慎)
.IGNORE: clean默认 make 会回显每条命令再执行。加 @ 只显示结果,输出更干净。
六、默认目标与帮助
第一个目标是默认目标。常见做法是放一个 help:
.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"$ make
help 显示帮助
deploy 同步到生产服务器
backup 打包备份站点目录
logs 实时看服务端错误日志七、本站的 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 脚本
| 维度 | Makefile | shell 脚本 |
|---|---|---|
| 入口 | 多个具名目标 | 一个(或参数分派) |
| 依赖 | 内建,未变则跳过 | 要自己写判断 |
| 参数 | 变量覆盖 make x K=v | 位置参数 $1 |
| 补全 | make + Tab 能列出目标 | 无 |
| 复杂逻辑 | 弱 | 强 |
判断标准:
- 一组独立命令(部署、备份、测试、清理)→ Makefile;
- 有复杂判断、循环、需要传多参数 → shell 脚本;
- 两者可以结合:
make deploy内部调用scripts/deploy.sh。
九、常见错误
| 报错 | 原因 |
|---|---|
missing separator | 用了空格缩进,必须是 Tab |
command not found: deploy | 目标名拼错,或没加 .PHONY |
| 变量取不到值 | 用了 = 延迟展开,改用 := |
$ 被 make 吃掉 | shell 变量要写 $$ |
# 错误:make 会把 $HOME 当成自己的变量(空)
backup:
echo $HOME
# 正确:shell 变量用双美元
backup:
echo $$HOMEMakefile 的价值不在「自动化」,而在把命令变成一个可发现、可补全、有文档的入口。
