Simple Frontend Builds With Makefiles
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.
Compiling SASS to CSS
Lets say we want to compile a SCSS file to CSS. Once SASS is installed, we can
type 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.
More Complex Folder Structures
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
those files.
Concatenating JavaScript with TypeScript
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
it, while still allowing you to write pure JavaScript if you want to.
Compiling TypeScript to Javascript is similar to SASS to CSS, except that in our
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
files inside 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.
Multiple Make Targets
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.
The .PHONY
target at the top lets Make know that this target is not actually
outputting any files, so it will always run when requested.
Clean and Rebuild Targets
A common pattern in Makefiles is to add clean
and 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 .PHONY
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.
Copying Images and Other Assets Directly
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 rsync
tool
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 img
target.
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.
The -rupE
switches
tell rsync to copy recursively, update only newer files, and preserve permissions
and executability.
Final Comments
The full Makefile is available as a GitHub gist.
Makefiles do have some downsides. Although Make is available on most platforms,
the Makefiles themselves are not quite as cross-platform as
the JavaScript based front-end build configurations, since we’re relying on shell
tools like rsync
.
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.
Further Reading
- The Ultimate Frontend Build Tool:
make
The Rdio Engineering Blog - GNU Make Manual Online gnu.org
- Using GNU Make as a frontend build tool mikkolehtinen.com
- TypeScript
- SASS Syntactically Awesome StyleSheets