Makefile and make¶
makeis a build automation tool driven by a Makefile. In data/AI projects it is often less about compiling C and more about giving teammates a small, consistent set of commands: install, lint, test, train, clean, deploy.
A Makefile is not magic. It is a list of targets, dependencies, and shell commands, with a little syntax for variables and pattern rules. When kept small and readable, it is very effective.
Quick Reference¶
| Pattern | Meaning |
|---|---|
target: |
a rule name |
target: dep1 dep2 |
run this target if deps are newer |
\tcommand |
recipe line, must start with a tab |
.PHONY: target |
target is not a real file |
$(VAR) or ${VAR} |
variable reference |
VAR = value |
variable assignment |
make target |
run a specific target |
make -n target |
dry run |
make -j N |
run with N parallel jobs |
A tab in a recipe is required. Spaces do not work there, which is one of the most common first-time Makefile errors.
Smallest Useful Makefile¶
.PHONY: install test clean
install:
pip install -r requirements.txt
test:
pytest
clean:
rm -rf .pytest_cache
find . -type d -name "__pycache__" -exec rm -rf {} +
Usage:
The point is not to impress anyone with cleverness. The point is that a new contributor can run the same few commands without reading tribal knowledge.
Targets¶
A target is the name of something to build or do.
Targets can depend on other targets:
Before running run, make checks whether install and test need to be re-run, based on file dependencies and timestamps.
Phony Targets¶
If a target does not correspond to an actual file, mark it .PHONY.
This matters because if a file named test exists, make might think the target is already up to date and do nothing.
Common phony targets:
- test
- lint
- clean
- install
- dev
- run
Recipes¶
A recipe is one or more shell commands.
Each line in a recipe runs in its own shell by default, unless you use line continuations.
A backslash-newline continues a single shell command across multiple Makefile lines.
Variables¶
Variables reduce repetition.
Variable assignment forms:
A = value # recursive expansion
A := value # immediate expansion
A ?= value # set only if not already set
A += value # append
For most small project Makefiles, = or := is enough. The distinction matters more in larger build systems.
Command-Line Variables¶
You can override variables from the command line:
Makefile:
This is a clean way to expose a few knobs without writing many similar targets.
A Practical Data Science Makefile¶
SHELL := /bin/bash
PYTHON := python
PIP := pip
TEST := pytest
LINT := ruff
.PHONY: install deps test lint format clean run dev
install:
$(PIP) install -r requirements.txt
deps:
$(PIP) install -r requirements-dev.txt
test:
$(TEST)
lint:
$(LINT) check .
format:
$(LINT) format .
clean:
rm -rf .pytest_cache
rm -rf .ruff_cache
find . -type d -name "__pycache__" -exec rm -rf {} +
run:
$(PYTHON) main.py
dev:
$(PYTHON) -m uvicorn main:app --reload
This is the kind of Makefile that helps a project feel organized without becoming a build-system project in itself.
Pattern Rules¶
Pattern rules let one rule apply to many files.
Here:
- % matches a stem
- $< is the first dependency
- $@ is the target
This is more common for file-to-file workflows than for general project commands.
Automatic Variables¶
Useful ones:
- $@ — target file
- $< — first prerequisite
- $^ — all prerequisites
- $(@D) / $(@F) — directory or file part of target
These are most helpful in pattern rules and file-based rules.
Silencing and Debugging¶
Or per-command:
The @ prefix hides the command itself, showing only its output.
For debugging:
make -n test # show commands without running them
make --debug # more verbose make behavior
make -p # print database of rules and variables
Common Patterns¶
One target per common task¶
Group related tasks under a higher-level target¶
Keep local overrides out of the shared file¶
The dash avoids an error if the file is missing.
Make vs Shell Scripts¶
A Makefile is good when: - You want named tasks with dependencies. - You want a small project-level interface. - Multiple people should run the same commands.
A shell script is good when: - The logic is mostly sequential. - You need conditionals and loops that are easier in bash/Python. - The command is a one-off procedure.
Use the tool that matches the job. Many projects use both.
Common Mistakes¶
- Using spaces instead of a tab in recipes.
- Forgetting
.PHONYfor non-file targets. - Making the Makefile more clever than the team can read.
- Relying on implicit rules when explicit ones are clearer.
- Hardcoding absolute paths where a variable would be better.
- Writing a Makefile that only works on one contributor's machine.
- Using make for tasks that are really scripting problems.
- Recreating the same environment-dependent logic in every recipe instead of centralizing it.
Make and Environment Variables¶
Makefiles do not replace environment configuration. Often the best pattern is: - Makefile runs the command. - Environment variables or config files carry deployment-specific values. - The Makefile stays generic.
Example:
Then:
This keeps the Makefile from becoming a secret store or a deployment script.
Related Cheat Sheets¶
- python-dotenv — environment-based configuration that Make can rely on
- curl — calling services from make-driven workflows
- ssh — remote execution where make targets may trigger deployment or sync
- yaml — config files often consumed by make-driven pipelines
- toml — tool configuration that Make may install or validate
Official Documentation¶
- GNU make manual, or the make documentation for your system
- Your project's conventions, since many teams extend make in their own way
A good Makefile is the one your teammates can understand in a minute and use without asking.
Last Updated¶
2026-09-06 — Initial version.