| name | makefile-build |
| description | Write Makefiles for any project type. Use when setting up build automation, defining multi-target builds, managing dependencies between tasks, creating project task runners, or using Make for non-C projects (Go, Python, Docker, Node.js). Also covers Just and Task as modern alternatives. |
| metadata | {"clawdbot":{"emoji":"🔨","requires":{"anyBins":["make","just","task"]},"os":["linux","darwin","win32"]}} |
Makefile & Build
Write Makefiles for project automation across any language. Covers targets, dependencies, variables, pattern rules, phony targets, and using Make for Go, Python, Docker, and Node.js projects. Includes Just and Task as modern alternatives.
When to Use
- Automating build, test, lint, deploy commands
- Defining dependencies between tasks (build before test)
- Creating a project-level task runner (consistent across team)
- Replacing long CLI commands with short memorable targets
- Managing multi-step build processes
- Any project that needs a
make build && make test && make deploy workflow
Makefile Basics
Structure
build: src/main.go
go build -o bin/app src/main.go
test: build
go test ./...
clean:
rm -rf bin/
Variables
CC = gcc
CFLAGS = -Wall -O2
FILES = $(wildcard src/*.go)
VERSION := $(shell git describe --tags --always)
PORT ?= 8080
build:
$(CC) $(CFLAGS) -o app main.c
@echo "Version: $(VERSION)"
Automatic variables
bin/app: src/main.go src/util.go
go build -o $@ $^
%.o: %.c
$(CC) -c -o $@ $<
Phony targets (not files)
.PHONY: build test clean lint fmt help
build:
go build -o bin/app ./cmd/app
test:
go test ./...
clean:
rm -rf bin/ dist/
help:
@grep -E '^[a-zA-Z_-]+:.*?
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
Self-documenting Makefile
.DEFAULT_GOAL := help
build: ## Build the application
go build -o bin/app ./cmd/app
test: ## Run all tests
go test -v ./...
lint: ## Run linters
golangci-lint run
clean: ## Remove build artifacts
rm -rf bin/ dist/
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
Language-Specific Makefiles
Go
BINARY_NAME := myapp
VERSION := $(shell git describe --tags --always --dirty)
LDFLAGS := -ldflags "-X main.version=$(VERSION)"
GOFILES := $(shell find . -name '*.go' -not -path './vendor/*')
.PHONY: all build test lint clean run
all: lint test build
build: ## Build binary
CGO_ENABLED=0 go build $(LDFLAGS) -o bin/$(BINARY_NAME) ./cmd/$(BINARY_NAME)
test: ## Run tests
go test -race -coverprofile=coverage.out ./...
test-coverage: test ## Show coverage report
go tool cover -html=coverage.out
lint: ## Run linters
golangci-lint run ./...
fmt: ## Format code
gofmt -w $(GOFILES)
run: build ## Build and run
./bin/$(BINARY_NAME)
clean: ## Clean build artifacts
rm -rf bin/ coverage.out
build-linux: ## Build for Linux
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-amd64 ./cmd/$(BINARY_NAME)
build-all: ## Build for all platforms
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-linux-amd64 ./cmd/$(BINARY_NAME)
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-darwin-arm64 ./cmd/$(BINARY_NAME)
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY_NAME)-windows-amd64.exe ./cmd/$(BINARY_NAME)
Python
PYTHON := python3
VENV := .venv
BIN := $(VENV)/bin
.PHONY: all install test lint fmt clean run
all: install lint test
$(VENV)/bin/activate:
$(PYTHON) -m venv $(VENV)
$(BIN)/pip install --upgrade pip
install: $(VENV)/bin/activate ## Install dependencies
$(BIN)/pip install -r requirements.txt
$(BIN)/pip install -r requirements-dev.txt
test: ## Run tests
$(BIN)/pytest -v --cov=src --cov-report=term-missing
lint: ## Run linters
$(BIN)/ruff check src/ tests/
$(BIN)/mypy src/
fmt: ## Format code
$(BIN)/ruff format src/ tests/
run: ## Run application
$(BIN)/python -m src.main
clean: ## Remove venv and caches
rm -rf $(VENV) __pycache__ .pytest_cache .mypy_cache .ruff_cache
find . -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
Node.js / TypeScript
.PHONY: all install build test lint clean dev
all: install lint test build
node_modules: package.json
npm install
@touch node_modules
install: node_modules ## Install dependencies
build: node_modules ## Build TypeScript
npx tsc
test: node_modules ## Run tests
npx vitest run
test-watch: node_modules ## Run tests in watch mode
npx vitest
lint: node_modules ## Lint code
npx eslint src/ --ext .ts,.tsx
npx tsc --noEmit
fmt: node_modules ## Format code
npx prettier --write 'src/**/*.{ts,tsx}'
dev: node_modules ## Run in development mode
npx tsx watch src/index.ts
clean: ## Clean build artifacts
rm -rf dist/ node_modules/.cache
Docker
IMAGE_NAME := myapp
VERSION := $(shell git describe --tags --always)
REGISTRY := ghcr.io/myorg
.PHONY: build push run stop clean
build: ## Build Docker image
docker build -t $(IMAGE_NAME):$(VERSION) -t $(IMAGE_NAME):latest .
push: build ## Push to registry
docker tag $(IMAGE_NAME):$(VERSION) $(REGISTRY)/$(IMAGE_NAME):$(VERSION)
docker tag $(IMAGE_NAME):latest $(REGISTRY)/$(IMAGE_NAME):latest
docker push $(REGISTRY)/$(IMAGE_NAME):$(VERSION)
docker push $(REGISTRY)/$(IMAGE_NAME):latest
run: ## Run container
docker run --rm -p 8080:8080 --name $(IMAGE_NAME) $(IMAGE_NAME):latest
stop: ## Stop container
docker stop $(IMAGE_NAME) 2>/dev/null || true
clean: ## Remove images
docker rmi $(IMAGE_NAME):$(VERSION) $(IMAGE_NAME):latest 2>/dev/null || true
compose-up: ## Start with docker compose
docker compose up -d --build
compose-down: ## Stop compose
docker compose down
compose-logs: ## Follow compose logs
docker compose logs -f
Advanced Patterns
Conditional logic
UNAME := $(shell uname -s)
ifeq ($(UNAME),Darwin)
SED := sed -i ''
else
SED := sed -i
endif
ENV ?= development
ifeq ($(ENV),production)
CFLAGS += -O2
LDFLAGS += -s -w
else
CFLAGS += -g -O0
endif
HAS_DOCKER := $(shell command -v docker 2>/dev/null)
docker-build:
ifndef HAS_DOCKER
$(error "docker is not installed")
endif
docker build -t myapp .
Multi-directory builds
SERVICES := api worker scheduler
.PHONY: build-all test-all $(SERVICES)
build-all: $(SERVICES)
$(SERVICES):
$(MAKE) -C services/$@ build
test-all:
@for svc in $(SERVICES); do \
echo "Testing $$svc..."; \
$(MAKE) -C services/$$svc test || exit 1; \
done
Include other Makefiles
include mk/docker.mk
include mk/test.mk
include mk/deploy.mk
-include .env.mk
Silent execution and output control
install:
@echo "Installing dependencies..."
@npm install
.SILENT: help clean
MAKEFLAGS += --no-print-directory
Just (Modern Alternative)
Justfile syntax
# justfile — simpler than Make, no TAB requirement
# Set shell
set shell := ["bash", "-euo", "pipefail", "-c"]
# Variables
version := `git describe --tags --always`
default_port := "8080"
# Default recipe (first one)
default: lint test build
# Recipes
build: ## Build the application
go build -ldflags "-X main.version={{version}}" -o bin/app ./cmd/app
test: ## Run tests
go test -race ./...
lint: ## Run linters
golangci-lint run
run port=default_port: build ## Run with optional port
./bin/app --port {{port}}
clean: ## Clean artifacts
rm -rf bin/ dist/
# Recipes with dependencies
deploy: build test
./scripts/deploy.sh
# OS-specific
[linux]
install-deps:
sudo apt install -y build-essential
[macos]
install-deps:
brew install go golangci-lint
# List recipes
help:
@just --list
just
just build
just run 9090
just --list
Task (Go Task Runner)
Taskfile.yml
version: '3'
vars:
VERSION:
sh: git describe --tags --always
BINARY: myapp
tasks:
default:
deps: [lint, test, build]
build:
desc: Build the application
cmds:
- go build -ldflags "-X main.version={{.VERSION}}" -o bin/{{.BINARY}} ./cmd/{{.BINARY}}
sources:
- ./**/*.go
generates:
- bin/{{.BINARY}}
test:
desc: Run tests
cmds:
- go test -race ./...
lint:
desc: Run linters
cmds:
- golangci-lint run
[]
{{}}
{{}}
task
task build
task --list
Make vs Just vs Task
| Feature | Make | Just | Task |
|---|
| Config format | Makefile (TAB-sensitive) | justfile | Taskfile.yml |
| Dependencies | File-based + phony | Recipe-based | Task-based |
| File change detection | Built-in | No | sources/generates |
| Variables | Yes (complex) | Yes (simple) | Yes (YAML) |
| Cross-platform | Needs make installed | Single binary | Single binary |
| Learning curve | High | Low | Low |
| Best for | C/C++ builds, complex deps | Task runner replacement | YAML-native projects |
Tips
- The number one Makefile bug: using spaces instead of tabs for indentation. Make requires literal TAB characters in recipes.
.PHONY every target that isn't a real file. Without it, make clean won't run if a file named clean exists.
- Use
@ prefix to suppress command echo for cleaner output: @echo "Building..." prints only "Building...", not the echo command itself.
- The self-documenting
help target (with ## comments) is worth adding to every Makefile. make help becomes the project's command reference.
- Make is overkill for simple task running. If you just want named commands, Just or Task are simpler and don't have the TAB footgun.
- Use
?= for variables users might want to override: PORT ?= 8080 lets PORT=9090 make run work.
- For polyglot projects (Go + Python + Docker), a Makefile at the root that delegates to language-specific tools is a clean pattern.
- Make's file-based dependency tracking is genuinely powerful for build systems. If your project compiles files, Make's
target: prerequisites model avoids unnecessary rebuilds.