2.0.0
Copyright © 2002–2005 Iowa State University
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
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.
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.
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.
Table of Contents
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.
Table of Contents
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:
The generated makefiles were too complex to debug
Use of compilers other than GCC was too difficult
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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.
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.
“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.”
Table of Contents
Table of Contents
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.
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.
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.
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.
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.
This target is the same as
'build'.
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.
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'.
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'.
Build only the static debugging
version of the libraries. This does the same thing as
'debug' but does not compile the dynamic
libraries.
Build only the dynamic debugging
version of the libraries. This does the same thing as
'debug' but does not compile the static
libraries.
Build only the static optimized
version of the libraries. This does the same thing as
'optim' but does not compile the dynamic
libraries.
Build only the dynamic optimized
version of the libraries. This does the same thing as
'optim' but does not compile the static
libraries.
Build only the static profiled
version of the libraries. This does the same thing as
'profiled' but does not compile the
dynamic libraries.
Build only the dynamic profiled
version of the libraries. This does the same thing as
'profiled' but does not compile the
static libraries.
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.
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.)
This target is the same as
'install'.
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 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 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.
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.
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)
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'.
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.
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.)
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'.
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.
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 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.
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.
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.
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.
Set up the developer pseudo-installation environment.
Remove the developer pseudo-installation environment.
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.
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.
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.
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.
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.
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.
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{ external;
modules/vapor;
} module Tweek
{ depend VPR;
modules/tweek;
}
![]() | These lines declare two modules named “VPR” and “Tweek” respectively. |
![]() | These lines list directories upon which the containing
module depends. Each of these directories must contain a
script called |
![]() | 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;
}
module Tweek
{
depend VPR;
modules/tweek: TWEEK_CONFIG=tweek-config, TWEEK_BASE_DIR=instlinks;
}![]() | As before, these lines list directories upon which the
containing module depends. This time, we have added
environment variable settings for the variables
|
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.
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 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.
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 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:
The basic list of libraries that are distributed with the module in question
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).
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.
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:
To manage potential differences in semantics between module compilation and module use.
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.
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:
Find the module's -config script. If
the script cannot be found, execute the user-specified failure
steps and “return”[4].
Using the -config script, get the
version of the installation and compare it against the
user-specified minimum required version.
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.
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):
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
(-).
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.
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.
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.
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.
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.
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.
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).
Table of Contents
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.
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.
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:
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.
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.
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.
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 pdfinstall-docs: install-html install-chunk-html install-pdf
NAME= build.system
XML_FILES= $(NAME).xml
HTML_FILES= $(NAME).html
PDF_FILES= $(NAME).pdf
XSLT_TOOL= Saxon
# Fill these in!! Together, they make up the installation prefix. webroot= $(HOME)/public_html/jugglerweb
instdir= docs/juggler.build.system
prefix= $(webroot)/$(instdir)
INSTALL_FILES= $(webroot)/base_style.css
NEED_DB_IMAGES= 1
$(NAME).html: $(NAME).xml
$(NAME).pdf: $(NAME).xml $(NAME).fo
include ../docbook.mk
![]()
![]() | 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. |
![]() | 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
|
![]() | 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. |
![]() | 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. |
![]() | Similar to |
![]() | The variable |
![]() | These variables deal with the installation process. The
critical variables are |
![]() | Defining the variable |
![]() | These lines simply list dependencies as a way to help
make(1) in doing its job effectively. Here,
the |
![]() | Finally, the last line (and it should always be the last
line) includes the |
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.”
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.