This is a template for starting new C++ projects that conform to the IDI C++ project structure requirements.
- CMake >= 3.26
- GCC 11+ / Clang 11+ / MSVC (VS2019+)
Dependencies are automatically fetched during CMake configuration.
In root of project, run the following commands:
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build . # or generated build system command (make, Visual Studio etc.)CMake arguments:
- [Windows]
$ cmake .. -A x64for 64-bit build (-A Win32for 32-bit build) - [Windows]
$ cmake --build . --config Releasefor Release mode build (Debugfor Debug mode build)
CMake IDI arguments:
Use with cmake .. -D<argument>=<value>.
<P> is the upper-case project name (IDICMAKE_PROJECT_NAME), e.g. TEMPLATE_PROJECT_BUILD_TESTS.
IDICMAKE_PLATFORM_CONFIG(default:platform-config.cmake) - Platform configuration file (only for top-level project; a relative path is tried against the current directory, then the build directory, then the source directory)<P>_BUILD_DEMOS(default:1,0as a subproject) - Build demo applications if applicable.<P>_BUILD_TESTS(default:1,0as a subproject) - Build unit tests. (long)<P>_BUILD_DOCS(default:1,0as a subproject) - Add thedocstarget when Doxygen is found.<P>_DO_TEMPLATE_COMPONENT_TESTorDO_TEMPLATE_COMPONENT_TEST(default:0) - Generate unit test template component and build unit tests for template.NEW_COMPONENT_NAME- Create new component (or use./new_component.sh <name>).CODE_COVERAGE(default:OFF) - Coverage build, see Code Coverage.IDICMAKE_SANITIZERS(default: empty) - Sanitizer build, see Sanitizers.
$ ctest -C DebugScripts for the common tasks. The cmake/idi/tools/ ones are part of the framework and are
updated with it; each has a .sh (Linux/macOS) and a .bat (Windows) version, and --help.
| Script | Does |
|---|---|
./new_component.sh <name> |
Create a new component in src/<name>/ |
cmake/idi/tools/lint.sh |
clang-format check + clang-tidy gate (Linting) |
cmake/idi/tools/coverage.sh |
Coverage build, test run and HTML report (Code Coverage) |
cmake/idi/tools/docs.sh |
Doxygen API docs (API Documentation) |
cmake/idi/tools/update.sh |
Guided framework update (Updating the Framework) |
The template is based on around a very generalized and flexible CMake framework for building executables, shared libraries, and static libraries that are cross platform for Linux and Windows (Mac OS support is possible in the near future).
Targets are built around collections of object libaries that are then combined into a static or shared library depending on the project's main type. If the target is an executable, then all object files are still built to a core shared library and the final executable linked against it.
The object libraries that are used to build these final targets are called "components" in the language of the framework. There is one default component called base that provides boilerplate versioning and other useful information related to project configuration. Additional components are created with:
$ ./new_component.sh component_name # Windows: new_component.bat component_name(the same as cmake -S . -B build -DNEW_COMPONENT_NAME=component_name). This generates a boilerplate component in src/component_name/. Then add add_subdirectory(component_name) to src/components.cmake and re-configure. Run ./new_component.sh --help for the layout of the generated files.
A component includes a CMakeLists.txt file that contains the component definition call. See the documentation below on the CMake idi_component function for more details.
Alongside the CMakeLists.txt file is is an objects.cmake. This file is where source files for the object library are added. See the documentation on the CMake function idi_add_sources for more information.
Includes are structured in a specific way to easily enable the installation or inclusion of headers from the project in other projects.
The include folder in each component contains exactly one subfolder that is named after the CMake IDICMAKE_PROJECT_NAME configuration value. Inside that folder exists any interface header files that can be used with static libraries. In addition, a folder called public contains any headers that are used as header interfaces to a shared library. These generally will be C exported interfaces for shared object ABI compatibility.
The public headers are .h files with extern "C" declarations, but they are consumed from C++ only. The C ABI is there so the shared library boundary has unmangled names and plain struct layouts that stay stable across compilers and library versions; it is not there for C callers. C++ inside these headers (default member initializers, bool, nullptr, <cstdint>) is fine. Keep structs that cross the boundary standard-layout and trivially copyable. Generated components carry a short note saying this at the top of their public header.
Any header files that are not to be consumed by end-users should not be included in the include folder structure. Those headers will remain private to the project and it's components (there is currently no way to restrict headers to just the component in which it lives).
src/
|--- some_component/
|--- some_implementation.cpp # built as part of the object
|--- include/
|--- project_name/ # actual folder name via the IDICMAKE_PROJECT_NAME var
|--- some_implementation.hpp # static library avaialable header
|--- public/
|--- exported_impl.h # C exported function headers for shared lib
A component declared with idi_component(<name> HEADER_ONLY ...) is an INTERFACE library with no sources. In a shared library project, a header-only component is the way to publish C++ headers that other projects compile against without linking the shared library: for example the port definitions, interface contracts and data types of a plugin that is loaded at run time by a loader. By convention such a component is called contracts, so consumers link <project_name>_contracts. All of its headers are installed (under includes/<project_name>/), not only public ones, and its dependencies should be header-only too.
The <project_name>_public target is different: it exposes the C ABI headers and links the shared library itself, so it is not for consumers of a plugin that a loader loads.
The include/project_name folder will also include a include.cmake file that declares the header files. This file also includes the public/public.cmake file. See the documentation below for idi_add_includes and idi_add_public_includes respectively for more information.
Tests are contained in the component's tests folder. See idi_component_test and idi_component_test_public function documentation below for more information on how tests can be added. Tests are built around CTest and use Catch2 by default.
Each tests folder has a .clang-tidy that inherits the project's and turns off two checks that misfire on Catch2 tests: bugprone-unchecked-optional-access (it cannot see that a failed REQUIRE stops the test) and readability-function-cognitive-complexity (a test case with many SECTIONs and REQUIREs scores high without being hard to read).
IDICMAKE_SANITIZERS builds every target (components, tests, demos and the dependencies the project adds) with the given sanitizers, so ctest runs under them. Use one build folder per set:
$ cmake -B build-asan -DIDICMAKE_SANITIZERS="address;undefined"
$ cmake --build build-asan && ctest --test-dir build-asan --output-on-failure
$ cmake -B build-tsan -DIDICMAKE_SANITIZERS=threadValues: address, undefined, leak, thread (GCC/Clang; thread cannot be combined with address or leak), and address only on MSVC. Undefined behaviour stops the program, so a test with UB fails instead of printing a warning. Run the address set before a release and after changing memory or plugin-loading code: it finds overflows and leaks that the tests alone pass over.
The framework instruments components and tests with GCC/Clang's --coverage flag and builds reports with lcov or gcovr.
- Compiler: GCC or Clang (MSVC has no gcov-style coverage; on Windows use clang with Ninja)
- Report tools (at least one):
lcov+genhtml— HTML report (preferred)gcovr— HTML and Cobertura XML (for CI)
$ sudo apt install lcov # lcov + genhtml
$ pip install gcovr # gcovrOne step, in its own build-coverage/ directory:
$ cmake/idi/tools/coverage.sh --open # Windows: cmake\idi\tools\coverage.bat
$ CC=clang-21 CXX=clang++-21 cmake/idi/tools/coverage.sh # another compiler
$ cmake/idi/tools/coverage.sh --fail-under 80 # fail below 80% line coverageOr by hand: configure with -DCODE_COVERAGE=ON (use a separate build directory, the option sticks in the cache) and build a target:
| Target | Description |
|---|---|
ccov-all |
Reset counters, run all tests with ctest, write the HTML report (plus Cobertura XML when gcovr is installed) and print Code Coverage: NN.NN% |
ccov-all-xml |
Same test run, Cobertura XML only (requires gcovr; for CI) |
ccov-clean |
Remove the reports and reset the counters |
$ cmake -S . -B build-coverage -DCODE_COVERAGE=ON
$ cmake --build build-coverage
$ cmake --build build-coverage --target ccov-allReports are written to <build>/reports/coverage/: html/index.html, coverage.info (lcov) and coverage.xml (gcovr).
idi_component, idi_component_test and idi_component_test_public call target_code_coverage() on their targets. When CODE_COVERAGE=ON:
- Component object libraries and test executables are compiled with
--coverage(in Debug). - At the end of configuration the report targets are created. They run every ctest test, so tests a project registers with plain
add_test()(scripts, system tests) count too, and a failing test fails the target. - Test code (
*/tests/*),lib/, Catch2,_depsand system headers are excluded from the reports. - The gcov tool matches the compiler:
gcov-<major>for GCC,llvm-cov-<major> gcovfor Clang.
When CODE_COVERAGE=OFF (the default), the coverage functions do nothing.
Settings (in cmake-hooks/post-options.cmake or with -D):
| Variable | Meaning |
|---|---|
IDICMAKE_COVERAGE_FAIL_UNDER |
Fail the report targets below this line coverage percent |
IDICMAKE_COVERAGE_CTEST_ARGS |
Extra ctest arguments, e.g. --timeout;120 |
More excludes: set_property(GLOBAL APPEND PROPERTY IDICMAKE_COVERAGE_EXCLUDES "generated/*") in a hook (relative patterns are relative to the project root).
In GitLab CI, coverage: '/^Code Coverage: \d+(?:\.\d+)?%$/' picks up the printed percentage (see .gitlab-ci.yml).
clang-format and clang-tidy run over the project's own sources in src/, excluding the framework's src/base/ and generated *.out.* files. Demos in demo/ are not linted unless you ask for it (--demos, or IDICMAKE_LINT_DEMOS).
$ cmake/idi/tools/lint.sh # format check + tidy gate; Windows: cmake\idi\tools\lint.bat
$ cmake/idi/tools/lint.sh --fix --format-only # reformat everything
$ cmake/idi/tools/lint.sh src/foo/foo.cpp # only some files
$ cmake/idi/tools/lint.sh --clang-version 21 # same tools as CI
$ cmake/idi/tools/lint.sh --all-checks # also report the full .clang-tidy list (advisory)
$ cmake/idi/tools/lint.sh --demos # also lint demo/The script configures and builds build-lint/ (clang-tidy needs compile_commands.json and any generated sources). The same checks are available as targets in any configured build:
| Target | Description |
|---|---|
format |
Apply clang-format to all project files |
format-check |
Fail if a file is not formatted |
tidy |
clang-tidy with the gate checks; fails on any finding, on a bad clang-tidy config, or when it finds no files to check |
tidy-report |
clang-tidy with the full .clang-tidy check list; report only |
lint |
format-check + tidy |
install-git-hooks |
Install a pre-commit hook that checks clang-format on staged files (skip once with git commit --no-verify) |
The gate is a short list of checks that catch real bugs plus the naming rules from .clang-tidy. The broader .clang-tidy set (cppcoreguidelines, cert, modernize, the static analyzer, ...) is advisory. In the template's CI, format-check and tidy both fail the pipeline; a new project starts with no findings, so keep it that way. A project that adopts the gate with findings to clear can add allow_failure: true to its tidy job for a while.
Before checking files, tidy and tidy-report run clang-tidy --verify-config on the root .clang-tidy, every .clang-tidy under the lint folders, and the gate list. clang-tidy otherwise ignores a glob that matches no check (for example clang-analyzer-core-*, where the analyzer names use a dot) or a misspelled option without a word.
clang-tidy needs the Ninja or Makefile generator (Visual Studio generators do not write compile_commands.json); on Windows, run lint.bat from a Developer Command Prompt with Ninja installed.
Settings (in cmake-hooks/post-options.cmake or with -D):
| Variable | Default | Meaning |
|---|---|---|
IDICMAKE_CLANG_TOOLS_VERSION |
newest installed | LLVM major version of clang-format/clang-tidy |
IDICMAKE_LINT_DIRS |
src |
Directories to lint |
IDICMAKE_LINT_DEMOS |
OFF |
Also lint demo/ (set ON in a hook to make it permanent; lint.sh --demos for one run) |
IDICMAKE_LINT_EXCLUDES |
*.out.*;src/base/*;src/unit_test_component/* |
Project-relative globs to skip |
IDICMAKE_LINT_TIDY_GATE_CHECKS |
see cmake/idi/functions/idi_lint.cmake |
clang-tidy checks the tidy target enforces |
.clang-format defines the layout and .clang-tidy the naming rules that the gate enforces:
namespace vendor::app { // namespaces: lower_case
constexpr int MAX_PEERS = 8; // global/static/class constants: UPPER_CASE
class PeerList { // classes, structs, enums, type aliases: CamelCase
public:
void add_peer(int peer_id); // functions, methods, variables, parameters: lower_case
protected:
void rebuild_index(); // protected methods: no prefix (subclasses call them)
int _count = 0; // protected/private members: '_' prefix
private:
void _mark_dirty(); // private methods: '_' prefix
bool _dirty = false;
};
} // namespace vendor::appTo silence a finding that is intended, put the reason next to it and use // NOLINTNEXTLINE(<check>) on the line above, or // NOLINTBEGIN(<check>) / // NOLINTEND(<check>) around a block. Avoid a trailing // NOLINT(<check>): when clang-format wraps a long line, the comment moves onto another line and stops applying. Turn a check off for a whole folder with a nested .clang-tidy that has InheritParentConfig: true (as the tests folders do), not in the project's root file.
The docs target runs Doxygen with the doxygen-awesome-css theme:
$ cmake/idi/tools/docs.sh --open # Windows: cmake\idi\tools\docs.bat --open
$ cmake --build build --target docs # same thingOutput: build/docs/html/index.html. The headers are collected from the components automatically: a shared library documents each component's include/<project>/public/ headers (the exported API) plus header-only components; static and executable builds document all of include/<project>/. The front page is docs/mainpage.md (other docs/*.md files become extra pages), or README.md if there is no docs/mainpage.md.
The theme is downloaded (pinned version, hash checked) the first time docs is built, so configuring needs no network. Use -DIDICMAKE_DOCS_THEME=OFF (or docs.sh --no-theme) for plain Doxygen output offline.
Settings (in cmake-hooks/post-options.cmake or with -D):
| Variable | Meaning |
|---|---|
<P>_BUILD_DOCS |
Add the docs target (default on for top-level builds) |
IDICMAKE_DOCS_THEME |
Use doxygen-awesome-css (default ON) |
IDICMAKE_DOCS_OUTPUT_DIR |
Output directory (default <build>/docs) |
IDICMAKE_DOCS_BRIEF |
One-line description under the project name |
IDICMAKE_DOCS_INPUTS |
Extra files/directories to document |
DOXYGEN_<KEY> |
Any Doxyfile setting, e.g. set(DOXYGEN_PREDEFINED "MY_EXPORT="); project values win |
The template's GitLab CI publishes the docs with GitLab Pages from the default branch (pages job).
The framework updates itself from a newer template without touching project code. Only the framework-owned paths are replaced:
| Path | Content |
|---|---|
cmake/idi/ |
The framework: functions, updater, templates, scripts, tools, CHANGELOG.md |
src/base/ |
Base component (its include directory is renamed to match your project) |
.claude/skills/idi-cmake/ |
Claude Code skill describing this framework version |
CMakeLists.txt |
Root CMake file |
src/CMakeLists.txt |
Source directory CMake file |
lib/.gitignore |
Library gitignore |
Everything else is yours and is not touched: platform-*.cmake, version.cmake, src/components.cmake, src/main/, your components, cmake-hooks/, lib/libraries.cmake, demo/, CI files, README.md. Never put project changes inside the paths above (for example extra headers in src/base/): the update deletes them. Use a hook, a platform file or your own component instead.
The list lives in cmake/idi/updater/managed_paths.cmake; the changes in each release are in cmake/idi/CHANGELOG.md.
$ cmake/idi/tools/update.sh --check # preview only
$ cmake/idi/tools/update.sh # Windows: cmake\idi\tools\update.batThe script:
- Refuses to run on a working tree with uncommitted changes, so the update is its own reviewable diff (
--allow-dirtyoverrides). - Fetches the template (GitHub
masterby default) and shows the version change, every file that will change, be added or be deleted in the framework-owned paths, and the changelog entries since your version. - Asks for confirmation (
-yto skip), then applies the update. - Shows the diff summary and the exact
gitcommand to roll back. - Re-configures, builds and runs the tests in
build/(--no-verifyto skip). - With
--commit, commits the framework-owned paths asUpdate IDI CMake framework <old> -> <new>.
Sources:
$ cmake/idi/tools/update.sh --ref v6.4.0 # a branch or tag on GitHub
$ cmake/idi/tools/update.sh --folder ../idi-cpp-template # a local checkout
$ cmake/idi/tools/update.sh --file idi-cpp-template.zip # a local archive
$ cmake/idi/tools/update.sh --url 'https://gitlab.example.com/idi/idi-cpp-template/-/archive/{ref}/idi-cpp-template-{ref}.zip' --ref masterSet IDI_TEMPLATE_UPDATE_URL (with {ref}) to make a mirror the default. If the incoming version is not newer the script stops; --force re-applies it. A major version update may need changes to project-owned files: they are listed under Manual steps in the changelog, and the script points them out.
Projects on framework 6.3.2 or older don't have the script yet. Update once with the CMake command below (6.3.2 and older can't update from a GitHub archive URL, so use a local checkout or archive of the template), then use update.sh from then on.
The script drives the same configure-time updater, which can also be run by hand:
$ cmake -S . -B build -DDO_FRAMEWORK_UPDATE=ON # GitHub master
$ cmake -S . -B build -DDO_FRAMEWORK_UPDATE=ON -DFRAMEWORK_UPDATE_MODE=folder \
-DFRAMEWORK_UPDATE_FOLDER_LOC=/absolute/path/to/template
$ cmake -S . -B build -DDO_FRAMEWORK_UPDATE=ON -DFRAMEWORK_UPDATE_MODE=file \
-DFRAMEWORK_UPDATE_FILE_LOC=path/to/template.zip
$ cmake -S . -B build -DDO_FRAMEWORK_UPDATE=ON -DFRAMEWORK_UPDATE_MODE=url \
-DFRAMEWORK_UPDATE_URL=https://example.com/template.zip| Mode | Source | Variable |
|---|---|---|
url (default) |
Zip archive download | FRAMEWORK_UPDATE_URL (default: GitHub master) |
file |
Local archive | FRAMEWORK_UPDATE_FILE_LOC (absolute, or relative to the project root) |
folder |
Local template folder | FRAMEWORK_UPDATE_FOLDER_LOC (absolute path) |
Archives may contain the template at the top level or inside one folder (as GitHub archives do). The updater refuses to apply a version that is not newer unless -DFRAMEWORK_UPDATE_FORCE=ON is given. Configuration stops after the update: run cmake again to configure normally.
After updating, the framework validates that your root and src/ CMakeLists.txt files meet the minimum required version numbers (IDICMAKE_ROOT_REQ_CML_V, IDICMAKE_SRC_REQ_CML_V, IDICMAKE_BASE_REQ_CML_V). The updater replaces these files, so they are normally already at the required version.
The template ships a Claude Code skill in .claude/skills/idi-cmake/. Claude loads it automatically when working on CMake files, components, dependencies, tests, installs, or framework updates in any project built from this template (or invoke it with /idi-cmake). It is framework-owned and refreshed by the updater, so it always matches the project's framework version. Put project-specific notes in your own CLAUDE.md rather than editing the skill.
When changing the framework, update the skill in the same commit; see .claude/skills/idi-cmake/reference/maintaining.md.
Template is licensed under a modified MIT License. See the TEMPLATE_LICENSE file for more information!