Skip to content
Go back

make for Project Automation (It's Not Just for C Code)

By SumGuy 5 min read
make for Project Automation (It's Not Just for C Code)

The Problem

You’ve got scripts everywhere.

scripts/
setup.sh
build.sh
test.sh
deploy.sh
migrate-db.sh
lint.sh
format.sh

Developers don’t know what to run first. Dependencies? You have to read each script. Parallel tasks? Good luck with bash.

What you need is a simple task runner. Something everyone understands immediately.

Enter make. It’s been around since 1977. It’s on every Unix system. It works.

Basic Makefile

.PHONY: help build test lint format
help:
@echo "Available tasks:"
@echo " make build - Compile the project"
@echo " make test - Run tests"
@echo " make lint - Lint code"
@echo " make format - Format code"
build:
@echo "Building..."
npm run build
test:
@echo "Running tests..."
npm test
lint:
@echo "Linting..."
eslint .
format:
@echo "Formatting..."
prettier --write .

Now:

Terminal window
make build # Builds
make test # Tests
make lint # Lints
make format # Formats
make help # Shows tasks

That’s it. Everyone knows what to do.

Task Dependencies

Tasks often depend on others. You need to lint before testing. Build before deploying.

.PHONY: help setup build test lint format deploy
setup:
npm install
build: setup
npm run build
test: lint
npm test
lint:
eslint .
format:
prettier --write .
deploy: build test
npm run deploy
help:
@echo "Available tasks: setup build test lint format deploy"

Now make deploy automatically:

  1. Runs build (which runs setup)
  2. Runs test (which runs lint)
  3. Runs deploy

No manual orchestration. No “remember to run X before Y.”

Variables and Conditionals

NODE_VERSION := 20
REGISTRY := ghcr.io
IMAGE := my-app:$(NODE_VERSION)
.PHONY: docker-build docker-push
docker-build:
docker build -t $(IMAGE) \
--build-arg NODE_VERSION=$(NODE_VERSION) \
.
docker-push: docker-build
docker push $(REGISTRY)/$(IMAGE)
# Conditional: only push if PUSH env var is set
ifdef PUSH
@echo "Pushing $(IMAGE)..."
endif

Usage:

Terminal window
make docker-build # Builds my-app:20
make docker-push # Builds and pushes
PUSH=1 make docker-push # Adds conditional logging

Real-World Example: Python Project

.PHONY: help venv install lint test format clean deploy
VENV := venv
PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
help:
@echo "Available tasks:"
@echo " make venv - Create virtual environment"
@echo " make install - Install dependencies"
@echo " make lint - Lint with flake8"
@echo " make test - Run pytest"
@echo " make format - Format with black"
@echo " make clean - Remove artifacts"
@echo " make deploy - Deploy to production"
venv:
python3 -m venv $(VENV)
install: venv
$(PIP) install -r requirements.txt
lint: install
$(PYTHON) -m flake8 src/
test: lint
$(PYTHON) -m pytest tests/
format:
$(PYTHON) -m black src/ tests/
clean:
rm -rf __pycache__ .pytest_cache .coverage
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
deploy: test
$(PYTHON) scripts/deploy.py

Developers just run:

Terminal window
make venv install test # Setup + lint + test
make deploy # Deploys (only after test passes)
make clean # Clears cache

No guessing. No scattered bash scripts.

Parallelizing Tasks

Tasks can run in parallel with -j:

.PHONY: test lint format
test:
npm test
lint:
eslint .
format:
prettier --write .
# This task depends on all three, but they can run parallel
check: test lint format
help:
@echo "make check - Run test, lint, and format in parallel"
Terminal window
make -j check # Runs test, lint, format simultaneously

On a 4-core machine, you save tons of time.

Conditional Execution

.PHONY: build deploy
build:
npm run build
deploy: build
@if [ "$(BRANCH)" = "main" ]; then \
npm run deploy:prod; \
else \
npm run deploy:staging; \
fi

Usage:

Terminal window
BRANCH=main make deploy # Deploys to prod
BRANCH=feature make deploy # Deploys to staging

Common Patterns

Database Migrations

migrate:
npm run knex migrate:latest
migrate-rollback:
npm run knex migrate:rollback
reset-db: migrate-rollback migrate # Rollback then re-run

Docker + Compose

up:
docker-compose up -d
down:
docker-compose down
logs:
docker-compose logs -f
shell:
docker-compose exec app sh
restart: down up

Git Hooks

setup-hooks:
@mkdir -p .git/hooks
@cp hooks/* .git/hooks/
@chmod +x .git/hooks/*
@echo "Git hooks installed"

Tips and Tricks

Silent Output (@ prefix)

lint:
@echo "Linting..." # Prints
eslint . # Prints command + output
vs.
lint:
@echo "Linting..." # Prints
@eslint . # Doesn't print the command, just output

Help from Comments

help: ## Show this message
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
sort | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
build: ## Compile the project
npm run build
test: ## Run unit tests
npm test

Now make help shows:

build Compile the project
test Run unit tests

.PHONY Explained

.PHONY: build
build:
npm run build

.PHONY tells make “build is not a file, always run it.” Without this, if a directory called build/ exists, make thinks it’s already done.

Always use .PHONY for your tasks.

Gotchas

Tabs matter. Recipes in Makefiles must start with a tab character, not spaces. Your editor might auto-convert tabs to spaces. Disable that:

In VS Code, add to .vscode/settings.json:

{
"[makefile]": {
"editor.insertSpaces": false
}
}

Variable expansion timing. $(VAR) expands immediately. $$(VAR) expands at runtime. Use $$ for shell variables:

iterate:
@for i in 1 2 3; do \
echo "Number $$i"; \
done

The Payoff

One make command replaces ten bash scripts. New developers run make help, see all tasks, never wonder “what do I run first?”

It’s simple. It’s portable. It’s been around since before most developers were born.

If you’re not using make, start tomorrow.

Your repo will thank you. Your team will thank you.

And your 2 AM self will definitely thank you when you can run make deploy instead of hunting for a script.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it may appear here.


Previous Post
Systemd Socket Activation: Start Services Only When Someone Actually Knocks
Next Post
Vault vs Infisical: Secrets Management for Teams Who've Learned the Hard Way

Related Posts