GNU Autotools II - Hola mundo con tests

Para bajar a tierra los conceptos del primer post, hoy seguiremos paso a paso la creación de un proyecto en lenguaje C usando Autotools como sistema de build.

1: Punto de entrada

En primer lugar, se crea el punto de entrada del programa, src/main.c. Es típico tener un directorio src para el código fuente, ya que al evolucionar el proyecto, probablemente se agreguen directorios como man para páginas de ayuda (man pages), data para archivos de datos, etc.

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

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

La inclusión de config.h viene dada porque ese archivo será generado por Autotools, y es donde estará definido PACKAGE_STRING.

2: Archivo configure.ac raíz

Los archivos configure.ac contienen instrucciones Autoconf que controlan la generación del script configure. Dichas instrucciones están en "lenguaje" Autoconf, que es un conjunto de macros que extienden el lenguaje de preprocesamiento m4. Cada línea está explicada en el comentario que la precede.

configure.ac
# Inicializar autoncof. Especifica nombre de paquete, versión y dirección de email para reportar bugs.
AC_INIT([hello_world], [1.0], [dario.fenix@gmail.com])

# Inicializar automake. Activar todos los warnings, y tratarlos como errores. El paquete que estamos construyendo se marca como
#"foreign" en el primer argumento. Cuando se activa este flag, se ignoran algunos estándares GNU
# , como los archivos INSTALL, README, AUTHORS, ChangeLog y otros.
# Si se quita este flag, automake nos forzará a definir estos archivos para avanzar.
AM_INIT_AUTOMAKE([foreign -Wall -Werror])

# Verificar si el sistema tiene un compilador C.
AC_PROG_CC

# Verificar si el sistema tiene la biblioteca check, en su versión 0.9.6 o posterior.
PKG_CHECK_MODULES([CHECK], [check >= 0.9.6])

# Declarar config.h como header de salida del script configure.
AC_CONFIG_HEADERS([config.h])

# Declarar Makefile, src/Makefile y tests/Makefile como archivos de salida del script configure.
AC_CONFIG_FILES([Makefile src/Makefile tests/Makefile])

# Generar los archivos de salida del script configure.
AC_OUTPUT

La biblioteca check sirve para desarrollar y ejecutar tests unitarios, y será utilizada más adelante.

3: Makefile.am raíz

Ya se tiene configure.ac para controlar la generación del script configure. Ahora bien, para controlar la generación de los Makefiles vía automake, es necesario definir archivos Makefile.am, que sirven de entrada para dicha generación. Estos archivos usan la misma sintaxis que un archivo Makefile. En primer lugar, se crea uno en el directorio raíz. Para este ejemplo, sería:

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

Lo único que indica este archivo es qué subdirectorios tienen Makefile propio. En este caso, src para la aplicación, y tests para los tests.

4: Makefile.am de la aplicación

src/Makefile.am
# Programas que serán instalados en el directorio bindir (por defecto, /usr/local/bin).
bin_PROGRAMS = hello_world

# Para compilar el binario hello_world, sólo hace falta el fuente main.c.
hello_world_SOURCES = main.c

5: Código de los tests

Por simplicidad, se escribirá un solo test que haga un assert trivial y falle, para asegurarse de que los tests se llamen y make check falle. En una aplicación más real, el test incluiría headers de la aplicación y llamaría las funciones a probar. Eso también requeriría ajustar el Makefile.am del test para linkear los fuentes necesarios de la aplicación. Más sobre esto en la próxima sección.

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;
}

Aquí se observa que un test de check necesita cierto boilerplate: una función para definir la test suite (foo_suite), y la creación y ejecución de un runner que corra la suite en main. Los test cases se definen usando las macros START_TEST y END_TEST. El uso de la biblioteca check podría ameritar su propio post; en el presente, nos limitaremos a lo mínimo indispensable.

6: Makefile.am de los tests

tests/Makefile.am

Cada línea está explicada por el comentario que la precede.

# El prefijo "check" indica que los PROGRAMS especificados deben ser compilados para ejecutar el target make check.
check_PROGRAMS = foo_test

# Indica qué archivos fuentes son necesarios para compilar el programa foo_test
foo_test_SOURCES = foo_test.c

# Flags para el compilador C = los necesarios para la biblioteca check
# (Autotools ya los conoce y los tiene en la variable CHECK_CFLAGS).
foo_test_CFLAGS = @CHECK_CFLAGS@

# Lo mismo para linkear.
foo_test_LDADD = @CHECK_LIBS@

# Indica qué programas correr al ejecutar el target make_check.
TESTS = $(check_PROGRAMS)

7: Generación y ejecución del sistema de build

Una vez en su lugar todo el código fuente y los archivos de configuración, es posible generar el sistema de build del paquete mediante el comando autoreconf --install. Es importante recordar que el comando autoreconf invoca todos los programas de la suite de Autotools en el orden correcto. Además de generar el script configure, crea varios scripts auxiliares y templates, como por ejemplo config.h.in, Makefile.in, src/Makefile.in, y tests/Makefile.in. Estos archivos con extensión .in son templates que serán adaptados para el sistema específico por el script configure. Por lo tanto, no deberían ser editados manualmente.

A continuación, se invoca al script configure: ./configure. Luego de verificar el sistema, configure crea Makefile, src/Makefile, tests/Makefile y config.h. Si la invocación a configure es exitosa, a partir de este punto es posible invocar los targets de make: make a secas para compilar la aplicación, make check para correr los tests, etc.

Si más adelante se desea modificar algo del sistema de build, porque se agregaron archivos fuente por ejemplo, será necesario actualizar configure.ac y potencialmente los Makefiles. Cada vez que se modifique el proceso de build, será necesario regenerar el sistema de build invocando a autoreconf de nuevo. Como se dijo antes, autoreconf se encarga de llamar a todos los programas de Autotools en el orden correcto. Sin embargo, dado que autoconf y automake tienen manuales separados, es importante saber que autoconf es el encargado de generar el script configure a partir del archivo de configuración configure.ac, y por otro lado automake es el encargado de generar archivos Makefile.in a partir de archivos de configuración Makefile.am. Esto es clave para saber qué parte del proceso de generación puede estar fallando.

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