VR Juggler

The Build System

Patrick Hartling

2.0.0

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with the Invariant Sections being Appendix C, GNU Free Documentation License, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in Appendix C, GNU Free Documentation License.

$Date: 2005-01-01 11:03:46 -0600 (Sat, 01 Jan 2005) $


Table of Contents

Software Choreography
I. Introduction
1. The Philosophy
2. Goals of Doozer++
Ease of Use
Platform Independence
Compiler Freedom
Language Freedom
Separation of Tools and Tasks
Configuration
Compilation
Further Developments
3. Goals of the VR Juggler Build System
Centralize Complexity
Minimize Special Steps
Port Quickly to New Platforms
II. Design
4. The “Global” Build
Follow the Rules
Required Targets
File and Directory Names
Installation Hierarchy
configure.pl
Build Configuration File
Dependency Management
The *-config Scripts and the *.m4 Macro Files
A Typical -config Script
A Typical .m4 File
The Developer Installation
Future Goals
5. The Documentation Build
The Rules
Required Makefile Targets
Installation Hierarchy
Configuration and Customization
III. Extension
6. Extending the Global Configuration Script
JugglerConfigure.pm
JugglerConfigure
JugglerModule
ModuleDependency
conifgure.pl
Command-Line Argument Handling
Help Output
Build Configuration
File Regeneration
7. Extending a Module's Configure Script
Basic Concepts
Comments
Language
Variable Naming
Parameters to Macros
Custom Preprocessor Information
Structure of a configure.in File
Initialization
Custom Arguments
System-Dependent Setup
External Program Checks
External Library Checks
Header File Checks
Library Function Checks
Makefile Substitution
File Generation
Specific Modules
Tweek
8. Extending a Module's Makefiles
Makefile Conventions
File Names
Variable Assignments
Variable References
The Glue Makefile
Basic Structure
Auxiliary Files
Extension
Individual Component Makefiles
Basic Structure
Extension
Specific Modules
Multiple Libraries in VR Juggler
Java in Tweek
IDL in Tweek
IV. Appendices
A. Helper Scripts
mtree(1) and mtree.pl
install-dir.pl
install-src.pl
makefiles-gen.pl
InstallOps Perl Module
make-ver.sh
mkmakefile.pl
B. Build System Usage
C. GNU Free Documentation License
PREAMBLE
APPLICABILITY AND DEFINITIONS
VERBATIM COPYING
COPYING IN QUANTITY
MODIFICATIONS
COMBINING DOCUMENTS
COLLECTIONS OF DOCUMENTS
AGGREGATION WITH INDEPENDENT WORKS
TRANSLATION
TERMINATION
FUTURE REVISIONS OF THIS LICENSE
ADDENDUM: How to use this License for your documents
Glossary
Index

Software Choreography

Within this book, we present the fundamentals of the VR Juggler 2.0 build system, a complex piece of software in its own right that manages the compilation of the various modules that make up VR Juggler 2.0 (and beyond, I suspect). Those few readers who may be familiar with the VR Juggler 1.0 build system may recognize a few similarities, but truly, the Doozer++-based build is quite different than what was originally intended to be backwards compatible with an IRIX-only build written for the first version of VR Juggler (then called VRLib).

Since 1998, the Autoconf-based VR Juggler build system has grown more and more complex. Originally, the build made use of Autoconf, GNU make, and a few small Perl scripts to simplify installations. Now, in mid-2002, the build system still makes use of all those tools, but it has many, many custom m4 macros (most of which come from Doozer++), makefile stubs, Autoconf-like scripts written in Perl, and custom CVS utilities. It may yet evolve to include Java- and Python-based tools.

With all this (growing) complexity, the VR Juggler 2.0 build system is a colossal effort in software choreography wherein all the pieces have to come together in such a way as to exude a Broadway-caliber performance. In this book, I strive to lay out the steps like one of Arthur Miller's finest teachers.

Part I. Introduction

In sooth, I know not why I am so sad:

It wearies me; you say it wearies you;

But how I caught it, found it, or came by it,

What stuff 'tis made of, whereof it is born,

I am to learn;

And such a want-wit sadness makes of me,

That I have much ado to know myself.

--William Shakespeare, The Merchant of Venice, Act 1, Scene 1

In this first part, we explain the basics of the VR Juggler build system. We will begin with the goals of the Doozer++ software package which forms the basis for the entire build. We move on to the specific goals of the VR Juggler build system. The scope of the VR Juggler build, while broad, is narrower than that of Doozer++, and hence, it will be useful to understand first why Doozer++ exists and what purpose it serves.

Chapter 1. The Philosophy

Nearly everything has a philosophy, and the VR Juggler build system is no different. The build system philosophy has evolved from the following observation: programmers think more about writing their code than about compiling it. Compiling takes time that could be spent writing code, so compiling must be fast. Command-line arguments have little to do with code, and thus they are easily forgotten. Paths to external dependencies are esoteric file system thingies that are never referenced in code. Because of these points, the basic philosophy of the VR Juggler build system and of Doozer++ in general is simple: automate everything.

Automation in a build system means doing as much as possible to avoid requiring programmers to type anything beyond the following familiar command:

configure ; make ; make install

Whether this is always possible is highly debatable, but either way, it should always be the goal. If the above cannot be achieved, some mechanism must exist so that programmers feel as though that is all they ever have to type.

There is much more to the philosophy of the VR Juggler build system than just automation, but someone looking at the code would see that steps to automate configuration and compilation make up a sizable percentage of the total line count. Beyond simplifying the process of building, we also have the goal of simplifying extensions to the build system.

Unfortunately, repeated experience has shown that no amount of simplification is enough to satisfy even the most patient of users. This hearkens back to the original point stating that programmers focus more on writing their code than on compiling it. A build system is always going to be foreign to the majority of the people on any given team, and taking the time to learn and understand a build system takes up valuable coding time. Despite these bleak statements, it is possible to put together a build system that requires little effort to extend when a new file must be compiled or a new source directory is added. Those who hope for more than this are likely to be let down.

More details on the goals of Doozer++ and the VR Juggler build system are provided in the next chapter. All of these goals are founded by the philosophy discussed above.

Chapter 2. Goals of Doozer++

When the first Autoconf-based VR Juggler build system was started in mid-1998, we had to make a decision: should we use Automake in addition to Autoconf? At the time, we felt that there were several problems with Automake, including the following:

  1. The generated makefiles were too complex to debug

  2. Use of compilers other than GCC was too difficult

  3. Restrictions on the structure and contents of the source tree were undesirable

In four years, we have only been proven wrong on point #3. In our experience, Automake still generates extremely complex makefiles (though it does it very nicely from extremely simple input), and use of compilers such as the MIPSpro Compilers or Microsoft Visual C++ is still very hard.

Based on our needs for VR Juggler (and many other projects that have been developed at Iowa State University's Virtual Reality Applications Center), we identified the following as key goals for Doozer++:

  • Simplify the use of Autoconf

  • Allow the use of any operating system

  • Allow the use of any compiler

  • Allow code to be written in any language

  • Use the best tool for a given task

Each of these goals will be addressed in the following sections.

Ease of Use

Time and time again, we have seen a resistance to the use of Autoconf because of its (seemingly) arcane language constructs. While macro languages are difficult to use and sh(1) syntax is not immediately intuitive, neither tool can be considered arcane. Indeed, sh(1) is a much more powerful language than most other shell scripting languages, but because of its unique syntax, people tend to shy away from its use.

Further complicating the issue is the complexity of Autoconf in general. It contains many, many macros, and it defines rules for the order of executing those macros. In order to use Autoconf effectively, build system developers must read a fair amount of documentation (all of which is readily available). In our experience, however, many people feel that development build system software should be immediately obvious[1] and should not require much effort to learn.

As a result of these issues, we have attempted to make Doozer++ easier to use than “raw” Autoconf-based configure scripts. Doozer++ macros hide some common details that often trip up configure script authors. Many utility macros are provided to reduce code duplication between configure.in files. For example, code for verifying that an existing installation of a software package meets a version requirement is provided. The code for performing the check is modularized to separate the common tasks involved with this. Namely, Doozer++ provides macros comparing two version numbers, acting on the results of the comparison, and caching the results to speed up future tasks. Each macro is used as the basis for the next so that users have the freedom to choose how they want their code to behave. We feel that this offers a level of flexibility not available with Autoconf 2.13.

Platform Independence

Because VR Juggler has always been a cross-platform tool, we have had the need for a cross-platform way to build it. The VR Juggler 1.0 build system achieved this to a limited degree, and Doozer++ goes a step further toward true platform independence. At this time, Doozer++ makes use of software utilities found on all modern operating systems. The list of utilities includes m4(1), sh(1), Perl, Autoconf, and GNU make. We have avoided the use of platform-specific tools because it is all too easy for the platform-specific parts of a build system to get out of sync with each other. Furthermore, we have tried to avoid the use of platform-specific code whenever possible. This is a more difficult goal to achieve, but later sections will address the extent to which platform-specific code can be reduced.

Compiler Freedom

As a cross-platform C++ library, VR Juggler must be compatible with the prevailing C++ compiler on a given platform. In the case of IRIX, that is SGI's MIPSpro Compilers. Similarly, the use of Microsoft Visual C++ is crucial on Win32 platforms. Open source operating systems such as FreeBSD and Linux use GCC, so we have not ignored that compiler whatsoever. Due to our limited needs, however, we have focused on compilers for the C, C++, Java, and IDL languages. Nothing prevents Doozer++ from being extended to allow compiling of code written in FORTRAN, Ada, Pascal, etc.

In Doozer++, an m4 macro sets up the basic platform-specific pieces. User-level code (in an Autoconf configure.in file) then uses that information to execute other macros that pick the appropriate compiler. This builds upon the foundation provided by Autoconf for detecting installed programs, but it goes further by allowing users to associate one or more compilers with a given platform. The end result is a “fallback” system wherein users specify the preferred compiler and zero or more alternatives if the preferred compiler is not available. To achieve this flexibility, the Doozer++ m4 macros must have no compiler-specific bits.

Language Freedom

Language freedom is slightly more complicated than compiler freedom. As will be discussed more fully in the following section, Doozer++ separates its work into two pieces: configuration and compilation. During configuration, the tools for compilation are chosen; during compilation, the chosen tools are put to use.

The primary project building tool is GNU make. We chose GNU make initially because of its portability and because it offered useful features over most basic make(1) implementations. Other implementations, such as BSD make, offer even more features, but they are not as portable as GNU make.

Because Doozer++ uses GNU make, users must write makefiles. Doozer++ provides a number of modular makefile “stubs” that collect common functionality. In particular, these stubs provide support for compiling code written in Java, C, C++, and IDL. Adding support for other languages can be done within Doozer++ or within user-level makefiles.

Separation of Tools and Tasks

The design of Doozer++ involves a distinct separation between the tools used and the tasks performed. As a result of this separation, there exists the possibility to extend Doozer++ to use tools other than Autoconf and GNU make. Moreover, different combinations are allowed. For example, we have found that make(1) does not deal well with compiling Java code in general. A more appropriate tool for this task has come out of the Apache Jakarta Project called Ant. Within Doozer++, we could make use of Ant instead of or in addition to GNU make to compile Java software. Nothing restricts users to Autoconf and/or GNU make.

The basic idea behind the separation is the following: detect and configure the static information needed to execute tools that compile a (potentially complex) software system. This leads to a two-step process: configuration and compilation. These steps are described in the following subsections.

Configuration

During the configuration stage, we determine what tools are available that meet a given set of needs. Typically, this means finding suitable compilers and determining what options the compilers support. Of course, most projects have more complicated needs than this. For example, VR Juggler can be compiled with several different versions of GCC (2.95.3 through 3.1 as of this writing). When moving between platforms, the GCC C++ compiler will almost always be called g++, but it may not always be the same version[2]. For that reason, it may be necessary to perform several version-specific detection steps, including, but not limited to, the following:

  • Whether a given option is required or even supported (for example, -fexceptions or -LANG:<arg>)

  • What header files are available (hash_map.h, hash_map, ext/hash_map, or none of the preceding, for example)

  • What libraries are needed for linking shared libraries or executables (libdl, libposix4, or ws2_32.lib, for example)

Moving beyond compiler-specific issues, it may be necessary to detect the installation of third-party libraries or programs. Often times, simply finding an installation is not sufficient. The version of the installation must also be checked to ensure compatibility with the user's project.

This all boils down to one thing: automation. The job of the configuration step is to automate as much as possible. In so doing, the code used to write the compilation can be simplified. In effect, the compilation step becomes a very generic process that is configured based on many platform-, compiler-, and site-specific details that cannot be detected easily by a tool designed for compiling.

Compilation

As we just described in the preceding section, the compilation step should be a very generic process. Compiling software tends to be a very serialized or step-by-step process. Subsequent steps depending on proper completion of preceding steps. Nothing in particular about compiling software (or code generation in general) has to be tied to a specific set of tools. The process should depend more on the source code and what steps are necessary to generate the desired outcome.

While it is possible to construct build system software that does everything in the compilation step (a la Doozer proper), such systems tend to be very inflexible. Everything that could provided more generically through the configuration phase must be defined statically in makefiles (or whatever specification file is being used to direct the build process) and in the code. For example, consider the following C++ code:

#if defined(HAVE_HASH_MAP)
#include <hash_map>
#elif defined(HAVE_EXT_HASH_MAP)
#include <ext/hash_map>
#elif defined(HAVE_HASH_MAP_H)
#include <hash_map.h>
#else
#error "std::hash_map is not available with this compiler"
#endif

The code above is not tied to any specific compiler or any specific compiler version. Now, consider the following code that achieves the same result:

#if defined(__GNUC__)
#  if __GNUC__ < 3 && __GNUC__ >= 2 && __GNUC_MINOR__ >= 95
#     include <hash_map>
#  elif __GNUC__ >= 3
#     include <ext/hash_map>
#  else
#     include <hash_map.h>
#  endif
#elif defined(__MSVC_VER__)
#  if __MSVC_VER__ >= 7
#     include <hash_map>
#  else
#     error "std::hash_map is not available with this compiler"
#  endif
#elif defined(__sgi__)
#  include <hash_map>
#else
#  error "std::hash_map is not available with this compiler"
#endif

The former is much shorter and hides all the platform- and compiler-specific details in the definition of the various HAVE_* symbols. The latter code is clearly much more complicated and only supports three compilers: GCC, Visual C++, and MIPSpro. (The latter example may also have inaccuracies. Think of it more as pseudo-code than something taken from real C++ code.) Certainly, the latter could be simplified by adding command-line options defined in platform- and compiler-specific makefile stubs (-DHAVE_HASH_MAP and so on), but such options would have to be provided for all possible cases. That method has obvious scalability problems, however.

Many other projects provide a variety of platform- and tool-specific makefile stubs that set up a build environment at the time of compilation. This inevitably leads to an ever growing number of stubs as the software becomes more portable or as external tools (especially compilers) evolve. For example, omniORB 3.0 (an excellent, freely available ORB implementation) has forty makefile stubs, four of which are for use on varying configurations Linux/i386. The upcoming omniORB 4.0, on the other hand, uses Autoconf to separate the platform-specific pieces from the generic, platform-independent compilation work.

Further Developments

There are many long-term goals for Doozer++ that are beyond the scope of this document. As should be evidenced from the previous sections, however, some near-term goals include the use of project builder tools besides make(1).



[1] Of course, little, if anything, is “immediately obvious” in reality. Users tend to want something that is similar to existing tools or something that allows them to make use of existing knowledge.

[2] On a platform with multiple GCC installations, the executable names typically vary based on the version. For example, a FreeBSD 4.x installation with multiple GCC builds may have the executables g++, g++30, g++31, and g++32 for versions 2.95.4, 3.0, 3.1, and 3.2 respectively. On RedHat Linux 7.2, g++ is GCC 2.96 while g++3 is GCC 3.0.4.

Chapter 3. Goals of the VR Juggler Build System

The VR Juggler 2.0 build system is based on Doozer++, and thus the goals of Doozer++ extend into the VR Juggler build system. The VR Juggler build system has a much narrower scope than Doozer++, so it has some unique goals, and they are as follows:

  • Centralize the complexity of the build code

  • Minimize what users must remember to configure and compile the source

  • Port quickly to new platforms

In the following sections, we discuss each of the above goals in more detail.

Centralize Complexity

Due to the size of the VR Juggler 2.0 source base (nearly 500,000 lines of C++ and Java by one estimate), it is important that the complexity of compiling be centralized rather than spread out over the entire source tree. Centralizing the complexity allows most of the work to be done once, the results of which can then be reused by the rest of the build code. This is the key tenet for the other goals.

Centralization of complexity allows the VR Juggler build to follow the goal of Doozer++ wherein tools and tasks are separated based on what they do. The result is that each of the module's has an Autoconf- and Doozer++-based configure script that bears the brunt of the work needed to hide complexity. Then, each module has one or more glue” makefiles that pull everything together. These glue makefiles typically direct a given module's build process so that steps occur in the correct order. Because they oversee the whole compilation process, they are often large files with many targets, each of which is responsible for a specific task. With a configure script and a glue makefile, all that remains is a listing of source files that must be compiled. The makefiles that list source files are often very, very short and easy for anyone to extend.

With this foundation, the complexity is separated thusly:

  • Platform-specific work happens in a configure script where a flexible programming language is readily available

  • Flow of execution during compilation is directed by a single, often long, glue makefile

  • Source listings (for any language) are placed in short makefiles that are called by the glue makefile

Other steps have been taken to offload complexity or repeated tasks into centralized components. For example, the directory juggler/macros contains Autoconf/m4 macros that can be used in configure scripts. Most of the macros are used to detect and provide information about the various modules that comprise VR Juggler. There are a few helper macros that build on top of Doozer++ and common idioms we have used to simplify configure scripts further.

Minimize Special Steps

As has been discussed at great length previously, it is important that users of the build system be freed from remembering many command-line arguments or special steps that must be taken to build a given piece of software. With several modules to configure and build, there is a lot to know about compiling VR Juggler. Most modules have external dependencies, the paths to which must be specified using command-line arguments. Furthermore, some modules may be compiled differently based on configuration-time settings. The so-called “global build” manages all of this, and so it has the responsibility of reducing the amount of information users must remember.

To that end, the global build offers some features for remembering command-line arguments so that users do not have to. Many of the configure scripts for the individual modules are written such that the default values for arguments are reasonable enough that users rarely have to pass them. The combination of these two features usually allows the typical developer to run the global configure script with no arguments. While this is not always the case (for example, on Win32, the path to NSPR must be specified unless the user has done something special in his or her environment), it happens often enough to keep people reasonably happy.

Port Quickly to New Platforms

VR Juggler as a whole is a cross-platform software system. Porting to new operating systems is a non-trivial task, and spending time porting the build system to new platforms is time that could be spent more effectively. Speaking less abstractly, before the source code can be ported to a new platform, the build system has to be capable of compiling the source code on the new platform. For that reason, it is important that the entire build system can be put to use right away so that the attention can be devoted to the more difficult task of porting C++ code.

The VR Juggler build system is limited in its portability by the portability of Doozer++. To its credit, Doozer++ is more portable than VR Juggler at this time, so the VR Juggler team still has some room to move. Even so, the VR Juggler build has its own quirks, and thus, people writing the code to build VR Juggler must always have portability in mind. For example, “BASH-isms” must never appear in VR Juggler configure scripts or makefiles. Most Linux distributions may use BASH for /bin/sh, but that certainly does not meant hat all operating systems vendors follow that unfortunate trend.

In keeping with the Doozer++ goal of separating tools and tasks, the VR Juggler build system offers good portability by putting all of the platform- and software-specific pieces in Autoconf configure scripts. In so doing, the makefiles rarely, if ever, have to be modified when a new platform is added to the list. Furthermore, makefiles for GNU make lack sufficient programmatic constructs to provide developers with the ability to write tests that provide more than limited portability. While the Doozer software is relatively portable through its use of GNU make and platform-specific makefiles, it is missing the expressiveness of something based on Autoconf (or a similar tool). As discussed in the previous chapter, the use of make(1) alone for portability requires much effort to make hard-coded, platform- and site-specific makefiles.

Part II. Design

Three Rings for the Elven Kings under the sky, Seven for the Dwarf Lords in their halls of stone, Nine for Mortal Men doomed to die, One for the Dark Lord on his dark throne, In the Land of Mordor, Where the Shadows lie. One Ring to rule them all, One Ring to find them, One Ring to bring them all, And in the darkness bind them, In the Land of Mordor, Where the Shadows lie.

--J.R.R. Tolkien, The Lord of the Rings, Volume 1, page 81

Chapter 4. The “Global” Build

In this chapter, we present the design of the so-called “global” build. We cover the high-level aspects of the build system that ties together all the other build systems. In a sense, this is the one build system to rule them all.

Follow the Rules

In order for the global build to work, the modules it wraps must follow certain rules. If a module does not comply with all the rules, there is no guarantee that it will be able to compile under all circumstances. In other words, “rogue” modules that only implement a few pieces of the puzzle quickly become the weak link in the chain, and module build system authors who want to live outside the rule set complicate matters for everyone else.

Because the VR Juggler build system is a complicated dance, there are many rules that must be followed. For example, certain targets must be defined so that recursive make(1) calls can proceed through the entire source tree. These targets include 'release', 'install-debug', 'links', and 'buildworld'. Other rules include restrictions on the names of files or directories, use of platform-specific conventions, the presence of a working -config script (see the section called “The *-config Scripts and the *.m4 Macro Files”), and provisions for the detection of a usable installation. The full list of rules are provided in the following subsections.

Required Targets

There are a number of targets required by the global build. Some of these were listed above. The following sub-subsections give a complete list of all targets that must be implemented by a module's glue makefile. The targets are grouped by the task they perform.

Build Targets

The build targets have the job of building source code. What this means is up to the individual module. It may, for example, include the generation of source code using tools such as an IDL compiler, an XSLT processor, or a Java compiler. The targets need not do anything if, for whatever reason, the generic concept of building source code does not apply.

build

This target builds everything. It executes the first phase of the 'world' target (i.e., only the build phase, not the install phase). Since it builds both debugging and optimized versions of a module without installing, it is useful for testing changes to the library code to ensure that it works in both the debugging and optimized cases. A profiled version of the module may also be built if the module build system supports that and the compiler can build such a version.

buildworld

This target is the same as 'build'.

debug

Build only the debugging version of the module. If the target platform supports both types, static and dynamic versions are compiled. In other words, the module is built so that debugging symbols are turned on. It is the combination of 'dbg' and 'dbg-dso' (see below). This is the default target and is what gets built if running make(1) with no arguments.

optim

Build only the optimized version of the library binaries (both static and dynamic). This is built with no debugging symbols at all. It is the combination of 'opt' and 'opt-dso'.

profiled

Build only the profiled version of the library binaries (both static and dynamic). This capability is dependent on the compiler being used. Not all compilers support the process of generating profiled code, so this target may have no effect. Profiled libraries are built with debugging symbols. This target is the combination of 'prof' and 'prof-dso'.

dbg

Build only the static debugging version of the libraries. This does the same thing as 'debug' but does not compile the dynamic libraries.

dbg-dso

Build only the dynamic debugging version of the libraries. This does the same thing as 'debug' but does not compile the static libraries.

opt

Build only the static optimized version of the libraries. This does the same thing as 'optim' but does not compile the dynamic libraries.

opt-dso

Build only the dynamic optimized version of the libraries. This does the same thing as 'optim' but does not compile the static libraries.

prof

Build only the static profiled version of the libraries. This does the same thing as 'profiled' but does not compile the dynamic libraries.

prof-dso

Build only the dynamic profiled version of the libraries. This does the same thing as 'profiled' but does not compile the static libraries.

Installation Targets

The installation targets set in motion the process of installing a module. As with build targets, what this means may vary from module to module. Each module is responsible for ensuring that its installation hierarchy exists before trying to copy files.

install

This is the complement to 'build' (described in the section called “Build Targets”), and in most cases, it is assumed that the build was performed before an installation is attempted. This target executes the second phase of the 'world' target. It performs a complete installation of debugging and optimized versions of a module. Installation of a profiled build will be performed if a profiled version was generated. Further, both the dynamic and static versions of a module will be installed if the target platform supports both. (This is of course assuming that the module builds one or more libraries.)

installworld

This target is the same as 'install'.

install-debug

Install only the debugging version of the module. If the module includes one or more libraries, both static and dynamic versions of the libraries are installed.

install-optim

Install only the optimized version of the module. If the module includes one or more libraries, both static and dynamic versions of the libraries are installed.

install-profiled

Install only the profiled version of the module. This may have no effect if the module build system does not support building profiled code or if the compiler cannot generate profiled code. If the module includes one or more libraries, both static and dynamic versions of the libraries are installed.

Multi-Step Targets

There are a few multi-step targets required by the global build. Essentially, these targets perform builds and installations, though they do not necessarily build and installed exactly the same thing. They are intended to be used for making releases or for users who simply want a one-step build/install of a module.

world

Clean up the build environment and then build and install everything using the default ABI and ISA. This is a simple target for those who just want to build and install the module as simply as possible. “Everything” in this case is the following:

  • Debugging, optimized, and profiled versions of the library binaries

  • Shared and static versions of the library binaries (if both are supported on the target platform)

  • Header files

  • Sample applications, test code, user tools, etc.

  • Data files (sample config files, model files, whatever)

world-all-abi

This is the same as the 'world' target except that it builds and installs all possible ABI and ISA combinations for the target platform. On IRIX, for example, this means that all combinations of N32, 64, mips3, and mips4 (debugging and optimized versions) are built and installed. Most platforms currently support only one ABI/ISA combination thus making this target the same as 'world'.

Note

As of this writing, the global build does not have this target. Some modules in the Juggler Project still do not support building multiple ABIs.

release

This target is similar to 'world' except that the installation tree is suitable for redistribution. Extra files such as the change logs, the release notes, and the license files are installed. In addition, the tree is stamped with a build time to help track possible differences between two releases of the same version. (This has only occurred for one VR Juggler beta release, but it seems like a good idea to have the build time included with a distribution.)

release-all-abi

This is the same as the 'release' target except that it builds and installs all possible ABI and ISA combinations for the target platform. On IRIX, for example, this means that all combinations of N32, 64, mips3, and mips4 (debugging and optimized versions) are built and installed. Most platforms currently support only one ABI/ISA combination thus making this target the same as 'release'.

Note

As of this writing, the global build does not have this target. Some modules in the Juggler Project still do not support building multiple ABIs.

Clean-Up Targets

There are three targets used to clean up the build environment. Each cleans the tree to a different degree. Of the following three, 'clean' and 'cleandepend' remove disjoint sets of files. The 'clobber' target performs at least the tasks of 'clean' and 'cleandepend'.

clean

Clean up everything in the build environment. This uses the 'clean' target defined by Doozer++ that is automatically included by all makefiles. The cleaning process is recursive just as the build process is. Each makefile may define which files are safe for cleaning, but generally core files, compiler-generated files, and object files are the only things removed during this process.

cleandepend

Clean up the automatically generated dependency files (the .d files in each directory). This method for cleaning up deletes only these files and nothing else—ever.

clobber

Clean up (clobber) the entire build environment except what was generated by configure. This runs the above clean-up targets and removes the object directory(ies) and lib directory(ies). Its purpose is to reset the build environment to its state just prior to running configure.

Developer Targets

Finally, there are two targets that are relevant only to developers. These relate to the developer installation (see the section called “The Developer Installation”). One creates the developer installation, and the other removes it.

links

Set up the developer pseudo-installation environment.

clean-links

Remove the developer pseudo-installation environment.

File and Directory Names

There are certain naming conventions for files and directories that must be followed in order to ensure consistency among all the modules in the Juggler Project. Not all of these relate directly to the build system, but the names may be influenced by the way the build system works.

Autoconf-Generated Header File

All the modules (save one) make use of an Autoconf-generated header file that sets up #defines based on tests performed by the module's configure script. To avoid overlap or confusion, these files are named based on the module's C++ namespace. Furthermore, the header files must be generated within the module's unique header directory. For example, in Gadgeteer, the C++ namespace is gadget. Hence, the header file is gadgetDefines.h, and it is generated to the gadget directory.

The reason for the redundancy is to prevent user errors by avoiding ambiguities. Consider the following bit of code:

#include <defines.h>
#include <vpr/Sync/Mutex.h>
#include <vrj/Kernel/Kernel.h>

Now, for the sake of this example, assume that a user had both -I$VJ_BASE_DIR/include/vpr and -I$VJ_BASE_DIR/include/vrj on his or her command line. If both VPR and VR Juggler had a defines.h file, there would be no way to distinguish which is which. While this is a textbook case of operator error, the naming convention we use avoids this case entirely.

The problem arises because the generated header files do not include any other headers in the project. As a result, the generated headers are not tied as tightly to the directory structure as are the static headers. Hence, the above case is not so far-fetched. It could happen very easily with an inexperienced user.

Module Configuration Header

Each module has a single header that includes the header file generated by running configure. The idea here is to have a single point where common actions are taken based on what comes in through the generated header. For example, based on platform settings, symbol export macros are defined in the module configuration header. This single header is then included by all the other files in the project.

The naming convention for the module configuration header is the same as that of the generated header except that the word “Config” is used instead of “Defines”. The reasoning for this convention is similar to that of the generated header, but in this case, at least one other file from the project is being included. Namely, the generated header file is always included on the first (non-comment) line of this header file. We decided long ago that the name Config.h was too common and needed an extra bit of uniqueness. Again, this is done to prevent user errors.

Data Directory

In VR Juggler 2.0, the installation of multiple modules must be managed so that one module's (optional) extra data does not conflict with that of another module. All data must be installed into the directory $(prefix)/share (to use some make(1) notation). To prevent conflicts with other modules (and with other software that may already exist on the target machine), each module must name a project data directory (the variable $(projdatadir) is used to store this in the makefiles). In most cases, the unique directory should be the name of the project in lowercase letters with no spaces. For example, the directory for JCCL would be $(prefix)/share/jccl, and the directory for VR Juggler would be $(prefix)/share/vrjuggler.

Installation Hierarchy

The structure of an installation hierarchy is fairly open, but there are several basic requirements. They are as follows:

  • Headers go in $prefix/include. Ideally, a module will use a subdirectory of that for its own header files.

  • User-accessible executables/scripts go in $prefix/bin.

  • Libraries go in subdirectories of $prefix/lib (or a variant thereof depending on platform-specific conventions). More specifically, optimized libraries go in $prefix/lib/opt, debugging libraries go in $prefix/lib/debug, and profiled libraries go in $prefix/lib/profiled. There is further subdivision within those directories based on the binary format (ELF, a.out, etc.) and the instruction set architecture (i386, i686, mips4, sparc, etc.). To make things more convenient for users, symlinks to (or copies of depending on the host platform) libraries should be made in $prefix/lib. For full releases, we make symlinks to the optimized libraries. In the developer installation, we make symlinks to the debugging libraries. Typically, profiled libraries will be named differently than their non-profiled counterparts, so symlinks to those can be made along side the non-profiled versions.

  • Project data files and sample code goes in $prefix/share/<project-name>. The use of the <project-name> subdirectory is to avoid conflicts with existing software.

configure.pl

The script configure.pl is a Perl script written to act as part of a build wrapper around an arbitrary collection of software modules. The modules are linked through some sort of (conceptual) dependency graph, in this case specified by a simple configuration file (see the section called “Build Configuration File”). configure.pl reads the configuration file and proceeds to configure the individual modules in an order that satisfies the dependencies. Along the way, environment variables are set or extended so that each subsequent module is configured with the correct settings. The processes of managing the dependencies and performing the configuration are the topics of this section.

Build Configuration File

Before explaining how configure.pl manages its dependencies, it will be helpful to understand the role played by the configuration file from which the dependencies are read. In the VR Juggler build system, the file is called juggler.cfg, and it is located in the top-level source directory. At a very high level, the configuration file defines one or more modules that must be configured and compiled. The modules may be independent of each other, or they may form a dependency graph. In the latter case, one module states that it depends on one or more other modules. The following code block shows an example of this:

module VPR                                                 (1)
{
   external;                                               (2)
   modules/vapor;                                          (2)
}

module Tweek                                               (1)
{
   depend VPR;                                             (3)
   modules/tweek;                                          (2)
}
1

These lines declare two modules named “VPR” and “Tweek” respectively.

2

These lines list directories upon which the containing module depends. Each of these directories must contain a script called configure that can be executed. This method of listing dependencies requires explicit paths, and it helps greatly if those dependencies exist within the local source tree.

3

This line indicates that the Tweek module depends on the VPR module. Here, note that depend is a keyword with special significance. In effect, the VPR module is included inside the Tweek module definition so that it picks up all of VPR's dependencies.

Going deeper into the module definition, we find that environment variables can be set with each directory listing using a comma-separated list. These variables provide extra information about the configuration environment after the configure script has completed successfully. To illustrate this, we extend the above example as follows:

module VPR
{
   external;
   modules/vapor: VPR_CONFIG=vpr-config, VPR_BASE_DIR=instlinks;          (1)
}

module Tweek
{
   depend VPR;
   modules/tweek: TWEEK_CONFIG=tweek-config, TWEEK_BASE_DIR=instlinks;    (1)
}
1

As before, these lines list directories upon which the containing module depends. This time, we have added environment variable settings for the variables $VPR_CONFIG, $VPR_BASE_DIR, $TWEEK_CONFIG, and $TWEEK_BASE_DIR.

While any environment variable can be set in the configuration file, those shown above have special significance[3]. Those variables ending in _CONFIG set the corresponding environment variable to include the full path to the named file (despite the fact that the full path is not given in the assignment). The path is constructed using the associated directory dependency. This directory is also added to the script's execution path. This extra little bit is needed so that a given -config script can be executed by another -config script if necessary. (The details about why all of this is necessary are discussed in the section called “The *-config Scripts and the *.m4 Macro Files”.)

Those variables ending in _BASE_DIR define the installation directory for the given module. In the VR Juggler build system, this is necessary for dependent modules to find the headers and libraries they need to compile. (Again, more information about this is given in the section called “The *-config Scripts and the *.m4 Macro Files”.) Once again, the value being assigned has special significance. If the value is the token instlinks, it is taken to mean that the full path to the installed module is in a directory relative to the current directory called instlinks. Any other value is used verbatim as the value of the environment variable.

Dependency Management

Dependencies within modules are maintained using a simple Perl data structure in the JugglerModule class. Parsing the configuration file results in instantiation of this data structure. There is one such instance for each module defined. Each instance contains an array of ModuleDependecy objects. Steps are taken to ensure that there is no duplication of dependencies within a single JugglerModule instance.

The *-config Scripts and the *.m4 Macro Files

The various *-config scripts (vpr-config, tweek-config, vrjuggler-config, etc.) play a vital role in the design of the global build. Unfortunately, this is also where the global build gets complex. Here, code that is intended for use by users of VR Juggler and associated modules is put to use by the code that compiles everything. It makes use of a strict set of behaviors wherein various environment settings and command-line options form a hierarchy of preferences and fallbacks. If anything goes wrong with a user's configuration process, it is almost always related to a .m4 file or a -config script giving unexpected results because of a misused command-line option or a “dirty” environment.

A Typical -config Script

We will now examine a typical -config script. We will not focus on any script in particular, but instead, we will describe the fundamental concepts and requirements shared by all implementations. Readers interested in implementations to use as references should consider the following:

  • vpr-config: This is the most basic -config script. It has no external dependencies, and it only deals with one library.

  • tweek-config: This script depends on vpr-config for proper execution, and it deals with some interesting special cases. Namely, it must be able to inform callers about information relating to C++, Java, and IDL. This script deals with one C++ library and multiple Java libraries distributed as JAR files.

  • vrjuggler-config: This script is interesting because it has the most dependencies (it depends on vpr-config, tweek-config, jccl-config, gadgeteer-config, and sonix-config) and because it deals with multiple libraries (libJuggler, libJuggler_ogl, and libJuggler_pf).

To proceed with the abstract discussion, we will now describe the job that must be performed by all -config scripts. We then explain how external dependencies are managed. We conclude with a discussion of how a -config script is generated as part of the configuration process.

The Job

The job of any -config script is simple: provide the information needed to compile against the associated library. This information can work in the context of building a higher level library or an application. The basic information that must be provided is as follows:

  • Module version number

  • C++ compiler flags including header paths and compiler-specific options such as -fexceptions or -LANG:std

  • C++ linker flags separated into two categories:

    1. The basic list of libraries that are distributed with the module in question

    2. The complete list of external dependencies needed to link an application

  • Static linking options

  • Profiled library linking options (if profiled libraries are available)

Other information may be provided as necessary (see tweek-config and jccl-config, for example).

External Dependencies

In the Juggler Project, we use the -config scripts in an interesting manner. A given script, say gadgeteer-config, will call all the -config scripts of the modules on which it depends. The collected output is compressed and returned to the user. In this way, we avoid trying to manage all the dependency information in every module. Instead, we rely on each module to report its information correctly. Then, the highest level module only has to collect it and print it out.

The key to this functionality is that all the -config scripts can be found in the user's path. Furthermore, because our -config scripts are written in Perl, we have easy access to the path used to invoke each script. (This is actually a side effect of the scripts being in the user's path, and the path information would be available regardless of the scripting language. Perl just makes it easy to extract and operate on the given information.)

The dependencies only come into play for certain information requests. Those requests are the following:

  • C++ flags (--cxxflags)

  • Include paths (--include)

  • Basic libraries (--libs)

  • External dependency libraries (--extra-libs)

Each of the above iteratively calls the -config script(s) from the dependency module(s) using at least a subset of the arguments specified by the user on the command line. One additional argument is given that reduces the amount of output: --min. This causes each script to print out only the minimal information needed for compiling. The motivation for doing this is to keep the compile lines short whenever possible.

Script Generation

Finally, we explain the last detail relating to all -config scripts used in the Juggler Project: script generation. As part of the global build, all these scripts must be generated at configuration time. This is necessary for each subsequent module to be configured correctly. That means that each module's configure script is making use of “external-config scripts to get command-line arguments and version information. This allows the use of pre-existing installations of dependencies. For example, Gadgeteer depends on JCCL and VPR. Normally, it would satisfy those dependencies using source from the same tree, but a user may already have JCCL and VPR built and installed. By setting up his or her path correctly, vpr-config and jccl-config can be found, and Gadgeteer can be built using the existing installations.

As of this writing, there are no hard and fast rules regarding script generation other than the fact that they must be generated as part of module configuration. The prevailing convention within a given configure.in is to make separate substitution variables used only in the -config.in template file. That is to say that these variables are separate from those used in template makefiles and other .in templates. The separation is done through syntax alone. Those variables that are substituted in the generation of a -config script are spelled using lowercase letters exclusively. Other substitution variables are spelled using all uppercase letters.

Again, this is not a hard and fast rule. Indeed, some variables are shared between all files (for example, $USE_GCC or $MAJOR_VERSION). In general, such exceptions are allowed because there is no difference in usage between a -config script and a generated makefile. The important distinctions arise with compiler and linker flags. In particular, there are some flags that should only be used in the process of building a given module but should not be exposed to users. An example is the MIPSpro -Woff flag that is used to disable compile-time warnings. Exporting this option would force users to disable the same warnings whether they want to or not. Essentially, it is up to the module build system author to use good judgement when deciding what to export and what to use internally.

To summarize, the separation of the substitution variables is done for two reasons:

  1. To manage potential differences in semantics between module compilation and module use.

  2. To make it clear to readers of the relevant files which variables are used for which purpose.

To date, this mechanism has worked well (at least for those few who know about it). In one case, failure to follow this convention caused compilation of a module to break because a linker variable was serving double duty. This variable was setting internal linker options and forcing the use of those same options for external code.

A Typical .m4 File

With an understanding of what information a -config file provides, we can move on to the m4 macros that make use of those files at configuration time. Each module in the Juggler Project has a corresponding m4 macro suitable for use within an Autoconf configure.in file. For example, Gadgeteer has a macro called GADGETEER_PATH. This macro is used in VR Juggler's configure.in to find a usable Gadgeteer installation.

The basic concept behind all these macros is the same: provide a way for a configure script to detect a usable installation. The way this is done is fairly straightforward. The basic step-by-step process is as follows:

  1. Find the module's -config script. If the script cannot be found, execute the user-specified failure steps and “return[4].

  2. Using the -config script, get the version of the installation and compare it against the user-specified minimum required version.

  3. If the version is sufficient, execute the user-specified success steps and set variables for compiler and linker arguments using the -config script. If the version comparison fails, execute the user-specified failure steps and “return”.

We now proceed into the details of a typical .m4 file. First, we will cover the mechanism used to deal with paths to installations. Then, we explain what variables must be set by a module's m4 macro.

Path Setting Preferences

The most complex part about understanding the Juggler Project .m4 files is the management of path setting preferences. The path in question is the path to the installed module. The installation may be on the local file system, or it may exist as a developer pseudo-installation (refer to the section called “The Developer Installation” for more information on that topic). The specification of that path by the user is where we now direct our interest. The following list gives the path setting preferences in order of decreasing preference (i.e., the first has highest precedence, and the last has lowest precedence):

  1. The _CONFIG environment variable which gives the full path to the module's -config script. The name of the environment depends on the specific .m4 file. For example, vpr.m4 checks for $VPR_CONFIG. In general, the environment variable name should match the name of the -config script except in capitalization and the use of an underscore (_) instead of a hyphen (-).

  2. The command-line argument --with-<module>-exec-prefix which specifies the directory containing the -config script. Here, the string <module> depends on the way the .m4 file is written to declare accepted command-line arguments.

  3. The command-line argument --with-<module>-prefix which specifies the directory containing the full module installation. The named directory must contain a bin subdirectory, and this subdirectory must contain the module's -config script.

  4. The module's _BASE_DIR environment variable. The directory named by the environment variable must contain a bin subdirectory that in turn contains the module's -config script.

  5. The user's path which must include the directory that contains the module's -config script.

The magic that happens in configure.pl is a result of setting the appropriate _CONFIG environment variable and extending the path to include the various module directories in the build tree. Through the combination of these steps, a given -config script is found using the _CONFIG environment variable, and any other -config scripts it needs are found using the path. For each module that is built, the environment in which configure.pl runs is extended. Refer back to the section called “Build Configuration File” for information about how the necessary file and environment variable names are provided to configure.pl.

Variables Defined

Upon successful completion of step 3 (see above), there are several shell variables defined for the calling code to use. Typically, these variables are concatenated with other variables to form the full set of options passed to the compiler and the linker. In order to maintain consistency across all the .m4 files, it is important to know what variables must be defined and why.

First, the variables can be separated into two broad categories: minimal and maximal. The basic idea with minimal versus maximal flags is to allow the user some flexibility in composing the full command-line options. The minimal variables provide only the minimum amount of information needed for compiling. No dependency information is included. For example, such settings usually include one or two header path extensions (-I options) and mandatory compiler flags such as -LANG:std. The maximal variables, on the other hand, include all the information needed for compiling including dependency data. More concretely, the minimal C++ compiler flags for Gadgeteer would give the header path for the Gadgeteer headers and any mandatory compiler flags. The maximal C++ compiler flags would include the minimal information as well as flags relating to JCCL, Tweek, and VPR (in that order). The same would be true for linker flags. To summarize, the minimal variables must be mixed with other minimal variables if the module has dependencies; the maximal variables can stand on their own.

Within the minimal and maximal categories, there are two more categories: compiler flags and linker flags. Based on the discussion above, this distinction should be fairly obvious. This distinction is made to deal with platforms where the compiler is not used to perform the link stage. Since some compilers can call the linker as necessary, linking flags[5] suitable for use with the compiler are provided. The distinction is made by the variable name. Linking flags that can be passed to the compiler have _CC_ in their name; linking flags for the linker have _LD_ in their name.

Continuing with the subdivision, the linking flags are divided into two categories: static and dynamic. This is done to allow static linking of one or more libraries instead of dynamic linking. The usual default would be dynamic linking, and the variable names reflect that. Those variables that contain static linking flags have _STATIC_ in their names.

The Developer Installation

At various times in the history of the VR Juggler project, the developer “pseudo-installation” has been a topic of great controversy. Questions (or arguments, depending on your perspective) regarding its usefulness and its differences from a release installation come up repeatedly. The reason for its existence is quite simple: to simplify the lives of developers. The differences between the developer installation are also fairly simple. In a nutshell, a developer installation uses debugging libraries by default and links applications statically. A release installation, on the other hand, uses optimized libraries by default and links applications dynamically. The reasoning behind this is a little more difficult to nail down, and for that reason, we will say that it is beyond the scope of the document.

To satisfy the only goal of the developer installation (simplification of developers' lives), the developer installation must act exactly like a release installation, but it must be inside the build tree. The developer installation is created automatically as part of the build process, and ideally, its construction is faster than that of a full release installation. In any case, if all goes well, a developer can treat this pseudo-installation as if it were a real installation for the purposes of running tests.

Prior to early August 2002, the developer installation was created separately from a release installation. This was done through the use of symlinks on UNIX-based platforms and file copies on Win32. Since August 2002, the developer installation still uses symlinks or file copies in the same manner, but there is no longer a separation between creation of the developer installation and the release installation. In other words, the release installation is used to make the developer installation, but it is directed to install into the build tree.

The decision to use symlinks or file copies is based on the host platform and on the use of the 'links' target that every module must define. Using a custom Perl script, bsd-install.pl, written to be fully compatible with BSD install(1), symlinks may be created instead of using file copies. (The bsd-install.pl script comes with Doozer++ 1.5 and beyond.) Within the script, a test is performed to determine if the host platform is a Win32 system. If so, copies are always used because there are no symlinks on Win32 file systems. All of these decisions had been made in each module's build system, but they have since been offloaded into bsd-install.pl. This is in keeping with the Doozer++ goal of centralizing complexity.

Future Goals

At one time, there was a long-term goal for this global build script, or “project builder”. In conjunction with cvs-gather.pl, a semi-arbitrary collection of software packages would be downloaded and compiled. The commonality between them would be the use of configure scripts that would be invoked by the project builder. The dependencies would be specified through some configuration file, possibly written in XML, that would be constructed on the fly based on the cvs-gather.pl dependency file.

These lofty plans have not materialized, and it is unclear whether the need for such a tool still exists. Nonetheless, the idea of a highly generalized project builder played a key role in the way that configure.pl was written. In particular, its highly generic nature was motivated by the potential for downloading arbitrary source code and running a configure script. The makefile generated by configure.pl initially followed this goal, too, but it has since had its scope narrowed to deal with the specific conventions of the VR Juggler project.



[3] The fact that any variable can be set but that some are treated as special cases is a deficiency in the design of configure.pl. The current use was put together out of necessity to provide the script with extra information needed for proper execution of each module's Autoconf-generated configure script.

[4] Actually, these macros do not return because the code is inlined. Moreover, the user-specified failure steps may include halting the configure script and exiting with failure status.

[5] Note the distinction between linker flags (flags for the linker program) and linking flags (flags used to link object files).

Chapter 5. The Documentation Build

The documentation for the individual modules is maintained separately from the source code build. This is done primarily because writing code to DocBook and related software would take too much time and gain us very little. For the most part, the Juggler Project documentation is intended for posting on the website. As such, the build environment is much more controlled than one that is provided for (easy?) use by the general public. The documentation build is still configurable, but in a different, less automatic method than what is used to build the source tree. This chapter explains the rules of the documentation build and how to configure it for use outside the VRAC lab.

The Rules

Any module with DocBook-based documentation can be added to the documentation build. As with the source code build, there are rules that must be followed. With this smaller, less complicated build system, there are fewer things to deal with.

Required Makefile Targets

Within this subsection, we present the full list of targets that must be implemented for correct operation within the documentation build. As a reference, refer to the file juggler/Makefile.docs. This list, containing all of two targets, is as follows:

docs

This target builds the documentation. It is up to each makefile to determine how much is built. In general, the makefiles are set up to generate HTML, PDF, and PostScript. This output may come from DocBook XML source or from output generated by Doxygen. Other tools may be used, but these two are the favored generators at this time.

install-docs

This installs the documentation built by the 'docs' target. By default, the documentation installation uses $HOME/public_html/jugglerweb as the base prefix. This truly reflects the intended use of this build system. (To redirect the installed output, simply change the setting for $(webroot) in juggler/Makefile.docs.)

Targets such as 'clean' and 'clobber' are needed as well, but those makefiles that include docbook.mk get them for free. If a documentation generating makefile is not using docbook.mk, it must make its own 'clean' and 'clobber' targets.

Installation Hierarchy

The individual documentation building makefiles are expected to behave when installing the generated documentation. That is, they should install to a subdirectory of $(webroot) that reflects the appropriate project. In many cases, the installation hierarchy should also reflect the version of the software against which the documentation was written.

Each makefile is responsible for creating its own installation hierarchy and for installing any related, external files. The documentation build offers some automation to help with this, but its abilities are limited. At this time, the documentation build can be directed to install the image files that come with the DocBook style sheets and any local images that the documentation needs.

Configuration and Customization

At this time, most of the documentation in the Juggler Project is written using DocBook. Because of that, the settings for building documents from DocBook files are centralized in the file juggler/doc/docbook.mk. This file is parameterized to the extreme so that including makefiles can override its default settings easily. Usually, makefiles that include docbook.mk direct the build to use OpenJade, Saxon, or Xalan to process the DocBook input and PassiveTeX, FOP, or XEP to create PDF files. Further configuration can be done that chooses different versions or installations of the DocBook style sheets, Saxon, Xalan, FOP, etc. The following is the makefile used to generate HTML and PDF versions of this document:

default: html

docs: html chunk-html pdf                                            (1)
install-docs: install-html install-chunk-html install-pdf            (1)

NAME=		build.system                                                  (2)

XML_FILES=	$(NAME).xml                                               (3)
HTML_FILES=	$(NAME).html                                             (4)
PDF_FILES=	$(NAME).pdf                                               (5)

XSLT_TOOL=	Saxon                                                     (6)

# Fill these in!!  Together, they make up the installation prefix.
webroot=	$(HOME)/public_html/jugglerweb                              (7)
instdir=	docs/juggler.build.system                                   (7)

prefix=		$(webroot)/$(instdir)                                       (7)
INSTALL_FILES=	$(webroot)/base_style.css                             (7)

NEED_DB_IMAGES=	1                                                    (8)

$(NAME).html: $(NAME).xml                                            (9)
$(NAME).pdf: $(NAME).xml $(NAME).fo                                  (9)

include ../docbook.mk                                                (10)
1

These are the required targets that all makefiles in the documentation build must have. Refer to the section called “Required Makefile Targets” for more information on these targets.

2

This variable lists the base name of the source document. Since the generated documents differ only in final extension, this variable is used internally as the basis for the name of the source file and the generated documents. If there are multiple source documents, multiple variables (such as $(NAME1), $(NAME2), etc.) would be used.

3

This variable lists all the DocBook XML source files that will be used as input to the XSLT processor. Each source file should have corresponding output files, also listed in this makefile.

4

The source document can be rendered as HTML, and so this variable is used to list the output file. All HTML output files must be listed in this variable. The names must reflect the non-chunked output file names. Chunked HTML output is handled separately since the file names cannot be listed easily in this context.

5

Similar to $(HTML_FILES), this lists the PDF file(s) that can be rendered from the XML input. This has the same semantics as $(HTML_FILES).

6

The variable $(XSLT_TOOL) is used to tell docbook.mk which XSLT processor should be used. The default tool is Xalan, but Saxon can be used instead, as shown above.

7

These variables deal with the installation process. The critical variables are $(webroot) and $(prefix). The variable $(webroot) will be overridden by the calling makefile (the top-level documentation makefile) to provide an alternate base directory as necessary. Setting it here provides a good default value to use when writing documentation and testing rendering. The $(prefix) variable is what tells docbook.mk where the document(s) will be installed. It must be set in order for an installation to succeed. Finally, $(INSTALL_FILES) provides any extra files that must be installed. In this case, we want to copy over the common stylesheet used by the rest of the VR Juggler website.

8

Defining the variable $(NEED_DB_IMAGES) informs docbook.mk that the DocBook images are needed by rendered versions of the source document(s). Symlinks will be created as part of the rendering process, and the images directory will be copied over during installation.

9

These lines simply list dependencies as a way to help make(1) in doing its job effectively. Here, the $(NAME) variable again plays a useful role.

10

Finally, the last line (and it should always be the last line) includes the docbook.mk makefile stub. All of the settings above have an effect on the way that the targets defined in docbook.mk behave.

Part III. Extension

At this point they came in sight of thirty forty windmills that there are on plain, and as soon as Don Quixote saw them he said to his squire, “Fortune is arranging matters for us better than we could have shaped our desires ourselves, for look there, friend Sancho Panza, where thirty or more monstrous giants present themselves, all of whom I mean to engage in battle and slay, and with whose spoils we shall begin to make our fortunes; for this is righteous warfare, and it is God's good service to sweep so evil a breed from off the face of the earth.

--Miguel de Cervantes, Don Quixote (English translation), opening of Chapter VIII

In this part, we discuss the topic that seems most fearsome to the majority of Juggler team members: build system extension. While this does not have to be a complex topic, most people shy away from it because they do not want to spend time trying to understand any given build system. Whether anyone will even bother to read this far into this book is debatable, and this is only the second attempt in four years to write this documentation. Perhaps this time, it will be worth the effort.