Makefiles make life easier
Web developers have long sought ways to make their projects easier to run, maintain and work with. From console apps like Composer and Artisan to custom implementations using things like Symfony Console, there’s a myriad of development tools aimed at running commonly needed commands like resetting a database or seeding information.
All of these tools, however, depend on a common set of things: the presence of PHP on the system OR access to a system like a running Docker instance. For me, this presents a problem: I don’t like to run PHP locally, and I don’t always have all of the dependencies involved that a project might require.
I solve this problem relatively simply: I use a Makefile. There are three reasons for this. First, make is almost always available on a system (assuming it’s a *Nix system), which makes it incredibly easy to write for. Second, since the syntax of the Makefile is simply writing for the shell, I have a multi-purpose language available at my fingertips. And finally, because I use Docker, I can invoke Docker from the Makefile and have it execute things that do require tools such as PHP or Node.
Rather than having to remember a long set of commands, enter a Docker container or otherwise mess around to solve a problem, I can simply type something like make init on a project and have it initialize the entire project, database and Composer dependencies. This makes my life much easier. I can also wrap other commands, like Artisan, with commands that are common to me, meaning that I don’t have to remember different commands depending on the framework I’m using.
Configuring a Makefile
Make is designed to actually build targets, so the way we’re using it isn’t exactly the way it was designed to be used. However, there are tools we can use to configure our Makefile.
First, we need to tell it that the Makefile is phony. We can do that like so:
.PHONY: *
This command goes at the very top of our Makefile (called `Makefile` with no extension). It tells `make` that everything in the Makefile won’t be a target to build but will be a command to run.
Next we can define some rules around documentation in our Makefile. Thanks to the ever-handy Rob Allen, I’ve learned that it’s possible to document a Makefile and have a command for that file to list out all the available commands and their documentation. We define our first Makefile command, as such:
.PHONY: *
list:
@grep -E '^[a-zA-Z%_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Now when we run `make list` on the command line, it will output the value of each command’s documentation.
Tabs vs. Spaces
There’s a huge debate amongst programmers about tabs versus spaces, but when it comes to the Makefile, the debate is settled: you MUST use tabs in Makefiles for indentation. Spaces will not work, so make sure your editor is configured properly. If you’re using PHPStorm or something similar, there’s likely a plugin for editing Makefiles which will default to tabs, and save you the trouble of having to remember or reconfigure your editor (assuming you used spaces before).
Writing Actual Commands
Now we can actually write a command using the Makefile and run it. The easiest way to test this is to write a typical “hello world”. To do this, we can simply add the following command to our Makefile:
hello: ## Introduces ourselves to the world echo "hello world"
Let’s talk about what we’ve done here. Our command name will be `hello` meaning we can call `make hello` and it will run our command. The comment on the same line (preceeded by the `##`) is the documentation we set up in the last command; if we run `make list` it will output information about that command based on what we put as documentation. Finally, on the next line indented by a tab, we have the shell command to run (`echo “hello world”`) which will run the command on the command line.
Give it a try. What did you get? You should have gotten `hello world` output for you on the command line!
Of course, this isn’t terribly useful. So we need to write more interesting commands.
One command that I like to write is a command called `make init` and I tell it to initialize my entire instance of an application. It looks something like this:
init: ## Initialize the application docker compose build docker compose up -d docker compose run --rm webapp composer install docker compose run --rm webapp migrate-database
Okay, so we’ve done a couple things here. First, we’ve created an `init` command to initialize our application. And note that we have multiple lines under `init` for what the `make init` command will do. That’s okay! Make will run each command separately, and stop if one fails. Each command gets run, one right after another.
Of course, sometimes these commands might be handy to have for use outside the `init` command. So let’s break up this command:
[bash] init: build up composer-install migrate-db ## Initialize the application build: ## Builds the Docker environment docker compose build up: ## Stands up the Docker environment docker compose up -d composer-install: ## Installs Composer dependencies docker compose run --rm webapp composer install migrate-db ## Migrates the database docker compose run --rm webapp migrate-database
Okay, so we’ve broken our commands out into individual commands that can be run stand-alone, but something we’ve also done is tell Make to have our `init` target run a bunch of other commands in sequence. This is handy because we don’t have to duplicate the code; we can simply tell Make to run these commands, and then explicitly define the commands for use as a collective (in `make init`) or individually.
Wrapping Up
Most systems have Make installed, and this is an easy way to write common commands for your projects. I hope that you find this useful. There’s almost no end to what you can do with a Makefile, from taking command-line inputs to providing complex arguments and conditionals. Since it’s just a shell script, you have the ability to do just about anything you can do in bash. Have fun!
For Reference
Below is the entire Makefile we just wrote, for reference:
.PHONY: *
list:
@grep -E '^[a-zA-Z%_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
init: build up composer-install migrate-db ## Initialize the application
build: ## Builds the Docker environment
docker compose build
up: ## Stands up the Docker environment
docker compose up -d
composer-install: ## Installs Composer dependencies
docker compose run --rm webapp composer install
migrate-db ## Migrates the database
docker compose run --rm webapp migrate-database
