/CrossPlatformCPlusPlusProject

Sample project for the article "How To Build a Cross-Platform C++ Project With Autotools" at https://digitalleaves.com

Primary LanguageM4

How To Build a Multiplatform, GNU-Compliant C++ Project

Project structure
We will illustrate the process with a simple project called helloWorld. Our project will build a very simple "hello world" program that will just show a message on screen. It contains a shared library: libHelloWorld.so, and a main program that calls this shared library.

Additionally, there will be some extra scripts (to be installed but not compiled), a man page for the main application, some documentation, examples and sample data.

The project should be structured in the following way:

sources in src/
documentation in doc/
man pages in man/
some scripts in scripts/ (in this directory, we will add files that need to be executed but not compiled)
examples in examples/
Our src/ directory will contain just three source files:

main.cpp:
#include "helloWorld.h"
int main() {
    helloWorld::printHelloWorldMessage();
    return 0;
}
helloWorld.h:
#include <iostream>
#pragma once
namespace helloWorld {
    void printHelloWorldMessage();
}
helloWorld.cpp:
#include "helloWorld.h"
namespace helloWorld {
    void printHelloWorldMessage() {
        std::cout << "Hello world, helloWorld() called in the helloWorld namespace." << std::endl;
    }
}
Prerequisites
Depending on our operating system, we need to follow a different process for installing autotools.

On linux based systems (i.e: Debian), all you need to do is installing the required packages (as root):

# aptitude install autotools-dev autoconf automake libtool

On Mac OS X, we can use brew for this:

$ brew install autoconf automake libtool

In order to install Brew on Mac OS X, you just need to run this command:

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

More detailed instructions and information on Brew can be found here.

Generating The Project Configuration
In this step, we will generate the required configuration files and the Makefile.am files for the different parts of our project.

These files will set the rules to indicate autoconf and automake which files should be included and how to process them.

Run autoscan:

$ autoscan

autoscan tries to produce a suitable configure.ac file (autoconf's driver) by performing simple analyses on the files in the package. This is enough for the moment (many people are just happy with it as permanent).

Use AutoScan file as initial Configuration file

Autoscan actually produces a configure.scan file, so let it have the name autoconf will look for:

$ mv configure.scan configure.ac

Modify the Configuration file

Now we need to adjust the generated configuration file to add some info, like the name and version of our package:

$ vim configure.ac

We will modify this initial file to adapt it to our project.

First, we will modify the AC_INIT property that defines the package name, version and bug reporting email address. Replace:

AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
With:

AC_INIT([helloWorld], [1.0], [contact@digitalleaves.com])
Also, we will change the ACCONFIGSRCDIR to point to our main.cpp file. Replace:

AC_CONFIG_SRCDIR([src/helloWorld.h])
With:

AC_CONFIG_SRCDIR([src/main.cpp])
Additionally, we will add some properties that will create the link between autoconf and automake. One of them will be the AMINITAUTOMAKE variable, where you will specify the package name and version again. Also, AC_OUTPUT, where we will specify the Makefile files to be generated by autoconf in all of our directories.

Last, we will add some checks to make sure that the system where our project is going to be built has a proper compiling environment for C++ and can link shared libraries.

The final file should look like this:

# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.69])
AC_INIT([helloWorld], [1.0], [contact@digitalleaves.com])
AM_INIT_AUTOMAKE
AC_CONFIG_SRCDIR([src/main.cpp])
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_MACRO_DIRS([m4])

# Checks for programs.
AC_PROG_CXX
AC_PROG_CC

# Checks for libraries.
LT_INIT
AC_ENABLE_SHARED
AC_DISABLE_STATIC
AC_PROG_LIBTOOL(libtool)

# Checks for typedefs, structures, and compiler characteristics.
AC_HEADER_STDBOOL
AC_C_INLINE
AC_TYPE_SIZE_T

# Output Makefile files.
AC_CONFIG_FILES([Makefile src/Makefile doc/Makefile examples/Makefile man/Makefile scripts/Makefile])
AC_OUTPUT
Create the root Makefile.am:

Now it's time to create the root Makefile.am, the seed for our root Makefile. It will specify the directories where automake should look for Makefile files.

$ vim Makefile.am

AUTOMAKE_OPTIONS = foreign
ACLOCAL_AMFLAGS = -I m4
AM_CXXFLAGS = -fPIC -Wall -Wextra
SUBDIRS = src doc examples man scripts
Other options here include commonly used settings for most projects.

Create the Makefile.am in the src/ directory

The src directory is where we keep all our source files (.cpp and .h). We want our application to be called helloWorld. Let's see how the Makefile.am generator for our source dir looks like.

$ vim src/Makefile.am

AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = $(ACLOCAL_FLAGS)

bin_PROGRAMS = helloWorld

helloWorld_SOURCES = main.cpp
helloWorld_LDADD = libHelloWorld.la

lib_LTLIBRARIES = libHelloWorld.la
libHelloWorld_la_LDFLAGS = -version-info 0:0:0
libHelloWorld_la_SOURCES = helloWorld.cpp helloWorld.h
As you can see, after defining some global options, we set the initial binary for the project, helloWorld, with the property bin_PROGRAMS.

Next, we define the sources and the libraries that should be included in this helloWorld object, with the _SOURCES and _LDADD properties respectively. Note how we use the extension .la instead of .so for the shared library. This is a convention.

Then, we define our small library (lib_LTLIBRARIES), set some flags (libHelloWorldlaLDFLAGS) and specify its source files. That's all we need. If you were to add additional source files, you will add them to helloWorld_SOURCES, and repeat the same process for libHelloWorld in order to add more libraries.

Create the Makefile.am in the man/ directory

Next step, we'll add the man page for our helloWorld executable.

We will create a very simple man page with this content:

.\" Manpage for helloWorld.
.\" Contact contact@digitalleaves.com for comments or help.
.TH man 1 "30 Nov 2017" "1.0" "helloWorld man page"
.SH NAME
helloWorld \- simple hello world example application
.SH SYNOPSIS
helloWorld
.SH DESCRIPTION
helloWorld is a simple test application to show how to build a C++ example project with Autotools for multiplatform compilation and use.
.SH OPTIONS
helloWorld does not take any arguments.
.SH BUGS
No known bugs.
.SH AUTHOR
Ignacio Nieto Carvajal (contact@digitalleaves.com)
Write that down in a file called helloWorld.1 (The man page number 1 is a convention, because our App is considered a "User command").

Now it's time to write our man/Makefile.am:

$ vim man/Makefile.am

man_MANS = helloWorld.1
Create the Makefile.am in the scripts/ directory

Now we will create a makefile.a, for our scripts directory.

$ vim scripts/Makefile.am

bin_SCRIPTS = helloWorldScript1.sh helloWorldScript2.sh
Create files helloWorldScript1.sh...

echo "Hello World!"
and helloWorldScript2.sh:

echo "Goodbye world!"
Create the Makefile.am in the examples/ directory

In the examples directory, we can add configuration sample files, data examples, or any other kind of resource we might want to add.

In this example, we will add a dummy sample configuration file called sample.data:

version = 1.0
author = Ignacio Nieto Carvajal <contact@digitalleaves.com>
Then, we will generate the Makefile.am:

$ vim examples/Makefile.am

exampledir = $(datarootdir)/doc/@PACKAGE@
example_DATA = sample.data
We can add as many files as we want (separated by whitespaces) in example_DATA.

Create the Makefile.am in the doc/ directory

In order to set a doc directory, we need to specify a variable that will depend on the name of the project specified in configure.ac, @PACKAGE@.

Then, we add a doc_DATA variable with the files we want to include. We will use two dummy files: README.md and changelog.

$ vim doc/Makefile.am

docdir = $(datadir)/doc/@PACKAGE@
doc_DATA = README.md changelog
Integrating Autoconf and Automake
Next, we need to build our configure file. In order to do that, we need to follow a series of steps:

First, let's call libtoolize to add the ltmain.sh file to link the shared libraries. In Mac OS X, it's called glibtoolize:

$ glibtoolize

Whereas in linux-based systems, it's called just libtoolize:

$ libtoolize

Next, we need to call aclocal:

$ aclocal

This command will generate the aclocal.m4 that contains all necessary information (including macros) needed for automake. Now we need to run autoheader to generate the configuration headers:

$ autoheader

Then, we can call autoconf to generate the configure command.

$ autoconf

Next, let's call automake then:

$ automake --add-missing

This command will go through all our directory structure and generate, for every Makefile.am file, a corresponding Makefile.in file. And voila! 👏🏻 Our multiplatform configure file is generated and ready to use in any GNU-compliant environment.

Let's see how to use it:

Invoking Configure And Make
First thing we need to do is calling configure:

$ ./configure

It will read our configure.ac file and scan all the required libraries and dependencies. If a dependency or library is missing, the proper error message will be delivered.

For every Makefile.in we generated previously, as specified in AC_OUTPUT, it will generate the plaform-dependent Makefile file.

Now, we just can run make to build our project:

$ make

If everything goes well, we will generate all the libraries and executable files. Now we can install our project to be available for the entire system (as superuser):

# make install

This command will tipically put the binaries in /usr/local/bin, the libraries in /usr/local/lib, the man pages in /usr/local/man and the docs in our @PACKAGE@ defined directory: /usr/local/share/helloWorld.

You can also uninstall everything by calling make uninstall (as superuser):

# make uninstall

Congratulations! You have built a multiplatform, GNU-compliant C++ project 😎.