Beautiful Makefiles with Wildcards

18 May 2020

Every single project we build includes a Makefile as a task runner. Every. Single. One.

Why? Because it allows someone to jump into a codebase and start working with the same set of tools and commands as everyone else.

Want to know how to install, build, test, deploy, and see what else you can do? Just open Makefile.

This may not be so important if you spend all your time on one monolith, but if you're jumping around to many services or clients, it's a lifesaver.

It's also so, so nice for open source projects, I'm surprised GitHub doesn't suggest it.

In this post and future ones, we'll go over some of the tricks we've learned building Makefiles in several dozen separate projects.

Today's lesson: Wildcards (%)

Dynamic Arguments with Wildcards (%)

Many popular languages include some (Make-inspired) script runner, so most commands look something like this:

migrate:
	bundle exec rake migrate
install:
	yarn install
runserver:
	python ./manage.py runserver

That's nice, but what about when you want to supply arguments to one of those CLI commands. For example, how can you run bundle exec rake migrate[50] to migrate to version 50, rather than latest?

Wildcards, that's how! For migrations, we can add a new command to migrate to a specific version:

migrate-to-%:
	bundle exec rake migrate[$(*)]

Now if you want to migrate to a specific version, you can run:

$ make migrate-to-50
Migrating to version 50...

Well, we think that's pretty cool, but what else can wildcards do?

Declarative Argument Dependencies

The other way to configure Makefiles is with environment variables. Let's say we use a consistent STACK environment variable for working with deployed Cloudformation stacks (as we actually do).

It can be pretty annoying to debug the errors if you forget to set STACK:

logs:
	awslogs get -w /ecs/$(STACK)_MyService

That can be invoked (correctly) as awslogs get -w /ecs/development_MyService or (incorrectly) as awslogs get -w /ecs/_MyService. If it's missing, your error will be about some unknown service or wrong file path, rather than about a missing parameter.

Well, we have a special command we've been using for years that we'll share with you now:

guard-%:
	if [ -z '${${*}}' ]; then echo 'ERROR: variable $* not set' && exit 1; fi

It's maybe our favorite two lines of code in the world.

Now, you can rewrite the logs target like this, which is also more declarative and clearer about its dependencies:

guard-%:
	if [ -z '${${*}}' ]; then echo 'ERROR: variable $* not set' && exit 1; fi
logs: guard-STACK
	awslogs get -w /ecs/$(STACK)_MyService

Finally, if you invoke logs without STACK set, you get a nice error:

$ make logs
ERROR: variable STACK not set
make: *** [guard-STACK] Error 1

Wait, there's more!

There are a couple other wildcard-based tricks we still have in store, as well as some more Makefile goodness.

Find this post useful, or interested in learning how we can help you make your next project? Please get in touch!.

Lithic Tech was founded in Portland, Oregon by experienced software engineers with a track record of high-quality software, happy customers, and successful businesses. We believe this success is a result of our unique processes, patterns, and convictions.

We are now offering that expertise to the wider community to build better software and software teams. You can learn more about us, and we’d love if you got in touch.

Email us at hello@lithic.tech or use this form: