4.1.2 building az-tags

The next step is to compile the program. We shall use Autotools, beginning with the simplest configure.ac we can:

AC_PREREQ([2.69])
AC_INIT([az-tags], [0.1], [sp1ff@pobox.com])
AC_CONFIG_MACRO_DIR([macros])
AC_CONFIG_SRCDIR([src/main.cc])
AC_CONFIG_AUX_DIR([build-aux])
AC_CONFIG_HEADERS([config.h])
AM_INIT_AUTOMAKE([-Wall -Werror])
LT_INIT
AC_RROG_CXX
AC_CONFIG_FILES([Makefile src/Makefile])

AC_PREREQ just asserts that Autoconf 2.69 is required to build a configure script from this template. AC_INIT is the Autoconf initialization macro. We’re going to need some custom macros for this project, so AC_CONFIG_MACRO_DIR tells Autoconf where to find them. AC_CONFIG_SRCDIR is just a sanity check– when running configure users will sometimes pass an incorrect value for --srcdir– this macro equips the generated configure script to catch that. AC_CONFIG_AUX_DIR tells Autoconf to place auxilliary scripts (missing & ionstall-sh, e.g.) in a sub-directory named build-aux.

AC_CONFIG_HEADERS tells Autoconf to generate a header file named config.h containing C preprocessor #defines for the project. Note that we need to generate a template file config.h.in via autoheader.

Finally, we initialize Automake, libtool, check for a C++ compiler & produce Makefile templates.

The Autmake template for the root makefile is trivial:

SUBDIRS = src

Let us begin the Makefile template in src:

bin_PROGRAMS = az-tags
az_tags_SOURCES = main.cc
AM_CXXFLAGS = -std=c++17

We will need to perform some one-time setup:

mkdir build-aux
touch NEWS README AUTHORS ChangeLog
autoheader
aclocal
autoconf
automake --add-missing

At this point, we can run ./configure, but make will fail miserably. Our program needs to be able to find scribbu, openssl and boost includes, along with the corresponding libraries. All the required libraries other than libscribbu provide pre-built macros which we can copy from the scribbu source distro into macros. Let us add the following lines to configure.ac, just before the call to AC_CONFIG_FILES:

PKG_CHECK_MODULES([GUILE], [guile-2.2])
AX_BOOST_BASE([1.58], [],
    [AC_MSG_ERROR([Scribbu requires boost_base 1.58 or later.])])
echo "Checkpoint 3: BOOST_LDFLAGS is $BOOST_LDFLAGS;" >&AS_MESSAGE_LOG_FD

AX_BOOST_IOSTREAMS
AX_BOOST_FILESYSTEM
AX_BOOST_SYSTEM
AX_CHECK_OPENSSL([],[AC_MSG_ERROR([Scribbu requires openssl.])])

Each of these will define Automake variables describing where we can find headers & libraries which we can add to src/Makefile.am, which now reads:

bin_PROGRAMS = az-tags
az_tags_SOURCES = main.cc
AM_CPPFLAGS = $(BOOST_CPPFLAGS)
AM_CXXFLAGS = -std=c++17 $(GUILE_CFLAGS)
AM_LDFLAGS = $(BOOST_LDFLAGS)
LDADD = $(GUILE_LIBS)           \
	$(BOOST_SYSTEM_LIB)     \
	$(BOOST_FILESYSTEM_LIB) \
	$(BOOST_IOSTREAMS_LIB)  \
	$(OPENSSL_LIBS)

This just leaves the question of where to find libscribbu. scribbu, at the time of this writing, provides no Autoconf macros (however, this sample provided the author the opportunity to prototype one).

We add the following code to configure.ac, just after the call to AC_PROG_CXX (it’s a lot of code; step-by-step explanation to follow):

AC_ARG_WITH([scribbu],
    AS_HELP_STRING([--with-scribbu=DIR],
                   [root directory of scribbu installation]),
    [
        case "$withval" in
	"" | y | ye | yes | n | no)
	    AC_MSG_ERROR([--with-scribbu takes a root directory]);;
	*)
	    scribbu_dirs="$withval";;
	esac
    ],
    [
        # Just use the defaults
	scribbu_dirs="/usr/local /usr /opt/local /sw"
    ])

dnl One way or another, we have one or more candidates in ${scribbu_dirs}
found=no
for scribbu_home in ${scribbu_dirs}; do
    AC_MSG_CHECKING([for scribbu/scribbu.h under ${scribbu_home}])
    if test -f "${scribbu_home}/include/scribbu/scribbu.hh"; then
        SCRIBBU_INCLUDES="-I${scribbu_home}/include/scribbu"
	SCRIBBU_LDFLAGS="-L${scribbu_home}/lib"
	SCRIBBU_LIBS="-lscribbu"
	found=yes
	AC_MSG_RESULT([yes])
	break
    else
        AC_MSG_RESULT([no])
    fi
done

if test "$found" != "yes"; then
    AC_MSG_ERROR([couldn't find scribbu])
fi

# try the preprocessor and linker with our new flags,
# being careful not to pollute the global LIBS, LDFLAGS, and CPPFLAGS
AC_MSG_CHECKING([whether compiling and linking against scribbu will work])

save_LIBS="$LIBS"
save_LDFLAGS="$LDFLAGS"
save_CPPFLAGS="$CPPFLAGS"
LIBS="$SCRIBBU_LIBS $LIBS"
LDFLAGS="$SCRIBBU_LDFLAGS $LDFLAGS"
CPPFLAGS="$SCRIBBU_CPPFLAGS $CPPFLAGS"

AC_LANG_PUSH([C++])
AC_CHECK_HEADER([scribbu/scribbu.hh], [scribbu_hh=yes], [scribbu_hh=no])
# I'd like to do AC_CHECK_LIB here, but I can't link against libscribbu
# in a test because it, in turn depends on a bunch of other libs
AC_CHECK_FILE([${scribbu_home}/lib/libscribbu.la],
    [scribbu_la=yes], [scribbu_la=no])
AC_LANG_POP([C++])

LIBS="$save_LIBS"
LDFLAGS="$save_LDFLAGS"
CPPFLAGS="$save_CPPFLAGS"

if test "yes" = "$scribbu_hh" && test "yes" = "$scribbu_la"; then
    AC_DEFINE([HAVE_SCRIBBU], [1], [Define to 1 if you have libscribbu])
else
    AC_MSG_ERROR([az-tags requires scribbu])
fi

AC_SUBST([SCRIBBU_CPPFLAGS])
AC_SUBST([SCRIBBU_LIBS])
AC_SUBST([SCRIBBU_LDFLAGS])

The first step is to locate libscribbu. We will form the variable scribbu_dirs containing one or more directories to check. Now, the user could always just tell us where it is. That is the reason we begin with AC_ARG_WITH: if the user invokes configure with --with-scribbu=... we will just use that. Otherwise, we will examine a default set of locations.

That’s what the for look does; for each location in scribbu_dirs, it checks for scribbu.hh in a sub-directory named include/scribbu of the current location. On success, we set a few variables recording that result & break. If we check all locations without success, then we fail.

Now, just because we found a header file at a given place doesn’t mean we can biuld against it or its associated library. The typical idiom is to execute the macros AC_CHECK_HEADER and AC_CHECK_LIB to make sure we can include the header and link against the library, respectively.

The problem in my case is that AC_CHECK_LIB will fail, not through any fault of libscribbu, but because it depends on a number of other libraries; the test will fail with unresolved externals & I can’t see how to add the relevant link flags in the macro. Instead, I settle for AC_CHECK_FILE.

If both these pass, we know we’re good to go; the question remains: how to record the information we’ve just discovered? The Autoconf manual states that one should never add options to user variables such as CPPFLAGS. The idiom seems to be to define new variables that the Automake author can add to their rules. In this case, create three new variables:

  1. SCRIBBU_CPPFLAGS to hold the -I option that will enable the build to find the libscribbu headers
  2. SCRIBBU_LIBS to hold the the -L options that will enable the build to link against libscribbu
  3. SCRIBBU_LDLAGS to hold any linker required flags

This lets us augment src/Makefile.am to:

bin_PROGRAMS = az-tags
az_tags_SOURCES = main.cc
AM_CPPFLAGS = $(BOOST_CPPFLAGS) $(SCRIBBU_CPPFLAGS)
AM_CXXFLAGS = -std=c++17 $(GUILE_CFLAGS)
AM_LDFLAGS = $(SCRIBBU_LDFLAGS) $(BOOST_LDFLAGS)
LDADD = $(SCRIBBU_LIBS)         \
        $(GUILE_LIBS)           \
	$(BOOST_SYSTEM_LIB)     \
	$(BOOST_FILESYSTEM_LIB) \
	$(BOOST_IOSTREAMS_LIB)  \
	$(OPENSSL_LIBS)

With that, we can configure:

$>: autoreconf -vfi
autoreconf: Entering directory `.'
autoreconf: configure.ac: not using Gettext
autoreconf: running: aclocal --force
...
$>: ./configure --prefix=$HOME
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
...
config.status: creating Makefile
config.status: creating src/Makefile
config.status: creating config.h
config.status: executing depfiles commands
config.status: executing libtool commands
$>: make
make  all-recursive
make[1]: Entering directory '/tmp/az-tags'
Making all in src
make[2]: Entering directory '/tmp/az-tags/src'
g++ -DHAVE_CONFIG_H -I. -I/home/mgh/doc/code/projects/az-tags/src -I..  -I/usr/include   -std=c++17 -pthread -I/usr/local/include/guile/2.2 -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o /home/mgh/doc/code/projects/az-tags/src/main.cc
...

We have a build! Let us take a look at a file downloaded from Amazon.com:

$>: scribbu dump lorca.mp3
"lorca.mp3":
ID3v2.3(.0) Tag:
452951 bytes, synchronised
...
COMM (<no description>):
Amazon.com Song ID: 203558254
...
9425708 bytes of track data:
MD5: 48ff9cadea7d842e9059db25159d2daa
ID3v1.1: The Pogues - Lorca's Novena
Hell's Ditch [Expanded] (US Ve (track 5), 1990
Amazon.com Song ID: 20355825
unknown genre 255

$>: src/az-tags lorca.mp3
lorca.mp3 has 1 ID3v2 tags, and an ID3v1 tag
updating the comment frame containing Amazon.com Song ID: 203558254
all tags processed; emplacing new tagset...
emplacing new tagset...done.
clearing ID3v1 comment
$>: scribbu dump lorca.mp3
"lorca.mp3":
ID3v2.3(.0) Tag:
452951 bytes, synchronised
...
COMM (amazon.com song id):
Amazon.com Song ID: 203558254
...
9425708 bytes of track data:
MD5: 48ff9cadea7d842e9059db25159d2daa
ID3v1.1: The Pogues - Lorca's Novena
Hell's Ditch [Expanded] (US Ve (track 5), 1990

unknown genre 255