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
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!
Top comments (1)
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:In the examples above, a task
today
is defined with a cmddate
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: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.