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
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.
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
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
argument if the user wants to decide where to install it. Specifying
PREFIX=/usr will attempt to install it to
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
make clean, also removes any files created building external
dependencies (e.g., a submodule or downloaded files) to ascertain a totally
🔗 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.
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
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
: 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.
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: rm -f output/default_config.json
-f to have it fail silently if there is nothing to remove.
depends on nothing and will therefore always run. Unless a file named
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.
as short command for only copying the config. Finally, we add
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).