BuildConfig: Making Build Configuration Information Available at Runtime

Drew Hall

Modern software projects often consist of tens or hundreds of source files and are created and maintained simultaneously by numerous programmers. With the recent blossoming of the open-source movement, an unbounded, geographically diverse set of developers is rapidly becoming the norm. In such environments, tight coordination is needed to guarantee that the source tree seen by each developer or customer is consistent. Thankfully, configuration management tools such as Concurrent Versions System (CVS), Revision Control System (RCS), SourceSafe, and PVCS, do a good job of automating the tedious task of software versioning.

Unfortunately, when a program is compiled and linked from its source files, all records of the source code configuration that went into the build are generally lost. For strongly-versioned software (think shrink-wrap), this is usually not a problem—a bug report from release 3.0 of a product points to a very specific snapshot of the source tree. However, a significant amount of software is developed under less structured release cycles; those who create custom software for a few clients or use rapid-prototyping or Extreme Programming development practices may have some difficulty determining the exact build configuration that resulted in a particular bug.

I created the BuildConfig versioning library after being repeatedly faced with this problem. The premise of the library is fairly simple—make a description of the build configuration available to the program at runtime, so that the exact configuration can be easily recreated when the need presents itself. A common use of the library is to have the program dump a text file containing this information at startup time (perhaps toggled by a command-line switch). When the maintenance programmer needs to reproduce a reported bug, he can simply use this output file to get the relevant versions of each file from his source control tool.

Using BuildConfig

BuildConfig was designed to be as unobtrusive as possible. Although it is necessary to add something to each source (and header) file to enable the BuildConfig capability, I was able to keep it down to a manageable two lines. In the file foo.cpp, we would add the following at file scope:

#include <BuildConfig.h>
FILE_VERSION(“$Revision:   $”, foo_cpp);

Although these lines look somewhat cryptic, they are easy to explain. In the first line, we include the library header file to make its capabilities available. Then, we use the FILE_VERSION macro to enable the configuration tracking capability.

In an ideal world, the arguments to the FILE_VERSION macro would be unnecessary, so it is worth understanding what purpose they serve. Although it may not look like much, the $Revision: $ string serves as a gateway to the programmer’s configuration management software. Through a capability known as keyword substitution (or keyword replacement), most source control systems will dutifully replace this string with something like $Revision: 1.0 $ when the file is first checked in and automatically update the string on each subsequent check-in. Fortuitously, almost all modern source control systems (notably, RCS, CVS, SourceSafe, and PVCS) support this capability using the same syntax that RCS introduced several years ago. Similar capabilities are likely to be available in other source control systems, so check your user manual if your system is not mentioned above.

The second argument serves a less glamorous purpose and is essentially an artifact of the underlying machinery. The FILE_VERSION macro expands into a static construction of a VersionStamp object (see Listing 1) that stores the file name, build time, date, and version number of the file. The foo_cpp argument is simply used to guarantee that this object has a unique name. I would love to find a way to do this automatically, but a better solution has eluded me thus far [2]. The standard trick of appending the line number to a fixed string will fail if you include several header files that all call the macro on the same local line.

By adding the above two lines (and modifying the foo_cpp parameter as necessary) to all source files in the project, we have successfully embedded configuration information in our executable code. But how can we harvest that information at runtime? This task falls to the BuildConfiguration class (Listing 2), a singleton [3] class that collects all the version information in a single place and makes it available through two useful interfaces. To dump all version information to the standard output stream, we would do the following:

#include <BuildConfig.h>
#include <iostream>
// Your header files omitted ...

int main()
{
  BuildConfiguration::Instance().Output(std::cout);
    
  // Rest of program follows
}

Figure 1 shows some example output from the Output() function. The BuildConfiguration class also provides an iterator interface that you might employ if you wish to add customized output capabilities (such as writing to a nicely formatted HTML file), but in practice the Output function has seen much more use. The BuildConfig library is intended to be minimal, rather than pretty, and the programmers who can make use of the data tend to appreciate the simplicity of the output.

How BuildConfig Works

Although the FILE_VERSION macro hides most of the details from the user, at its heart, BuildConfig is made up of just two classes. A static VersionStamp object is created in each compilation unit for each header file that it includes (directly or indirectly), along with another one representing the source file itself. The FILE_VERSION macro declares each object, passing the filename, build time and date, and version information as C-style parameter strings.

In its constructor, each VersionStamp object registers itself (Listing 3) with the BuildConfiguration singleton instance, which stores a pointer to that object in a standard set. By customizing the comparator function used by the set, we compare the objects themselves, rather than the pointers. As a result, the BuildConfiguration ends up with a single VersionStamp pointer for each file in the entire project, sorted alphabetically by file name.

Employing the Singleton pattern in the BuildConfiguration class is absolutely crucial. We need to ensure that all VersionStamp objects register themselves with the same BuildConfiguration object, which indicates that we should consider the Singleton and Monostate patterns [4]. However, since all the dirty work happens during static initialization, only Singleton allows us to guarantee that the BuildConfiguration object will be available when it is first needed. Although the Monostate pattern presents a cleaner interface, its dependence on class-static (rather than function-static) data makes it impossible for Monostate to make this same guarantee.

Conclusion

I have found the BuildConfig library to be an indispensible tool for navigating some of the uncharted backwaters of configuration management. BuildConfig was designed to be as simple as possible to use, and works well with a wide variety of source control tools. I hope that it will find a home in your software toolbox. The complete source code is available online at www.cuj.com/code.

References and Notes

[1] This example assumes the programmer uses CVS, RCS, SourceSafe, or PVCS. Modification of the “$Revision: $” string may be necessary for other source control systems.

[2] Suggestions would be greatly appreciated.

[3] Gamma, et. al., “Design Patterns: Elements of Reusable Object-Oriented Software”, (Addison-Wesley, 1995).

[4] <http://www.objectmentor.com/resources/articles/SingletonAndMonostate.pdf>.


Drew Hall is a graduate student in computer science at the University of California, Santa Barbara. Prior to returning to school, he developed simulation software for the defense industry. He can be reached at ahall@cs.ucsb.edu.