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
- When I run
make
, it will try to createlunch
. lunch
has dependenciesnoodles
andsoup
which don't exist, somake
will try to makenoodles
andsoup
soup
has dependenciesonion
andmushroom
which don't exist. However no rules are defined foronion
andmushroom
somake
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.