On makefiles and build scripts

tl;dr Using makefiles for small projects is a significant overhead over simpler solutions such as build scripts. However, ultimately it is worth it because of it encourages re-usable solutions and future-proofs the project.

The virtues of automated builds

Build systems (e.g. Ant, Maven, Make, Rake, Cabal, etc) are a great way to automate a lot of redundant tasks in the software development process:

  • Install dependencies
  • Compile and link
  • Run unit tests
  • Perform static analysis
  • Code style checking
  • Deploy

And all of that with a single shell command: the invocation of your favorite build system.
Furthermore, having a “one-click” way of setting up a piece of software will aid new users and contributors: who hasn’t been discouraged from using a piece of software (or contributing to an open source project) because it required a convoluted process to get running/tested/code reviewed?

But I can do that with a shell script!

Many build systems offer additional advantages that are indispensable for large projects: for instance, many build systems will only re-compile parts of a project that have changed which might save hours of compile-time on large projects.

However, what about small projects? Compile times are instantaneous, development teams are small or just a single person, overall project complexity is lower. Are build systems still worth it over alternatives such as a shell script with similar functionality?

Let us first have a look at some of the disadvantages of using a build system for small projects.

Many developers will already be relatively familiar with shell scripting, for example because they use the command line and Unix utilities to analyse logs. Writing a shell script to compile/deploy/etc will likely not take much time. Build systems, on the other hand, would be yet another skill to learn, hone and maintain.

Build system recipes are often “write once and forget” pieces of code: once a build file works as required, there often is little reason to go back and tinker with things. Therefore, it is likely that developers will have to re-learn the syntax and quirks of their build system whenever they touch it. (I sure do.) Queue context switching, mental overhead, getting distracted with looking up features of the build system rather than getting relevant work done, etc.

Some build systems have a substantial learning curve and often work in non-intuitive ways. GNU Make is an especially striking offender in this regard: superficially makefile recipes look like shell script syntax – but with some surprising gotchas (e.g. separate lines in a recipe fork in different sub-shells, indentation should be tab-stops, etc).

Some tasks are more difficult and non-intuitive to do in a build system than in shell scripts (e.g. handling file names with white-space, trying to do loops or conditionals in declarative makefile style, different ways to set the java classpath in ant, etc).

Basically, using a build system over a simple shell script in a small project will incur a significant mental overhead (yet another infrequently used tool, context switching, etc) at not much directly apparent gain.

And yet you should use a makefile

Nevertheless, using a proper build system over build scripts is very likely the way to go, even for small software projects.

A build system gives the user lots of code for free (e.g. easily referenced command line options, shell tab completion, etc). Not having to re-implement functionality time after time is a good thing.

Using a build system often means that all sorts of functionality is consolidated in one location (e.g. build, deploy, run tests, etc). Handling command line arguments is a bit of a pain in shell scripts – it is therefore likely that multiple shell scripts would be written instead of one build system instruction file. Queue more places to check, more code to maintain, possible code duplication, etc.

Similarly, build system scripts encourage the division of tasks. The natural way of structuring a build system script is to have small targets implementing atomic units of work (e.g. compile or run unit-tests) grouped together by larger recipes (e.g. one target grouping pre-deploy instructions, one target grouping post-deploy instructions, etc). This encourages good design and is likely to make the build system script re-usable, easier to test and maintainable. While the same effect is achievable with shell scripts, the design of the language does not encourage the same extent of division of labor.

On a related note, the complexity of build system scripts grows linearly (just add a target and leave the rest untouched) where build scripts can quickly dissolve into an in-extensible mess (“so I need to add this option here and that flag there, but now I need defaults for this function…”).

Additionally, many build systems operate in a declarative way (as opposed to the imperative nature of scripts). This makes the developer thing about the “what” of their requirements, not the “how”. While initially more difficult to comprehend, this likely leads to more re-usable solutions.

In essence, using build systems over hand-rolled build scripts encourages good development practices, increases re-usability and is likely to lead to maintainable solutions.

Future-proofing for free

The conclusion of the last section ties in nicely with an additional huge advantage of using build systems over build scripts: we get future-proofing for free! A software project being small today does not mean that it has to remain small forever. Using build systems instead of build scripts prepares a project for future growth.

It is much easier to “productionize” a build system recipe than a build script (e.g. GNU Make gives us free parallelism when compiling, running tests, etc with the -J option).

Build systems are an abstraction over the underlying operating system and shell. This makes build system recipes more portable meaning less work when the project has to be deployed to a different environment (e.g. there is a version of GNU Make for Windows).

Many build systems are industry standard ways of managing projects in certain languages (e.g. make for C, Ant for Java, Rake for Rubby, etc). This means that not having a build system recipe means that a project might be difficult to integrate with the rest of the software world (e.g. Qt for Android having a build script instead of a makefile was a show-stopper for integration with mainline Qt).

Using a build system is simply the right thing to do! They are indispensable for large scale software projects. While annoying, using them on small projects is an excellent learning opportunity, gives lots of code and power for free and prepares the project for when it becomes the next big thing.