A case for using a Makefile in your project
Jan 16, 2024 · 5 min readThis post is going to present a case for using a Makefile
in your project. This will be the top-level task runner for your project. You can use make
to run all of your project tasks such as building, linting, testing, and deploying your project. It doesn’t matter what language your project is written in.
What is a Makefile?
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.
Straight out of the gate, this doesn’t necessarily seem like something we could make use of, does it? However, at its core, a Makefile
allows you to define a bunch of instructions together. Each set of instructions can then be given a name, and this becomes the “target”. We use a tool called make
to run one of the targets defined in a Makefile
.
If you’re working on JavaScript projects, for example, you may be using tools like npm
, yarn
, or pnpm
to run your tasks. A Makefile
is a very similar concept, and I’d recommend you have both.
The reason for this recommendation is abstractions.
Abstraction
We can use make
to define an abstraction layer between your internal project decisions, and the outside world (a contract). The outside world could include what gets run in your CI/CD pipeline, or what engineers run in their terminals to build and test the project. By defining and making use of a Makefile
, you can change your internal project workings, but leave the contract intact.
A concrete example for a JavaScript project could be switching your project from using npm
to pnpm
. If you’re using tasks within your package.json
you have probably exposed npm run ...
tasks in your CI/CD pipeline, your README, and your engineers will be running those commands. If you change your tool to pnpm
you suddenly need to change a lot more code to bed this in. Your engineers also need to change their muscle memory to use the new version of the command. However, if you had a Makefile
as a facade for your project, your make build
task would stay the same to the outside world, but the internals of what make build
does would change to use pnpm
.
Another example could be for a project written in Go, where there really isn’t a task runner, and you may need to remember a rather complicated command to build your Go binaries. By having a Makefile
you can document all the key commands in one place, and reference them.
Example
Let’s take a look at a single make
target from this repo, which is a Go project.
.PHONY: test
test: ## Run the unit tests
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
Above shows a single make
target called test
. We can execute this target by running make test
.
❯ make test
go test ./... -coverprofile=coverage.out
ok github.com/benmatselby/walter/cmd 0.504s coverage: 18.2% of statements
ok github.com/benmatselby/walter/cmd/board 0.243s coverage: 75.7% of statements
ok github.com/benmatselby/walter/cmd/search 0.844s coverage: 90.6% of statements
ok github.com/benmatselby/walter/cmd/sprint 1.052s coverage: 85.5% of statements
go tool cover -func=coverage.out
github.com/benmatselby/walter/cmd/board/board.go:9: NewBoardCommand100.0%
github.com/benmatselby/walter/cmd/board/issues.go:15: NewIssueCommand33.3%
github.com/benmatselby/walter/cmd/board/issues.go:33: ListIssues 93.8%
github.com/benmatselby/walter/cmd/board/list.go:13: NewListCommand 33.3%
github.com/benmatselby/walter/cmd/board/list.go:30: DisplayBoards 100.0%
As you can see, we have output from the two commands defined in the test
target. This is the same command engineers use when testing the software on their machines, to what is run in the CI/CD platform for quality checks.
If we wanted to change the options to go test
, we could do this in one place, and it not impact everywhere else. It would be a trivial encapsulated change. We could also switch out go test
for another test runner, and it wouldn’t impact anything else either.
Automation
Taking this further, if you can define a standard Makefile
for all of your projects with the same target names, you can probably automate more things at a higher level. For example, you could do the following:
- Clone your all git repos for your solution.
- Iterate over each one and install their dependencies (
make install
). - Run the unit tests to make sure they work (
make test
). - Run each application (
make run
).
Perhaps this could be part of an automated system to spin up your entire project stack on your local machine. It wouldn’t matter if some of those projects were Go, React, Python, PHP for example, as the public contract (The Makefile
) is the same.
Summary
As with most things, you have to decide if adding an extra abstraction layer is worth it. I’ve been using a Makefile
in my projects since about 2013 when we transitioned a PHP project away from ant
/phing
. These projects have ranged in the language used, but what is consistent is the name of the Make targets. I can pretty much clone any personal repo, and remember all the make
commands irrelevant if the project is PHP, Python, JavaScript, Go etc.
As outlined in this post (2018) using make
to define how to run and test your project has gone into a full ecosystem to build development environments for engineers.