Doing the same thing over and over

Makefile

During development, sometimes you have to do the same things over and over again. Every time you do it, you expect the same results, but sometimes your fingers aren’t on the correct keys, or your brain has gone on a temporary vacation. You have to create your development environment, although docker-compose encapsulates that for us. I find it handy to jump into a shell running in the development container. Honestly, who wants to type docker compose exec php-fpm bash -c "/usr/bin/php artisan tinker" more than once a month?

And building a Jekyll blog just for development? Who can remember all those letters in the same order every time?

For commands run from the host environment, the solution is a Makefile. Instead of remembering all the switches and format of the docker command, a simple make build is a lot easier to remember and type.

However, the Makefile doesn’t have access to bash variables, so it makes it a bit more difficult to mount the current "${PWD}/" directory into your container. Since we want code to be portable, the development path cannot be hard-coded into the Makefile. Well, it could but it shouldn’t.

Instead of "${PWD}/", we need to create a Makefile variable, not a shell variable. At the top of your Makefile add a variable definition ROOT_DIR:=$(shell dirname $(realpath$(lastword $(MAKEFILE_LIST)))). This defines a variable name ROOT_DIR. The variable is set using := to a simply expanded variable, that is a variable that is calculated once, like a constant expression. The rest takes the Makefile and calculates its whole path.*

In the command list, we can use the ROOT_DIR just like any other shell directory, by starting it with a $ and wrapping it in parentheses: $(ROOT_DIR). At the time of writing, my Makefile looks like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))

build:
	docker run --rm  -v "$(ROOT_DIR):/src" ruby:latest sh -c 'cd /src && bundle install && bundle exec jekyll build --watch'

run: build
	docker run -d -p 80:80 -v "$(ROOT_DIR)/_site:/usr/share/nginx/html" --name apache nginx:alpine

shell:
	docker run --rm -it -v "$(ROOT_DIR):/src" ruby:latest bash

test:
	gitlab-runner exec docker test

updategem:
	docker run --rm -v $(ROOT_DIR):/usr/src/app -w /usr/src/app ruby:latest bundle lock --update

update:
	docker run --rm -v "$(ROOT_DIR):/src" ruby:latest sh -c "cd /src/ && bundle config set path 'vendor/bundle' && bundle lock"

newpost:
	echo "---\nlayout: post\ntitle: title\n---" > app/_drafts/`date +%Y-%m-%d-`title_`date +%H_%M`.md

My Makefile also has a handy little command to create the stub of a new blog post. Since I don’t want to start a new post completely from scratch each time, I add this just for a little reminder of how to post.

Here’s another fun snippet from a laravel project running in my own docker stack.

1
2
tinker:
	docker compose exec php-fpm bash -c "/usr/bin/php artisan tinker"

Once you start adding commands to your Makefile, the sky is the limit. Just remember to start all shell commands with a tab, not spaces. As a DRY concept, I’m even adding make commands in my .gitlab-ci.yml file just to make sure that the CI commands are the same commands I expect on the command line. Well, technically, it’s WET, but the duplication has been refactored into a single make command.

Caveat

It is possible when using `make` as a build system for compiled applications, that a `Makefile` includes another `Makefile`. In this instance, each `Makefile` is appended to the end of `MAKEFILE_LIST`, and you may not be calculating the correct directory.

Weather in Charlotte, NC