A case for using a Makefile in your project
Tags: tools, productivity
Published: 2024-01-17 20:12:23

This 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.

[Source](https://www.gnu.org/software/make/)

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](https://github.com/benmatselby/walter/blob/main/Makefile), which is a Go project.

```makefile
.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`.

```shell
❯ 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](https://github.com/benmatselby/walter/blob/main/.github/workflows/go.yml#L36) 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)](https://benmatselby.dev/post/development-environments/) using `make` to define how to run and test your project has gone into a full ecosystem to build development environments for engineers.

---

### See also

- [Development environments](https://benmatselby.dev/post/development-environments/)
- [Automating the startup of a dev workflow](https://benmatselby.dev/post/2024/automating-the-startup-of-a-dev-workflow/)
- [Setting up a VS Code Dev Container](https://benmatselby.dev/post/vscode-dev-containers/)