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.