20 Apr 2015
Until recently, I assumed Makefiles were something only C/C++ and systems programmers touched. Surely there are better tools around now, like Rake and Cake, Ant and Jake, Maven, Grunt, Broccoli, Gulp.
But the truth is Makefiles are not that complicated, and they are powerful.
Anything you can do from the command line you can do with Makefiles. If you want
to incorporate fancytemplateengine in your project, you don't need to search for
and download (or write!) a
grunt-contrib-fancytemplateengine extension. Simply
write out the CLI command as documented by fancytemplateengine.
Lets say we want to compile a SCSS file to CSS. Once SASS is installed, we can
sass --help from the command line to get documentation. Using the sass
CLI tool is simple, so create a blank file
Makefile and add the following lines:
site.css: site.scss sass site.scss > site.css
Then at the command line, run
make and your scss will be built.
How is this any different to writing a shell script? Essentially, Makefiles are like shell scripts that know what needs to be run and what doesn't, based on which files have changed. For large projects, this can speed up build time immensely.
Let's break down the above example. The first line specifies the make 'target', in this case we are saying that we want the file site.css to exist, and that it depends on the file site.scss. If you run make again, you'll get the message:
make: 'test.css' is up to date.
The second indented line specifies the command to be run. In this case, run the sass compiler on site.scss and output STDOUT to the file site.css.
Chances are we'll be building something more than just a css stylesheet, lets assume we have the following folder structure:
client |--- src This folder contains the input to the build process |--- js |--- app.ts |--- models.ts |--- views.ts |--- css |--- client.scss |--- lib |--- jquery.min.js |--- bootstrap.min.css |--- img |--- logo.jpg |--- build This folder contains the output and can be deleted |--- js |--- app.js |--- css |--- client.css |--- lib |--- jquery.min.js |--- bootstrap.min.css |--- img |--- logo.jpg
Now we need to make sure that the folder
/client/build/css exists before we can output anything to it.
client/build/css/site.css: client/src/css/site.scss mkdir -p client/build/css sass client/src/css/site.scss > client/build/css/site.css
The -p option tells
mkdir to create all intermediate directories and not give
any error if the directory already exists.
Note: Directories created with the
-p option are given full rwx permissions
(ie, chmod -R 777) - something to be aware of if you are planning to directly serve
Even if you don't intend to use the optional typing system, TypeScript is great for managing
front-end JS concatenation by using the
references system. What I love about TypeScript is that it gives you
optional ES6 features for free, as well as optional type safety only when you need
example we have multiple
.ts files. While the TypeScript compiler will take care
of including the relevant files where they are requested, Make will also need to
know that the final build depends upon these files.
tsfiles = $(shell find client/src/js -name '*.ts') client/build/js/app.js: $(tsfiles) mkdir -p client/build/js tsc --sourceMap --out client/build/js/app.js client/src/js/app.ts
The first line here creates a new variable
tsfiles and fills it with the
output of a shell command. In this case the shell command is searching for all
client/src/js with the
.ts extension. Now if we change any
of those files, re-running Make will detect that this target needs rebuilding.
Now that we have two make targets, simply running
make will only run the first
target in the file. What we need to do is create a new target which 'requires' the
two targets we have already created. Let's call this target 'client' and put the
following at the top of the Makefile:
.PHONY: client client: client/build/js/app.js client/build/css/app.css
There are no actions specific to the
client target, so we don't need to specify
any commands. Make will automatically detect that both the .js and .css files are
required and build those targets if they need to be updated.
.PHONY target at the top lets Make know that this target is not actually
outputting any files, so it will always run when requested.
A common pattern in Makefiles is to add
rebuild targets to remove all generated
code. This can come in handy when committing to source control or if you make
a mistake in your Makefile. You'll need to add the new targets to the
target as follows:
.PHONY: client clean rebuild ... # Note this target does not have any dependencies clean: rm -Rf client/build rebuild: clean client
Now simply run
make rebuild and the
client/build directory will be cleared
and built from scratch.
Often you'll have images and precompiled assets that you want to use directly
in the frontend, in this case we're using the minified jQuery and Bootstrap
libraries. These should be copied across without any modifications - the easiest
way to do this with the least overhead is to use the infinitely useful
included in most Linux installs.
COPYDIRS = lib img ... .PHONY: client clean rebuild copy $(COPYDIRS) client: copy client/build/js/app.js client/build/css/app.css ... copy: $(COPYDIRS) $(COPYDIRS): mkdir -p client/build rsync -rupE client/src/$@ client/build
Notice that we used a variable in the target name. This will be replaced with
lib img and expanded into two targets, a
lib target and an
When each of the targets are run, the
$@ in the contents will be replaced
with the name of the target. Since we have two directories (lib and img) that
we want to copy, this saves us the work of rewriting the target twice.
tell rsync to copy recursively, update only newer files, and preserve permissions
Makefiles do have some downsides. Although Make is available on most platforms,
the Makefiles themselves are not quite as cross-platform as
This article only touches the surface of what is possible with Makefiles. Using
tools like Facebook's Watchman, its
possible to create a highly flexible
watch target for automatically
rebuilding upon changes. You might even want to trigger an application server
restart, synchronise source with a backup server, or rebuild a Docker image. Anything
that's possible on the shell can be automated with a Makefile.