Advent 2023: Makefile: guard targets

A couple days ago, I wrote about Makefile. Today, I'm going to show a quick tip for writing "guard" targets.

Guard targets?

Maybe you have a target that requires root to run. Or maybe you have one that requires that a .env file is present. You could check for these conditions within your target:

deploy:
	@$(call MK_INFO,"Checking for root")
	@if [[ "$(shell id -u)" != 0 ]]; then $(call MK_ERROR,"This target requires root"); exit 1; fi
    @$(call MK_INFO,"Checking for .env")
	@if [[ ! -f .env ]]; then $(call MK_ERROR,".env file is missing"); exit 1; fi

What if you need to do these checks in multiple targets? You can't really make functions of these, so how can you prevent duplication?

The thing is, make already supports these sorts of things, because any target can already specify its prerequisites, which are just other targets!

When I define a prerequisite target that only exists to ensure certain conditions are met as a prerequisite to other targets, I call it a guard target.

Let's refactor:

root:
	@$(call MK_INFO,"Checking for root")
	@if [[ "$(shell id -u)" != 0 ]]; then $(call MK_ERROR,"This target requires root"); exit 1; fi

deploy: root .env ## Deploy the app
	# do the actual work...

Let's dissect this:

  • The root target checks to see if the current user is root. If not, it spits out an error message (see my previous article on Makefile to understand that MK_ERROR usage works), and then exits with an error status.
  • The deploy target marks root and .env as prerequisites; if either fails, it won't run.
  • Note that the root target does not have a ## comment; this means it won't show up in my usage messages (though I can still call it separately if I want).

Wait, what about .env?

The default assumption of make is that targets are files. If the file exists, the target will not be executed, as the file already exists, so no work needs to be done. By specifying .env as a target, we're saying that deploy can only run if .env exists!

But if targets are files...

So, if targets are files, how does make work at all for things like deployment?

Again, the assumption is if the file does not exist, then make has work to do, and if that file is listed as a target, then it needs to execute that target.

Generally, the targets you create for things like web application deployment will not have corresponding files or directories, so make will happily see that the target exists in your Makefile, no filesystem entry exists, and execute it. The fact that it doesn't actually generate the target file is of no matter.

What if the target does exist?

What if the target name does have a corresponding file in the filesystem?

Mark it as a "PHONY" target:

.PHONY: .env

If you do this, then even if the .env file exists, the .env target, if it exists, will still be run.

Final Thoughts

Guard targets are a great way to add preconditions to build targets. They are re-usable and succinct, and can be used to provide useful error messages to guide usage. Better, they leverage the native aspects of a Makefile and make, and with good naming conventions, it becomes easy to identify what the prerequisite is for a given target without even needing to see how it's defined.