This guide provides information for developers working on the Epiworld project, with a focus on the build system and development workflow.
- Prerequisites
- Build System Overview
- Building the Project
- Running Tests
- Running Examples
- Build Configuration
- Project Structure
- Advanced Usage
The project requires:
- C++17 compatible compiler (g++, clang++, etc.)
- GNU Make
- Perl (for build scripts)
- Optional: OpenMP support
- Optional: Code coverage tools (lcov)
- Optional: Doxygen (for documentation)
Epiworld uses a custom GNU Make-based build system designed for ease of use and maintainability. The build system is:
- Modular: Build rules are organized in
share/mk/*.mkfiles - Parallel-capable: Supports parallel builds with
make -j - Test-aware: Automatically discovers and runs test cases
- Coverage-ready: Integrated code coverage support
GNUmakefile: Main makefile entry pointshare/mk/: Shared makefile modulesepw.util.mk: Utility functions and macrosepw.compile.mk: Compilation rulesepw.prog.mk: Program building rulesepw.test.mk: Test suite integrationepw.example.mk: Example building and executionepw.artifact.mk: Build artifact management
tests/Makefile: Test suite configurationexamples/Makefile: Example projects configuration
The build system follows a consistent pattern for exposing functionality:
-
File-level operations become file targets: Operations tied to specific files (e.g., building a binary) are exposed as make targets corresponding to those files.
- Example:
make build/examples/00-hello-world/00-hello-world - Example:
make build/tests/tests
- Example:
-
Binary-level operations get dedicated targets: Operations that affect an entire binary or package (e.g., running a complete test suite) get their own phony targets.
- Example:
make test(runs entire test suite) - Example:
make examples(builds and runs all examples)
- Example:
-
Subsection operations use flags: Operations that target specific parts of a binary (e.g., running select test cases from a suite) are controlled via make variables.
- Example:
make test TESTS="test-name-1;test-name-2" - Example:
make test WITH_COVERAGE=1
- Example:
This design ensures consistency: files map to targets, binaries map to targets, and subdivisions of binaries use flags.
View available targets:
makeThis displays all available build targets including examples, tests, and READMEs.
Build a test suite (example using the default tests package):
make build/tests/testsOr use the provided task:
VERBOSE=1 CXXFLAGS="-gdwarf-4" WITH_OPENMP=0 make ./build/tests/tests -j$(perl script/sys-get-nproc.pl)The build output directory mirrors the source directory structure: source in <package>/ produces builds in build/<package>/.
Build and run all examples:
make examplesBuild a specific example (paths mirror source structure):
make build/examples/00-hello-world/00-hello-worldRun a specific example:
make example-00-hello-world-runmake testThis will:
- Build the test suite
- Run all test cases in parallel
- Generate JUnit XML reports
- Display a summary of results
To run specific test cases, use the TESTS variable with semicolon-separated test names:
make test TESTS="test-name-1;test-name-2"Test results are stored in build/<package-name>/.test/ where <package-name> mirrors your source directory:
build/<package-name>/.test/report.xml: JUnit XML formatbuild/<package-name>/.test/report.html: HTML summary
For example, the default test suite in tests/ produces reports in build/tests/.test/.
Each example can be run directly:
make example-<example-name>-runExample names include:
00-hello-world01-sir,01-sis,01-seir02-sir_multiple_runs03-simple-sir05-user-data06-sir-omp(requires OpenMP)07-surveillance- And more...
The build system supports several configuration variables:
Controls optimization and debug symbols:
debug:-g -O0 -DDEBUGwith extra safety checksrelease:-O2 -gwith optimizations
Usage:
make test BUILD_PROFILE=releaseEnable/disable OpenMP support:
1: Enable OpenMP (-fopenmp)0: Disable OpenMP
Usage:
make test WITH_OPENMP=0Enable code coverage instrumentation:
0: No coverage1: Enable coverage with--coverageflag
Usage:
make test WITH_COVERAGE=1Coverage reports are generated in build/<package-name>/.coverage/ (e.g., build/tests/.coverage/ for the default test suite).
Show full build commands:
make test VERBOSE=1Run specific test cases (semicolon-separated):
make test TESTS="SIR Model;SEIR Model"Variables can be combined:
BUILD_PROFILE=release WITH_OPENMP=1 WITH_COVERAGE=1 VERBOSE=1 make testepiworld/
├── GNUmakefile # Main build entry point
├── epiworld.hpp # Single-header amalgamated library
├── include/ # Header files
│ └── epiworld/ # Core library headers
| └── measles/ # Measles headers
├── tests/ # Default test suite package
│ ├── Makefile # Test configuration
│ ├── main.cpp # Test runner main
│ └── *.cpp # Individual test files
├── examples/ # Example programs package
│ ├── Makefile # Examples configuration
│ └── */ # Individual examples
├── share/mk/ # Build system modules
├── script/ # Build scripts
│ ├── amalgamate.pl # Header amalgamation
│ ├── test-gen-runner.pl # Test runner generation
│ └── junit-*.pl # Test reporting scripts
└── build/ # Build output (mirrors source structure)
├── tests/ # Built artifacts from tests/
└── examples/ # Built artifacts from examples/
Note: The build directory structure mirrors the source structure. Any package at <path>/ produces build artifacts at build/<path>/.
The test system uses Catch2 and automatically:
- Discovers test cases at build time
- Generates individual make targets for each test
- Runs tests in parallel
- Aggregates results into unified reports
Each test file should contain ONE EPIWORLD_TEST_CASE macro. For multiple test cases, create separate files (e.g., 22a-testname.cpp, 22b-testname.cpp).
Generate the single-header epiworld.hpp:
make build/epiworld.hppThis combines all headers from include/epiworld/ into one file using script/amalgamate.pl.
- Build with coverage enabled:
make test WITH_COVERAGE=1- Coverage data is collected in
build/<package-name>/.coverage/(e.g.,build/tests/.coverage/for the default test suite) - LCOV generates
coverage.infofile in that directory - Use with your favorite coverage visualization tool
Remove all build artifacts:
make purgeOr use the alias:
make cleanEnable verbose output to see full commands:
make test VERBOSE=1View internal build databases:
make program-database # List all programs
make test-database # List all testsWhen creating tests:
- Use Catch2 expectations (
REQUIRE,REQUIRE_THAT,CHECK) - Do NOT throw exceptions for test validation
- One
EPIWORLD_TEST_CASEper file - Use
run()for deterministic tests - Use
run_multiple()for stochastic tests that need statistical validation
Speed up compilation with parallel jobs:
make test -j$(nproc)Or use the helper script:
make test -j$(perl script/sys-get-nproc.pl)- Always run tests: Before submitting changes, run
make test - Use appropriate build profile: Debug for development, release for benchmarks
- Check coverage: Run with
WITH_COVERAGE=1periodically - Parallel builds: Use
-jfor faster compilation - Keep tests focused: One test case per file
- Document examples: Update each example's
README.mdafter modifying it - Understand path mirroring: Build artifacts in
build/<path>/correspond to sources in<path>/
- Check available targets:
make - View full documentation: See README.md
- Build system issues: Enable
VERBOSE=1 - Test failures: Check
build/<package-name>/.test/report.html(e.g.,build/tests/.test/report.html)