UP | HOME

Part I: Autoconf

Table of Contents

This page contains the GNU Autoconf tutorial, which is the first part of the Autotools tutorial.

1 Introduction

Building software on different platforms is a challenging task. Re-building it a few years later is even more challenging.

Things get more complicated when the project introduces external dependencies (e.g. libraries that are installed independently and outside of the source tree). Such dependencies can be located with some built-in tools like pkg-config. One could then modify the compilation flags to explicitly include installation paths, thus making the code compile on the current machine. This approach works fine as long as the code has to be compiled only locally and as long as the dependency is not moved to a different directory. However, the installation paths are likely to be different on another machine and the aforementioned build procedure might break.

Autoconf was designed to solve this problem by creating configuration scripts that work correctly on most systems, even on systems not initially considered by the developer. As opposed to CMake, all the burden is put on the shoulders of the developer to make the life simpler for the end user.

2 Demo project

In this tutorial, we will learn how to configure a particular project using Autoconf. This project consists of several C++ source files, which are compiled into a single program using custom Makefile. It is worth mentioning that you don't need to be a C++ programmer to be able to do this tutorial. We will learn how to configure and test the C++ compiler before building the project. The source files depend on the HDF5 library, which is a good example of external dependency. HDF5 is a binary file format, which is designed for high-performance data input/output. We will learn how to automatically locate the HDF5 library using macros.

Below is a source tree of our project. Header and source files can be found in the include/ and src/ directories, respectively. The tests/ directory contains the test_h5.cpp and dataset.hdf5 files needed to test compilation of the aforementioned source codes.

For this tutorial, the contents of the source and header files are not important. However, it is worth mentioning that they depend on the HDF5 library, which is not included in the source tree. Our goal is to demonstrate how cumbersome configuration/compilation steps can be facilitated by the use of Autoconf.

Sherman-Morrison/
├─ bin/
│  ├─ .gitignore
├─ include/
│  ├─ Helpers.hpp
│  ├─ SMWB.hpp
│  ├─ SM_Maroni.hpp
│  ├─ SM_Standard.hpp
│  ├─ Woodbury.hpp
├─ src/
│  ├─ Helpers.cpp
│  ├─ SMWB.cpp
│  ├─ SM_Maroni.cpp
│  ├─ SM_Standard.cpp
│  ├─ Woodbury.cpp
├─ tests/
│  ├─ test_h5.cpp
│  ├─ dataset.hdf5
├─ LICENSE
├─ Makefile
├─ README.md
├─ .gitignore

The source code comes from the Sherman-Morrison GitHub repository of the TREX-CoE. It is distributed under BSD 3-Clause License.

3 Makefile

Below is the Makefile of our project:

## Specify the C++ compiler as well as preprocessor, compiler and linking flags
CXX      = g++
CXXFLAGS = -g -O2 -std=c++11
CPPFLAGS = -I/usr/include -I/usr/include/hdf5/serial  -I./include/
LIBS     = -lhdf5_hl -lhdf5  -lpthread -lsz -lz -ldl -lm  -lhdf5_cpp
LDFLAGS  = -L/usr/lib/x86_64-linux-gnu/hdf5/serial

## Directory structure
SRC_DIR := src
TST_DIR := tests
INC_DIR := include
BIN_DIR := bin

EXEC    := $(BIN_DIR)/test_h5

## Dependencies
DEPS_CXX = $(SRC_DIR)/SM_Maponi.o \
           $(SRC_DIR)/SM_Standard.o \
           $(SRC_DIR)/Woodbury.o \
           $(SRC_DIR)/SMWB.o \
           $(SRC_DIR)/Helpers.o

## Build tagets
.PHONY: all clean

all: $(EXEC)

clean:
        @rm -rf -- $(SRC_DIR)/*.o $(TST_DIR)/*.o $(EXEC)

## Compiling dependencies
.SUFFIXES: .cpp .o

## Linking
$(BIN_DIR)/test_h5: $(DEPS_CXX) $(TST_DIR)/test_h5.o
        $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(TST_DIR)/test_h5.o $(DEPS_CXX) -o $(EXEC) $(LDFLAGS) $(LIBS)

Let's have a look at the following part of our Makefile (from now on we will only focus on the following lines):

CXX      = g++
CXXFLAGS = -g -O2 -std=c++11
CPPFLAGS = -I/usr/include -I/usr/include/hdf5/serial  -I./include/
LIBS     = -lhdf5_hl -lhdf5  -lpthread -lsz -lz -ldl -lm  -lhdf5_cpp
LDFLAGS  = -L/usr/lib/x86_64-linux-gnu/hdf5/serial

What are advantages and disadvantages of specifying compiler flags as above?

Among other things, the crucial issue here is the portability of the hard-coded paths to HDF5 (see CPPFLAGS and LDFLAGS).

Let's execute make command and examine the output. Normally, you should get a lot of errors, which are impossible to debug. So what happened?

The compilation crashed due to several issues. One of them is that the aforementioned Makefile was adapted for one single machine. Another issues comes from the fact that compiler flags are overwritten in Makefile instead of being appended to the existing ones. It means that even if the current environment (e.g. the one created with conda) did some smart job and pre-set some compiler options, this information is still lost.

Let's append to the existing flags instead of overwriting them. For this we will use += syntax of Makefile.

CXX       = g++
CXXFLAGS += -std=c++11
CPPFLAGS += -I./include/
LIBS     += -lhdf5 -lhdf5_cpp -lz
LDFLAGS  += -L/home/user/.conda/envs/autotools/lib

If we now run make command the output may look like this:

$ make

g++ -fPIC -isystem /home/user/.conda/envs/autotools/include -std=c++11   -I./include/  -c -o src/SM_Maponi.o src/SM_Maponi.cpp
g++ -fPIC -isystem /home/user/.conda/envs/autotools/include -std=c++11   -I./include/  -c -o src/SM_Standard.o src/SM_Standard.cpp
g++ -fPIC -isystem /home/user/.conda/envs/autotools/include -std=c++11   -I./include/  -c -o src/Woodbury.o src/Woodbury.cpp
g++ -fPIC -isystem /home/user/.conda/envs/autotools/include -std=c++11   -I./include/  -c -o src/SMWB.o src/SMWB.cpp
g++ -fPIC -isystem /home/user/.conda/envs/autotools/include -std=c++11   -I./include/  -c -o src/Helpers.o src/Helpers.cpp
g++ -fPIC -isystem /home/user/.conda/envs/autotools/include -std=c++11   -I./include/  -c -o tests/test_h5.o tests/test_h5.cpp
g++  -I./include/ -fPIC -isystem /home/user/.conda/envs/autotools/include -std=c++11 tests/test_h5.o src/SM_Maponi.o src/SM_Standard.o src/Woodbury.o src/SMWB.o src/Helpers.o -o bin/test_h5  -L/home/user/.conda/envs/autotools/lib -lhdf5 -lhdf5_cpp -lz

So it works.

What is going to happen when you try to use the updated Makefile outside of the conda environment?

Unfortunately, our solution does not resolve the portability issue. In this example, we demonstrated that defining compiler flags is challenging, especially for programs with external dependencies.

Would it be better if we could automate this process? Well, this is where Autoconf comes into play.

4 Autoconf

In this section, we will talk about several files:

  • configure.ac
  • configure
  • Makefile.in
  • Makefile

The ultimate goal of the Autoconf is to produce the configure shell script based on the configure.ac file. The configure script then detects the Makefile.in file and replaces some variables there to produce the final Makefile, which is used to build a project.

diagram.png

Figure 1: Figure from The Basics of Autotools

This might be confusing at the beginning. Let's create the simplest configure.ac file for our project with the following contents:

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

# Initialize autoconf
AC_INIT([sherman-morrison], [0.0.1], [])

AC_OUTPUT

Here we provided the name of our project [sherman-morrison] and the current version [0.0.1] to the AC_INIT macro. These are now stored in the $PACKAGE_NAME and $PACKAGE_VERSION variables, respectively. We will not cover the details of the M4 syntax that is used internally by the Autoconf. For more information, see the Autoconf documentation page.

Most of the built-in Autoconf macros start with the AC_ prefix.

We know that the source code is written in C++ programming language (it can also be guessed from the .cpp extension). Thus, we would like the Autoconf to find and possibly run some checks using the C++ compiler that is available on the current machine.

Let's add a few additional macros for that and examine their output:

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

# Initialize autoconf
AC_INIT([sherman-morrison], [0.0.1], [])

# do the tests with C++ flags
AC_LANG(C++)
# search for the C++ compiler
AC_PROG_CXX
# check if the C++ compiler accepts -c and -o simultaneously
AC_PROG_CXX_C_O

AC_OUTPUT

echo \
"-------------------------------------------------

${PACKAGE_NAME} Version ${PACKAGE_VERSION}

CXX ...........:  ${CXX}
CXXFLAGS ......:  ${CXXFLAGS}

--------------------------------------------------"

The contents of the current directory should be as follows:

bin/  configure.ac  include/  LICENSE  Makefile  README.md  src/  tests/

Now run autoreconf command in your terminal and examine the directory again. After succesfull execution, a configure shell script should appear.

If you want to see what autoreconf does, use the -v option.

Let's run it by calling ./configure. Several checks have been performed by Autoconf and in the end one might get the following (the CXX output will be different on your machine):

-------------------------------------------------

sherman-morrison Version 0.0.1

CXX ...........:  /home/user/.conda/envs/autotools/bin/x86_64-conda-linux-gnu-c++
CXXFLAGS ......:  -fPIC -isystem ${CONDA_PREFIX}/include

--------------------------------------------------

configure produces a file named config.log which contains a log of all the tests that were run.

In the example above, the configure script has detected the GNU C++ compiler and expanded the ${CXX} variable with the corresponding name.

From now on we will focus mainly on $-prefixed variables and calls to the Autoconf macros.

Whenever you make a change in the configure.ac file, do not forget to execute autoreconf -i to update the generated configure script.

The configure script can also define its own variables (e.g. $PACKAGE_NAME above) and pass them to other files. To benefit from the previously detected C++ compiler and its flags, let's propagate them to Makefile.

This requires 2 actions:

  • Move Makefile to Makefile.in (this is the standard naming convention in Autotools) and replace the hard-coded variables with the ones defined by the configure script. For demonstration purposes, we only replace CXX and CXXFLAGS at the moment.
  • Indicate to Autoconf that producing Makefile is now its responsibility (due to the variables that will be substituted by ./configure call). This can be achieved by adding AC_CONFIG_FILES([Makefile]).

configure.ac:

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

#initialize autoconf
AC_INIT([sherman-morrison], [0.0.1], [])

# do the tests with C++ flags
AC_LANG(C++)
# search for the C++ compiler
AC_PROG_CXX
# check if the C++ compiler accepts -c and -o simultaneously
AC_PROG_CXX_C_O

AC_CONFIG_FILES([Makefile])

AC_OUTPUT

echo \
"-------------------------------------------------

${PACKAGE_NAME} Version ${PACKAGE_VERSION}

CXX ...........:  ${CXX}
CXXFLAGS ......:  ${CXXFLAGS}

--------------------------------------------------"

Makefile.in (remember, only the top part has to be changed):

CXX       = @CXX@
CXXFLAGS  = @CXXFLAGS@
CPPFLAGS += -I./include/
LIBS     += -lhdf5 -lhdf5_cpp -lz
LDFLAGS  += -L/home/user/.conda/envs/autotools/lib

Variables in between @ signs are called substitution variables and are important part of the Autotools toolkit.

Let's reconfigure (autoreconf -i && ./configure) and examine the changes between the input Makefile.in and output Makefile (you can use diff or vimdiff for example):

$ diff Makefile.in Makefile

< CXX      = @CXX@
< CXXFLAGS = @CXXFLAGS@
---
> CXX       = /home/user/.conda/envs/autotools/bin/x86_64-conda-linux-gnu-c++
> CXXFLAGS  = -fPIC -isystem ${CONDA_PREFIX}/include

This tells us that the substitution variables @CXX@ and @CXXFLAGS@ have been replaced with the values detected by the configure script.

In the example above, we forgot to add a compiler flag. Which one?

Good, but we still have the hard-coded path to lib directory in LDFLAGS. Among other things, this directory contains the HDF5 library. Let's try to find this library using Autoconf functionality.

To re-create quickly the Makefile without re-running ./configure, just run ./config.status and the options of the previous ./configure will be used.

5 Detecting external dependencies

You can download the modified project, which contains the checkpoint corresponding to the beginning of this section. This might be useful if you did not type along during the previous part. The link to the checkpoint version of the project is here: https://github.com/q-posev/Sherman-Morrison/archive/refs/tags/v0.1-checkpoint.tar.gz

It is possible to write your our own macros to detect external dependencies. However, in this example we will try to locate the HDF5 library, which is widely used. Luckily for us, the GNU website has an archive of Autoconf macros for many packages, including HDF5. The ax_lib_hdf5.m4 file can be downloaded/copied from this webpage.

It is considered good practice to put Autoconf macros in the m4 directory of your source tree. This has been already done for you in the aforementioned checkpoint version of the project.

Once the file is in the m4 directory, one can add the following lines to configure.ac in order to locate and call the ax_lib_hdf5.m4 macro:

# tell Autoconf the name of directory with external M4 macros
AC_CONFIG_MACRO_DIR([m4])
# call the m4 macro to locate the HDF5 library
AX_LIB_HDF5()

The description of this macro can be found here. Notably, it says:

If HDF5 is successfully found, this macro calls

  AC_SUBST(HDF5_VERSION)
  AC_SUBST(HDF5_CC)
  AC_SUBST(HDF5_CFLAGS)
  AC_SUBST(HDF5_CPPFLAGS)
  AC_SUBST(HDF5_LDFLAGS)
  AC_SUBST(HDF5_LIBS)
  AC_SUBST(HDF5_FC)
  AC_SUBST(HDF5_FFLAGS)
  AC_SUBST(HDF5_FLIBS)
  AC_SUBST(HDF5_TYPE)
  AC_DEFINE(HAVE_HDF5)

and sets with_hdf5="yes". ...

The AC_SUBST macro defines a non-standard (e.g. project-specific) substitution variable. Similarly, AC_DEFINE macro defines a preprocessor symbol, which can be used to write preprocessor conditionals.

This means that the macro will define a set of variables that can be manupulated within configure.ac and/or Makefile.in files. In this example, we choose the former and will append the necessary flags with the HDF5_ ones.

configure.ac:

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

#initialize autoconf
AC_INIT([sherman-morrison], [0.0.1], [])

# do the tests with C++ flags
AC_LANG(C++)
# search for the C++ compiler
AC_PROG_CXX
# check if the C++ compiler accepts -c and -o simultaneously
AC_PROG_CXX_C_O

# tell Autoconf the name of directory with external M4 macros
AC_CONFIG_MACRO_DIR([m4])
# call the m4 macro to locate the HDF5 library
AX_LIB_HDF5()

# raise an error if HDF5 is missing
if test "x${with_hdf5}" = xno; then
  AC_MSG_ERROR([
    ------------------------------------------
    Configuring with the HDF5 library is
    required to build this project.
    ------------------------------------------])
fi

# prepend compiler and linking flags with that of HDF5
CXXFLAGS="${HDF5_CFLAGS} ${CXXFLAGS} -std=c++11"
CPPFLAGS="${HDF5_CPPFLAGS} ${CPPFLAGS} -I./include/"
LDFLAGS="${HDF5_LDFLAGS} ${LDFLAGS}"
LIBS="${HDF5_LIBS} ${LIBS} -lhdf5_cpp"

AC_CONFIG_FILES([Makefile])

AC_OUTPUT

echo \
"-------------------------------------------------

${PACKAGE_NAME} Version ${PACKAGE_VERSION}

CXX ...........:  ${CXX}
CXXFLAGS ......:  ${CXXFLAGS}
CPPFLAGS ......:  ${CPPFLAGS}
LDFLAGS .......:  ${LDFLAGS}
LIBS ..........:  ${LIBS}

Package features:
  Compilation with HDF5 ..:  ${with_hdf5}
  HDF5 version ...........:  ${HDF5_VERSION}

--------------------------------------------------"

Makefile.in (only the top part has to be changed):

CXX      = @CXX@
CXXFLAGS = @CXXFLAGS@
CPPFLAGS = @CPPFLAGS@
LIBS     = @LIBS@
LDFLAGS  = @LDFLAGS@

Now reconfigure and examine the output produced by the configure script. In the end, you will see a more verbose summary of the detected flags and newly added package features. The exact output may differ on your machine depending on the detected compiler and/or the HDF5 library.

-------------------------------------------------

sherman-morrison Version 0.0.1

CXX ...........:  /home/user/.conda/envs/autotools/bin/x86_64-conda-linux-gnu-c++
CXXFLAGS ......:   -fPIC -isystem ${CONDA_PREFIX}/include -std=c++11
CPPFLAGS ......:  -I/home/user/.conda/envs/autotools/include   -I./include/
LDFLAGS .......:   -L/home/user/.conda/envs/autotools/lib  
LIBS ..........:  -lhdf5_hl -lhdf5  -lcrypto -lcurl -lrt -lpthread -lz -ldl -lm   -lhdf5_cpp

Package features:
  Compilation with HDF5 ..:  yes
  HDF5 version ...........:  1.12.1

--------------------------------------------------

You can now see how all hard-coded HDF5 paths have been detected by the ax_lib_hdf5.m4 macro. Moreover, all flags are propagated to the resulting Makefile due to the use of substitution variables.

Examine the difference between Makefile.in and Makefile. Is it consistent with the aforementioned summary?

Try to reconfigure using ./configure --without-hdf5 or ./configure --with-hdf5=no. Which part of the configure.ac script produces the output message?

6 pkg-config

The pkg-config program is a powerful tool to get information about installed libraries. Autoconf incorporates some of the pkg-config functionality.

The PKG_CHECK_MODULES can be configured to detect and check the required version of the HDF5 library (see the Autotools Mythbuster website for more details). For example, PKG_CHECK_MODULES([HDF5], [hdf5 >= 1.8]) verifies that the HDF5 version is not older than 1.8.

configure.ac:

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

#initialize autoconf
AC_INIT([sherman-morrison], [0.0.1], [])

# do the tests with C++ flags
AC_LANG(C++)
# search for the C++ compiler
AC_PROG_CXX
# check if the C++ compiler accepts -c and -o simultaneously
AC_PROG_CXX_C_O

# tell Autoconf the name of directory with external M4 macros
AC_CONFIG_MACRO_DIR([m4])
# call the m4 macro to locate the HDF5 library
AX_LIB_HDF5()

# raise an error if HDF5 is missing
if test "x${with_hdf5}" = xno; then
  AC_MSG_ERROR([
    ------------------------------------------
    Configuring with the HDF5 library is
    required to build this project.
    ------------------------------------------])
fi

# prepend compiler and linking flags with that of HDF5
CXXFLAGS="${HDF5_CFLAGS} ${CXXFLAGS} -std=c++11"
CPPFLAGS="${HDF5_CPPFLAGS} ${CPPFLAGS} -I./include/"
LDFLAGS="${HDF5_LDFLAGS} ${LDFLAGS}"
LIBS="${HDF5_LIBS} ${LIBS} -lhdf5_cpp"

# clean the output of AX_LIB_HDF5 macro since the same variables are substitutted by PKG_CHECK_MODULES
HDF5_CFLAGS=""
HDF5_LIBS=""
# locate HDF5 with pkg-config
PKG_CHECK_MODULES([HDF5], [hdf5 >= 1.8])

AC_CONFIG_FILES([Makefile])

AC_OUTPUT

echo \
"-------------------------------------------------

${PACKAGE_NAME} Version ${PACKAGE_VERSION}

CXX ...........:  ${CXX}
CXXFLAGS ......:  ${CXXFLAGS}
CPPFLAGS ......:  ${CPPFLAGS}
LDFLAGS .......:  ${LDFLAGS}
LIBS ..........:  ${LIBS}

Package features:
  Compilation with HDF5 ..:  ${with_hdf5}
  HDF5 version ...........:  ${HDF5_VERSION}
  .........................
  pkg-config CFLAGS ......:  ${HDF5_CFLAGS}
  pkg-config LIBS ........:  ${HDF5_LIBS}

--------------------------------------------------"

Reconfiguration should fail with the following error message:

checking for hdf5 >= 1.8... no
configure: error: Package requirements (hdf5 >= 1.8) were not met:

No package 'hdf5' found

This might happen when package developers forgot to provide the configuration file for pkg-config or when it is installed in some non-standard location.

Many Autoconf macros (including PKG_CHECK_MODULES) allow to specify actions to be taken if a given file/program/library has been found or not. In the case of PKG_CHECK_MODULES, the syntax is the following:

PKG_CHECK_MODULES(prefix, list-of-modules, action-if-found, action-if-not-found)

Modify the call to the PKG_CHECK_MODULES macro in order to raise a warning when HDF5 cannot be detected with pkg-config. This can be done using AC_MSG_WARN macro.

Notably, the raised warning allows the configuration to pass (unlike AC_MSG_ERROR).

7 Conclusion

In this Tutorial you have learned how to make your in-house Makefile compatible with Autoconf. This is achieved by converting Makefile into Makefile.in and introducing substitution variables. You have also learned how to use Autoconf to create the configure script, which sets up and checks programs and libraries on the host machine. The configure script is also responsible for producing the final Makefile, which is used to build the project. Finally, you used the Autoconf macro from the GNU archive to configure the project, which has an external dependency (i.e. the HDF5 library).


Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Author: Evgeny Posenitskiy, Anthony Scemama

Created: 2021-11-09 Tue 17:47

Validate