Makefiles are nothing new; they have been around since +/- forever. But since I'm currently enjoying the wonderful combination of Windows 10 + WSL I get the best of both worlds and feel my workflow has improved greatly with availability of tools on both platforms.

One of those tools is make which we now use quite a bit in projects at work - it is a quick way of getting things set up for new developers joining the team and making tedious tasks reproducable and efficient in everyones daily routine.

More specifically: We use it for things like running tasks that have numerous arguments that you normally don't change - like docker-compose or docker build. Even things like pulling down project Git repos.

These things aren't the most complex tasks, but the idea is that they are easily reproducible for the entire team - one-stop-shop for most of the tasks you need in one script.

There are numerous tutorials and docs on how to create a Makefile so I won't spend time on that here, but as a simplification you can say that a Makefile contains two things:

  • Variables
  • Rules

As an example, let's create a Makefile which does the tedious task of building X number of Docker images.

First - we need some variables.

DOCKER_REGISTRY                 ?=      mydemocontainers.azurecr.io
DOCKER_BUILD_FLAGS              :=
IMAGES                          =       MyApp.Frontend MyApp.Backend.SvcA MyApp.Backend.SvcB
GIT_TAG                         =       $(shell git describe --tags --always 2>/dev/null)
VERSION                         ?=      $(GIT_TAG)
IMAGE_TAG                       ?=      $(VERSION)
AZURE_SUBSCRIPTION              ?=      <insert your subscription id here>

These can be overriden by the user when calling make - also, for variables such as DOCKER_REGISTRY we say "only use this value, if the environment variable $DOCKER_REGISTRY is not already set". We do this by assing the value using ?=

Next, we need a rule to call when we wish to build the images. Let's call this docker-build.

To avoid conflicts with filenames, we first specify the rulename as a Phony target. Then we specify the rule itself.

.PHONY: docker-build
docker-build: 
	docker build $(DOCKER_BUILD_FLAGS) -t $(DOCKER_REGISTRY)/MyApp-Backend-SvcA:$(IMAGE_TAG) src/MyApp.Backend.SvcA

But, this will only build one image (which can be fine if thats all you have). If you look at the IMAGES variable we defined it contains a list of folder names that are assumed to exist under src/ in this rule.

So, how do we make this generic and build them all with one rule?

Let's introduce patterns and replace the rule above with:

%-image:
	docker build $(DOCKER_BUILD_ARGS) -t $(DOCKER_REGISTRY/$*:$(IMAGE_TAG) src/$*

.PHONY: docker-build
docker-build: $(addsuffix -image,$(IMAGES))

Now we have two rules: one general which will use a built-in feature addsuffix to append -image to all paths in the $IMAGES variable and then call the correspendingly named rule.

Our other rule says "I accept everything named according to the pattern whatever-image.

This means we can now call make docker-build and it will in return call %-image rule for every image specified in $IMAGES.

We do have a problem with this particular configuration, though. Docker tagging doesn't like periods in the name - which our image paths have.

Now we define a function to handle this for us:

make_image_name_from_path =     $(subst .,-,$(shell echo $(1) | tr A-Z a-z))

This will take in an argument and replace all periods with dashes.

Let's improve the %-image rule one last time:

%-image:
        docker build $(DOCKER_BUILD_FLAGS) -t $(DOCKER_REGISTRY)/$(call make_image_name_from_path,$*):$(IMAGE_TAG) src/$*

Read more about call here.

If we now execute make docker-build we will build three images with one command (and can add more just by appending to the IMAGES variable) tagged with the Git commit tag for versioning.

Final Makefile should now look something like this:

DOCKER_REGISTRY                 ?=      mydemocontainers.azurecr.io
DOCKER_BUILD_FLAGS              :=
IMAGES                          =       MyApp.Frontend MyApp.Backend.SvcA MyApp.Backend.SvcB
GIT_TAG                         =       $(shell git describe --tags --always 2>/dev/null)
VERSION                         ?=      $(GIT_TAG)
IMAGE_TAG                       ?=      $(VERSION)
AZURE_SUBSCRIPTION              ?=      <insert your subscription id here>

    .PHONY: docker-build
docker-build:   $(addsuffix -image,$(IMAGES))

%-image:
        docker build $(DOCKER_BUILD_FLAGS) -t $(DOCKER_REGISTRY)/$(call make_image_name_from_path,$*):$(IMAGE_TAG) src/$*

make_image_name_from_path =     $(subst .,-,$(shell echo $(1) | tr A-Z a-z))

Makefiles support so much more and I can't possibly cover that here, but using just these simple things you can now easily add more helpful rules such as docker-push