Makefiles: a standard for project tasks
Tags: cli, programming
Published: 2023-05-13 16:57:20

I've been using Makefiles to create a standard CLI interface for all my projects, no matter what programming language and/or frameworks I'm using.

[Makefiles](https://en.wikipedia.org/wiki/Make_(software)#Makefiles) are configuration files for [GNU Make](https://www.gnu.org/software/make/) that define `make` tasks for a project (eg.: `build`, `test`, etc.).

I have projects in Bash, Elixir, Python, JavaScript, etc., and each of these requires different commands to build, test, run, etc., so I've been using Makefiles to standardize how I run these tasks.

Below is a basic `Makefile` template I've been using:

```
.PHONY: build run test

build:    
    # here goes the command(s) to build the project

run:
    # here goes the command(s) to run the project

test:
    # here goes the command(s) to test the project
```

If your project deals with databases, no matter which one (Postgres, MySQL, SQLite, etc.), you might want to include these tasks too:

```
.PHONY: db.setup db.reset db.create db.migrate db.seed db.drop

db.setup:
    make db.create \
    && make db.migrate \
    && make db.seed

db.reset:
    make db.drop \
    && make db.setup

db.create:
    # here goes the command(s) to create the database

db.migrate:
    # here goes the command(s) to migrate the database

db.seed:
    # here goes the command(s) to seed the database

db.drop:
    # here goes the command(s) to drop the database
```

This way your projects can have a "standard CLI interface" for common tasks like building, testing, running, or common database operations like creating and dropping, migrating the schema, etc.

## A bit of historical context from GNU Make

Historically speaking, `make` was used to build files, which often meant compiling source code. Each task was actually a group of commands needed to build a given file if it wasn't present. If the file were in place, the commands wouldn't be executed.

Imagine a JavaScript project that has the following files:

- `api-sdk.js` (functions to use the back-end APIs)
- `utils.js` (general utility functions)
- `app.js` (the main web app code: routes, pages, etc.)

In webdev, it's a common practice to bundle JS files before deploying them to production, so users get to download fewer files, etc., so this project's `Makefile` could look like this:

```
bundle.js:
    cat utils.js api-sdk.js app.js > bundle.js
```

These commands would concatenate all JS source code in these files into one `bundle.js`. So, this file could be built with the following command:

```
$ make bundle.js
```

If the file is absent, Make will run commands to generate it. But then, if you tried to run this command a second time, you would see the following result:

```
$ make bundle.js
make: `bundle.js' is up to date.
```

No errors, just this message – but no commands were executed. That's because, by default, Make builds files, and if these are present there's no need to build them again.

However, it's possible to remove this dependency of a file's absence by using the `.PHONY` special target. You basically add all tasks not tied to a file presence or absence, separated by spaces:

```
.PHONY: bundle.js

bundle.js:
    cat utils.js api-sdk.js app.js > bundle.js
```

This way, Make would consistently execute the following commands when `make bundle.js` is run, no matter if `bundle.js` already exists or not. It might be useful during development, for instance, to rebuild the file with updated content!

So, the `.PHONY` special target is generally something you want to use. Just keep it somewhere in your `Makefile` with a list of all your Make tasks that are file-independent, and it should be good to go!