I always thought Makefile was exclusive to C++ projects, and seeing this file made me quite overwhelmed, as it inevitably requires executing a bunch of make commands to wait for compilation and linking.
However, I recently learned that the make command isn't as mysterious as it seems; on the contrary, it can effectively organize various commands, environment variables, shell commands, etc., that will be used in development projects.
In addition, one major benefit of Makefile is that it can configure some automated pre-steps, such as pip install for Python projects, npm install for Node projects, and so on.
Finally, Makefile can also depend on local files; if a file hasn't been updated, the corresponding tasks won't be executed, somewhat similar to local CI.
Next, let's get acquainted with the simple writing and examples of Makefile.
Basic Syntax of Makefile#
.PHONY#
Since the original intention of Makefile is to monitor whether local files have been updated and then trigger a series of actions, pure shell command execution needs to use .PHONY (phony) to achieve this. For example:
dev:
@echo "Hello, world!"
.PHONY: dev
Dependencies#
Makefile operates in steps, and each step will have its prerequisite tasks. The general syntax is:
target1 [target2 ...]: [pre-req1 pre-req2 pre-req3 ...]
[recipes
...]
A simple example would be if we want to execute a local preview of a next.js project, its prerequisite must be that the corresponding dependencies are already installed, so we can write:
dev: node_modules ## Start local service
@./node_modules/.bin/next dev
.PHONY: dev
node_modules: package.json
@yarn install
This forms a sequence of dependent execution:
- Executing make dev will check the prerequisite node_modules (note that this is a step name).
- The node_modules step depends on package.json, meaning it will first execute yarn install, and only if package.json is updated will it execute again.
From the above simple example, we can see that Makefile allows us to manage some common development steps clearly and orderly, avoiding "tragedies" such as modifying package.json but forgetting to execute yarn install.
Help Documentation#
In the above example, comments can be written using ##
.
At this point, if we add the following segment to the Makefile:
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-12s\033[m %s\n", $$1, $$2}'
@echo ""
.PHONY: help
It can automatically generate comments, meaning that entering make help will automatically print the help documentation. Isn't that amazing? hh
Simple Example: Using Makefile to Manage Your Hugo Blog#
Hugo has a few simple commands for managing blogs, such as starting the server, generating static site files, etc. We can put them all in a Makefile:
all: help ## Default opens help documentation
serve: ## Start server
@echo "Starting Hugo server..."
hugo server
build: ## Build site
@echo "Building the site..."
hugo
clean: ## Clean generated files
@echo "Cleaning up the public directory..."
rm -rf public/
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"
@grep -E '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-12s\033[m %s\n", $$1, $$2}'
@echo ""
.PHONY: all serve build clean help
The default command all
simply displays the help documentation, and the generation of help documentation can be automatically completed by writing clear comments. Now, entering the make command yields the following effect:
$:~/develop$ make
Specify a command. The choices are:
all Default opens help documentation
serve Start server
build Build site
clean Clean generated files
help Show this help
Of course, this is just the simplest demonstration; you can also add corresponding dependency files and integrate operations like new post as needed.
Step by Step Using Makefile to Manage Python Projects#
Simple Version#
First, let's assume our Python project entry is app.py, so our final target is:
run: ## Remember to add comments, which can automatically generate help documentation
python app.py
.PHONY run ## Since there are no dependency files, we need to use .PHONY
Next, Python certainly has prerequisite dependencies that need to be installed, which are generally listed in requirements.txt, so we have:
setup: requirements.txt
pip install -r requirements.txt
Of course, we would generally also add a clean command, so we have:
clean:
rm -rf __pycache__
Advanced Version#
Since everyone's local Python environment variables are different, this can also be solved using Docker, but Python itself provides a more lightweight solution called venv, so we can:
python3 -m venv venv
-m venv means using the venv command to create a virtual environment located in the venv directory.
At this point, installing dependencies can use:
./venv/bin/pip install -r requirements.txt
Because pip is located in the ./venv/bin/ directory.
So we get the Makefile:
venv/bin/activate: requirements.txt
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
However, writing ./venv/bin/
every time can be cumbersome, so we can simplify it using a variable.
That is, declare a variable VENV
with a default value of the venv
directory, but you can also modify it through command-line arguments.
The way to reference the variable is $(VENV)
, which can be understood as a simple string replacement. Thus, the Makefile can be modified to:
VENV = venv
PYTHON = $(VENV)/bin/python3
PIP = $(VENV)/bin/pip
run: $(VENV)/bin/activate
$(PYTHON) app.py
.PHONY run
$(VENV)/bin/activate: requirements.txt
python3 -m venv $(VENV)
$(PIP) install -r requirements.txt
clean:
rm -rf __pycache__
rm -rf $(VENV)
.PHONY clean
Simple Summary#
With GPT, Makefile can completely be handled by GPT. One can have a simple understanding of its syntax and be able to comprehend it, so I only listed the simplest syntax and corresponding examples.
I must say that with AI, such tasks no longer require developers to worry. From another perspective, what used to seem troublesome and not worth understanding often has many unexpected benefits.
Specifically regarding Makefile, it can be treated as a series of command encapsulations and steps that are cumbersome to record manually, and it can even be used as a README file.