cross-posted from: https://lemmy.ml/post/6856563
When writing a (GNU) Makefile, there are times when you need a particular target(s) to be run before anything else. That can be for example to check the environment, ensure variables are set or prepare a particular directory layout.
… take advantage of GNU Make’s mechanism of
include
ing andmake
ing makefiles which is described in details in the manual:
One recent example is a makefile (in a subproject), w/ a dozen of targets to provision machines and run Ansible playbooks. Almost all the targets need at least a few variables to be set. Additionally, I needed any fresh invocation to clean the “build” directory before starting the work.
At first, I tried capturing those variables w/ a bunch of
ifeq
s,shell
s anddefine
s. However, I wasn’t satisfied w/ the results for a couple of reasons:clean
target as a shell command at the top of the file.Then I tried capturing that in a target using
bmakelib.error-if-blank
andbmakelib.default-if-blank
as below.############## .PHONY : ensure-variables ensure-variables : bmakelib.error-if-blank( VAR1 VAR2 ) ensure-variables : bmakelib.default-if-blank( VAR3,foo ) ############## .PHONY : ansible.run-playbook1 ansible.run-playbook1 : ensure-variables cleanup-residue | $(ansible.venv) ansible.run-playbook1 : ... ############## .PHONY : ansible.run-playbook2 ansible.run-playbook2 : ensure-variables cleanup-residue | $(ansible.venv) ansible.run-playbook2 : ... ##############
But this was not DRY as I had to repeat myself.
That’s why I thought there may be a better way of doing this which led me to the manual and then the method I describe in the post.
That is true! My concern is that when the number of targets which don’t need that initialisation grows I may have to rethink my approach.
I’ll keep this thread posted of how this pans out as the makefile scales.
Love the attitude! I’m on the same boat. I could have just kept doing what I already knew but I thought a bit of manual reading is going to be well worth it.
To solve your DRY problem, you may not realize that you can generate target rules from built-in functions
eval
andforeach
and a user-defined new-line macro. Think of it like a preprocessor step.For example:
# This defines a new-line macro. It must have two blank lines. define nl endef # Generate two rules for ansible playbooks: $(eval $(foreach v,1 2,\ .PHONY : ansible.run-playbook$v $(nl)\ \ ansible.run-playbook$v : ensure-variables cleanup-residue | $$(ansible.venv)$(nl)\ ansible.run-playbook$v :;\ ... $(nl)\ ))
I winged it a bit for you, but hopefully I got it right, or at least right enough you get what I’m doing with this technique.
You may like an approach I came up with some time ago.
In my
include
d file that’s common among myMakefile
s:# Ensure the macro named is set to a non-empty value. varchk_call = $(if $($(1)),,$(error $(1) is not set from calling environment)) # Ensure all the macros named in the list are set to a non-empty value. varchklist_call = $(foreach v,$(1),$(call varchk_call,$v))
At the top of a
Makefile
that I want to ensure certain variables are set before it runs:$(call varchklist_call,\ INSTDIR \ PACKAGE \ RELEASE \ VERSION)
I usually do these checks in sub-Makefiles to ensure someone didn’t break the top level Makefile by not passing down a required macro.