2.0.0
Copyright © 2002–2007 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: 2007-01-01 10:26:55 -0600 (Mon, 01 Jan 2007) $
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 |
|