This is a template for new C projects using GNU Autotools, a tool-assisted module system, and unit testing with Unity Test and CMock. It takes a simple but opinionated view on module organization and unit testing. This repo includes a few example modules illustrating module dependencies and unit tests.
This template supposes that a module may consist of multiple source files. For a
simpler template that assumes single-file modules and does not generate its
Makefile.am
with a tool, see
dansanderson/c-autotools-template-small.
Project maintainers need Python 3.x and Ruby 2.x installed. The source distribution can be built on any POSIX-compliant system without Python or Ruby.
A project based on this template organizes its C code into modules, defined
below. You use a tool to generate the Makefile.am
from the module layout and
configuration.
Note: Be sure to clone this repo (or your own project repo based on this
repo) with git clone --recurse-submodules
so that the CMock testing library
in third-party/
and its submodules are also installed. If you omitted this
when cloning, run this to finish the process:
git submodule update --init --recursive
A module is a self-contained collection of functionality, implemented as one or more C source files. A module can define a program, or it can define a library used by other modules. A library module exposes a public interface with a header file.
Each module has a name consisting of a letter followed by zero or more letters and numbers. The module name is used for the module source directory, the module's header file and primary source file, the module tests directory, and when declaring dependencies from other modules.
The following example files are included in the template under src/
and
tests/
. They define three library modules (cfgfile, executor, reporter) and
one program module (myapp):
src/
cfgfile/
cfgfile.c
cfgfile.h
cfgmap.c
cfgmap.h
module.cfg
executor/
executor.c
executor.h
module.cfg
reporter/
reporter.c
reporter.h
module.cfg
myapp/
myapp.c
module.cfg
tests/
cfgfile/
test_cfgfile.c
executor/
test_executor.c
reporter/
test_reporter.c
Each module.cfg
file describes the module, including whether it is a library
or a program, and on which other modules (if any) it depends. For example,
executor/module.cfg
looks like this:
[module]
library = executor
deps = cfgfile
myapp/module.cfg
looks like this:
[module]
program = myapp
deps = executor reporter
The module source directory src/{module}/
must contain a {module}.h
header
file that declares the module's public interface and a {module}.c
that
implements it. The directory can optionally contain additional source files
that are compiled and linked with the module.
A source file can #include
any internal header by its name. To #include
the
public interface of a dependency, use the path from src/
:
#include "executor.h"
#include "cfgfile/cfgfile.h"
When a program module is built, it is linked statically with all of its
dependencies (and all of their dependencies, and so on). It is recommended to
use C-style name prefixes for all non-static
symbols. Specifically:
- Module public symbols should begin with the module name and an underscore:
executor_do_something()
- Module internal symbols shared across files should begin with an underscore,
the module name, and another underscore:
_executor_private()
- Symbols internal to a single source file should be declared
static
. No prefix is needed forstatic
symbols. - The header guard
#define
for the module public header should be named after the module:EXECUTOR_H_
- The header guard
#define
for a module private header should use a prefix of an underscore, the module name, and an underscore:_EXECUTOR_PRIVATE_H_
A program module must define int main(int argc, char **argv)
. A library
module must not.
Each source file under tests/{module}/
named test_{suite}.c
is a Unity
Test test suite. A test suite should
#include
the header of the module under test with its src/
relative path.
It must also #include "unity.h"
.
Each function with a name beginning with test_
is a unit test in the suite.
It should have a void
parameter specification and a void
return type. It is
recommended to include the name of the function under test, the condition of
the test, and the expected result in the name of the test_...()
function,
spelled CamelCase and delimited by underscores.
The test function contains code for the test and Unity Test assertions. See the Unity Assertions Reference.
A test suite can optionally define void setUp(void)
and
void tearDown(void)
, to be called before and after each test, respectively.
Each test suite is linked with the module under test, along with mock modules for each dependency of the module under test, generated with CMock. Every call that the function under test makes to a dependency module must be declared with a CMock expectation. The actual dependency function is not called.
#include "executor/executor.h"
#include "mock_cfgfile.h"
#include "unity.h"
void test_Square_UsesExampleTwo(void) {
cfgfile_func_ExpectAndReturn(7, 49);
int result = executor_doit(7);
TEST_ASSERT_EQUAL_INT(49, result);
}
A module can have more than one test suite. Suite names must be unique across the project, so a suite not named after the module under test should use the module name as a prefix. It is recommended that each library module have at least one test suite named after the module.
GNU Autotools uses configure.ac
and Makefile.am
to generate build scripts.
This template further uses a script to generate Makefile.am
from the module
source layout and configuration files. To generate Makefile.am
:
python3 scripts/makemake.py
This script is intentionally not invoked from either configure.ac
or
Makefile.am
. A project maintainer must run it directly when module source
files are added or deleted, or when module.cfg
files are modified. It is
recommended to commit the generated Makefile.am
to the project repo.
After the Makefile.am
file is generated, you can use GNU Autotools as normal.
autoreconf --install
./configure
make
To build all programs:
make
To build just one program, use the name of the program module as the make target:
make myapp
To build and run all unit tests:
make check
To build all unit tests and run a specific test suite:
make check TESTS='tests/runners/test_cfgfile'
To make and validate the source distribution:
make distcheck
In general:
- Run
autoreconf --install
then./configure
after checking out the repo for the first time, or after runningpython3 scripts/superclean.py
. - Run
python3 scripts/makemake.py
then./configure
after creating or deleting files, after changing amodule.cfg
, or aftermake distclean
. - Running
make
ormake check
is otherwise sufficient when changing source files.
In theory, none of these commands causes permanent damage, and any can be re-run at any time. To completely reset the workspace:
python3 scripts/superclean.py
autoreconf --install
python3 scripts/makemake.py
./configure
make
Each test suite is built to a "runner" program, then run as part of
make check
. The runner programs are created under tests/runners/
, and named
after the test suite source file.
To build just one test suite runner:
make tests/runners/test_cfgfile
To run a test suite runner after it is built, simply run the program:
./tests/runners/test_cfgfile
You can attach a debugger to a test runner and set breakpoints in the module
under test. I have included Visual Studio
Code project configuration (in .vscode/
) for
a "Debug a test" configuration. Select a test suite file, then run this
configuration to build and run the test in the VSCode debugger interface.
GNU Autotools generates Makefile targets to clean up build output:
make clean
: deletes files created bymake
make distclean
: deletes additional files created by./configure
Neither target deletes files created by autoreconf --install
.
Because it is sometimes useful to completely restore the project directory to
the way it was, I have included a script, scripts/superclean.py
, that deletes
all files ignored by Git and .gitignore
, and deletes all untracked files in
Git submodules (such as the provided third-party/CMock
). Use the --dry-run
option to cause it to print everything that will be deleted without actually
deleting it.
python3 scripts/superclean.py --dry-run
python3 scripts/superclean.py
Makefile.am
is not deleted by superclean.py
because it is not in
.gitignore
. Makefile.am
is safe to delete, if necessary. You can recreate
it with scripts/makemake.py
.
Module source files are simple enough that you can create them by hand. I wanted this to be even easier, so there's a script:
python3 scripts/newmod.py modulename
python3 scripts/newmod.py --program modulename
The source distribution generated by GNU Autotools from your project should build on any operating system with a POSIX-compatible environment, including Linux, macOS, and Windows with MinGW. The built program runs with even fewer requirements. In particular, a MinGW-built binary can run on any Windows machine without MinGW itself installed.
To develop the project itself, you'll need a gcc-compatible C compiler, make, GNU Autotools, Python 3.x for the module management tools, and Ruby 2.x for Unity Test and CMock code generators.
On Linux, you can install these prerequisites with your system's package manager. For example, on Ubuntu:
sudo apt-get update
sudo apt-get install build-essential autotools-dev autoconf ruby-full git clang-format python3.10
On macOS, install Homebrew. Simply installing Homebrew also installs the XCode Command Line Tools, including a gcc-compatible C compiler and GNU Autotools. You can install additional tools like so:
brew install ruby python git clang-format
On Windows, install MinGW MSYS2. The
instructions describe how to open an MSYS terminal and run the pacman
package
manager. You can use pacman
to install the MinGW toolchain and other tools:
pacman -S base-devel mingw-w64-x86_64-toolchain mingw-w64-x86_64-libusb clang autotools git
Note: Take care to build in a MinGW shell, and not an "MSYS" shell. From the MSYS
shell, builds will require the MSYS DLL to run. A build in the MinGW shell
produces a standalone .exe
program. Because I always want a MinGW standalone
binary, this template's configure.ac
will abort if built under MSYS.
It is possible to cross-build a Windows MinGW standalone .exe
from Linux. To
do this, install additional packages:
sudo apt-get install binutils-mingw-w64 mingw-w64-common gcc-mingw-w64 libz-mingw-w64-dev
Then tell ./configure
that the target OS is Windows:
./configure --build=x86_64-pc-linux-gnu --host=x86_64-w64-mingw32
The make
will produce a file named myapp.exe
that can be run on Windows.
The template uses a built-in Autotools feature to detect the target platform for the build, then set Makefile and C preprocessor defines accordingly. It supports the following targets:
WINDOWS
: Windows MinGW, including cross-compilation from Linux to MinGWLINUX
: LinuxAPPLE
: macOS
You'll need to extend Automake definitions to link in additional libraries.
If provided, makemake.py
will insert certain files into the generated
Makefile.am
, so you don't need to modify the makemake.py
script directly.
project.mk
, in the project root directory, gets added to the end ofMakefile.am
.module.mk
, in a module source directory, gets added to the end of the module's section inMakefile.am
.
This insertion is different from a file being include
d by the final Makefile.
In particular, project.mk
and module.mk
can extend Automake variables prior
to ./configure
running Automake. This also means that these files are
restricted to Automake-compatible syntax, which is a subset of Makefile syntax.
The generated Makefile.am
defines these list variables, so these files can
extend them with the +=
operator:
ACLOCAL_AMFLAGS
AM_CPPFLAGS
AM_LDFLAGS
bin_PROGRAMS
noinst_LTLIBRARIES
check_PROGRAMS
check_LTLIBRARIES
CLEANFILES
BUILT_SOURCES
TESTS
EXTRA_DIST
The module section also defines these, which can be extended similarly in
module.mk
(where {modname}
is the module name):
lib{modname}_la_SOURCES
lib{modname}_la_LIBADD
for library modules{modname}_LDADD
for program modules
Each test suite generates these (where {suitename}
is the test suite name,
typically test_{modname}
):
tests_runners_{suitename}_SOURCES
tests_runners_{suitename}_LDADD
tests_runners_{suitename}_CPPFLAGS
It might be useful to write larger (non-unit) test programs that use a
combination of real (non-mock) modules and mocks. A test.cfg
file that could
request a linkage combination for a given test suite, without the need for
custom rules.
It's not obvious how to mock third-party libraries. It may be sufficient to run
the CMock generator tool on a third-party library header file. This is not yet
a built-in facility of makemake.py
. This could be another feature of
test.cfg
files.
The name prefix best practices are important enough that it'd be good to have a
script that validates that they are followed consistently. The compiler will
report collisions, but it won't report non-collisions that might become
collisions later. A tool could scan built .o
object files with the command
nm -Uj file.o
to determine the names of functions and global storage. It
would need to invoke a C parser (or fake it) to properly identify typedef
s in
header files.
This template and related tools are released under The Unlicense. See LICENSE for complete text.
You are not required to use this license for your project. Replace the
LICENSE
file with whatever is appropriate.
I started this thinking it'd just be a demonstration of novice best practices
for organizing a GNU Autotools project. I tried to avoid writing a module
management tool, but I couldn't get the Makefile.am
boilerplate for tests and
mocks succinct enough to my satisfaction. I concluded that writing
my own tool would be better for my projects than trying to reuse other module
management systems like
gnulib-tool.
Feedback is welcome! If you have ideas for how this can be improved, fixed, or made more generally useful, please file an issue. Pull requests are also welcome, though please pardon me if I take a while to respond.
See also my blog entry on this subject.
Thanks!
— Dan (contact@dansanderson.com)