Makefile is just a simple file but throughout my career, it bring a huge boost in the productivity, reusability and collaboration between colleagues. By combine a series of command under the makefile entries, people can easily figure out how to run project.

If you combine with a set of convention, it will help to structure your team project workflow. Also, in this post, I will share some of the common workflow and Makefile example that you can use right way with your existing project.

What is make and Makefile

GNU Make is a tool which controls the generation of executables and other non-source files of a program from the program's source files. - GNU make
Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the non-source files and how to compute it from other files. When you write a program, you should write a makefile for it, so that it is possible to use Make to build and install the program. - GNU make

These are the definition from the official GNU, but in short, make is a command that will execute based on the instruction inside the Makefile. So, we can write the command and its execution inside makefile and later we can just use it out of the box.

The benefit of Makefile

  • Make enables the end user to build and install your package without knowing the details of how that is done -- because these details are recorded in the makefile that you supply.
  • Make figures out automatically which files it needs to update, based on which source files have changed. It also automatically determines the proper order for updating files, in case one non-source file depends on another non-source file.
  • Make is not limited to any particular language. For each non-source file in the program, the makefile specifies the shell commands to compute it.
  • Make is not limited to building a package. You can also use Make to control installing or deinstalling a package, generate tags tables for it, or anything else you want to do often enough to make it worth while writing down how to do it.

We usually see make being used heavily with the compiler or building and compiling packages. I used them in my project, developing frontend, backend, devops and see its potential here, too. Basically anything relate invoking shell command, we can use make.

Writing your first Makefile

make already installed on unix operating system, if you are using windows, you will need to use WSL in order to run and use it.

First, create a file named Makefile in the working directory

$ touch Makefile

What we need to do is to define rule. A rule in Makefile tells Make how to execute a series of commands in order to build a target file from source files. It also specifies a list of dependencies of a target file.

Here is what a simple rule looks like:

target:   dependencies ...

Let write a simple rule to print "Hello world" to the shell

	echo "Hello"

To execute this rule, simple run make print-hello.

Okay, now let do some meaningful examples. I want you to jump into your current working project, create a Makefile, add rule to install dependencies.

For me, I working on a python project using fastAPI right now, and the Makefile look like this:

	pip install -r src/requirements.txt

	PYTHONPATH=./src:./lib uvicorn app.main:app --reload

Let talk about benefit of this:

  • First, it save the instruction inside each target, so we don't need to worry that one year from now, or several years from now, we might forget how to run this.
  • Second, we set it up once, and then we only need to remember the target name, or look at the Makefile to know how to run this. Imagine one year from now, could you still remember the unicorn command above?
  • Third, other member in team can use it out of the box.

To run the fastAPI project above, if this is the first time you run the project, you will need to install dependencies, and it can be done via make install-deps. After that, to run project, just make run.

It is simple, right, even if you are not a backend developer, you still know how to run this project by looking at its Makefile

My Makefile workflow

It depends on the type of the project, I usually have the same set of make targets to be configured in the Makefile

For Frontend project

Common actions in frontend project are: install dependencies, run dev server, build, run test, deploy. So my Makefile always has the aforementioned target, example as below:

	yarn install

	yarn dev

	yarn lint

build: lint
	yarn build

	yarn test

You might wonder, why do I need to wrap yarn command under the make target, why not run it directly, right?

Okay, I though about this too, when I first write it long time ago. And the short answer is:

  • Because you work on it now, so you might remember it, but says one year from now, you mostly forget how it run, and that might take some time to figure out what command it was used to run. So this create a concrete command that it is used to run and successfully run.
  • It build a habit of create and running project, if you do this for every projects or apply it into your team. You will save time when try to run something. Lint with make lint, test with make test, run with make run-dev without worry about what under the hood.
  • Sharing project between team and document better. Your team don't need to read docs, they just need to read make file. When you want team member to run something, just tell them to run make this or make that.

For Backend project

Likewise, when working on backend project, I also list out common actions into the Makefile: install project dependencies, run project, run other dependencies (database, redis, etc...), create migration, run test, build, etc...

This is an example in my current fastAPI project

PYTEST:= pytest --order-scope=module
CURRENT_TIMESTAMP:=`date +'%Y%m%d_%H:%M:%S'`

	pip install -r src/requirements.txt

	PYTHONPATH=./src:./lib uvicorn app.main:app --reload

	PYTHONPATH=./src:./lib celery -A app.worker worker -B -l info

	PYTHONPATH=./src:./lib celery -A app.worker flower

	cd src && PYTHONPATH=. alembic revision -m "${CURRENT_TIMESTAMP} migration"

	cd src && PYTHONPATH=. alembic revision --autogenerate -m "${CURRENT_TIMESTAMP} migration"

	cd src && PYTHONPATH=. alembic upgrade head


	$(MAKE) test-module MODULE="sample"

	$(MAKE) test-module MODULE="facebook"

Let's say you have no idea about my current project. If you look at the above Makefile, do you know how to run my project?

For Devops project

The same will be done with devops related commands, I use docker for devops so my common tasks are: build image, rebuild image, docker up and down on each environment, logs, backup database, etc...

The following are my Makefile for DevOps related things:

CURRENT_TIME:=`date +'%y.%m.%d_%H%M%S'`

	# refactor into bash script
	mkdir -p ${BUILD_DIR}
	rm -rf ${BUILD_DIR}/*
	rsync -avL --progress ./src ${BUILD_DIR}/ --exclude ${BUILD_DIR} --exclude ${GIT_DIR} --exclude .sandbox
	rsync -avL --progress ./lib ${BUILD_DIR}/ --exclude ${BUILD_DIR} --exclude ${GIT_DIR} --exclude .sandbox
	rsync -avL --progress image/ ${BUILD_DIR}

	echo "Building image"
	cd ${BUILD_DIR} && docker build -t ${IMAGE_NAME} .

rebuild: prepare-build build

	cd env/${TARGET_ENV} && docker-compose up -d

	cd env/${TARGET_ENV} && docker-compose down

	$(MAKE) dc-up TARGET_ENV="local"

	$(MAKE) dc-down TARGET_ENV="local"

	$(MAKE) dc-up TARGET_ENV="dev"

	$(MAKE) dc-down TARGET_ENV="dev"

	cd env/dev && $(MAKE) dc-logs

deploy-staging: rebuild
	$(MAKE) dc-up TARGET_ENV="staging"

deploy-prod: rebuild
	$(MAKE) dc-up TARGET_ENV="prod"


You now have a glance view at Makefile and how it is used via some of my examples. I suggest you to employ it to your current project and you will see its benefit several months from now. Happy making....