Build System Coding Style

I use the following coding style and best practices for the build system of any software project that requires compilation. Currently, this page only documents software composed of C source; Perl packages work differently. Documentation for Perl build system coding style will be added when I have a chance.

Table of Contents

General Guidelines
Autoconf
Automake
Shared Libraries
Creating a Distribution

General Guidelines

All C software should use Autoconf and Automake as its build system. Details about how to use Autoconf and Automake, including which Automake options to use, is the topic of much of the rest of this document. Packages that build shared libraries should use Libtool to build and install those shared libraries. That's the current best way to maintain portable shared libraries. This includes loadable modules such as PAM modules.

All C software must support a separate build directory from the source directory, configured by creating an empty build directory and then invoking configure with the full path to the script in the source directory. In this mode, no rule should attempt to write to the source directory. This can be verified using the Automake distcheck target, mentioned under creating a distribution. Since the build systems for Perl, Python, and PHP add-on modules do not support this sort of separate build environment, the surrounding build system generally has to copy the relevant files for those add-ons into the build directory before recursing into the language-specific build infrastructure.

All C software must support installation into a different root directory from that configured via the configure script by installing all files relative to the DESTDIR variable, which can be set on the make command line. Automake by and large handles this automatically, but be careful of writing custom installation rules that don't support DESTDIR. This too can normally be verified with the Automake distcheck target.

When using Autoconf and Automake, put an executable shell script at the top of the source tree named autogen. This should start with:

    #!/bin/sh
    set -e
    autoreconf -i --force
    rm -rf autom4te.cache

to rebuild all the Autoconf and Automake files. It should also use pod2man to generate any manual pages, generate any protocol documentation, and do anything else required to go from the contents of the revision control repository to what's distributed as part of the end-user-ready package.

Autoconf

Software configuration and portability should be done using GNU Autoconf. I feel free to require the version available from current Debian unstable when I'm working on the package if there are any new features that are useful for that package. Do not bother reinventing wheels or patching around Autoconf bugs in order to remain compatible with old versions of Autoconf. The end-user distribution contains pre-built scripts, and life is too short to resolve problems already solved by others.

For details on writing good configure scripts, review the Autoconf manual (available via info autoconf). Reading the manual through once is highly recommended, to get an idea fo the tests that are already available. In addition to Autoconf best practices from the manual, I also follow these rules:

Automake

General Rules

Always use non-recursive Automake with a single Makefile.am at the top level that builds the entire source tree. This means referring to all files using relative paths and following the Automake manual documentation on how to use non-recursive Automake. It also requires adding subdir-objects to AUTOMAKE_OPTIONS in Makefile.am and adding AM_PROG_CC_C_O to configure.ac. There are many reasons to favor non-recursive Automake, summed up by "Recursive Make Considered Harmful".

Always use Automake in foreign mode (adding foreign to AUTOMAKE_OPTIONS in Makefile.am). Otherwise, it requires too many boilerplate files from the GNU coding standards that aren't useful to us.

Autoconf Integration

Always use the following rules in configure.ac:

    AM_INIT_AUTOMAKE([1.11 check-news foreign silent-rules subdir-objects
        -Wall -Werror])
    AM_MAINTAINER_MODE

check-news is an additional check during distcheck that ensures that the NEWS file is updated with the current release version before the release. foreign suppresses various checks for files that don't follow naming conventions I use. silent-rules allows people to select silent (Linux-kernel-style) build output, which makes it much easier to see warnings. Silent rules requires Automake 1.11 or later. subdir-objects forces use of non-recursive make.

Finally, enabling maintainer mode support allows disabling the automated rebuild of build system files when timestamps differ, which is often undesireable for automated builds (such as in Debian package builds).

For project source organized into subdirectories, also add:

    AM_PROG_CC_C_O

which is required for proper non-recursive make support. (Automake will warn that this is required.)

Makefile.am Best Practices

Start any Makefile.am with:

    ACLOCAL_AMFLAGS = -I m4

and then the setting of EXTRA_DIST, which lists all files not involved in rules or Autoconf templates that should be included in the distribution (such as documentation files).

The GNU coding standards doesn't remove the results of running Autoconf and Automake even on make maintainer-clean, which I think is a bug. That target should reverse all the results of running ./autogen. This means something like:

    MAINTAINERCLEANFILES = Makefile.in aclocal.m4 build-aux/compile \
            build-aux/config.guess build-aux/config.sub build-aux/depcomp \
            build-aux/install-sh build-aux/ltmain.sh build-aux/missing \
            config.h.in config.h.in~ configure m4/libtool.m4 m4/ltoptions.m4 \
            m4/ltsugar.m4 m4/ltversion.m4 m4/lt~obsolete.m4

if you're also using Libtool with this package. Without Libtool, there will be fewer files.

Include a warnings target that builds the source tree with aggressive warnings enabled. For the latest version of this target, see rra-c-util's Makefile.am. I also enable quiet rules (V=0) for this target to make it easier to see warnings.

Shared Libraries

General Rules

Support libraries internal to the package should not be built as shared, but all libraries intended for consumption by users outside of the package should be built as shared and static libraries by default, using the standard Libtool support for building libraries. All shared libraries should be built using Libtool, including loadable modules such as PAM modules.

Follow the library versioning instructions in the Libtool documentation, only changing library versions at most once per release. Any changes to library interfaces that result in an ABI change must result in a change to the SONAME, which is handled automatically by Libtool if one follows the versioning instructions. Changes in functionality that do not change the ABI need not change the SONAME unless they're likely to break programs written following the previous assumptions.

All objects that are included in shared libraries, including helper utility libraries, must be built both shared and non-shared. This means they need to be included in a helper libtool library that's then linked with the final library. The Automake documentation explains how to do this.

Visibility Control

All shared libraries should use visibility control where possible. There are three ways to do this: during compilation, using shared library versioning, and using the libtool symbol export list. The first should always be used where possible. The third is a poor substitute for the second so should only be used when using real shared library versioning isn't possible. For information on compile-time visibility control, see the C coding style.

To use shared library versioning, first probe for whether it is supported by including m4/ld-version.m4 from rra-c-util in the package and calling it in configure.ac with:

    RRA_LD_VERSION_SCRIPT

This will set an Automake conditional indicating whether ld supports version scripts. Then, use code like the following in Makefile.am:

    if HAVE_LD_VERSION_SCRIPT
        VERSION_LDFLAGS = -Wl,--version-script=${srcdir}/lib/<library>.map
    else
        VERSION_LDFLAGS = -export-symbols ${srcdir}/lib/<library>.sym
    endif

where <library> is the name of the library, and use $(VERSION_LDFLAGS) on the link line of the shared library (or do something similar but with separate variables or conditionalizing the LDFLAGS setting if there are multiple shared libraries).

This uses two files, which unfortunately have to be kept in sync. The file matching the name of the library but with the .map extension should be a version script, which looks like this:

    LIBRARY_1.0 {
        global:
            library_func1;
            library_func2;

        local:
            *;
    };

There are other, more complex things one can do with versioning scripts, but this covers nearly every need. LIBRARY should be the short name of the library, and the following version number the interface. The version here is used to ensure that if both the old and new version of the shared library are loaded at the same time, the appropriate function is called. It's therefore essential to change the version number if the calling parameters of functions changed (such as via changes to the internal structure of a private struct so that structs returned by the new library would cause problems when passed to the old library or vice versa).

All public symbols provided by the library, functions or variables, must be listed in the global section. (Public variables are generally a bad idea and should normally be avoided.) The local section then hides all other symbols.

The .sym file is much simpler. It's used by Libtool to hide non-public symbols on platforms that don't provide symbol versioning and should be a simple list of all public interfaces in the library, one per line. Be sure to update both this file and the .map file whenever adding or removing an interface.

pkg-config support

pkg-config is a tool to provide the flags required to compile or link against a shared library. It's a replacement for complex Autoconf probes and for all of the incompatible and occasionally buggy *-config scripts. All shared libraries should install a pkg-config configuration file.

To do this, provide a template pkg-config configuration in a file named after the library but with a .pc.in extension. Here is an example:

    prefix=@prefix@
    exec_prefix=@exec_prefix@
    includedir=@includedir@
    libdir=@libdir@

    Name: <library>
    Description: <short description>
    URL: <upstream url>
    Version: @PACKAGE_VERSION@
    Cflags: -I${includedir}
    Libs: -L${libdir} -l<library>
    Libs.private: @KRB5_LDFLAGS@ @KRB5_LIBS@

Replace <library> with the official name of the library (which isn't necessarily the library name with lib removed). This is only informational, may contain spaces, and isn't used to locate the file. Its filename is used for that.

Replace <short description> with a short description of the purpose of the library. Often the short description of the package from the start of the README file works well here. Replace <upstream url> with the URL of the package's distribution page.

Normally, all Cflags needs to contain is what's indicated above, which lets the compiler find the header files if installed in a non-standard location. For Libs, note that pkg-config distinguishes between the shared libraries that must be linked against on a system with proper transitive dependency support and the libraries that must be linked for a static build or on a system with poor support for shared libraries. For a well-designed shared library, one should rarely need anything in Libs other than what's listed above. Libs.private, on the other hand, needs all the LDFLAGS and LIBS of everything the library itself links with external to this package. In this example, the shared library is linked with the Kerberos libraries and therefore needs the Kerberos LDFLAGS and LIBS; modify as needed for your example.

If the dependencies of the shared library also use pkg-config, just list the other pkg-config file names in Requires or Requires.private instead of using Libs or Libs.private for them.

Finally, add the Automake code to build and install the pkg-config configuration file. It should look like this:

    pkgconfigdir = $(libdir)/pkgconfig
    nodist_pkgconfig_DATA = lib/libfoo.pc

    lib/libfoo.pc: $(srcdir)/lib/libfoo.pc.in
            sed -e 's![@]prefix[@]!$(prefix)!g' \
                -e 's![@]exec_prefix[@]!$(exec_prefix)!g' \
                -e 's![@]includedir[@]!$(includedir)!g' \
                -e 's![@]libdir[@]!$(libdir)!g' \
                -e 's![@]PACKAGE_VERSION[@]!$(PACKAGE_VERSION)!g' \
                -e 's![@]KRB5_LDFLAGS[@]!$(KRB5_LDFLAGS)!g' \
                -e 's![@]KRB5_LIBS[@]!$(KRB5_LIBS)!g' \
                $(srcdir)/lib/libfoo.pc.in > $@

Replace libfoo with the actual name of the library and change its path in the source package as needed. Note the pattern above and use it for any Autoconf substitution variables that you need to replace in the pkg-config template. The above example adds KRB5_LDFLAGS and KRB5_LIBS to the variables; you may have other ones you need, particularly to construct Libs.private.

The above installs the pkg-config configuration file under $(libdir)/pkgconfig. $(datadir)/pkgconfig is appropriate instead if one is certain that the flags required to link with this library will never vary by architecture, but that's not the case for most libraries due to Libs.private.

Note that the Automake documentation recommends using comma as the sed delimiter for doing this sort of trick, but LDFLAGS frequently contains a comma since it's the separator for parts of a linker-specific option. I use an exclamation point instead.

Rather than using this sort of sed script approach, one can instead just have Autoconf generate the pkg-config configuration (via AC_OUTPUT). However, this has two problems:

You may wish to substitute PACKAGE_NAME and PACKAGE_URL as well instead of hard-coding them in the pkg-config template. I usually hard-code them since they rarely change and have to be searched out and changed in multiple places in the source tree if they change anyway.

Creating a Distribution

Using Automake means that most of the work in creating a distribution and doing some basic testing is already handled for you. To generate the distribution tarball, run make distcheck. This will create a subdirectory, copy the distributed files into it, and then test builds outside the source directory, make check, make install, and make uninstall, and only if all that works, create the distribution tarball.

Most problems with a distribution are from omitting files that Automake isn't aware of since they're not involved in creating any compiled software, or aren't mentioned in the expected rules. Such files should be listed in EXTRA_DIST in Makefile.am.

Packages that install Perl modules by default won't be able to use the uninstallation check, since the Perl build system doesn't provide make uninstall. For those packages, add a rule like:

    # Alas, we have to disable this check because there's no way to do an
    # uninstall from Perl.
    distuninstallcheck:
            @:

to Makefile.am to skip that check.

Last spun 2022-02-06 from thread modified 2021-10-24