DEV Community

Joel Jucá
Joel Jucá

Posted on • Edited on

Makefiles: a standard for project tasks

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 are configuration files for GNU 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (1)

Collapse
 
joeljuca profile image
Joel Jucá

Here's one more learning from my exp w/ Makefiles: avoid hiding cmds behind each task.

So, when you run a task, make outputs its cmds to STDOUT before running them:

# Makefile (example)
.PHONY: today
today:
    date
Enter fullscreen mode Exit fullscreen mode
$ make today
date
Thu Jul  4 13:27:58 -03 2024
Enter fullscreen mode Exit fullscreen mode

In the examples above, a task today is defined with a cmd date which, in turn, prints the current date and time. As you can see, make prints the task cmd before executing it. It's possible to "disable" this behavior by adding a @ at the beginning of your cmd:

# Makefile (example)
.PHONY: today
today:
    @date
Enter fullscreen mode Exit fullscreen mode
$ make today
Thu Jul  4 13:27:58 -03 2024
Enter fullscreen mode Exit fullscreen mode

I'd recommend you to avoid doing it. I find it quite useful to see the cmds you'll run out of your Makefiles whenever you run them. After a couple of months of using them, you'll most likely forget what these cmds do, so seeing them repeatedly whenever you run your Make tasks might help you remember params and options passed to cmds, and change them when necessary if smt changes in your project.