mirror of
https://github.com/dansanderson/c-autotools-template
synced 2025-03-13 16:21:12 -04:00
467 lines
16 KiB
Markdown
467 lines
16 KiB
Markdown
# c-autotools-template
|
|
|
|
This is a template for new C projects using [GNU
|
|
Autotools](https://www.gnu.org/software/automake/manual/html_node/index.html),
|
|
a tool-assisted module system, and unit testing with [Unity
|
|
Test](http://www.throwtheswitch.org/unity) and
|
|
[CMock](http://www.throwtheswitch.org/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](https://github.com/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`
|
|
|
|
## C Modules
|
|
|
|
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):
|
|
|
|
```text
|
|
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:
|
|
|
|
```ini
|
|
[module]
|
|
library = executor
|
|
deps = cfgfile
|
|
```
|
|
|
|
`myapp/module.cfg` looks like this:
|
|
|
|
```ini
|
|
[module]
|
|
program = myapp
|
|
deps = executor reporter
|
|
```
|
|
|
|
## Module source files
|
|
|
|
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/`:
|
|
|
|
```c
|
|
#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 for `static` 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.
|
|
|
|
## Module unit tests and mocks
|
|
|
|
Each source file under `tests/{module}/` named `test_{suite}.c` is a [Unity
|
|
Test](http://www.throwtheswitch.org/unity) 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](https://github.com/ThrowTheSwitch/Unity/blob/master/docs/UnityAssertionsReference.md).
|
|
|
|
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](http://www.throwtheswitch.org/cmock). Every call that the function
|
|
under test makes to a dependency module must be declared with a [CMock
|
|
expectation](https://github.com/ThrowTheSwitch/CMock/blob/master/docs/CMock_Summary.md).
|
|
The actual dependency function is not called.
|
|
|
|
```c
|
|
#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.
|
|
|
|
## Generating the Makefile.am
|
|
|
|
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`:
|
|
|
|
```text
|
|
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.
|
|
|
|
## Building the project and running Autotools targets
|
|
|
|
After the `Makefile.am` file is generated, you can use GNU Autotools as normal.
|
|
|
|
```text
|
|
autoreconf --install
|
|
./configure
|
|
make
|
|
```
|
|
|
|
To build all programs:
|
|
|
|
```text
|
|
make
|
|
```
|
|
|
|
To build just one program, use the name of the program module as the make
|
|
target:
|
|
|
|
```text
|
|
make myapp
|
|
```
|
|
|
|
To build and run all unit tests:
|
|
|
|
```text
|
|
make check
|
|
```
|
|
|
|
To build all unit tests and run a specific test suite:
|
|
|
|
```text
|
|
make check TESTS='tests/runners/test_cfgfile'
|
|
```
|
|
|
|
To make and validate the source distribution:
|
|
|
|
```text
|
|
make distcheck
|
|
```
|
|
|
|
In general:
|
|
|
|
* Run `autoreconf --install` then `./configure` after checking out the repo for
|
|
the first time, or after running `python3 scripts/superclean.py`.
|
|
* Run `python3 scripts/makemake.py` then `./configure` after creating or
|
|
deleting files, after changing a `module.cfg`, or after `make distclean`.
|
|
* Running `make` or `make 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:
|
|
|
|
```text
|
|
python3 scripts/superclean.py
|
|
autoreconf --install
|
|
python3 scripts/makemake.py
|
|
./configure
|
|
make
|
|
```
|
|
|
|
## Running and debugging a unit test
|
|
|
|
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:
|
|
|
|
```text
|
|
make tests/runners/test_cfgfile
|
|
```
|
|
|
|
To run a test suite runner after it is built, simply run the program:
|
|
|
|
```text
|
|
./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](https://code.visualstudio.com/) 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.
|
|
|
|
## Cleaning up build output
|
|
|
|
GNU Autotools generates Makefile targets to clean up build output:
|
|
|
|
* `make clean` : deletes files created by `make`
|
|
* `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.
|
|
|
|
```text
|
|
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`.
|
|
|
|
## Creating new modules
|
|
|
|
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:
|
|
|
|
```text
|
|
python3 scripts/newmod.py modulename
|
|
|
|
python3 scripts/newmod.py --program modulename
|
|
```
|
|
|
|
## Building for specific operating systems
|
|
|
|
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](https://www.mingw-w64.org/). 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](https://gcc.gnu.org/)-compatible C compiler, make, [GNU
|
|
Autotools](https://www.gnu.org/software/automake/manual/html_node/index.html),
|
|
[Python](https://www.python.org/) 3.x for the module management tools, and
|
|
[Ruby](https://www.ruby-lang.org/en/) 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:
|
|
|
|
```text
|
|
sudo apt-get update
|
|
sudo apt-get install build-essential autotools-dev autoconf ruby-full git clang-format python3.10
|
|
```
|
|
|
|
On macOS, install [Homebrew](https://brew.sh/). 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:
|
|
|
|
```text
|
|
brew install ruby python git clang-format
|
|
```
|
|
|
|
On Windows, install [MinGW MSYS2](https://www.msys2.org/#installation). 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
./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.
|
|
|
|
## Detecting the target platform
|
|
|
|
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 MinGW
|
|
* `LINUX` : Linux
|
|
* `APPLE` : macOS
|
|
|
|
## Defining custom rules
|
|
|
|
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 of
|
|
`Makefile.am`.
|
|
* `module.mk`, in a module source directory, gets added to the end of the
|
|
module's section in `Makefile.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`
|
|
|
|
## Still to do
|
|
|
|
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.
|
|
|
|
## License
|
|
|
|
This template and related tools are released under [The
|
|
Unlicense](https://unlicense.org). See [LICENSE](./LICENSE) for complete text.
|
|
|
|
You are _not_ required to use this license for your project. Replace the
|
|
`LICENSE` file with whatever is appropriate.
|
|
|
|
## Note from the author
|
|
|
|
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](https://www.gnu.org/software/gnulib/manual/html_node/Invoking-gnulib_002dtool.html).
|
|
|
|
Feedback is welcome! If you have ideas for how this can be improved,
|
|
fixed, or made more generally useful, please [file an
|
|
issue](https://github.com/dansanderson/c-autotools-template/issues). [Pull
|
|
requests](https://github.com/dansanderson/c-autotools-template/pulls) are also
|
|
welcome, though please pardon me if I take a while to respond.
|
|
|
|
See also [my blog entry on this
|
|
subject](https://dansanderson.com/lab-notes/autotools-in-2022/).
|
|
|
|
Thanks!
|
|
|
|
— Dan (contact@dansanderson.com)
|