GNU Autotools II - "Hello world" project with tests

In order to explore the concepts from the first post in a more practical fashion, today we will look at a step by step guide for creating a C programming language project built and tested using Autotools.

1: Application entry point

This will be src/main.c. It's typical to have a src directory for source code, because when the project grows, it will probably need other top-level directories such as man for man pages, data for data files, and so on.

src/main.c
#include <config.h>
#include <stdio.h>

int main (void) {
    puts("Hello World!");
    puts("This is " PACKAGE_STRING ".");
    return 0;
}

Including config.h is necessary because that's where PACKAGE_STRING will be defined. In turn, config.h will be generated by Autotools.

2: Top-level configure.ac file

All configure.ac files contain Autoconf instructions which control the generation of the configure script. These instructions are mostly prebuilt macros, defined using the m4 preprocessing language. Each line is explained by its preceeding comment.

configure.ac
# Initialize autoncof. Specify package name, version and bug report email address.
AC_INIT([hello_world], [1.0], [dario.fenix@gmail.com])

# Initialize automake. Turn on all warnings, and treat them as errors. The package we are bulding is a
# "foreign" package, hence the first parameter. When "foreign" is set, it allows us to ignore some GNU
# coding standards, such as INSTALL, README, AUTHORS, ChangeLog and other conventional files.
# If we remove this flag, automake will force us to define these files for the build to advance.

AM_INIT_AUTOMAKE([foreign -Wall -Werror])

# Check if this system has a C compiler.
AC_PROG_CC

# Check if this system has the check library installed, version 0.9.6 or later.
PKG_CHECK_MODULES([CHECK], [check >= 0.9.6])

# Declare config.h as the output header.
AC_CONFIG_HEADERS([config.h])

# Declare Makefile, src/Makefile and tests/Makefile as outputs of the configure script.
AC_CONFIG_FILES([Makefile src/Makefile tests/Makefile])

# Actually generate the declared output files.
AC_OUTPUT

The check library is used for writing and running unit tests, and will be used later on.

3: Top-level Makefile.am file

We already have configure.ac for controlling the generation of the configure script. Now, for controlling the generation of the Makefiles via automake, we need to define Makefile.am files, which serve as an input for said generation. These files use the same syntax as a Makefile. To begin, we create one in the project root directory. For this example, it could be:

Makefile.am
# Build recursively into the /src and /tests directory
SUBDIRS = src tests

The only thing this file does is indicate which subdirectories have a Makefile of their own. In this case, src for the application, and tests for tests.

4: The application's Makefile.am file

src/Makefile.am
# Programs which will be installed in the bindir directory (by default, /usr/local/bin).
bin_PROGRAMS = hello_world

# In order to build hello_world, the only source file needed is main.c.
hello_world_SOURCES = main.c

5: Test code

For the sake of simplicity, we will write a single test that performs a trivial assert and fails, to ensure that tests are called and make check fails. In a more "real" application, the test code would include application headers and call the application functions under test. This would require adjusting the tests' Makefile.am file in order to link the necessary source files. More on this when we get to that file for this example.

tests/foo_test.c
#include <check.h>
#include <stdlib.h>

START_TEST (test_foo) {
  ck_assert_int_eq(1, 0);
}
END_TEST

Suite *foo_suite() {
  Suite *s;
  TCase *tc;

  s = suite_create("Foo");
  tc = tcase_create("Core");

  tcase_add_test(tc, test_foo);

  suite_add_tcase(s, tc);
  return s;
}

int main(void) {
  int fc;
  SRunner *sr;

  sr = srunner_create(foo_suite());

  srunner_run_all(sr, CK_NORMAL);
  fc = srunner_ntests_failed(sr);
  srunner_free(sr);

  return fc == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}

From this code, it appears that writing a check test requires some boilerplate code: namely, a function to define the test suite (foo_suite), and the runner creation and execution inside main. Test cases are defined using the START_TEST and END_TEST functions. Using the check library could warrant its own post; presently, we will limit ourselves to the bare minimum, because Autotools are the focus here.

6: The tests' Makefile.am file

tests/Makefile.am

Each line is explained by its preceeding commentary.

# The "check" prefix indicates that the specified PROGRAMS are needed for running the make check target.
check_PROGRAMS = foo_test

# Indicates which source files are needed for building the foo_test program.
foo_test_SOURCES = foo_test.c

# Flags for the C compiler = the ones needed for the check library.
# (Autotools knows which ones they are and has that information in the CHECK_CFLAGS variable).
foo_test_CFLAGS = @CHECK_CFLAGS@

# Same for the linker.
foo_test_LDADD = @CHECK_LIBS@

# Indicates which programs to run when calling the make check target.
TESTS = $(check_PROGRAMS)

7: Build system generation and execution

Now that all the source code and configuration files are in place, it's possible to generate the build system using the autoreconf --install command. It's important to remember that autoreconf calls all the Autotools programs in the right order. Asides from generating the configure script, this also generates ancillary scripts and template files, such as config.h.in, Makefile.in, src/Makefile.in, and tests/Makefile.in. These files with the .in extension are templates which will be adapted for a specific system by the configure script. Therefore, they should not be edited manually.

Next up, we run the configure script: ./configure. After verifying the system, it will create Makefile, src/Makefile, tests/Makefile and config.h. If this execution succeeds, from this point it's possible to call all make targets: plain make for building the application, make check for running the tests, and so on.

If later on we want to modify the build system, adding source files for example, we'll need to update configure.ac and potentially the Makefile.am files. Every time the build process is changed, the build system should be regenerated by running autoreconf again. As previously said, autoreconf takes care of calling all the Autotools programs in the right order. However, since autoconf and automake are separate programs with separate manuals and potential errors, it's essential to know that autoconf is in charge of generating the configure script from the configure.ac file, and automake is in charge of generating Makefile.in files from Makefile.am files. This is key to understanding which part of the generation process is failing when troubleshooting it.

Comments

Popular posts from this blog

VB.NET: Raise base class events from a derived class

Apache Kafka - I - High level architecture and concepts

Upgrading Lodash from 3.x to 4.x