Monty Taylor one of the primary contributors to the drizzle project has done some great work putting together a automake/autoconf framework which has been released as pandora-build The framework came about as a result of trying to maintain and improve the build system for several projects mostly related to drizzle or at least belonging to the drizzle developers. It was a pain to duplicate the work in multiple places and it was obvious the setup would be of great use to other projects and so it was pulled out, cleaned up, and released as a skeletal project. One of the best “features” of the project is that it turns on as much -W* and related flags as possible thus ensuring your code is as clean, portable, and bug-free as the compiler is capable of. It can take some getting used to, but trust me this is a very good thing.
If you go and grab the tarball you’ll have a pretty good start on a build system for you project, but as it stands there’s essentially no documentation on what to do next. Given that I’m intending to write a quick-start guide to getting a project off the ground with pandora-build. This post will also give you a starts on working with dynamically linked libraries and executables that depend on them. We’ll also use build out a basic unit test framework using cppunit.
Before we get started I wanted lament the lack of decent autotools documentation on the internet and to mention a good book/source of information that I grabbed several years ago: GNU Autoconf, Automake, and Libtool. There’s another book on the subject that is not yet available (as of this writing May/2010,) but it looks promising: GNU Autoconf, Automake, and Libtool
Ok, with that taken care of the first you’ll want to do is to make sure you have a few dependencies satisfied, a rough list of which follows:
- g++/make/etc.
- automake
- autoconf
- autoconf-archive
- libtool
- python
- libcppunit (devel libs)
On ubuntu you can run the following to install them:
sudo apt-get install build-essential automake autoconf autoconf-archive libtool \
python libcppunit-dev
To check out the results of this tutorial download pandora-example.tar.gz.
Once those are out of the way we can move on to downloading a release of pandora-build which can be found on the right-hand side of the pandora-build project page. Next we need to extract the files and rename the directory.
$ tar xvzf pandora-build-*.tar.gz
$ mv pandora-build-*/ pandora-example/
$ cd pandora-example
We can now look around at the base files. Most of them we’ll leave as-is, but there’s a few changes we’ll need to make. We’ll first look at configure.ac and modify the header and AC_INIT information to suit our project. After that we’ll add a AM_PATH_CPPUNIT line just before AC_CONFIG_FILES and add our pkg-config file to AC_CONFIG_FILES directive. Finall we’ll add some parameters to PANDORA_CANONICAL_TARGET to indicate that we want warnings, that we require cxx, and that we want to skip the shared library visibility checks and support, which you actually shouldn’t do, but it complicates the example. (see http://gcc.gnu.org/wiki/Visibility for information about visibility and look for a future post on the subject.) Anyway, after making these changes configure.ac should look like the following:
# pandora-example
AC_DEFUN([PANDORA_EXAMPLE_VERSION],[0.01])
AC_INIT([pandora-example],PANDORA_EXAMPLE_VERSION,
[http://something.pandora-example.com])
AC_CONFIG_SRCDIR([m4/pandora_canonical.m4])
AC_CONFIG_AUX_DIR(config)
PANDORA_CANONICAL_TARGET(warnings-always-on, require-cxx, skip-visibility)
AM_PATH_CPPUNIT(1.9.6)
AC_CONFIG_FILES(Makefile helloworld/helloworld.pc)
AC_OUTPUT
We now need to make a few modifications to Makefile.am, namely removing most of it’s contents leaving only ACLOCAL_AMFLAGS. Following that we’ll place a few variable declarations up top and add a couple include directives. We’ll end up with the following Makefile.am:
# pandora-example ACLOCAL_AMFLAGS= -I m4 # includes append to these: SUFFIXES = TESTS = check_PROGRAMS = noinst_HEADERS = nobase_nodist_include_HEADERS = nobase_dist_include_HEADERS = bin_PROGRAMS = sbin_PROGRAMS = lib_LTLIBRARIES = noinst_LTLIBRARIES = noinst_PROGRAMS = include helloworld/include.am include tests/include.am
We won’t be making use of all of those variables now, but it doesn’t hurt anything to have them in there so that if/when they’re necessary we don’t have to go back and add them. If you prefer feel free to remove everything we’re not immediately using.
Ok, that’s it for the editing portion of things, now we’ll need to create some directories and files. We’ll start by building the directory structure:
$ mkdir -p helloworld/include/helloworld-1.0 tests
And then continue on to create some files in those directories starting with helloworld/include.am:
# vim:ft=automake # library INCNAME=helloworld-1.0 lib_LTLIBRARIES+=helloworld/libhelloworld.la helloworld_libhelloworld_ladir=$(includedir)/$(INCNAME) helloworld_libhelloworld_la_CPPFLAGS=-I. -I$(srcdir)/helloworld/include helloworld_libhelloworld_la_LDFLAGS= helloworld_libhelloworld_la_HEADERS=\ helloworld/include/$(INCNAME)/phrase.h helloworld_libhelloworld_la_SOURCES=\ helloworld/phrase.cpp pkgconfigdir= $(libdir)/pkgconfig pkgconfig_DATA= helloworld/helloworld.pc # exec helloworld_helloworld_CPPFLAGS=-I. -I$(srcdir)/helloworld/include helloworld_helloworld_LDFLAGS= helloworld_helloworld_LDADD=helloworld/libhelloworld.la helloworld_helloworld_SOURCES=\ helloworld/helloworld.cpp bin_PROGRAMS+=helloworld/helloworld
There’s a couple things going on there as we’re defining both a shared library and an executable that depends on it. The first variable, INCNAME, is just a helper that prevents us from having to duplicate a value in multiple places. The lib_LTLIBRARIES line adds a new shared library to the set of things we’re going to build and the next set of lines tell automake about the shared library. I don’t want to get too far in to what’s going on here or else I’ll end up (re-)writing a book, but I do want to point out the use of named and versioned include directories as it’s a useful best-practice. In this case helloworld’s headers will be placed in the directory $(includedir)/helloworld-1.0/ and thus will be included by apps that need them with directives like “#include <helloworld-1.0/parser.h>.” This allows multiple major versions of your library to co-exist on a single system. The final piece of the library section tells automake where to find our pkg-config file and where to install it (more later.)
Ok the exec section is more straightforward, to anyone familiar with automake anyway, and just defines an executable that depends on libhelloworld.so that we defined above and makes sure that the app can get at the headers when it is compiling (before they’re installed or more correctly make sure that it uses the source versions, not installed ones.) The final line bin_PROGRAMS adds our exec to the list of executables that automake should build and install.
Now we’ll move on to the include.am file in the tests directory. It’s similar to the one we just created, but doesn’t create (shared) libraries or installed executables. It instead creates check_PROGRAMS, executables that are built and run with make check.
# vim:ft=automake
TESTS+=\
tests/phrase_test
check_PROGRAMS+=$(TESTS)
tests_phrase_test_CPPFLAGS=$(CPPUNIT_CFLAGS) -I$(srcdir)/helloworld/include
tests_phrase_test_LDFLAGS=$(CPPUNIT_LIBS) helloworld/libhelloworld.la
tests_phrase_test_SOURCES=tests/phrase_test.cpp
We have one more “infrastructure” file to deal with for now which will add pkg-config functionality to our shared library. The following needs to be placed in helloworld/helloworld.pc.in
prefix=@prefix@
exec_prefix=@exec_prefix@
libdir=@libdir@
includedir=@includedir@
Name: helloworld
Description: Hello World Pandora Build Example
Version: @VERSION@
Libs: -L${libdir} -lhelloworld
Cflags: -I${includedir}
autoconf/configure will take the above file and create helloworld.pc substituting all of the actual values in place of their variables and automake will install it to the appropriate directory thanks to the lines we added to helloworld/include.am’s library section.
That leaves us with the code. We won’t spend too much time on it as that’s not the purpose of the post. I’ll just tell you the file name and provide the contents to place in it. Here we go.
helloworld/include/helloworld-1.0/phrase.h:
#ifndef _HELLOWORLD_1_0_PHRASE_H_
#define _HELLOWORLD_1_0_PHRASE_H_
#include <string>
namespace helloworld
{
class Phrase
{
private:
std::string _text;
public:
Phrase (const std::string & text);
virtual ~Phrase ();
const std::string & getText() const
{
return this->_text;
}
void setText(const std::string & text)
{
this->_text = text;
}
protected:
Phrase (const Phrase & rhs);
Phrase & operator= (const Phrase & rhs);
};
}
#endif // _HELLOWORLD_1_0_PHRASE_H_
helloworld/phrase.cpp:
#include "config.h"
#include "helloworld-1.0/phrase.h"
using namespace helloworld;
Phrase::Phrase (const std::string & text) : _text (text)
{
}
Phrase::~Phrase ()
{
}
Phrase::Phrase (const Phrase & rhs) : _text (rhs._text)
{
}
Phrase & Phrase::operator= (const Phrase & rhs)
{
this->_text = rhs._text;
return *this;
}
helloworld/helloworld.cpp:
#include "config.h"
#include <stdio.h>
#include "helloworld-1.0/phrase.h"
int main (int argc, char ** argv)
{
(void)argc;
(void)argv;
helloworld::Phrase phrase ("Hello World!");
fprintf (stdout, "%s\n", phrase.getText().c_str());
return 0;
}
tests/phrase_test.cpp:
#include <cppunit/BriefTestProgressListener.h>
#include <cppunit/CompilerOutputter.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/TestRunner.h>
#include <cppunit/TestResult.h>
#include <cppunit/ui/text/TestRunner.h>
#include <helloworld-1.0/phrase.h>
class PhraseTest : public CPPUNIT_NS::TestFixture
{
CPPUNIT_TEST_SUITE (PhraseTest);
CPPUNIT_TEST (testPhrase);
CPPUNIT_TEST_SUITE_END ();
public:
void setUp ();
void tearDown ();
void testPhrase ();
};
CPPUNIT_TEST_SUITE_REGISTRATION (PhraseTest);
void PhraseTest::setUp ()
{
}
void PhraseTest::tearDown ()
{
}
void PhraseTest::testPhrase ()
{
std::string str = "Hello World!";
helloworld::Phrase phrase (str);
CPPUNIT_ASSERT_EQUAL (str, phrase.getText ());
CPPUNIT_ASSERT ("" != phrase.getText ());
std::string empty = "";
phrase.setText (empty);
CPPUNIT_ASSERT_EQUAL (std::string (empty), phrase.getText ());
CPPUNIT_ASSERT (str != phrase.getText ());
}
int main (int argc, char ** argv)
{
(void)argc;
(void)argv;
// informs test-listener about testresults
CPPUNIT_NS::TestResult testresult;
// register listener for collecting the test-results
CPPUNIT_NS::TestResultCollector collectedresults;
testresult.addListener (&collectedresults);
// register listener for per-test progress output
CPPUNIT_NS::BriefTestProgressListener progress;
testresult.addListener (&progress);
// insert test-suite at test-runner by registry
CPPUNIT_NS::TestRunner testrunner;
testrunner.addTest
(CPPUNIT_NS::TestFactoryRegistry::getRegistry ().makeTest ());
testrunner.run (testresult);
// output results in compiler-format, with a custom error format that works
// with a top-level Makefile.am and include.am's
CPPUNIT_NS::CompilerOutputter compileroutputter (&collectedresults,
std::cerr,
"%p:%l:");
compileroutputter.write ();
// return 0 if tests were successful
return collectedresults.wasSuccessful () ? 0 : 1;
}
Ok that’s it. You can now run autorun.sh, configure, and make and you “should” end up with a working shared library, pkg-config file, and executable. To test it all out do:
$ ./config/autorun.sh
$ ./configure --prefix=/tmp/helloworld-test
$ make
$ make check
$ make install
Congrats, you now have a skeletal pandora-build based system. You can go in and add/remove shared libraries, headers, execs, tests, … as needed mostly by copy-n-pasting the above code and stuff you can find in other projects (both libdrizzle and drizzle are good, albeit it complicated, examples.) Feel free to ask questions if you have them and be sure to subscribe as I plan to make this a series of posts on the subject. Some of the planned topics include what needs to be in my repository (.gitignores,) integrating valgrind and other code checking/debugging utilities, logging, distribution/tarring things up, and maybe even building debian and redhat packages.