- Author : Christopher Batten
- Date : Sep 2008, Sep 2009, Aug 2015
The Modular C++ Build System is a set of makefiles, scripts, and policies for managing, testing, and building large C++ projects. The fundamental idea is that we divide the large project into several smaller pieces called subprojects. Subprojects are both a physical concept (ie. how the files are named and organized) as well as a logical concept (ie. how the classes and functions are named and organized). Each subproject should encapsulate a moderate amount of related functionality; subprojects usually contain tens of classes and source files. The build process is modular since the header files, source files, make fragment, and autoconf fragment for a subproject are all contained within one subdirectory. We should be able to easily copy subprojects between projects (assuming they are using the same modular build system) and the only change we need to make is updating the list of subprojects in the top-level autoconf script.
This user guide is divided into two parts: the first part is a tutorial,
and the second part contains general user reference. The build system
includes an integrated unit test framework (which is also an example of a
subproject), so developers should also read the testing documentation in
utst/utst.md
.
-
Part 1: Tutorial
- 1.1. Building a Project
- 1.2. Adding a Basic Subproject
- 1.3. Adding a More Advanced Subproject
- 1.4. Managing Optional Subprojects
- 1.5. Installing Subprojects
- 1.6. Including External Subprojects
- 1.7. Distributing Package Tarballs
-
Part 2: Reference
- 2.1. Overview of Build Process
- 2.1. Basic Autoconf Toolflow
- 2.2. Managing Subprojects in configure.ac
- 2.3. Subproject Autoconf Fragments
- 2.4. Basic Make Toolflow
- 2.5. Subproject Makefile Fragments
- Tutorial
This part walks through using the Modular C++ Build System to create a
new project, add subprojects, and then build, test, and install the
project. You can follow along through the tutorial yourself by typing in
the commands marked with a %
symbol at the shell prompt. Tasks to try
on your own are denoted with (*). To cut and paste commands from this
tutorial into your bash shell (and make sure bash ignores the %
character) just use an alias to "undefine" the %
character like this:
% alias %=""
Before creating a new project we need to decide what to name the
project. We should choose both a longer descriptive name (eg. Image
Processing Tools) as well as a short abbreviated name (eg. ipt
). For
this tutorial we will use the abbreviated name mcppbs-tut
. Once we
know the abbreviated name for our project, we then need to download and
extract the mcppbs
template from the public git repository. We unpack
the template into a directory with the abbreviated project name, and to
simplify simplify the rest of the tutorial we will also define a
$PROJROOT
environment variable which contains the absolute path to the
project's top-level root directory.
% TGZ=<location-of-template>
% wget -O- $TGZ | tar xzf -
% mv cbatten-sw-mcppbs mcppbs-tut
% cd mcppbs-tut
% PROJROOT=$PWD
Take a look at the files in the top-level project directory.
README
: Project documentationmcppbs-uguide.md
: Build system documentationconfigure.ac
: Input for autoconf toolsaclocal.m4
: Local macros for autoconf toolsMakefile.in
: Template forconfigure
scriptconfig.h.in
: Generated byautoheader
configure
: Generated byautoconf
scripts
: Subdirectory with build system scriptsutst
: Subdirectory with unit testing framework
After acquiring the template, we need to update the metadata in
configure.ac
with the new project's name, maintainer's name, and a
shorter abbreviated form of the project name. The abbreviated form
should be all lowercase, uses a dash as a separator, and usually is the
same as the project directory name. We want to change the metadata in
configure.ac
so that it looks like this:
m4_define( proj_name, [MCPPBS Tutorial])
m4_define( proj_maintainer, [Christopher Batten])
m4_define( proj_abbreviation, [mcppbs-tut])
m4_define( proj_version, [0.0])
For the tutorial we use sed to make the change, but you can also use
your favorite text editor. Note that after we change configure.ac
we
need to rerun the autoconf tools.
% cd $PROJROOT
% sed -i.bak \
-e 's/\( proj_name,\).*/\1 [MCPPBS Tutorial])/' \
-e 's/\( proj_maintainer,\).*/\1 [Christopher Batten])/' \
-e 's/\( proj_abbreviation,\).*/\1 [mcppbs-tut])/' \
-e 's/\( proj_version,\).*/\1 [0.0])/' configure.ac
% autoconf && autoheader
This might also be a good time to update the README
with information
specific to the new project. You are encouraged to keep a pointer in the
README
to the documentation on the build system (mcppbs-uguide.md
)
so that your end-users can learn how the build system works.
The first thing we will try is just building the project, although the
only subproject in the default template is the unit testing framework.
Take a look at the unit testing subproject in the utst
subdirectory.
You will see the following files:
utst-guide.md
: Unit testing framework documentationutst.h
: Top-Level header fileutst-*.h
: Header filesutst-*.inl
: Inline header/source files (see style guide)utst-*.cc
: Source filesutst.t.cc
: Unit test source fileutst.ac
: Autoconf fragmentutst.mk.in
: Makefile fragmentutst.pc.in
: Metadata file for installing subproject library
The build system is structured so that we do not intermingle the generated files with the source files. This makes it much easier to manage larger projects. You should always create a separate build directory in which to build the project. To do a clean build you just need to delete the build directory and start over. Separate build directories also makes it trivial to work with multiple builds of the same project at the same time. This is particularly useful when building a project for multiple architectures. The following commands create a build directory, configure the project, and then builds/runs all the unit tests.
% cd $PROJROOT
% mkdir build
% cd build
% ../configure
% make
% make check
Notice that we run the portable configure
shell script to customize
the build system for the current platform. The configure
script
displays information about what kind of checks it is performing. Take a
look at what is in the build directory. You will see the following
files:
Makefile
:configure
generated makefileconfig.log
: Log fromconfigure
scriptconfig.status
: Cached version of theconfigure
scriptutst.mk
:configure
generated makefile fragmentutst/config.h
:configure
generated header fileutst/*.d
: Auto-generated dependency file per.cc
utst/*.o
: Object file for each.cc
utst/libutst.a
: Library forutst
subprojectutst/utst.t.d
: Dependency file forutst.t.cc
unit testutst/utst.t.o
: Object file forutst.t.cc
unit testutst/utst-utst
: Executable forutst.t.cc
unit testutst/utst-utst.out
: Output from runningutst-utst
The configure
script takes the Makefile.in
file in the project root
directory and makes some substitutions for special variables denoted with
the @variable@
syntax. Look for @srcdir@
in Makefile.in
and compare
that line to the same line in the generated Makefile
. You can see that
the configure
script has substituted the correct path to the top-level
directory for the C++ project. No matter where you put the build
directory, configure
will fill this variable in correctly so that the
build system can always find the source files. Compare Makefile.in
and
config.h.in
to the generated Makefile
and utst/config.h
to see what
other changes the configure
script customized based on your specific
platform. Notice that if a subproject is enabled then a C preprocessor
macro will be defined (eg. UTST_ENABLED
) so that developers can write
conditional code based on which subprojects are available. Unlike many
other projects, we do not use a single global config.h
file. Instead
each subproject has its own config.h
file (eg. utst/config.h
).
Although all of these header files are identical, separating them by
subproject enables us to install the subproject's header files (including
the appropriate config.h
file) and avoid filename clashes. Subprojects
should only #include
their own config.h
file.
To clean up the build directory we can use the clean
make target. Take
a look at what is in the build directory before and after running make clean
. Afterwards you should just see the scripts generated by
configure
. To remove everything so that the build directory is
completely empty we can use the distclean
make target.
% cd $PROJROOT/build
% make clean
Each subproject is contained in a subdirectory beneath the main project
directory. The name of the subdirectory should be an abbreviated form
(four to eight characters) of the full name of the subproject. For
example, the unit testing subproject is contained in the utst
subdirectory. The subdirectory includes all of that subproject`s header
and source files. To add a new subproject, we need to take the following
steps:
- Create a new subdirectory with the abbreviated subproject name
- Add the subproject's source files
- Add autoconf and makefile fragments
- Include the subproject in the top-level
configure.ac
We will now go through these steps for an example subproject named
Simple Geometric Primitives (sgp
). This subproject might encapsulate
various classes and functions to represent and manipulate points, lines,
and polygons. For this tutorial, we will just add a very simple class
representing a point in a 2D plane. The first step is to create a new
subdirectory.
% cd $PROJROOT
% mkdir sgp
The next step is to write three new C++ files for our point class:
sgp/Point.h
, sgp/Point.cc
, and sgp/Point.t.cc
. The sgp/Point.t.cc
file is a unit test for the Point
class. For more information on the
unit testing framework see the general documentation in utst/utst.md
.
So first we write the sgp/Point.h
header file.
% cd $PROJROOT
% cat > sgp/Point.h \
<<'END'
#ifndef SGP_POINT_H
#define SGP_POINT_H
#include <iostream>
namespace sgp {
class Point {
public :
Point();
Point( int x, int y );
int get_x() const;
int get_y() const;
private :
int m_x, m_y;
};
std::ostream& operator<<( std::ostream& os, const Point& pt );
bool operator==( const Point& pt1, const Point& pt2 );
Point operator+( const Point& pt1, const Point& pt2 );
}
#endif
END
Notice that the Point
class is placed in the sgp
namespace and that
we use a SGP_
prefix for preprocessor macros. Next we write the source
file.
% cd $PROJROOT
% cat > sgp/Point.cc \
<<'END'
#include "sgp/Point.h"
namespace sgp {
Point::Point() : m_x(0), m_y(0) { }
Point::Point( int x, int y ) : m_x(x), m_y(y) { }
int Point::get_x() const { return m_x; }
int Point::get_y() const { return m_y; }
std::ostream& operator<<( std::ostream& os, const Point& p ) {
return ( os << "(" << p.get_x() << "," << p.get_y() << ")" );
}
bool operator==( const Point& p1, const Point& p2 ) {
return ((p1.get_x() == p2.get_x()) && (p1.get_y() == p2.get_y()));
}
Point operator+( const Point& p1, const Point& p2 ) {
return Point( p1.get_x()+p2.get_x(), p1.get_y()+p2.get_y() );
}
}
END
Notice how #include
must always be with respect to the top of the
project. In other words, you must always include the subproject directory
in front of the corresponding header. We need to define an insertion
operator and an equality operator to be able to use some of the basic
checks in the unit testing framework. Here is the sgp/Point.t.cc
file
which does a simple unit test to check if the addition of two points is
working.
% cd $PROJROOT
% cat > sgp/Point.t.cc \
<<'END'
#include "utst/utst.h"
#include "sgp/Point.h"
UTST_AUTO_TEST_CASE( TestAddition )
{
sgp::Point pt1(1,2);
sgp::Point pt2(2,3);
UTST_CHECK_EQ( pt1 + pt2, sgp::Point(3,5) );
}
int main( int argc, char* argv[] ) {
return utst::auto_command_line_driver( argc, argv, "sgp" );
}
END
We might also want to have a command-line program in our subproject. The following little example takes two points specified on the command line and outputs their sum.
% cd $PROJROOT
% cat > sgp/add-points.cc \
<<'END'
#include "sgp/Point.h"
#include <cstdlib>
int main( int argc, char* argv[] ) {
if ( argc != 5 )
return 1;
sgp::Point pt1(atoi(argv[1]),atoi(argv[2]));
sgp::Point pt2(atoi(argv[3]),atoi(argv[4]));
std::cout << pt1 << " + " << pt2 << " = " << (pt1+pt2) << std::endl;
return 0;
}
END
Now that we have added our source code and the unit test to our
subproject, the next step is to add the autoconf fragment (sgp.ac
) and
the makefile fragment (sgp.mk.in
). We should always include a call to
the MCPPBS_SUBPROJECT
macro in our autoconf fragment. This macro will
expand out to various setup steps in the configure
script, and later
in this tutorial we will see how we can use this macro to specify
dependencies between subprojects. We will also see later how we can use
additional autoconf macros in this fragments to check installed
libraries or compiler features.
% cd $PROJROOT
% cat > sgp/sgp.ac \
<<'END'
MCPPBS_SUBPROJECT([sgp])
END
The MCPPBS_SUBPROJECT
macro will set various variables which will be
substituted into our makefile fragment when we run configure
. For
example, configure
will substitute in the appropriate compiler and
linker flags so that we can properly build this subproject. The makefile
fragment should ultimately set special make variables to tell the
top-level makefile about these compiler and linker flags as well as the
source files which make up the sgp
subproject.
% cd $PROJROOT
% cat > sgp/sgp.mk.in \
<<'END'
sgp_intdeps = @sgp_intdeps@
sgp_cppflags = @sgp_cppflags@
sgp_ldflags = @sgp_ldflags@
sgp_libs = @sgp_libs@
sgp_hdrs = sgp/Point.h
sgp_srcs = sgp/Point.cc
sgp_test_srcs = sgp/Point.t.cc
sgp_install_prog_srcs = sgp/add-points.cc
END
We use sgp_
as a prefix for all make variables in the makefile
fragment. Here is a list of all the recognized make variables for a
subproject named sproj
.
sproj_hdrs
: Headers (.h
)sproj_inls
: Inline header/source files (.inl
)sproj_srcs
: Source files (.cc
,.c
,.S
)sproj_test_srcs
: Sources for unit tests (.t.cc
)sproj_prog_srcs
: Sources for programs (.cc
,.c
)sproj_install_prog_srcs
: Sources for programs to install (.cc
,.c
)
Notice how we must use the subproject directory as a prefix when
specifying all source files. Also note that we can include C, C++, and
assembly source files in our sources list (sprojs_srcs
), C and C++
source files in our program sources (sproj_proj_srcs
,
sproj_install_prog_srcs
), but only C++ test programs
(sproj_test_srcs
). Even though we can include these different type of
sources everything is always compiled with the C++ compiler.
Now that our sgp
subproject is ready to go, the last step is to
include the subproject in the top-level configure.ac
file. If you look
through the configure.ac
file you will see that there is already a
call to the MCPPBS_INCLUDE_INTERNAL
macro for utst
. We simply need
to add a new line for the new sgp
subproject. For the tutorial, we use
sed to make this change. After modifying configure.ac
we need to rerun
the autoconf tools. Remember that we always run autoconf
and
autoheader
together.
% cd $PROJROOT
% sed -i.bak '/MCPPBS_INCLUDE_INTERNAL(\[utst\])/ a \
MCPPBS_INCLUDE_INTERNAL([sgp])
' configure.ac
% autoconf && autoheader
There are primarily two types of subprojects: internal and external. Internal subprojects are ones where we actually include the source code for the subproject as a subdirectory. External subprojects are ones where we simply leverage a previously installed library for a subproject. External subprojects are not currently supported in this new version of the build system which does not use file-name prefixes.
We are now finally ready to build the project with our newly added sgp
subproject. We simply go into the build directory and use make
to build
a library (sgp/libsgp.a
) which includes all of the object files for the
subproject as well as any subproject programs. Then we use make check
to build and run all of the unit tests.
% cd $PROJROOT/build
% make
% make check
Let's take a closer look at the output. Notice that the makefile
automatically reran the configure
script since it was modified by
autoconf
. The configure
script reports which subprojects are being
included in the current project. If you look at the output from the
configure
script you will see the following two lines:
configure: configuring internal subproject utst
configure: configuring internal subproject sgp
The makefile has rules to build the unit test executable
sgp/Point-utst
from the sgp/Point.t.cc
source file. You should see
the following output from running sgp/Point-utst
:
Unit Tests : sgp/Point-utst
Running test suite : default
+ Running test case : TestAddition
The output from each unit test is saved in a .out
file, and the output
tells us that the TestAddition
test case was run and that there were no
errors. make check
also displays a summary of all the unit test results
(by grepping the .out
files) producing this output:
Unit Tests : utst/utst
Unit Tests : sgp/Point-utst
If there were any errors they would be displayed in this summary as well
as in the per unit test output. () Run the sgp/add-points
command line
program and verify that it works. () Try purposely changing the unit
test so that it fails and observe how the output from make check
changes. () Try also adding a new subtraction operator for points and
the associated unit test. () Finally, add a new class to the sgp
subproject called sgp::Rectangle
which uses two points to represent a
rectangle.
In this section we will add a second subproject called the Image
Processing Library (ipl
). This subproject might include support for
various image file formats and image processing routines like color
conversion or coordinate transformations. For the tutorial, we will
implement a new class called RasterImage
. Ideally this class would
take a diagram made up of many primitives from the sgp
subproject and
rasterize them into an image. For this tutorial, we will just use a
single point instead of a full diagram. This section will illustrate
making one subproject depend on another subproject, and also show how to
add a subproject autoconf fragment. As with the sgp
subproject, we
first create a new subdirectory.
% cd $PROJROOT
% mkdir ipl
We now create the header file ipl/RasterImage.h
.
% cd $PROJROOT
% cat > ipl/RasterImage.h \
<<'END'
#ifndef IPL_RASTER_IMAGE_H
#define IPL_RASTER_IMAGE_H
#include "sgp/Point.h"
#include <iostream>
namespace ipl {
class RasterImage {
public :
RasterImage();
void set_diagram( const sgp::Point& pt );
sgp::Point get_diagram() const;
private :
sgp::Point m_pt;
};
std::ostream& operator<<( std::ostream& os, const RasterImage& im );
bool operator==( const RasterImage& im1, const RasterImage& im2 );
}
#endif
END
As before, we use the abbreviated subproject name as a namespace and as a
prefix for preprocessor macros. Next we create the source file
ipl/RasterImage.cc
.
% cd $PROJROOT
% cat > ipl/RasterImage.cc \
<<'END'
#include "ipl/RasterImage.h"
namespace ipl {
RasterImage::RasterImage() { }
void RasterImage::set_diagram( const sgp::Point& pt ) { m_pt = pt; }
sgp::Point RasterImage::get_diagram() const { return m_pt; }
std::ostream& operator<<( std::ostream& os, const RasterImage& im ) {
return ( os << "[ " << im.get_diagram() << "]" );
}
bool operator==( const RasterImage& im1, const RasterImage& im2 ) {
return ( im1.get_diagram() == im2.get_diagram() );
}
}
END
As you can see, the RasterImage
is just a little wrapper around a
single point. We now add a simple unit test.
% cd $PROJROOT
% cat > ipl/RasterImage.t.cc \
<<'END'
#include "utst/utst.h"
#include "sgp/Point.h"
#include "ipl/RasterImage.h"
UTST_AUTO_TEST_CASE( TestBasic )
{
ipl::RasterImage im1, im2;
im1.set_diagram( sgp::Point(1,2) );
im2.set_diagram( sgp::Point(1,2) );
UTST_CHECK_EQ( im1, im2 );
}
int main( int argc, char* argv[] ) {
return utst::auto_command_line_driver( argc, argv, "ipl" );
}
END
We now need to add the autoconf fragment (ipl/ipl.ac
) and the makefile
fragment (ipl/ipl.mk.in
). Our autoconf fragment will use the second
argument to MCPPBS_SUBPROJECT
to tell the build system that the ipl
subproject depends on the sgp
subproject. The build system can then
take care of making sure that we link the appropriate libraries together
when building the ipl
subproject. The build system can also discover
indirect dependencies. So if the sgp
subproject in turn depended on a
subproject foo
the build system would discover this and make sure that
ipl
will build correctly.
% cd $PROJROOT
% cat > ipl/ipl.ac \
<<'END'
MCPPBS_SUBPROJECT([ipl],[sgp])
END
Our makefile fragment looks similar to the sgp
subproject except that
now we use ipl_
as a prefix for all make variables.
% cd $PROJROOT
% cat > ipl/ipl.mk.in \
<<'END'
ipl_intdeps = @ipl_intdeps@
ipl_cppflags = @ipl_cppflags@
ipl_ldflags = @ipl_ldflags@
ipl_libs = @ipl_libs@
ipl_hdrs = ipl/RasterImage.h
ipl_srcs = ipl/RasterImage.cc
ipl_test_srcs = ipl/RasterImage.t.cc
END
Finally, we include the ipl
subproject in the top-level
configure.ac
, build the project, and run the unit tests.
% cd $PROJROOT
% sed -i.bak '/MCPPBS_INCLUDE_INTERNAL(\[sgp\])/ a \
MCPPBS_INCLUDE_INTERNAL([ipl]) \
' configure.ac
% cd build
% make check
We add ipl
after the other subprojects since the subprojects must be
ordered such that subprojects only depend on those included earlier.
Since ipl
depends on sgp
, sgp
must be included before ipl
.
Notice that this time we did not explicitly rerun the autoconf tools.
The makefile detects that configure.ac
has changed and reruns the
autoconf tools. The configure
script output now mentions the ipl
subproject.
configure: configuring internal subproject utst
configure: configuring internal subproject sgp
configure: configuring internal subproject ipl
Also notice that the makefile only builds the ipl
subproject. There is
no need to rebuild the utst
nor the sgp
subprojects since they have
not changed. The automatic dependency checking helps minimize what needs
to be recompiled.
Look carefully at the compiler and linker command lines which the build
system is using to build the new subproject. You should see something
like this when building ipl/RasterImage.o
g++ -Wall -g -O3 -MMD -MP -I. -I.. -c -o ipl/RasterImage.o \
../ipl/ipl-RasterImage.cc
You should see something like this when linking the
ipl/RasterImage-utst
unit test executable.
g++ -L. -o ipl/RasterImage-utst ipl/RasterImage.t.o \
-Lipl -lipl -Lsgp -lsgp -Lutst -lutst
Each subproject builds a library which can then be used by that
subproject's executables and also by other subprojects. The build system
has correctly used libipl.a
and libsgp.a
when linking the unit test
case. The build system will automatically determine the proper order of
the libraries for the linker command line.
(*) To understand how dependencies between subprojects work, try removing
sgp
from the MCPPBS_SUBPROJECT
autoconf macro in ipl/ipl.ac
. If you
then just rerun make check
it won't actually cause an error, because of
some subtleties in how make manages dependencies (only the compiler and
linker command lines change and make isn't smart enough to know this
means we need to recompile the unit tests). So let's do a clean build by
first using make clean
and then using make check
. You should get link
errors indicating that ipl cannot find the appropriate library. If you
look at the link command line you will see that -lsgp
is not present.
Go ahead and add sgp
back to the MCPPBS_SUBPROJECT
autoconf macro and
verify that things work once again.
In the last part of this section, we will illustrate how to use a
subproject autoconf fragment. Let's say that the ipl
subproject wants
to use the globally installed libjpeg.a
library to read and write JPEG
files. If this library is not installed, then we still want to build our
project; it just won't have JPEG support. The following is some example
autoconf code which we can add to ipl.ac
to do this check.
% cd $PROJROOT
% cat >> ipl/ipl.ac \
<<'END'
AC_CHECK_HEADERS([jpeglib.h],[have_jpeg="yes"],[have_jpeg="no"])
AS_IF([test "$have_jpeg" = "yes"],[
AC_SEARCH_LIBS([jpeg_std_error],[jpeg],[],[have_jpeg="no"])
])
AS_IF([test "$have_jpeg" = "no"],[
AC_MSG_WARN([Could not find jpeg library])
AC_MSG_WARN([Build will not include jpeg support])
],[
AC_DEFINE([IPL_HAVE_JPEG],,[Define if jpeg library is available])
AS_VAR_APPEND([sgp_libs],"-ljpeg ")
])
END
This code first uses the AC_CHECK_HEADERS
macro to see whether or not
the jpeglib.h
header file exists. If so, then we also check if the
library libjpeg.a
exists with the AC_SEARCH_LIBS
macro. We need to
pass a function which is in the library so that the configure
script
can try to link this function against the library. Although we could
just use main
, using a function which is specific to that library is
more robust. If the header and the library exist, then we use the
AC_DEFINE
macro to define the C preprocessor macro IPL_HAVE_JPEG
and
we also append the library to sgp_libs
which will eventually be
substituted into our makefile fragment. Notice that as with other
macros, we use the abbreviated subproject name as a prefix. Most of the
built-in checks (eg. AC_CHECK_HEADERS
, AC_SEARCH_LIBS
) are cached so
that it is perfectly fine to include the same check in different
subproject autoconf fragments. Note that autoconf macros use the
compiler set with AC_PROG_CXX
to do these checks, so if you are using
a cross compiler and have installed the desired libraries in that
cross-compiler's sysroot, then the checks should work.
Now that we have written the check, we also need to update our code so
that our subproject can work with our without the JPEG library. We do
this by including ipl/config.h
(which is generated by the configure
script) and then writing conditional code based on whether or not the
IPL_HAVE_JPEG
macro is defined. For illustrative purposes we just
print out a message in ipl/RasterImage.h
based on this macro. We want
to add this to the top of ipl/RasterImage.h
:
#include "ipl/config.h"
#ifdef IPL_HAVE_JPEG
#warning "Building with the jpeg library"
#else
#warning "Building without the jpeg library"
#endif
For the tutorial, we use sed to update the ipl/RasterImage.h
file:
% cd $PROJROOT
% sed -i.bak '/#include "sgp\/Point.h"/ a \
#include "ipl/config.h" \
#ifdef IPL_HAVE_JPEG \
#warning "Building with the jpeg library" \
#else \
#warning "Building without the jpeg library" \
#endif \
' ipl/RasterImage.h
So let's test our changes out by rebuilding the project.
% cd $PROJROOT/build
% make check
Notice that the makefile realizes that ipl/ipl.ac
has changed and
reruns the autoconf tools before rebuilding the project. If the JPEG
library is available on your system then the configure
script should
generate this output:
configure: configuring internal subproject ipl
...
checking jpeglib.h usability... yes
checking jpeglib.h presence... yes
checking for jpeglib.h... yes
checking for library containing jpeg_std_error... -ljpeg
If the JPEG library is not available on your system then you will see this output:
configure: configuring default subproject : ipl
...
checking jpeglib.h usability... no
checking jpeglib.h presence... no
checking for jpeglib.h... no
configure: WARNING: Could not find jpeg library
configure: WARNING: Build will not include jpeg support
Make sure you see the correct output from preprocessing
ipl/RasterImage.h
indicating whether or not the library is available.
Take a look in ipl/config.h
and find the IPL_HAVE_JPEG
preprocessor
macro and see if it is defined or not.
In addition to checking for external libraries, a subproject's autoconf
fragment can be useful for checking if the compiler supports a certain
standard library or language feature. For example, the following
fragment checks to see if the STL vector class is available and if so it
defines the IPL_HAVE_STL_VECTOR preprocessor macro. We could potentially
add code to our subproject to use the STL vector
if it is available or
use some other data structure if it is not available.
% cd $PROJROOT
% cat >> ipl/ipl.ac \
<<'END'
AC_CACHE_CHECK(
[whether compiler supports std::vector],
[ac_cv_have_std_vector],
[
AC_LANG([C++])
AC_COMPILE_IFELSE([AC_LANG_PROGRAM(
[[
#include <vector>
]],[[
std::vector<int> vec;
]])],
[ac_cv_have_std_vector="yes"],
[ac_cv_have_std_vector="no"])
])
AS_IF([test "$ac_cv_have_std_vector" = "yes"],[
AC_DEFINE([IPL_HAVE_STD_VECTOR],,[Define if std::vector is available])
])
END
This autoconf check essentially creates a small little test program
which just instantiates a vector
object and then tries to compile the
test program to see if the compiler supports the vector
class. See the
autoconf documentation for more information on the AC_COMPILE_IFELSE
and the AC_LANG_PROGRAM
macros. Notice that entire check is wrapped in
AC_CACHE_CHECK
so that we only do the check once even if the same code
is used in two subprojects. Rerun the autoconf tools and take a look at
the output from configure
and the contents of ipl/config.h
% cd $PROJROOT/build
% make check
Making a subproject optional is useful when the subproject includes
early development code or when the subproject provides auxiliary
functionality. To mark a subproject as optional, we just need to add a
*
to the appropriate subproject name in configure.ac
. For example,
to make both the sgp
and the ipl
subprojects optional we would
change the configure.ac
to look like this:
MCPPBS_INCLUDE_INTERNAL([sgp*])
MCPPBS_INCLUDE_INTERNAL([ipl*])
Let's make this change and then rebuild the project:
% cd $PROJROOT
% sed -i.bak \
-e 's/MCPPBS_INCLUDE_INTERNAL(\[sgp/&*/' \
-e 's/MCPPBS_INCLUDE_INTERNAL(\[ipl/&*/' configure.ac
% cd build
% make check
You will notice that only the utst
subproject is included in the
build. You can see this by looking at the output from the configure
script and at which unit tests are actually run. You can include the
optional subprojects with command line arguments to the configure
script. (*) Try configure --help
to see a list of valid command line
arguments, including the arguments which enable optional subprojects.
For example, here is how we might enable both optional subprojects.
% cd $PROJROOT/build
% ../configure --with-sgp --with-ipl
% make check
We can also enable all optional subprojects with
--with-optional-subprojects
. The build system will use the dependency
information given with the MCPPBS_SUBPROJECT
autoconf macro to enable
dependencies as necessary. For example, if we just use --with-ipl
the
build system will also enable the sgp
subproject since ipl
depends
on sgp
.
% cd $PROJROOT/build
% ../configure --with-ipl
% make check
You can change this behavior by including an asterisk suffix in the list
of dependencies give with MCPPBS_SUBPROJECT
. You should only do this
if your subproject can correctly build and function without the
dependency. The user would then need to explicitly enable the
corresponding subproject if desired.
Before moving on we make all the subprojects required again.
% cd $PROJROOT
% sed -i.bak \
-e 's/\(MCPPBS_INCLUDE_INTERNAL(\[sgp\)\*/\1/' \
-e 's/\(MCPPBS_INCLUDE_INTERNAL(\[ipl\)\*/\1/' configure.ac
% autoconf && autoheader
% cd build
% ../configure
% make check
The install
make target installs your subprojects' programs and
libraries. By default, all of the enabled subprojects are installed into
/usr/local
. In the following example we use the --prefix
command
line argument to the configure
script to install the project into an
alternative directory.
% cd $PROJROOT/build
% ../configure --prefix=$PWD/install
% make
% make install
Take a look at the files in the install
directory. The build system has
installed the sgp-add-points
program into the install/bin
directory.
Notice how the build system uses the subproject name as a prefix when
installing binaries to avoid name conflicts. Sometimes we want to install
the libraries and header files for some of our subprojects so that other
projects can leverage them. To do this we use the MCPPBS_INSTALL_LIBS
macro in the top-level configure.ac
file.
As an example we will install the libraries for the utst
, sgp
, and
ipl
subprojects. Before installing a subproject as a library we must
create a subproject.pc.in
file in the subproject subdirectory. This
file is a package config metadata file which helps users of our library
know how to correctly configure their compiler and linker command lines.
See the pkg-config
man page for more information, and utst.pc.in
for
an example. For now we simply copy the utst.pc.in
file and modify it
appropriately.
% cd $PROJROOT
% cp utst/utst.pc.in sgp/sgp.pc.in
% sed -i.bak \
-e 's/utst/sgp/g' \
-e 's/\(Description :\).*/\1 Simple geometric primitives/' sgp/sgp.pc.in
% cd $PROJROOT
% cp utst/utst.pc.in ipl/ipl.pc.in
% sed -i.bak \
-e 's/utst/ipl/g' \
-e 's/\(Description :\).*/\1 Image processing library/' ipl/ipl.pc.in
You can customize these package config metadata files as needed to make
sure that the installed libraries are easily leveraged by other
projects. We can now use the MCPPBS_INSTALL_LIBS
macro in our
configure.ac
to tell the build system that we would like to install
libraries for all three subprojects.
% cd $PROJROOT
% sed -i.bak '/MCPPBS_INCLUDE_INTERNAL(\[ipl\])/ a \
MCPPBS_INSTALL_LIBS([utst,sgp,ipl]) \
' configure.ac
% autoconf && autoheader
Now let's reinstall our project and take a look at what is installed into the install directory.
% cd $PROJROOT/build
% make
% make install
The build system has installed three libraries into install/lib
and
the associated header files into subdirectories of install/include
.
Also notice the package config metadata files have been installed into
install/share/pkgconfig
.
Often we would like to install our project into a common directory
shared by many other projects (eg. /usr/local
). The problem with using
the above approach is that it can be very difficult to uninstall an old
version of the project or upgrade to a new version of the project. So
instead we can use the program stow
to stage the installation in a
separate location and then create symlinks from the shared location.
Here is an example of stowing the project.
% cd $PROJROOT/build
% rm -rf install
% ../configure --prefix=$PWD/install
% make
% make install prefix=$PWD/install/pkgs/mcppbs-tut
% cd install/pkgs
% stow mcppbs-tut
Take another look in the install
directory. You should see symlinks
from the header and library files in install/include
and install/lib
back into the install/pkgs/mcppbs-tut
directory. If we want to
uninstall a package we can just use the --delete
option to stow
.
% cd $PROJROOT/build/install/pkgs
% stow --delete mcppbs-tut
The build system provides support for automatically using stow
for
installation. We can enable this feature with the --enable-stow
command line option to the configure script. This option tells the
makefile to create the staging area and to run stow appropriately.
% cd $PROJROOT/build
% rm -rf install
% ../configure --enable-stow --prefix=$PWD/install
% make
% make install
Compare the result to what we saw earlier. Notice that the
pkgs/mcppbs-tut
subdirectory includes the version number as a suffix.
The version number comes from configure.ac
or from the
scripts/vcs-version
script. The build system also has support for a
default stow prefix specified with the $STOW_PREFIX environment variable.
If this variable is set there is no longer any need to specify the prefix
explicitly when running configure.
% cd $PROJROOT/build
% rm -rf install
% export STOW_PREFIX=$PWD/install
% ../configure --enable-stow
% make
% make install
By default pkg-config
only looks in some standard system directories
for its metadata files. So even though we have installed the package
config metadata files, the actual pkg-config
program does not yet know
it should look in this newly created install directory. We can set the
PKG_CONFIG_PATH
environment variable to tell pkg-config
where to
look for its metadata files. You might want to set your
PKG_CONFIG_PATH
as a function of your STOW_PREFIX
in your startup
scripts.
% cd $PROJROOT/build
% export PKG_CONFIG_PATH="$PWD/install/share/pkgconfig:$PKG_CONFIG_PATH"
% pkg-config --exists utst sgp ipl; echo $?
Once we have installed a set of subprojects, other projects can easily
include them with the MCPPBS_INCLUDE_EXTERNAL
autoconf macro in their
top-level configure.ac
. There is a tradeoff between copying the source
code of a subproject into your subproject and making it an internal
subproject versus linking against and external subproject. Internal
subprojects allow a developer to customize the subproject's source code
and insulate the developer from changes to the original source code, but
they also require developers to always compile these subproject's and to
manually merge in updates from the original source. By using an external
subprojects, a developer can avoid constantly rebuilding the subproject
and automatically leverages any updates, but if those updates break an
interface the developers will have to immediately update their own code.
Of course, external subprojects also create an external dependency - we
have to build and install these subprojects before we can build a
project which depends on them.
To illustrate external subprojects we will modify our project to use the
just installed versions of the utst
and sgp
subprojects. First we
update our configure.ac
.
% cd $PROJROOT
% sed -i.bak \
-e 's/\(AC_CONFIG_SRCDIR\).*/\1([ipl\/ipl.ac])/' \
-e 's/\(MCPPBS_INCLUDE\)_INTERNAL(\[utst\])/\1_EXTERNAL([utst])/' \
-e 's/\(MCPPBS_INCLUDE\)_INTERNAL(\[sgp\])/\1_EXTERNAL([sgp])/' \
-e 's/MCPPBS_INSTALL_LIBS.*//' configure.ac
% autoconf && autoheader
Notice that we need to change AC_CONFIG_SRCDIR
since our project no
longer includes the utst/utst.h
source file. Now we rebuild the
project in a new build directory.
% cd $PROJROOT
% mkdir build-extsprojs
% cd build-extsprojs
% ../configure
% make
% make check
The configure
script will report that it has found the external utst
and sgp
subprojects, and that it is configuring the internal ipl
subproject.
checking for external subproject utst... yes
checking for external subproject sgp... yes
configure: configuring internal subproject ipl
If you take a close look at the compiler and linker command lines you
can see that things have been configured such that the project is now
linking against the libraries we installed in the previous section.
Remember that pkg-config
is only able to find these libraries because
we set the PKG_CONFIG_PATH
environment variable appropriately.
The make dist
target will create a package tarball. A package tarball
is simply a single compressed file which contains the build system and
all of the project's source code. This tarball contain all of the
subprojects included in the top-level configure.ac
regardless of
whether or not they are enabled. The distcheck
make target will create
a tarball, then try and "check" that this tarball is complete by
decompressing it and running make
, make check
, and make distclean
.
Here is an example:
% cd $PROJROOT/build
% make dist
% make distcheck
Notice that the tarball includes the version number as a suffix. If your
project is under version control then you should set the version number
to ?
in your top-level configure.ac
. This tells the build system to
use the scripts/vcs-version
script to determine the version number
using your version control system. Currently only git is supported.
We are now done with the tutorial, so we can remove the tutorial's working directory.
% cd $PROJROOT/..
% rm -rf $PROJROOT
- Reference
In this part we provide more detailed reference on the various parts of the build system including how the Autoconf and Make toolflow works as well as some more details on subprojects. As mentioned above, subprojects are both a physical concept (ie. how the files are named and organized) as well as a logical concept (ie. how the classes and functions are named and organized).
Each subproject is contained in a subdirectory beneath the main project
directory. The name of the subdirectory should be an abbreviated form
(four to eight characters) of the full name of the subproject. The
subdirectory includes all of that subproject's header and source files.
Subprojects should create a top-level header file (eg. utst.h
) which
uses the #include
preprocessor directive to include all of the header
files in that subproject. Developers are strongly encouraged to include
general user documentation in the spirit of utst.md
to help users of
the subproject. Every subproject should also include two files which
tells the build system about the subproject: an autoconf fragment tells
the top-level autoconf script how to configure the subproject (eg.
utst/utst.ac
) and a makefile fragment tells the top-level makefile how
to build the subproject (eg. utst/utst.mk.in
). The build system is
similar in spirit to the non-recursive makefile approach advocated by
Peter Miller in his paper titled "Recursive Make Considered Harmful".
In addition to the physical aspects of the subproject, there are also
policies regarding the logical aspects. All functions and classes in the
subproject should be in a C++ namespace having the same name as the
abbreviated subproject name. For example, all of the functions and
classes in the unit testing framework are in the utst
namespace.
Preprocessor macros should include a prefix which also corresponds to
the uppercase form of the abbreviated subproject name (eg. the
UTST_TEST_CASE
preprocessor macro).
If there are two parts to a subproject's name they should be separated
with a dash (eg. foo-bar
) for physical purposes (ie. the subdirectory
name and the file name prefix) and separated with an underscore
(eg. foo_bar
) for logical purpose (ie. the name of the subproject's
C++ namespace and the preprocessor macro prefix).
The basic build process involves two steps. First, we run a portable
shell script named configure
which examines the end-user's build
platform and customizes the build system so that it will work correctly.
Then we use the make
command to build the project. The build system is
made to work in any directory, and developers are strongly discouraged
from building their project directly in the source directory. A separate
build directory keeps the source separate from the generated scripts,
object files, and binaries. The basic build process is shown below
assuming we start in the root directory of the project.
> mkdir build
> cd build
> ../configure
> make
Designing C++ projects such that they are portable across various platforms can be particularly challenging. The Modular C++ Build System leverages the GNU Autoconf tools to help configure the process for the desired build platform. The basic toolflow for using the GNU Autoconf tools is shown below:
(configure.ac)-. .->[autoheader]->(config.h.in)--. .->(config.h)
+-+ \ |
(aclocal.m4)--' '-->[autoconf]------------>[(configure)]-+->(Makefile)
/
(Makefile.in) -----------------------------------'
In this figure, files are denoted with parentheses ()
and tools are
denoted with brackets []
. The process begins with the configure.ac
script which is "written" in the m4 macro language. This script contains
macros which tell the build system about the project and also specify
various checks to perform. For example, it might specify that we should
check for a specific library or check to see if the compiler supports a
specific language feature. Based on these checks, the build system can
change our makefile or conditionally compile various portions of our
project so that we can correctly build it on different platforms. The
aclocal.m4
file contains additional macro functions that can be used
with the standard autoconf macros in configure.ac
.
The configure.ac
script is used as input to the autoconf
command
which generates a portable shell script named configure
. The
configure
shell script is executed by the end-user to customize the
build system for their specific platform. The configure
script is
annotated with [()]
since it is a file which is generated by
autoconf
, but then it is executed as a tool to customize the build
system for the current platform. The configure
script takes as input
the Makefile.in
and the config.h.in
files and does variable
substitutions to generate the Makefile
and the config.h
files. For
example, if a specific library is installed we might add that library to
the linker command line in the Makefile
, or if a specific language
feature is available then we might define a preprocessor macro in
config.h
. Once the Makefile
and config.h
are generated we can use
make
to build the project.
The autoheader
command automatically generates config.h.in
by
scanning configure.ac
for certain macros. The details are not too
important, except that developers should always run autoconf
and
autoheader
at the same time so that the various scripts stay
synchronized and up-to-date. To regenerate the configure
script and
config.h.in
developers can just execute the following command in the
top-level project directory.
> autoconf && autoheader
Given the basic GNU Autoconf toolflow, the following commands will create a new build directory and configure the build system (assuming we start in the top-level project directory).
> mkdir build
> cd build
> ../configure
Note that most of the time we don't actually need to rerun autoconf
and autoheader
; we just execute the portable configure
shell script
to do the configuration. We only need to rerun autoconf
and
autoheader
when we change configure.ac
. So to simplify the build
process for the end-users we should always include the configure
script in the distribution.
We should also note that the Modular C++ Build System actually doesn't
use a single config.h
header file. Instead config.h.in
is used to
generate a unique configuration header file for each subproject (eg.
utst/config.h
). Per subproject configuration header files avoid
filename conflicts when we install and then use subproject libraries.
New projects need to update configure.ac
with the project's name,
version number, maintainer's name, and the shorter abbreviated project
name. They then need to rerun autoconf
and autoheader
. Here is an
example of what this metadata looks like in configure.ac
:
m4_define( proj_name, [Image Processing Tools])
m4_define( proj_maintainer, [Christopher Batten])
m4_define( proj_abbreviation, [ipt])
The version metadata deserves special mention. Usually we are working on
a project which is checked into some kind of version control system
(VCS). In this case we don't really want to hard-code a version number
into the configure.ac
file since it will quickly be out of date with
respect to the VCS. So instead we usually mark the version as ?
in the
configure.ac
file and just use the standard VCS files to track
revisions. When we make a distribution tarball though, the source code
is exported from the VCS and we will loose version information. So the
build system includes a script called vcs-version
in the scripts
subdirectory which creates a version string suitable from the VCS. We
can then use this string in the distribution. The makefile's dist
target will execute vcs-version
and substitute the version string
into the tarball name, the README, and the configure.ac
script so that
users of the tarball will know what version the source code is from.
This is just a very simplified description of the autoconf toolflow. See
the GNU Autoconf documentation (http://www.gnu.org/software/autoconf
)
for more details on using these tools.
The Modular C++ Build System includes a couple changes to this basic GNU
Autoconf toolflow. Several new macros are defined in aclocal.m4
which
can be used in configure.ac
. The following macros initialize the build
system and check for some standard tools used by the build system.
MCPPBS_INIT
: Initialize build systemMCPPBS_PROG_INSTALL
: Check for install script, stow-based installMCPPBS_PROG_RUN
: Check for isa sim for non-native builds
The configure.ac
should also include subprojects with either the
MCPPBS_INCLUDE_INTERNAL
or the MCPPBS_INCLUDE_EXTERNAL
autoconf
macros. An internal subproject means that the source code for that
subproject is included as a subdirectory of the project, while an
external subproject means that this project will simply be linking
against an externally installed subproject library. The following is an
example using these macros for a project with four subprojects:
MCPPBS_INCLUDE_EXTERNAL([utst])
MCPPBS_INCLUDE_EXTERNAL([sproj-a])
MCPPBS_INCLUDE_INTERNAL([sproj-b])
MCPPBS_INCLUDE_INTERNAL([sproj-c*])
The subprojects should be ordered such that subprojects only depend on
those included earlier. In this example, sproj-b
might use some of the
classes in sproj-a
so we need to make sure that we include sproj-a
first and then include sproj-b
. For this example, we assume that both
sproj-b
and sproj-c
depend on sproj-a
.
All projects need to include the utst
subproject. If you do not want
to use the utst
unit testing framework then you will need to
explicitly modify the Makefile.in
.
The *
suffix (eg. sproj-c*
) denotes an optional subproject. Optional
subprojects are not included in the build by default. Making a
subproject optional is useful when the subproject includes early
development code or when the subproject provides auxiliary
functionality. We can enable optional subprojects with command line
arguments to the configure
script. To see a list of possible command
line arguments use --help
. To configure the example project with
sproj-c
enabled we can use the following command (assuming we are in
the build directory):
> ../configure --with-sproj-b
To enable all optional subprojects we can use the
--with-optional-subprojects
command line argument.
The final MCPPBS_INSTALL_LIBS
autoconf macro tells the build system
which (if any) subproject libraries you would like to install.
Subproject executables are always installed (see below), but the
library for a subproject is only installed if it is on the list
specified with MCPPBS_INSTALL_LIBS
. The build system uses the
pkg-config
tool and its associated package config metadata files for
both installing subproject libraries and using external subprojects. So
any subproject on this list must have a corresponding package config
metadata file (utst/utst.pc.in
). You can use special autoconf
variables in this metadata file which the configure script will then
fill in when you build your project.
The main problem with the build system as described so far, is that if a
subproject requires platform dependent checks then we would need to add
those checks to the top-level configure.ac
. For example, assume we have
a subproject which makes use of an external library. We would need to
modify configure.ac
to check if that library is available and if not
cause an error so that the end-user can figure out how to proceed. But
if we modify configure.ac
then the subproject is no longer modular in
the sense that we cannot just copy it into another project; we also have
to copy some code from configure.ac
into the new project.
The Modular C++ Build System addresses this problem by allowing
subprojects to have their own local autoconf fragment. The fragment
filename should be the abbreviated subproject name with the .ac
extension (eg. utst/utst.ac
). This script can contain standard
autoconf macros such as those used in configure.ac
. Even if a
subproject does not need any custom configuration, it still must include
an empty autoconf fragment. The top-level configure.ac
script will go
through and include the autoconf fragments in each enabled subproject.
Each subproject's autoconf fragment should always begin with a call to
the MCPPBS_SUBPROJECT
autoconf macro with the subproject's name as the
fist argument. This macro takes care of setting up the configure
information for the subproject. You can also optionally specify a second
argument which lists the internal and external subproject dependencies
for this subproject. The following is an example:
MCPPBS_SUBPROJECT([sproj-c],[sproj-a,sproj-b])
This tells the build system that sproj-c
depends on sproj-a
and
sproj-b
. The build system can handle indirect dependencies (eg. if
sproj-a
depends on other unlisted subprojects) so you should only list
the true direct dependencies. You can mark some of these dependencies
with an *
to indicate that this is an optional dependency. The build
system will not complain if an optional dependency is not present, but
will complain if a required dependency is not included.
A common use for the rest of the autoconf fragment is to check for an
external library. You can use the AC_CHECK_HEADERS
and the
AC_SEARCH_LIBS
macros to see whether or not an external library is
installed. Sometimes libraries and the associated header files are
installed in non-standard locations. It is easy to tell the build system
to add a path to the linker and the compiler command lines. Simply set
the LDFLAGS
and the CPPFLAGS
environment variables when configuring
the project. Here is an example which will allow the build system to
find a library if it is installed in /opt/local
:
> ../configure LDFLAGS='-L/opt/local/lib' \
CPPFLAGS='-I/opt/local/include'
In addition to checking for external libraries, we can also use the
autoconf fragment to check for various language features. Often for
these types of checks we can use a macro someone else has written and
added to the Autoconf Macro Library (http://autoconf-archive.cryp.to
).
For example, if we wanted to check if the build platform supported the
GCC ABI name mangling extension we could copy the
AX_CXX_GCC_ABI_DEMANGLE
macro into our autoconf fragment.
A developer can add whatever they want to their autoconf fragment, but they should probably prefix any macro names with their abbreviated subproject name to avoid name collisions. For more complicated checks which might need to be performed by multiple subprojects, it is suggested that the autoconf fragment use a shell variable to see if the check has already been done (possibly by a different subproject) to speedup the configuration process.
To drive the actual build process the Modular C++ Build System leverages
the ubiquitous make
tool. To use make
, a developer first writes a
Makefile
which contains a set of rules. Each rule specifies a target
file, a set of prerequisite files, and a command to generate the target
file from the prerequisites. The make
tool reads in a makefile
(usually named Makefile
) and executes the appropriate commands to
build, test, and install the project. When a developer changes a subset
of the source files, make
determines which target files are
out-of-date based on the timestamps of the target and prerequisite
files. make
then runs the minimum number of commands to regenerate all
out-of-date targets, and thus efficiently rebuilds the project. A
makefile can also have non-file targets which act as aliases for a set
of files to build or commands to run. The Modular C++ Build System
requires the GNU version of make since it uses several GNU make
extensions. See the GNU Make documentation
(http://www.gnu.org/software/make
) for more details on the make
tool.
Obviously writing a makefile from scratch for every new project is undesirable. The Modular C++ Build System provides a top-level makefile with most of the common make targets; the developer just needs to include a list of the project's source files. So the following commands will configure and build the project (assuming we start in the root directory of the project).
> mkdir build
> cd build
> ../configure
> make
The default make target is to build all of the project's libraries as
well as all of the program executables. To efficiently build C++
projects, it is important to track all of the dependencies between
header and source files. Each header file might depend on several other
header files by using the #include
preprocessor directive, and then
these header files might include even more header files. Ideally, we
want to accurately capture these dependencies in our makefile so that
make will just rebuild what is necessary, but this can be quite tedious.
By default, the Modular C++ Build System includes support for automatic
dependency generation. The makefile uses the -MMD -MP
command line
options to gcc
so that whenever gcc
builds an object file it also
generates a dependency makefile fragment (with the .d
extension). This
file captures all of the prerequisites required to build that object
file, and it is included by the top-level makefile so that the build
system automatically tracks all source file dependencies. The top-level
makefile also includes support for detecting when the configure
shell
script is out-of-date and automatically rerunning the autoconf tools.
As mentioned before, we organize our project into one subdirectory per
subproject. When including header files you must always use the
subdirectory as a prefix. For example, let's assume subproject A
has a
source file foo.h
which uses the #include
directive to include source
file bar.h
in subproject B
. Then foo.h
must use #include "B/bar.h"
. This is true even if you are including headers within the
same subproject.
We can also specify a desired target to build on the make
command
line. For example, to build the unit test framework's tutorial program
we use:
> make utst-utst
The top-level makefile also includes several standard "non-file" targets. Some of them are listed below:
default
: build all libraries and programscheck
: build and run all unit testsinstall
: install headers, project library, and some programsclean
: remove all generated content (except autoconf files)dist
: make a source tarballdistcheck
: make a source tarball, untar it, check it, clean itdistclean
: remove everything
So for example, to build and run all of the unit tests we use:
> make check
By default, the build system installs the project into /usr/local
. If
the abbreviated project name is foo
and the install directory is
/usr/local
, then the binaries are installed into /usr/local/bin
, the
header files are installed into /usr/local/include/foo
, and a single
library for the entire project (with the name libfoo.a
) is installed
into /usr/local/lib
. To install the project somewhere else, a
developer needs to use the configure
script's --prefix
command line
argument as follows:
> ../configure --prefix=$HOME/install
> make
> make install
There are two additional installation make targets which install a subset of the project:
install-libs
: Install header & libraries for some subprojectsinstall-exes
: Install just the executable binaries
Sometimes files are installed one place but accessed from a different
directory through symbolic links. For example, the GNU Stow package
management system stages installs and uses symbolic links to help keep
installed packages organized. For these type of installs, we need to set
make's prefix
variable when we actually do the install. Set
configure --prefix
to where the files will be accessed, and set
make install prefix=
to where the files should actually be copied.
> ../configure --prefix="access-dir"
> make
> make install prefix="staged-dir"
The build system includes support for automatically using stow-based
installation. To enable this feature use the --enable-stow
command
line option to the configure script.
> ../configure --enable-stow --prefix="access-dir"
> make
> make install
The configure script will check the $STOW_PREFIX environment variable and if it is set then it will use the corresponding path as the default prefix for stow-based installation. This allows a user to specify this variable once and then have all packages use stow to install in the same place.
> export STOW_PREFIX="access-dir"
> ../configure --enable-stow
> make
> make install
The problem with a single top-level makefile is that it requires each
subproject to change the common makefile. To address this, the Modular
C++ Build System allows each subproject to have its own local makefile
fragment. The fragment filename should be the abbreviated subproject
name with the .mk.in
extension (eg. utst/utst.mk.in
). The makefile
fragment should define the following make variables (assuming sproj
is
the name of the subproject):
sproj_intdeps
: Internal subproject dependenciessproj_cppflags
: Header include path (-I
)sproj_ldflags
: Library search paths (-L
)sproj_libs
: Libs required to build subproject (-l
)sproj_hdrs
: Header files (.h
)sproj_inls
: Inline header/source files (.inl
)sproj_srcs
: Source files (.cc
,.c
,.S
)sproj_test_srcs
: Sources for unit tests (.t.cc
)sproj_prog_srcs
: Sources for programs (.cc
,.c
)sproj_install_prog_srcs
: Sources for programs to install (.cc
,.c
)
The MCPPBS_SUBPROJECT
autoconf macro in subproject.ac
will
automatically figure out what compiler and linker flags are required to
build this subproject. So if you use the following in your make
fragment, then the configure
script will fill in these four values.
sproj_intdeps = @sproj_intdeps@
sproj_cppflags = @sproj_cppflags@
sproj_ldflags = @sproj_ldflags@
sproj_libs = @sproj_libs@
The top-level makefile adds additional targets and rules for each
subproject. For example, the makefile builds a library for each
subproject (eg. libsproj.a
). The top-level makefile also adds a target
which will build and run all of the unit tests for a single project. For
example, the following will run all of the unit tests for the foo
subproject:
> make check-foo
Developers can add whatever they want to the makefile fragment but they should probably prefix any macro names with their abbreviated subproject name to avoid name collisions.