During development you may sometimes notice you run a lot of commands to set up, build, test, and manage your project. Sometimes, these end up requiring several steps, or you have trouble remembering the exact command. One way to manage this is to set up aliases in your shell configuration. While this would work fine for yourself and for a single build system, it would not be possible to share it in a convenient manner.
One alternative is shell scripts, but a more popular and flexible way is Makefiles. Make is a build automation tool originally made for building C projects, but can be used for any task and programming language. However, every language may not benefit as much as C does from it. Many languages today come with already complete build systems; don’t expect Make to replace them. It would rather be a layer on top for adding custom build steps.
To use Make, start by creating a file called Makefile
, usually in the root
directory of a project. This file is automatically used when invoking make
.
Like many build systems, one of Make’s key features is keeping track of file
changes. Make looks at the last modified
field of a file to determine if it
should “build” it again. “build” in this context could be anything. It doesn’t
have to be a compiler. It could be any shell command, even something as simple
as copying a file to another directory in preparation for the build step. It
could be a pre-build script to generate a file dynamically.
Here’s a list of common Make commands to have and what they typically do.
make ๐
Without any arguments Make will run the first target specified in the makefile.
This is typically the all
target, equivalent to make all
. It should build
the whole program with all its dependencies. The command can be invoked
repeatedly to rebuild the program.
make run ๐
Runs the program. Useful if the program needs some sort of setup before starting the actual program. Maybe starting an emulator. If you happen to be developing an OS it can start the kernel in a virtual machine.
make install ๐
Installs the program. Is usually invoked with sudo
and after doing make
to
build it first. Instructions in README files often looks like
make
sudo make install
Make can handle separate install instructions for Windows and Mac/Linux when
selecting where to install it. install
should typically support PREFIX=
argument if the user wants to decide where to install it. Specifying
PREFIX=/usr
will attempt to install it to /usr/bin/
.
You can provide an uninstall
target as well. But be careful to not delete any
third-party dependencies that other packages also depends on. Even if your
program installed it doesn’t mean it is the only one using it.
make clean ๐
Removes any files generated by the build system. It provides a clean template
for doing a completely new build. Sometimes issues arise from partial builds
and cached data, in which case it is nice with an easy way to start from a
clean plate. Clean can be complemented with a make distclean
that, on top of
doing a make clean
, also removes any files created building external
dependencies (e.g., a submodule or downloaded files) to ascertain a totally
clean build.
make dist ๐
Create a release tarball. When doing an official release you may want to package just the essential files (tends to not include .git), and sometimes multiple tar, or even exe, files preconfigured for different platforms. This will usually read the version name from a configuration file and use in the filename.
make doc ๐
Builds documentation. A popular way to handle documentation is through a website. This might generate the HTML from some other format, perhaps from markdown files using Pandoc, or some tool that parses the code and comments.
Example ๐
We will use the below project structure.
|------------------------
|- Makefile
|- src/
| |- default_config.json
|- output/
| |- program.exe
-------------------------
In this scenario we want to copy the default_config.json
to our output/
directory when building because program.exe
requires it in the same folder
during runtime. To do this we will use Make’s feature of keeping track of
changes. Whenever the default config is changed we want those changes to be
reflected in the output, just as if we changed a line of code. output/
is not
checked into Git and exists only as a place to store the output of the build. It
could be created by the Makefile as well.
output/default_config.json: src/default_config.json
cp src/default_config.json output/default_config.json
In Make terminology this is called a target
. The target is
output/default_config.json
, which is the file we want to create. After the
colon :
comes the dependency src/default_config.json
, the original file.
Indented by one tab are the commands. Here we only use one command to copy the
file. This target says that if the target does not exist then it must be
created by executing the commands. The dependency indicates that if it has been
modified since last build it should also run the commands.
Invoking make
now will run the one and only target available. Do it again and
it says there is nothing to be done. Change something in
src/default_config.json
and try again. Now it has to copy it again.
To improve our Makefile we will make some more additions. Let’s start with a
clean
command.
clean:
rm -f output/default_config.json
I’m using -f
to have it fail silently if there is nothing to remove. clean
depends on nothing and will therefore always run. Unless a file named clean
happens to exist. In that case it will never run because the target already
exists an no dependency will cause it to be rebuilt. To avoid collisions
between commands and filenames we can add .PHONY: clean
to the makefile. Now
clean
is never interpreted as a filename.
Next we add some commands to more easily continue building upon it later.
all: copy_config
copy_config: output/default_config.json
.PHONY: all copy_config clean
With this we can invoke make all
to do everything, placing it first will also
make it the default when invoking make
without arguments. Building
program.exe
could also be included as a dependency here. make copy_config
as short command for only copying the config. Finally, we add all
and
copy_config
to .PHONY
.
In the end we are left with this:
.PHONY: all copy_config clean
all: copy_config
copy_config: output/default_config.json
output/default_config.json: src/default_config.json
cp src/default_config.json output/default_config.json
clean:
rm -f output/default_config.json
Now you’ve gotten a little peek of the utility of Make. You can do much more with variables, wildcards, parallel build tasks, ordered dependencies, and even writing Make code; it is a Turing complete language (though not everything should be done in it).