Streamline your projects using Makefile
make is one of the tools that we use heavily for streamlining tasks on our projects. It has proven to be helpful specifically for streamlining the development process, repeating mundane tasks with custom CLI like subcommands and mainly onboarding new team members.
With a set of rules in Makefile, you can get up and running in no time, keeping the process sane and saving time and effort for everyone in the team. We’ll be going through the basics to some interesting stuffs we can do with Makefile.
There are two pieces to this equation, one is the
make CLI tool and the next is the Makefile . The basics is
make reads the rules from the Makefile and executes them. What I will be showing today is just a small part of what
make is capable of.
If you have worked with
YAML files before then you will feel right at home writing Makefiles.
Anatomy of Rules
Makefile consists of rules with the anatomy of:
- target: target can be an executable, object or just a name for an action that we want to carry out. We will be using targets purely with the placeholder name for the rule. Be mindful about the name, as it should resonate with the action we want to perform with no confusion whatsoever.
- dependencies: Dependencies are the rules that needs to be executed, in order for the current rule to work.
- recipe: recipe is the meat of the
Makefile, it is the action that we want to perform with our
targetname. Make sure to put a
tabcharacter at the start of every recipe line (just like YAML). You can also replace the
tabcharacter with anything you want using the
Next we will be looking into some examples on how to make use of
Makefile. These examples will be based on setting up development environments.
A basic rule where you just want to put some alias is straight forward.
Let’s say you have a python project and you want to hand it over to a new team member. How do you streamline the setup process. Maybe it can look something like this.
#is for comments.
@symbol is to disable printing the recipe to stdout.
Test without the
@symbol at the beginning of the recipe.
:=is the expansion operator which prevents using subsequent value with the same variable name.
You can do something similar with your existing project.
Now to get up and running, all you have to do is:
$ make venv
$ . venv/bin/activate
$ make setup
$ make format
# and so on
Rules with Dependencies
Taking the reference from the example above, suppose we want to print out the output of
check target every time we run the
format target. So how do we create that dependency? It’s plain simple, we just have to update the
format target to look something like this:
format: check # run the formatter on files.
We have added the dependency of
check to the right of the target, just like showcased on the anatomy Anatomy of Rules section.
We can also define variables if we have some piece of command for repeated use. For this example we will be taking the reference of the
Django management command.
Variables are normally written with all caps and uses
:= to assign variable name to a value. Variables can be accessed using either
Also your SHELL environment variables are converted in to Makefile environment variables, so you can directly make use of them while creating your rules.
In our shell we can export an environment variable called
$ export INFO="Run make help to show all the available rules."
And now in the Makefile we can refer to it as any variable.
info: # show project info
If you just run
make on your command line nothing is going to happen. But we can change that by using the
.DEFAULTGOAL special variable and assigning the target we want to run by default.
.DEFAULT_GOAL := run
Now, next time you run
make it is going to run the
Django server by default.
Now we have bunch of targets on our
Makefile and we also called this combo as a custom mini CLI app. Wouldn’t it be great, if we could have a help command similar to a real CLI app? Say no more, thanks to the blog from Victoria Drake we have the script to do so.
Just create a
help target and assign it as a
.DEFAULT_GOAL. With this, all the comments we have been writing on our target gets converted into a nice help message.
Include other Makefiles
We can separate out Makefiles based on the tasks they perform and
include them into the main
Makefile. We usually have separate
Makefile managed for environment variables, Docker and Kubernetes. This offloads all the tasks from project set up to Deployment to the Makefile.
I will show a brief example of each of the file just to give an example:
Note: Since make runs each recipe on a new instance of the shell, we can lazy evaluate the variables using
?=meaning, they are initialized only when referenced for a single shell instance.
Makefile Root makefile composed of other Makefiles.
APP_ROOT := $(PWD)
TMP_PATH := $(APP_ROOT)/.tmp
VENV_PATH := $(APP_ROOT)/.venv
export ENVIRONMENT_OVERRIDE_PATH ?= $(APP_ROOT)/env/Makefile.override
Environment Variables Makefile.override Makefile containing just the essential environment variables.
STAGE ?= <stage>
SERVICE_NAME ?= <service-name>
AKS_RESOURCE_GROUP ?= <resource-group>
AKS_CLUSTER_NAME ?= <cluster-name>
REGISTRY_URL ?= <registry-url>
AZ_ACR_REPO_NAME ?= <repo-name>
Docker Makefile.docker Makefile containing docker rules.
export GIT_COMMIT ?= $(shell cut -c-8 <<< `git rev-parse HEAD`)
export BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
export DOCKER_BUILD_FLAGS ?= --no-cache
export DOCKER_BUILD_PATH ?= $(APP_ROOT)
export DOCKER_FILE ?= $(APP_ROOT)/Dockerfile
export TARGET_IMAGE ?= $(REGISTRY_URL)/$(AZ_ACR_REPO_NAME)/$(SERVICE_NAME)
export TARGET_IMAGE_LATEST ?= $(TARGET_IMAGE):$(BRANCH)-$(GIT_COMMIT)
az acr login --name $(AZ_ACR_REPO_NAME)
docker build $(DOCKER_BUILD_FLAGS) -t $(SERVICE_NAME) -f $(DOCKER_FILE) $(DOCKER_BUILD_PATH)
docker tag $(SERVICE_NAME) $(TARGET_IMAGE_LATEST)
docker push $(TARGET_IMAGE_LATEST)
Makefile.k8s Makefile containing rules for Kubernetes.
export OVERLAY_PATH ?= $(APP_ROOT)/k8s/overlays/$(STAGE)/
cd $(OVERLAY_PATH) && kustomize edit set image api=$(1) && \
kustomize build $(OVERLAY_PATH)
kustomize build $(OVERLAY_PATH) | kubectl apply -f -
az aks get-credentials --resource-group $(AKS_RESOURCE_GROUP) --name $(AKS_CLUSTER_NAME)
kubectl delete namespace $(STAGE)-api
Now we have orchestrated all these Makefiles, it is easier to keep track of all the rules and makes working with Makefiles sane, if you are doing a lot with it.
So with the use of
Makefile we can streamline a lot of redundant tasks in our projects without having to remember overwhelmingly long and varying commands. It increases the productivity of the whole team; with easier project setup and redundant tasks outsourced to the
Makefile with intuitive target names, leaving the devs to focus on more serious tasks at hand.