Gentle Introduction to Makefile

Makefile contains blocks of shell code organized under rules. The GNU make program interprets Makefile and automatically optimizes the order in which your code runs.

Shell Basics

To understand the examples, we use two commands for demonstration:

# prints text to a file
$ echo "some text" > file

# concatenates the contents of two files into a third
$ cat first second > third

# appends the concatenation of two files into a third
$ cat first second >> third

Rules

Rules consist of a target and any number of dependencies. Both targets and dependencies should be files.

lunch: noodles soup
    cat noodles soup > lunch

lunch is the target, it is a file which does not yet exist. It can be created if its dependencies noodles and soup exist. The command cat noodles soup > lunch is the shell code which creates the target lunch.

Full Example

lunch: noodles soup
    cat noodles soup > lunch

tea:
    echo "boiling water and leaves" > tea

all: lunch tea

noodles:
    echo "bought dried noodles from the store" > noodles

soup: onion mushroom
    echo "today's soup:" > soup
    cat onion mushroom >> soup

clean:
    $(RM) lunch noodles soup tea onion mushroom

.PHONY: clean
  1. When I run make, it will try to create lunch.
  2. lunch has dependencies noodles and soup which don't exist, so make will try to make noodles and soup
  3. soup has dependencies onion and mushroom which don't exist. However no rules are defined for onion and mushroom so make cannot do anything.
make: *** No rule to make target 'onion', needed by 'soup'.  Stop.

Let's create onion and mushroom manually and see if things work:

$ echo Shiitake > mushroom
$ echo scallion > onion
$ make

echo "bought dried noodles from the store" > noodles
echo "today's soup:" > soup
cat onion mushroom >> soup
cat noodles soup > lunch

Do It Again

$ make    
make: 'lunch' is up to date.

There is no need to rerun anything since lunch already exists.

But what if my lunch gets stale and fresher ingredients come in? In technical speak, what happens when a dependency is updated so that its modification time is more recent than the time of its target?

$ echo "fresh noodles, hand-pulled" > noodles

Since noodles was modified more recently than lunch, lunch is now out-of-date and it should be remade.

$ make

cat noodles soup > lunch

Since onion and mushroom have not been changed, soup is still up-to-date and it is not remade.

Where is my tea?

make will run the first rule appearing at the top of the file, unless you specify a target. Tea can be made by running make tea.

Common Conventions

Common convention is to define an all rule:

all: lunch tea

make clean is another convention, which should delete all targets and build artifacts defined in Makefile.

Advanced: Variables

Implicit Variables

Instead of running the rm -f command, in the clean rule I instead used the variable $(RM). Using variables is usually a best practice. $(RM) is an implicit variable. Variables improve cross-platform compatibility and also flexible usage, e.g. instead of rm, I might want to use trash-put instead by overriding the RM variable:

$ make clean RM=trash-put

Automatic Variables

The names of targets and dependencies is duplicated in the code everywhere. We can define variables ourselves and also use automatic variables.

LUNCH = lunch

$(LUNCH): noodles soup
    cat $^ > $@

It is thanks to these variables from where Makefile gets its poor reputation of being illegible.

History

Makefile is an amazing concept but limited in design. Targets and dependencies are expected to be files accessible on the filesystem, which is not always the case in today's world with commands such as docker build.

The most common use case of Makefile is to compile C programs. Modern programming languages tend to have a language-specific build tool, e.g. pip for Python, cargo for Rust, go for Go, ...

The Big Picture

Shell scripts using #!/bin/bash are interpreted line-by-line, whereas Makefile can be executed out-of-order for efficiency. In fact, separate rules in Makefile can be executed in parallel by using the flag make -j8.

Makefile is an example of a declarative programming paradigm, whereas shell scripts are procedural. Procedural programming can be more intuitive for new programmers because it's how people think and computers work. However, declarative code tends to be more "elegant:" more concise and with fewer bugs.

frontiersin.org/articles/10.3389/fninf.2016..