Introduction
I’m writing this because I have the feeling that many developers underestimate
the power of Makefiles and they are simply not aware of this nice and handy tool
which is installed on nearly every Unix-like machine.
To be honest, who have never executed a make install
or something similar?
Most tutorials I’ve found out there are bloated with stuff, more complex than
they would have to and you have to read pages after pages to get the basics.
IMPORTANT
Please use tabs when following these examples as make will complain when used with spaces as indentation.
Targets
A Makefile exists of targets. These targets are the ones you execute.
When you execute make install
you trigger the install
target of the Makefile,
when you run make build
you trigger the build
target.
A target can be as simply defined as:
# this is the target
install:
# this will be executed when make install is called
echo Hello World
install
is the target in this case and echo Hello World
will be executed
when you trigger this target.
These targets can also execute multiple commands like this:
# target
install:
# will be executed first
echo hello
# will be executed second
echo World
If one of the commands is false the Makefile will stop the execution:
# target
install:
# will be executed first
echo Hello
# will break here
false
# will not be executed
echo World
This will output:
$ make install
# will be executed first
echo hello
hello
# will break here
false
make: *** [Makefile:6: install] Error 1
You can see that in line 6 the error occurred.
Dependents
So, but what is the cool thing about these targets? They remember when you have
executed them at last and only do this stuff again when the file is newer than
the last time you have called it.
But for that, your target has to be a file.
Create a file myfile.sh
and add a basic
#!/usr/bin/env bash
echo Hello World
Then create a Makefile
:
# mytarget depends on myfile.sh
mytarget: myfile.sh
# copy myfile.s to mytarget
cp -f myfile.sh mytarget
If everything is done correctly
you will get this output when running make mytarget
:
cp -f myfile.sh mytarget
. If you run it a second time you will see:
make: 'mytarget' is up to date.
. You can try this as many times as you want.
If you now change the target, a simple touch myfile
is enough since this
updates the last touched timestamp from the file, it will redo the copying.
But you can of course not only depend on files, you can also depend on other targets. Watch this:
target2:
cp myfile.sh target2
echo Hello
# target1 depends on target2
target1: target2
echo World
target1
depends on target2
, therefore target2
is executed first.
What if target1
depends on our former friend myfile.sh
?
Check this out:
target2: myfile.sh
cp myfile.sh target2
# target1 depends on target2
target1: target2
echo World
At the first run you get this as output as expected:
$ make target1
cp myfile.sh target2
echo Hello
Hello
echo World
World
But at the second run you only get this:
$ make target1
echo World
World
target2
is already up to date so make doesn’t have to rebuild this target.
When you touch myfile.sh
or remove the target2
-file you will get the output
of the former run. target1
is no file, so sadly this target
will be rebuild
every time.
A target doesn’t necessarily need to have a body. It could also only be dependent on other targets. This would be valid:
target2: myfile.sh
cp myfile.sh target2
target1: target2
And runs the cp
command when make target1
is called.
Of course, this is most useful when one target is dependent on more than one
targets. For example:
all: lint test build
build: someFile
buildCommand
test: someFile
testCommand
lint: someFile
someLintCommadn
Now with make all
everything is executed.
Or when you only want to execute build when the tests are passing:
all: lint test build
build: test someFile
buildCommand
test: someFile
testCommand
lint: someFile
someLintCommadn
If one command returns false
- or you simply write false
the make process
will be aborted there.
But, what if the target name is a folder?
Let’s see, create a folder called dist: mkdir dist
dist: myfile.sh
cp myfile.sh dist/myfile.sh
Now run make dist
and you will get what output?
If you have read this article carefully you should know whats going on.
$ make dist
make: 'dist' is up to date.
The dist
-folder was created after the file myfile.sh
was last touched.
So make thinks dist
was built after the last change of myfile.sh
and should
be up to date. A simple touch myfile.sh
is helping again.
PHONY
To rebuild a target every time it is called independent of last changes you
could add the special .PHONY
keyword to that target.
.PHONY: dist
dist: myfile.sh
cp myfile.sh dist/myfile.sh
Now every time you run make dist
it is redoing what it is told.
Wildcards
There is a handy thing called wildcard. I think it does what everyone would
expect. For that create a bunch of sh
files: touch {1,2,3,4,5}.sh
Adjust your makefile to this:
target: $(wildcard ./*.sh)
touch target
And after the first run, nothing is done, except you touch one of the shell scripts regardless which one.
Variables
You could set variables inside the Makefile and use them. For example:
ENTRY_FILE = Main.hs
test: $(ENTRY_FILE)
testCommand
build: $(ENTRY_FILE)
buildCommand
....
Now you only need to change the ENTRY_FILE
definition if the name of your
entry file should change. I think you can imagine many scenarios where this
could be useful.
Variables can be accessed with surround $()
.
Of course, this can also be arrays. For this create two files:
touch file1 file2
and try this:
MY_FILES = file1 file2
myTarget: $(MY_FILES)
echo should rebuild
touch myTarget
After the second turn, it doesn’t rebuild. But touch one of both files and It will rebuild.
The Wildcard would be working as a variable too:
MY_FILES = $(wildcard ./*.sh)
myTarget: $(MY_FILES)
echo should rebuild
touch myTarget
Loops
Loops are also possible.
For example, if you have a bunch of binaries and want to compress them after
building from mybinaryX
to mybinaryX.tar.gz
:
BINARIES = mybinary1 mybinary2 mybinary3
compress-all-binaries: build-all-binaries
for f in $(BINARIES); do \
tar czf $$f.tar.gz $$f; \
done
@rm $(BINARIES)
Shell
You could even run shell commands or scripts from inside the Makefile. For example something like this:
PROGNAME = Today
LOWER_PROGNAME = $(shell echo $(PROGNAME) | tr A-Z a-z)
Exports/$PATH
It’s also possible to export variables, this is especially useful for something
like the $PATH
-variable. When you run JavaScript with npm-dependencies for
example all dependencies which have a binary would be sym-linked into
./node_modules/.bin/
and you could do this:
# Add node_modules binaries to $PATH
export PATH := ./node_modules/.bin:$(PATH)
Then you didn’t have to put the full path before such a command, you could simply call it like you would within an npm-script.
if and else
What is also a really cool feature which I’ve learned last week is that you could natively use if and else statements in Makefiles.
For example, could I test if my Makefile was called with a specific environment variable like this:
# @ tells make to not print the command itself to stdout
# only the commands output
called-with-version:
ifeq ($(VERSION),)
@echo No version information given.
@echo Please run this command like this:
@echo VERSION=1.0.0 make release
@false
else
@echo do some other stuff
endif
This means if the variable $(VERSION)
, which would be passed via an environment
variable is empty execute the first block else the second.
There is also ifneq
which means if not equal.
Misc
- You need
tabs
as indentation in a Makefile else wise it will complain - If you put an
@
before any command the command itself will not be passed tostdout
but you will receive the output of this command. - With the
-j
parameter you could run make with parallel execution - The complete documentation can be found here: Link
Real-life Makefiles
Here are some real Makefiles I’ve written myself. Some examples are taken from there but I think this can give you some inspiration how to use them in real projects. They are sorted from simpler to more complex ones but that might change over time.