DEV Community

Santiago
Santiago

Posted on • Edited on • Originally published at woile.dev

Digesting Make and Makefiles

Make and its makefiles, are a fantastic tool to keep track of the commands needed to build or run an application.

Just create a Makefile at the root of your project, start adding commands, and done... right? Not really.

Let's explore a bit more what I mean, and then try to build a mental model that matches reality a bit better,
closing with some magic you can do with Makefiles.

Set up a practice field by running in the terminal:

mkdir /tmp/practice cd /tmp/practice touch index.html 
Enter fullscreen mode Exit fullscreen mode

And write a minimal Makefile with your favourite editor (E.g: vim Makefile)

build: mkdir dist cp index.html dist/index.html 
Enter fullscreen mode Exit fullscreen mode

Then execute in your terminal:

make build 
Enter fullscreen mode Exit fullscreen mode

This will create a folder dist, and copy index.html into the folder dist.

This is good. Particularly for me, it documents the commands used by the project, and helps future me.

Let's refactor a bit, to show some extra functionality:

dist: mkdir dist build: dist cp index.html dist/index.html 
Enter fullscreen mode Exit fullscreen mode

Notice the build: dist, it means: call dist command before running build

make build 
Enter fullscreen mode Exit fullscreen mode

The functionality remains the same.

Recap

We created two commands: build and dist.

And dist is executed by the build before running itself.

This is ok, but it's not necessary a correct mental model.
In the Makefile world, commands are not commands... but files.

New mental model

In a nutshell, make builds a dependency graph of files and folders.

  • build and dist are actually target files (or folders).
  • doing build: dist means that build depends on dist existing first.

If we had a file called build , doing make build, wouldn't execute anything.

touch build make build $ make: `build' is up to date. 
Enter fullscreen mode Exit fullscreen mode

If we want to actually treat build as a command, we have to add .PHONY.

dist: mkdir dist .PHONY: build build: dist cp index.html dist/index.html 
Enter fullscreen mode Exit fullscreen mode

This way Make no longer sees build as a file, but instead as a recipe, and it will be
executed everytime.

Back to targets

Remember what we said about targets and dependency graph? No? Me neither.
Don't worry, I actually didn't say anything.

If you treat your targets as files, Make can keep track of the files that have
changed, and update only those.

By knowing this our previous example could be refactored into:

dist: mkdir dist dist/index.html: dist cp index.html dist/index.html 
Enter fullscreen mode Exit fullscreen mode

And now we tell Make to create our target file:

make dist/index.html 
Enter fullscreen mode Exit fullscreen mode

If we run multiple times, we get:

make: `dist/index.html' is up to date. 
Enter fullscreen mode Exit fullscreen mode

We would expect that by modifying index.html, and running make dist/index.html, it would be rebuilt,
but we are getting the same message.

Dependencies

We need to tell Make that it depends on another file: index.html

dist: mkdir dist dist/index.html: dist index.html cp index.html dist/index.html 
Enter fullscreen mode Exit fullscreen mode

This time, if index.html is modified, make dist/index.html will run again.
Thus, when dependecies are updated, target files are recreated.

Makefile targets and dependencies

Patterns

Now let's say we have many html files

touch about.html privacy.html docs.html 
Enter fullscreen mode Exit fullscreen mode

And we want to do the same for all the files, without creating many commands in Make.
For this case we use a pattern (%), and some Make variables:

  • $@: the target file
  • $<: the input file
dist: mkdir dist dist/%.html: %.html dist cp $< $@ 
Enter fullscreen mode Exit fullscreen mode
make dist/about.html 
Enter fullscreen mode Exit fullscreen mode

And we would be explicitly telling Make which file to create.

But what if we want to copy all the files at the same time?

Our current implementation, doesn't know about the available files.
And we are providing the about to the make dist/about.html.

We have to find a way to "know" all the possible targets.

Variables

We are gonna find the source files (*.html files which are not in the dist folder), store in a variable,
and then use that information to create the target html files.

SRC_HTMLS := $(shell find . -name '*.html' -depth 1) TARGET_HTMLS := $(SRC_HTMLS:./%.html=dist/%.html) .PHONY: build build: $(TARGET_HTMLS) $(info Done) dist: mkdir dist dist/%.html: %.html dist cp $< $@ 
Enter fullscreen mode Exit fullscreen mode
make build 
Enter fullscreen mode Exit fullscreen mode

What happens is:

  • build has all the TARGET_HTMLS files as dependency
  • Make also sees dist/%.html and the pattern will fit the criteria for each TARGET_HTMLS
  • In dist/%.html we have as dependency the %.html, so Make takes the pattern and checks if a %.html file exist
  • If the conditions are met, it runs the cp command for each file, unless they are already present and not updated

You can think of

build: $(TARGET_HTMLS) 
Enter fullscreen mode Exit fullscreen mode

As

build: dist/index.html dist/about.html dist/docs.html dist/privacy.html 
Enter fullscreen mode Exit fullscreen mode

Variables can be reference using $() or ${}.

We also use $(info Done) to send information messages to the user.
Make also provides $(warning text…) for warnings, and
$(error text...) to exit earlier with an error code different than 0.
See Make-Control-Functions

One more time

Let's start over by removing the dist folder, and see what happens

rm -rf dist 
Enter fullscreen mode Exit fullscreen mode
❯ make build mkdir dist cp index.html dist/index.html cp about.html dist/about.html cp docs.html dist/docs.html cp privacy.html dist/privacy.html Done ❯ make build Done make: Nothing to be done for `build'. 
Enter fullscreen mode Exit fullscreen mode

As you can see, files that have not changed, won't be rebuilt by Make.

Now, try using touch on the different html files and running make build to see what happens.

More functionality

Make is a powerful tool, and provides much more functionality, so far with the web stack
I haven't had the need for more complexity.

I usually wrap docker commands, and make use of different variables, but if the need arises,
make has extra functionality, life:

  • functions
  • if/conditions
  • change the shell in which the commands are executed
  • .delete_on_error

Notes

Make is usually a good way to keep track of a project's commands, as it's available in Unix systems,
and you can be up and running fast.
But by no means is perfect, it can sometimes be hard to read, or use.
It's not available on Windows. And because it was designed for the C,C++ era, it plays well with files,
but it doesn't mean it fits perfectly the web development paradigm, where you don't "transform" files much
and where docker is used a lot.

So depending on your situation, there are some popular alternatives:

Resources

Thanks for reading! 👋

If you are interested in what I write, follow me on twitter

Top comments (0)