Makefile و Task Runner

چرا Makefile؟

وقتی روی یک پروژه کار می‌کنید، مجموعه‌ای از دستورهای تکراری دارید که باید بارها اجرا شوند:

1npm install
2npm run lint
3npm test
4npm run build
5docker build -t myapp .
6docker push myapp

مشکل این است که:

  • باید همهٔ این دستورها را به خاطر بسپارید
  • وقتی عضو جدیدی به تیم می‌پیوندد، باید آن‌ها را یاد بگیرد
  • اگر دستوری عوض شود، باید همه را مطلع کنید
  • در CI/CD باید دقیقاً همان دستورها را تکرار کنید

Makefile این مشکل را حل می‌کند: شما دستورهای پروژه را یک‌بار در Makefile تعریف می‌کنید، بعد همه فقط make test یا make build می‌زنند.

Makefile چیست؟

make اصلاً برای compile کردن برنامه‌های C/C++ ساخته شده، اما امروزه بیشتر به‌عنوان task runner استفاده می‌شود — ابزاری برای اجرای دستورهای استاندارد پروژه.

یک Makefile از targetها تشکیل شده. هر target یک نام دارد و یک یا چند دستور:

1test:
2	npm test
3
4build:
5	npm run build
6
7deploy:
8	./scripts/deploy.sh

حالا به‌جای یادآوری دستور دقیق، فقط می‌زنید:

1make test
2make build
3make deploy

ساختار Makefile

یک Makefile ساده:

 1# این یک comment است
 2
 3.PHONY: help
 4help:
 5	@echo "Available commands:"
 6	@echo "  make install  - Install dependencies"
 7	@echo "  make test     - Run tests"
 8	@echo "  make build    - Build the application"
 9
10.PHONY: install
11install:
12	npm install
13
14.PHONY: test
15test:
16	npm test
17
18.PHONY: build
19build:
20	npm run build

نکات مهم:

  • Tab، نه Space: خطوط دستور باید با Tab شروع شوند (نه space)
  • .PHONY: به make می‌گوید این target یک فایل نیست، یک دستور است
  • @: جلوی دستور باعث می‌شود خود دستور چاپ نشود، فقط خروجی‌اش

Target با Dependency

یک target می‌تواند به targetهای دیگر وابسته باشد:

 1.PHONY: all
 2all: clean install test build
 3
 4.PHONY: clean
 5clean:
 6	rm -rf dist node_modules
 7
 8.PHONY: install
 9install:
10	npm install
11
12.PHONY: test
13test:
14	npm test
15
16.PHONY: build
17build:
18	npm run build

حالا وقتی make all می‌زنید، به‌ترتیب اجرا می‌شود:

1make clean
2make install
3make test
4make build

استفاده از Variable

می‌توانید متغیر تعریف کنید:

 1APP_NAME := myapp
 2VERSION := $(shell git rev-parse --short HEAD)
 3DOCKER_REGISTRY := ghcr.io/myorg
 4
 5.PHONY: docker-build
 6docker-build:
 7	docker build -t $(DOCKER_REGISTRY)/$(APP_NAME):$(VERSION) .
 8
 9.PHONY: docker-push
10docker-push:
11	docker push $(DOCKER_REGISTRY)/$(APP_NAME):$(VERSION)
12
13.PHONY: docker-all
14docker-all: docker-build docker-push
  • :=: تعریف متغیر
  • $(shell ...): اجرای یک دستور و قرار دادن خروجی در متغیر
  • $(VAR): استفاده از متغیر

مثال واقعی: Makefile یک پروژهٔ Node.js

 1.DEFAULT_GOAL := help
 2
 3APP_NAME := myapp
 4VERSION := $(shell git describe --tags --always --dirty)
 5NODE_ENV := production
 6
 7.PHONY: help
 8help:
 9	@echo "Usage: make <target>"
10	@echo ""
11	@echo "Available targets:"
12	@echo "  install       Install dependencies"
13	@echo "  lint          Run linter"
14	@echo "  test          Run tests"
15	@echo "  build         Build for production"
16	@echo "  dev           Run development server"
17	@echo "  clean         Remove build artifacts"
18	@echo "  docker-build  Build Docker image"
19	@echo "  all           Run lint, test, and build"
20
21.PHONY: install
22install:
23	npm ci
24
25.PHONY: lint
26lint:
27	npm run lint
28
29.PHONY: test
30test:
31	npm test
32
33.PHONY: build
34build:
35	NODE_ENV=$(NODE_ENV) npm run build
36
37.PHONY: dev
38dev:
39	npm run dev
40
41.PHONY: clean
42clean:
43	rm -rf dist node_modules coverage
44
45.PHONY: docker-build
46docker-build:
47	docker build -t $(APP_NAME):$(VERSION) .
48
49.PHONY: all
50all: lint test build

استفاده:

 1# نصب dependencies
 2make install
 3
 4# اجرای تست
 5make test
 6
 7# ساخت برای production
 8make build
 9
10# همهٔ مراحل پیش از deploy
11make all
12
13# ساخت Docker image
14make docker-build

چرا Makefile در DevOps؟

1. Interface یکنواخت

تیم شما ممکن است پروژه‌هایی با زبان‌های مختلف داشته باشد:

  • Node.js → npm test
  • Python → pytest
  • Go → go test ./...

با Makefile، همهٔ پروژه‌ها interface یکسان دارند:

1make test    # در هر پروژه‌ای
2make build   # در هر پروژه‌ای

2. Reproducibility

دستورات دقیق در یک جا نوشته شده‌اند. هم developer محلی و هم CI/CD از همان دستورها استفاده می‌کنند:

1# .github/workflows/ci.yml
2jobs:
3  test:
4    runs-on: ubuntu-latest
5    steps:
6      - uses: actions/checkout@v3
7      - run: make install
8      - run: make test
9      - run: make build

3. مستندسازی

یک Makefile خوب با help خودش را مستند می‌کند:

1$ make
2Usage: make <target>
3
4Available targets:
5  install       Install dependencies
6  lint          Run linter
7  test          Run tests
8  build         Build for production

جایگزین‌های Makefile

make همه‌جا نصب است و ساده است، اما جایگزین‌هایی هم دارد:

  • Task (Go-based) — Makefile مدرن‌تر با YAML
  • Just (Rust-based) — syntax ساده‌تر از Make
  • npm scripts — برای پروژه‌های Node.js

اما make مزیت دارد که تقریبا روی هر سیستم Linux/macOS موجود است و نیازی به نصب ابزار اضافی ندارد.

منابع یادگیری

مستندات و مقالات