Easy Automation with GNU Make- Beginner's Guide

Mar 13th, 2019

tutorial

Make is a build tool that became (relatively) popular with the rise of Unix. GNU Make is the most common version, present on both Linux and MacOS. It's used to build even complicated software like the Linux kernel.

Of course, Make does not have to be complicated. Like most other Unix tools, it's based on a few simple ideas!

Make Just Builds Files

A Makefile is usually just a set of instructions on how to build a file. Instructions generally are programs on the system (though not necessarily run by a shell!).

hello.txt:
      touch hello.txt

Try it out! If you wrote this in a Makefile, then entered make hello.txt, you would have the file hello.txt created in your directory.

If you ran it again, it'd look like this:

$ make hello.txt
touch hello.txt
$ make hello.txt
make: 'hello.txt' is up to date

Make sees that there is a hello.txt file, and does not run the command again.

Files Have Dependencies

Sometimes, you'll need to do more than just create text files using touch. Surprising right? Thankfully, you can chain your contrived dependencies together using Make, and it's super easy:

awesome.txt: hello.txt
        cp hello.txt awesome.txt

hello.txt:
        touch hello.txt

The instructions on creating an awesome.txt now include hello.txt as a dependency. Make looks for instructions on how to create a file of the same name, and checks if it's up to date.

That's also how to create the ubiquitous make build or make test commands, by the way:

.PHONY: build
build: awesome.txt

awesome.txt: hello.txt
        cp hello.txt awesome.txt

hello.txt:
        touch hello.txt

PSST: .PHONY: <command> just means that the command name does not correspond to a file. Otherwise, Make would look for a file named build, which is probably not what you want.

Automate Docker Releases

The Docker Logo

Look at this adorable whale! www.docker.com

Docker is pretty easy to use, but I have found that building and saving releases is awkward. It's a little too easy to lose track of version numbers and to forget the "latest" container.

Plus, any command entered by hand that warrants a backslash probably shouldn't be entered by hand.

As you can probably guess, Make to the rescue. Let's automate image building and tagging! Create a new directory that looks something like this:

make-docker/
├── dist/
├── Dockerfile
├── Makefile
└── VERSION

1 directory, 4 files

You can use whatever you would like in your Dockerfile! Here's a "Hello World" Docker image.

FROM alpine:latest

RUN echo "Hello World" > greeting.txt

CMD cat greeting.txt

We'll mark this as version 0.1.0 in our VERSION file:

0.1.0

Finally, let's add the contents of our Makefile:

VERSION := $(shell cat VERSION)
IMAGE := my-hello-world

.PHONY: build
build: dist/$(IMAGE)-$(VERSION).tar.gz

dist/$(IMAGE)-$(VERSION).tar.gz:
	docker build -t "$(IMAGE):$(VERSION)" -t "$(IMAGE):latest" .
	docker save "$(IMAGE):$(VERSION)" > dist/$(IMAGE)-$(VERSION).tar.gz

Our Makefile reads the value stored in VERSION upon any invocation of make. Make then relays the commands to Docker, which builds our images for us. Docker tags the images as both $VERSION and latest.

By saving a .tar file for the docker image, our requirements for make build are met. Subsequent runs with the same version will not create extra images!

Congrats! You can now create a new release of your image with just one command.

Wrap Up

That's a quick intro to Make! You now know enough of the basics to get out there and use Make in your projects.

To learn more about Make, the manual is a pretty great resource that explains a lot more than this overview. Happy Making!