很多人觉得Makefile是C/C++的东西,跟自己没关系。
其实Makefile就是一个任务自动化工具,什么项目都能用。我现在Python、Go、前端项目都会写个Makefile,把常用命令封装起来。
这篇讲讲Makefile的基本写法,看完就能上手。
为什么用Makefile
项目里常用的命令:
# 启动开发环境docker-composeup -dexport$(cat.env|xargs)python manage.py runserver# 跑测试pytest tests/ -v# 构建镜像dockerbuild -t myapp:v1.2.3.dockerpush myapp:v1.2.3# 部署sshserver"cd /app && git pull && systemctl restart myapp"每次都敲一遍很烦,而且新人来了不知道怎么跑。
写个Makefile:
.PHONY: dev test build deploy dev: docker-compose up -d python manage.py runserver test: pytest tests/ -v build: docker build -t myapp:$(VERSION) . deploy: ssh server "cd /app && git pull && systemctl restart myapp"然后:
makedev# 启动开发环境maketest# 跑测试makebuildVERSION=v1.2.3# 构建镜像makedeploy# 部署新人来了,先make help或者看Makefile就知道怎么跑项目。
基本语法
最简单的形式
目标: 依赖 命令注意:命令前面必须是Tab,不是空格,这是很多人踩的坑。
hello: echo "Hello, World!"makehello# echo "Hello, World!"# Hello, World!依赖
build: clean test echo "开始构建" clean: rm -rf dist/ test: pytest执行make build时,会先执行clean和test。
变量
# 定义变量 APP_NAME = myapp VERSION = 1.0.0 # 使用变量 build: docker build -t $(APP_NAME):$(VERSION) .命令行传入变量:
makebuildVERSION=2.0.0环境变量
# 从环境变量获取,有默认值 DB_HOST ?= localhost DB_PORT ?= 3306 run: DB_HOST=$(DB_HOST) DB_PORT=$(DB_PORT) python app.pymakerun# 使用默认值DB_HOST=prod.dbmakerun# 使用环境变量makerunDB_HOST=prod.db# 命令行传入伪目标
如果目录里有个文件叫test,执行make test会说"已是最新"。
用.PHONY声明伪目标:
.PHONY: test clean build test: pytest所有不生成文件的目标都应该声明为.PHONY。
静默执行
默认make会打印执行的命令,加@可以隐藏:
hello: @echo "Hello"或者全局静默:
make-s hello多行命令
默认每行命令在单独的shell里执行,上一行的变量下一行拿不到:
# 这样不行 test: cd /tmp pwd # 还是在原来的目录 # 这样可以 test: cd /tmp && pwd # 或者用反斜杠 test: cd /tmp && \ pwd && \ ls错误处理
默认命令失败就停止。加-可以忽略错误:
clean: -rm -rf dist/ # 即使失败也继续或者用 || true:
clean: rm -rf dist/ || true实用模式
帮助信息
.PHONY: help help: @echo "可用命令:" @echo " make dev - 启动开发环境" @echo " make test - 运行测试" @echo " make build - 构建镜像" @echo " make deploy - 部署到服务器" # 把help放第一个,make不带参数就显示帮助 .DEFAULT_GOAL := help更高级的写法,自动从注释生成:
.PHONY: help help: ## 显示帮助信息 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' dev: ## 启动开发环境 docker-compose up -d test: ## 运行测试 pytest build: ## 构建镜像 docker build -t myapp .makehelp# dev 启动开发环境# test 运行测试# build 构建镜像include其他文件
# 引入其他Makefile include config.mk # 如果文件不存在不报错 -include local.mk条件判断
ifeq ($(ENV), prod) DB_HOST = prod.db.com else DB_HOST = localhost endif run: @echo "连接 $(DB_HOST)"获取Git信息
GIT_COMMIT := $(shell git rev-parse --short HEAD) GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) BUILD_TIME := $(shell date +%Y%m%d-%H%M%S) build: docker build \ --build-arg GIT_COMMIT=$(GIT_COMMIT) \ --build-arg BUILD_TIME=$(BUILD_TIME) \ -t myapp:$(GIT_COMMIT) .检查命令是否存在
check-docker: @which docker > /dev/null || (echo "请先安装docker" && exit 1) build: check-docker docker build -t myapp .实际项目示例
Go项目
.PHONY: build test run clean help APP_NAME := myapp VERSION := $(shell git describe --tags --always --dirty) BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME) help: ## 帮助 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' build: ## 构建 go build -ldflags "$(LDFLAGS)" -o bin/$(APP_NAME) . test: ## 测试 go test -v ./... run: build ## 运行 ./bin/$(APP_NAME) clean: ## 清理 rm -rf bin/ lint: ## 代码检查 golangci-lint run docker: ## 构建Docker镜像 docker build -t $(APP_NAME):$(VERSION) . .DEFAULT_GOAL := helpPython项目
.PHONY: install dev test lint format clean help PYTHON := python3 VENV := .venv PIP := $(VENV)/bin/pip PYTEST := $(VENV)/bin/pytest help: @echo "make install - 安装依赖" @echo "make dev - 启动开发服务" @echo "make test - 运行测试" @echo "make lint - 代码检查" @echo "make format - 格式化代码" $(VENV): $(PYTHON) -m venv $(VENV) install: $(VENV) $(PIP) install -r requirements.txt dev: install $(VENV)/bin/python manage.py runserver test: install $(PYTEST) tests/ -v lint: $(VENV)/bin/flake8 src/ $(VENV)/bin/mypy src/ format: $(VENV)/bin/black src/ tests/ $(VENV)/bin/isort src/ tests/ clean: rm -rf $(VENV) __pycache__ .pytest_cache .mypy_cache find . -type d -name "__pycache__" -exec rm -rf {} +前端项目
.PHONY: install dev build test lint deploy help NODE_ENV ?= development help: @echo "make install - 安装依赖" @echo "make dev - 启动开发服务" @echo "make build - 构建生产版本" @echo "make deploy - 部署" install: npm ci dev: npm run dev build: NODE_ENV=production npm run build test: npm test lint: npm run lint deploy: build rsync -avz --delete dist/ server:/var/www/app/Docker项目
.PHONY: up down logs ps build push clean help COMPOSE := docker-compose IMAGE := myapp REGISTRY := registry.example.com VERSION := $(shell git describe --tags --always) help: @echo "make up - 启动所有服务" @echo "make down - 停止所有服务" @echo "make logs - 查看日志" @echo "make build - 构建镜像" @echo "make push - 推送镜像" up: $(COMPOSE) up -d down: $(COMPOSE) down logs: $(COMPOSE) logs -f ps: $(COMPOSE) ps build: docker build -t $(REGISTRY)/$(IMAGE):$(VERSION) . docker tag $(REGISTRY)/$(IMAGE):$(VERSION) $(REGISTRY)/$(IMAGE):latest push: build docker push $(REGISTRY)/$(IMAGE):$(VERSION) docker push $(REGISTRY)/$(IMAGE):latest clean: $(COMPOSE) down -v --rmi local docker system prune -f常见问题
Tab vs 空格
命令前必须是Tab,不是空格。如果你的编辑器把Tab转成空格了,会报错:
Makefile:2: *** missing separator. Stop.VSCode可以在状态栏点击"空格:4"切换成Tab。
变量展开
# 立即展开 A := $(shell date) # 延迟展开 B = $(shell date):= 定义时就计算,= 使用时才计算。
Windows兼容
Windows上没有make命令,可以:
- 用WSL
- 安装MinGW的make
- 用nmake(语法略有不同)
或者干脆用跨平台的工具如just、task。
Makefile不复杂,核心就是:
- 目标、依赖、命令
- 变量
- .PHONY
写好一个Makefile,整个团队都能统一操作方式,还能当文档用。
值得花半小时学一下。