A build system is a relatively simple functional program that takes code as input and produces deployable software as output. It could be as simple as a makefile or a Visual Studio solution. The most important function of a build system is to compile source code (assuming a compiled language, of course). However, we often want to do more than that as a part of the build process. For example, in a production build system, we might want to:
- Update version numbers
- Run unit tests
- Perform style or coverage analysis
- Coalesce the output of several projects into a single artifact
- Publish the artifact(s) to a publicly accessible location
If your organization has many different software products, it is beneficial to do these things roughly the same way across them all. One-off makefiles and post-build steps in project files won’t scale up to the challenge of consistently building dozens or hundreds of applications across the organization. To meet that challenge, you will want to create a build system that can be shared across all these projects. You should create a build system with the same amount of care as you would create any professional software product (remember, a build system is just a piece of software). Here are a few basic principles that will help you create a high-quality build system for your organization. They’ve certainly helped me.
- The build system should have very few environmental dependencies. Ideally, your build system should not require that anything special be installed on the machine where it runs, with the exception of the scripting language in which it is written. Even better, the scripting language could come along with your chosen platform, like MSBuild (which installs with the .NET framework). If your build system makes use of third party libraries (it probably will), you should deploy those along with your build system. If your build system uses a particular unit test runner, deploy it with your build system. Strive to place as few requirements as possible on the environment in which your system runs.
- The build system should not be dependent on a continuous integration server. It’s very tempting to conflate the notions of “build system” and “build server”. Every aspect of the build system should be runnable anywhere — not just on the “production” build machine. That includes version numbering, unit test execution, and deployable package creation. Resist the urge to use your continuous integration server to define a compile/test/package workflow; implement that in the build system instead (for example, CruiseControl.NET explicitly discourages you from using their NUnit task). This will make it much easier to debug issues that arise during your production build. In fact, I don’t really like the term “production build” — you don’t need a build machine or CI server to have a quality build system. The CI server simply automates execution of the build system. This keeps you loosely coupled to a particular CI server — it should not be a huge challenge to switch to a new one.
- The build system should do all the right things out-of-the-box. The build system is a software product that is used by your organization’s developers. With that in mind, it should be designed for ease of use. It should come prepackaged with subroutines that handle common build tasks your developers need. It should give them a version numbering convention for free. It should automatically find their unit tests and execute them. A developer should find it trivially easy to create a build process for a new product. Following this principle also makes it simple to change your organization’s conventions (on version numbering, for example) across all products. The build system is a software product; it needs to be easy (dare I say enjoyable?) to use.
- The build system should be extensible and flexible. While your build system should perform all your organization’s standard build steps by default, it should remain an extensible platform that your developers can tweak do non-standard things. Implementing your build system as a suite of scripts in a well-known scripting language is an important first step, as this allows your developers to add new components or rewrite existing components if necessary. Beyond this, your build system should be pluggable and extensible, allowing users to swap out one element while keeping the others, inject an extra step, or reorder the workflow. Striking a balance between flexibility and simplicity is what will allow your build system (just like any other software product) to stand the test of time.
If you take the time to create a build system that satisfies these properties, you will have significantly reduced the friction of setting up a new project or altering an existing build. You’ll make it easy to automate all your builds in a standard way and set up a CI server to consume build output. You’ll make the world a little more efficient.