- Published on
Why I Like Using Makefiles to Manage Reusable Commands
developer tools- Authors

- Name
- Ndamulelo Nemakhavhani
- @ndamulelonemakh
As an engineer working across AI, Cloud Engineering, and Data Science projects, I have come to rely on tools that streamline workflows and reduce repetitive tasks. One such tool that has become a staple in my toolkit is the humble Makefile. In this post, I'll share why I love using Makefiles to manage reusable commands, drawing from my experience and the practical benefits they bring to complex projects.
What is a Makefile?
For the uninitiated, a Makefile is a configuration file used with the make utility, a build automation tool originally designed for compiling software. It defines a set of tasks (or rules) with dependencies and commands, allowing you to automate repetitive processes. While Makefiles are often associated with C/C++ projects, their utility extends far beyond, making them a versatile tool for any developer or data scientist.
Think of a Makefile as a script that organizes your project's commands into reusable, dependency-aware tasks. Instead of typing long, error-prone commands in the terminal repeatedly, you define them once in the Makefile and run them with a simple make <target>.
For example, consider a typical machine learning pipeline:
- Preprocess data:
python scripts/preprocess.py --input data/raw --output data/processed - Train model:
python scripts/train.py --data data/processed --model output/model - Evaluate:
python scripts/evaluate.py --model output/model --test data/test
With a Makefile, I can consolidate these into clean, reusable tasks:
preprocess:
python scripts/preprocess.py --input data/raw --output data/processed
train: preprocess
python scripts/train.py --data data/processed --model output/model
evaluate: train
python scripts/evaluate.py --model output/model --test data/test
Running make evaluate automatically ensures preprocess and train are executed in the correct order due to the dependency chain. This saves time and reduces errors from forgetting a step.
Why I Choose Makefiles Over Alternatives
1. Simplicity Meets Power
Unlike complex task runners or build systems, Makefiles strike the perfect balance between simplicity and functionality. The syntax is straightforward, yet powerful enough to handle sophisticated workflows. Compare this to npm scripts (limited to Node.js projects) or Python's invoke (requires additional dependencies)—Makefiles work everywhere with minimal overhead.
2. Dependency Management Done Right
One of the killer features of Makefiles is their dependency tracking. Each task can depend on other tasks or files, and make only reruns what's necessary. For instance, if my data/processed files haven't changed, make skips the preprocess step, making workflows efficient.
This timestamp-based dependency checking can save hours in large projects. When working with multi-gigabyte datasets or lengthy model training processes, avoiding unnecessary reruns is crucial for productivity.
3. Cross-Platform Consistency
As someone who works across different environments (local machines, cloud instances, or CI/CD pipelines), I need tools that ensure consistency. Makefiles are plain text, portable, and work on any system with make installed—which includes virtually every Unix-based system and Windows with WSL, MinGW, or chocolatey.
Whether I'm on my local Ubuntu machine or a cloud-based Jenkins pipeline, make venv ensures the same setup process, reducing environment-specific bugs.
Note for Windows users: While make isn't native to Windows, it's easily available through WSL2, Git Bash, or chocolatey (choco install make).
4. Readability and Maintainability
Makefiles are declarative and easy to read. Each task is a named rule with clear inputs (dependencies) and outputs (commands). This makes it simple for team members to understand and contribute to the project. Unlike shell scripts, which can become a tangle of conditionals and loops, Makefiles keep things modular and straightforward.
The self-documenting nature of well-written Makefiles means new team members can run make help (if you implement it) or simply read the file to understand available commands.
5. Extensibility for Any Project
Makefiles aren't limited to one domain. In Data Science, I use them to automate data pipelines. In Cloud Engineering, I use them to manage infrastructure scripts (e.g., Terraform or Kubernetes deployments). In AI projects, they streamline model training and deployment. For example:
deploy:
terraform apply -auto-approve
kubectl apply -f k8s/deployment.yaml
# Parallel execution example
test-all:
make -j4 test-unit test-integration test-e2e test-performance
This flexibility means I can adapt Makefiles to any project, from a small script to a full-fledged microservices architecture.
A Real-World Example
Let's look at a practical Makefile I use in a typical AI project:
# Project-wide variables
PYTHON := python3
VENV := .venv/bin/python
DATA_DIR := data
MODELS_DIR := models
# Setup virtual environment
venv:
$(PYTHON) -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -r requirements.txt
# Run tests
test: venv
$(VENV) -m pytest tests/ -v
# Generate processed data (file-based dependency)
$(DATA_DIR)/processed/features.csv: $(DATA_DIR)/raw/dataset.csv venv
$(VENV) scripts/preprocess.py --input $(DATA_DIR)/raw --output $(DATA_DIR)/processed
# Train model
$(MODELS_DIR)/model.pkl: $(DATA_DIR)/processed/features.csv
mkdir -p $(MODELS_DIR)
$(VENV) scripts/train.py --data $(DATA_DIR)/processed --output $(MODELS_DIR)/
# Convenience targets
preprocess: $(DATA_DIR)/processed/features.csv
train: $(MODELS_DIR)/model.pkl
# Clean up
clean:
rm -rf .venv $(DATA_DIR)/processed $(MODELS_DIR)/
find . -type f -name "*.pyc" -delete
find . -type d -name "__pycache__" -delete
# Help target
help:
@echo "Available targets:"
@echo " venv - Create virtual environment"
@echo " test - Run tests"
@echo " preprocess - Process raw data"
@echo " train - Train ML model"
@echo " clean - Clean generated files"
.PHONY: venv test preprocess train clean help
This Makefile demonstrates several advanced concepts:
- File-based dependencies:
$(DATA_DIR)/processed/features.csvonly rebuilds when the raw data changes - Directory creation:
mkdir -pensures output directories exist - Variable usage: Consistent paths and commands through variables
- Help system: Self-documenting with
make help
Running make train ensures the environment is set up, data is processed (only if needed), and the model is trained—all with one command.
Advanced Tips and Best Practices
Performance Optimization
- Use
make -j4for parallel execution of independent tasks - Leverage file timestamps for efficient rebuilds
- Use
$(wildcard *.py)for dynamic file lists
Debugging
- Use
make -nfor dry runs to see what would execute - Add
@echo "Starting task X..."for progress tracking - Use
make -dfor verbose dependency information
Team Collaboration
# Document prerequisites
check-deps:
@which python3 >/dev/null || (echo "Python3 required" && exit 1)
@which docker >/dev/null || (echo "Docker required" && exit 1)
setup: check-deps venv
@echo "Project setup complete!"
Common Pitfalls and Solutions
While Makefiles are powerful, they have some quirks:
| Issue | Solution |
|---|---|
| Tabs vs Spaces | Use tabs for indentation in rules. Configure your editor to show whitespace. |
| Complex Logic | Keep Makefiles simple. For complex scripting, call external shell or Python scripts. |
Missing .PHONY | Declare non-file targets as .PHONY to prevent conflicts with actual files. |
| Path Issues | Use variables for paths and test on different systems. |
When NOT to Use Makefiles
Makefiles aren't always the right choice:
- GUI-heavy workflows: Better suited for command-line operations
- Windows-first projects: Native Windows alternatives might be more appropriate
- Language-specific builds: Use language-native tools (e.g.,
cargofor Rust,go buildfor Go) - Complex conditional logic: Shell scripts or task runners might be clearer
Conclusion
Makefiles have become my go-to for managing reusable commands because they're simple, powerful, and versatile. They save me time, reduce errors, and keep my projects organized, whether I'm training an AI model, deploying cloud infrastructure, or preprocessing data. The combination of dependency management, cross-platform compatibility, and self-documenting nature makes them invaluable for any technical project.
By adopting Makefiles, you can bring the same clarity and efficiency to your workflows. The learning curve is minimal, but the productivity gains are substantial.
If you're intrigued, try writing a small Makefile for your next project. Start with a few tasks, like setting up an environment or running tests, and gradually expand as you see the benefits. As always, I'd love to hear your thoughts—drop a comment or subscribe to my newsletter at blog.ndamulelo.co.za for more tech insights!
Enjoyed this post? Subscribe to my newsletter for more AI, Cloud Engineering, and Data Science tips delivered straight to your inbox. Follow me on GitHub for more practical examples and code samples.
