Software developers interact with build systems many times a day. However, development and improvement of the build system is often not a high priority. After all, developers are time-poor and need to devote their energy to the software itself. Once a passable system is in place, it will often be tolerated, and the team will get on with the immediate job at hand.
This attitude belies the fact that a better build system will actually save time in the long run. The best build systems not only build and test the software efficiently and effectively, they can also provide crucial feedback during the development cycle. This feedback helps improve team communication, software quality and overall productivity.
This article describes the properties of an effective build system, starting from the most basic requirements and working towards more advanced features. Follow the article step-by-step to drag your build system out of the darkness and attain build enlightenment!
Before discussing the build system proper, an absolute requirement for an effective build system is use of a source/version control server such as Subversion. Source control brings many advantages, most of which are beyond the scope of this document. Crucially for the build system, the source control server provides a central repository containing the One True copy of the project source. The project source, including the build system itself, should be accessible from the source control server via a simple checkout.
Step 1: Machine Independence
The build must be independent of the machine it is executed on. Rather than having a single "build machine" that is set up to perform builds, every development machine should be capable of executing a full build. Even external dependencies (such as compilers and libraries) should be kept to a minimum, and made available as simply as possible. One way to ensure easy availability is to check these dependencies into your source control server. Some build tools (such as maven) will themselves assist in managing some types of dependencies.
- All development machines are equal: any developers can run the build at any time while working on the product.
- No single point of failure: multiple people and environments both have the resources and know how to make a build.
- Dependency management: consider using your source control server as a central repository for all dependencies. You may also write a script that can set up a development environment automatically from scratch. Investigate tools that handle dependencies for you.
Step 2: Scripted Builds
Put simply, you must be able to build your project by executing a single command from a command-line/shell. The build process must be automatic and repeatable. Scripted builds are typically achieved using a build tool such as ant, make, or MSBuild. The advantage of using such tools is they allow large parts of the build to be scripted declaratively, and have support for common tasks such as dependency analysis. Don't roll your own build tool: leverage what is already available.
- Builds are simple, predictable and repeatable: you need to be able to build the product many times a day during development, an automatic process saves a lot of time and effort.
- The build process is strictly specified by the build scripts: there is no question about how a build is performed, and no risk of losing that knowledge.
- Set up cost: an effective scripted build takes time to design and implement. Use available build tools to reduce development time and experience from previous projects to get a head start. Consider tools that provide a standard project layout and framework out of the box.
Step 3: Scripted Tests
Being able to build your software is not very useful if you have no way to determine whether the built product actually works. To determine this, you need to test the product. Further, developers need to be able to do this frequently (several times a day). The only practical way to achieve this is via an automated test suite. Using one of the many "xUnit" testing libraries (born out of JUnit) you can write code to exercise and verify the functionality of your product. Once you have an automated test suite in place, you should integrate it with your scripted build. With a single command, you should be able to build and test your software.
- Testing (or at least a large part of it) is simple and repeatable: an automatic test suite is much more efficient than manually testing and re-testing.
- Confidence to refactor: a good quality test suite gives developers the confidence to improve existing code knowing that regressions will be detected by the test suite. A better quality test suite gives a higher level of confidence.
- Test development and maintenance: an automated test suite does not come for free. Ensure test development time is spent wisely (don't over-test), and avoid brittle tests that need to be updated frequently. Identify and refactor brittle tests just as you would brittle features.
- Difficult to test code: some code does not lend itself to automatic testing. Use techniques such as mock objects and dependency injection to isolate code for easier testing. Learn where to draw the line and accept that the cost of some automated tests is not worth the benefit.
Step 4: Regular Builds
Now that you can build and test your software automatically, the next stage is to do so regularly. This build should run on one or more independent machines (i.e. machines not used during regular developments). This will help identify unwanted dependencies on the development environment. A regular build provides vital feedback: if your build or tests break, it is best to find out as soon as possible. Think of it as a regular health checkup for your project. Again, various tools (including our product Pulse) are available to help set up regular builds. These tools offer various features to help schedule builds and report on the results.
- Early feedback: when something breaks, you find out soon. The cost of fixing a problem increases the longer the problem goes unnoticed.
- The product is Always Working: a well maintained regular build means you always have a working version of the product, even though it may not be feature complete. The team has confidence that they are on track, and can demonstrate concrete progress.
- Set up cost: there is an initial cost of the build machine and the time required to set up regular builds. Use purpose-built tools to simplify the setup and get more from the results.
Taking it to the Next Level
Step 5: Continuous Integration
Once you have the facility to perform regular builds, a natural question is how often you should execute the build. The answer is simple: as frequently as possible. One particularly useful way to schedule automated builds is whenever a change is committed to the source control server. This facilitates early feedback when a change breaks the build, allowing the problem to be addressed immediately. The longer a build breakage goes unnoticed, the more team members will be affected, and the harder it will be to track down the change(s) that caused the problem. Scheduling builds in this way is one of the core practices of continuous integration. I strongly recommend that you read the original article by Martin Fowler, which both gave a name to and helped popularise this practice.
- Even earlier feedback: tracking down the change that caused a problem becomes trivial. Developers check code in and out with confidence.
- Long build times: a long build is the enemy of fast feedback. Consider breaking larger projects down into dependent modules that can be built and tested quickly. Split test suites so that fast tests are run every build and slower tests only run overnight. Use incremental builds throughout the day and full rebuilds overnight.
Step 6: Automated Releases
By now, you should have a taste for the efficiency benefits of automation. A natural extension of your automated builds is handling releases of your software. This typically includes tasks such as assiging a version number, tagging the project source code, building the product, creating release notes, creating installation packages and publishing them to a repository. By scripting the release process, you make building releases trivial. Apart from being more efficient, this allows releases to be made quickly in response to customer requirements. By releasing early and often, you can enjoy feedback from your customers earlier in the development cycle.
- Reliability: a release process with manual steps introduces the possibility of human error.
- Responsiveness: need to patch a bug and create a new release? An automated release process makes this as quick and easy as possible.
- Reproducibility: need to recreate an old release for some reason? A scripted process (with scripts versioned in source control) allows any given release to be reproduced exactly.
- Automatibility: some steps in the release process may be difficult to automate. Leverage the tools you have, e.g. use your issue tracker to help generate release notes. Automate in stages, gradually removing manual steps over time.
Step 7: Building in Multiple Environments
Most software is designed to support multiple environments, such as different operating systems, runtimes and so on. To properly verify the software, it should be tested in as many environments as possible. This can be achieved by running builds on multiple machines, and/or by parameterising the build script so that it can target different environments. A good automated build system will support building in multiple environments, including the ability to execute builds across multiple machines.
- Early feedback: obscure, environment-specific problems are found quickly as part of the regular build.
- Developer productivity: let your automated system relieve developers of the burden of manually checking many different environments. Developers can work on their platform of choice confident that their work is tested on other platforms.
- Non-trivial setup: a system that performs builds across multiple machines is not trivial to create. Use existing tools that support distributed building and testing rather than wasting development effort.
From the venerable lint to newer products such as Coverity and PMD there are many, many tools that can statically analyse your code and warn of potential problems. After the initial setup cost, an effective static analysis tool will easily pay for itself by detecting problems early in the development process. Integrating static analysis into your automated build system will ensure any problems found by these tools are noticed as soon as they are introduced.
Running your build and test suite frequently provides a prime opportunity for dynamic analysis of your software as it runs. For example, you may employ a code coverage tool such as Clover or BullseyeCoverage to report how well your code is exercised by your test suite. This is especially useful for identifying sections of your code that need further testing.