Version 0.1.0 · C++20 · CMake 3.26
A sandbox repository for exploring The Pitchfork Layout (PFL) with Separate Header Placement for a modern C++20 / CMake 3.26 project.
The primary goal is to prove out the CMake patterns needed before applying them to a large HPC C++ framework refactoring:
- Hierarchical library targets (
CMakeSandbox::geo::shapes, …) - Proper
PUBLIC/PRIVATE/INTERFACEusage - Install +
find_package()/FetchContent()support - Generator-expression-driven compiler and build-type flags
- CMake presets and workflows
clang-tidyintegrationVersion.hppgenerated from a CMake template
CMakeSandbox/
├── .gitignore
├── .clang-tidy # clang-tidy configuration
├── CMakeLists.txt # Top-level project (CMake 3.26, C++20)
├── CMakePresets.json # Configure / build / test / workflow presets
├── LICENSE.md
├── README.md
│
├── cmake/
│ ├── CMakeSandboxConfig.cmake.in # Package config template
│ ├── CompilerOptions.cmake # cmsb_set_compiler_options() helper
│ └── Version.hpp.in # Configured into build tree at configure time
│
├── extern/ # External dependencies (git submodules)
│ └── .gitkeep
│
├── include/ # Public headers – separate from sources (PFL)
│ └── CMakeSandbox/
│ ├── geo/shapes/ # Geometric shapes (runtime + compile-time polymorphism)
│ │ ├── concepts.hpp
│ │ ├── shape.hpp
│ │ ├── circle.hpp
│ │ ├── rectangle.hpp
│ │ └── triangle.hpp
│ ├── bio/animals/ # Animals (runtime polymorphism / virtual dispatch)
│ │ ├── animal.hpp
│ │ ├── dog.hpp
│ │ ├── cat.hpp
│ │ └── bird.hpp
│ └── math/ # Header-only math utilities (templates + concepts)
│ ├── vec2d.hpp
│ └── algorithms.hpp
│
├── src/
│ └── CMakeSandbox/
│ ├── CMakeLists.txt # Umbrella INTERFACE targets (sandbox_geo, sandbox_bio)
│ ├── geo/shapes/
│ │ ├── CMakeLists.txt
│ │ ├── circle.cpp
│ │ ├── rectangle.cpp
│ │ └── triangle.cpp
│ ├── bio/animals/
│ │ ├── CMakeLists.txt
│ │ ├── animal.cpp
│ │ ├── dog.cpp
│ │ ├── cat.cpp
│ │ └── bird.cpp
│ └── math/
│ └── CMakeLists.txt # INTERFACE target – no .cpp files
│
├── apps/
│ ├── CMakeLists.txt
│ ├── shapes_demo.cpp
│ ├── animals_demo.cpp
│ └── math_demo.cpp
│
└── tests/
├── CMakeLists.txt
├── test_shapes.cpp
├── test_animals.cpp
├── test_math.cpp
└── test_version.cpp
| CMake target | Type | What it provides |
|---|---|---|
CMakeSandbox::geo::shapes |
SHARED lib | Circle, Rectangle, Triangle + concepts |
CMakeSandbox::geo |
INTERFACE | Alias grouping all geo targets |
CMakeSandbox::bio::animals |
SHARED lib | Dog, Cat, Bird |
CMakeSandbox::bio |
INTERFACE | Alias grouping all bio targets |
CMakeSandbox::math |
INTERFACE | Vec2D<T>, Numeric concept, algorithms |
CMakeSandbox::version |
INTERFACE | Generated version.hpp |
- CMake 3.26 (minimum required; see CMake version note below)
- A C++20 compiler (GCC 13+ or Clang 16+)
- Ninja (optional but used by the presets)
# GCC debug build
cmake --preset gcc-debug
cmake --build --preset gcc-debug
# Clang release build
cmake --preset clang-release
cmake --build --preset clang-releasectest --preset gcc-debugcmake --workflow --preset cicmake --build --preset gcc-release
cmake --install build/gcc-release --prefix /opt/CMakeSandboxcmake --preset clang-tidy
cmake --build --preset clang-tidyfind_package(CMakeSandbox REQUIRED)
target_link_libraries(my_app PRIVATE CMakeSandbox::geo::shapes)FetchContent lets you pull CMakeSandbox directly from GitHub without a prior
install step. This section shows a complete, production-ready setup.
cmake_minimum_required(VERSION 3.26)
project(MyApp)
include(FetchContent)
FetchContent_Declare(CMakeSandbox
GIT_REPOSITORY https://github.com/Pitt0s/CMakeSandbox.git
GIT_TAG v0.1.0 # pin to a release tag, not "main"
GIT_SHALLOW TRUE # fetch only the tagged commit
)
FetchContent_MakeAvailable(CMakeSandbox)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE CMakeSandbox::geo::shapes)When CMakeSandbox is consumed as a library you almost certainly do not want
its demo apps or its test suite to be compiled inside your project. Disable
them via cache variables before calling FetchContent_MakeAvailable:
set(CMSB_BUILD_APPS OFF CACHE BOOL "" FORCE)
set(CMSB_BUILD_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(CMakeSandbox)CMakeSandbox honours the standard BUILD_SHARED_LIBS CMake variable.
# CMakeSandbox defaults BUILD_SHARED_LIBS to ON; override here for static
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(CMakeSandbox)| Target | Type | What it provides |
|---|---|---|
CMakeSandbox::geo::shapes |
SHARED lib | Circle, Rectangle, Triangle + concepts |
CMakeSandbox::geo |
INTERFACE | Umbrella – links all geo targets |
CMakeSandbox::bio::animals |
SHARED lib | Dog, Cat, Bird |
CMakeSandbox::bio |
INTERFACE | Umbrella – links all bio targets |
CMakeSandbox::math |
INTERFACE | Vec2D<T>, Numeric concept, algorithms |
CMakeSandbox::version |
INTERFACE | Generated version.hpp |
cmake_minimum_required(VERSION 3.26)
project(MyConsumerApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# ── Fetch CMakeSandbox ────────────────────────────────────────────────────────
set(CMSB_BUILD_APPS OFF CACHE BOOL "" FORCE)
set(CMSB_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS ON CACHE BOOL "" FORCE) # or OFF for static
FetchContent_Declare(CMakeSandbox
GIT_REPOSITORY https://github.com/Pitt0s/CMakeSandbox.git
GIT_TAG v0.1.0
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(CMakeSandbox)
# ── Your application ──────────────────────────────────────────────────────────
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE
CMakeSandbox::geo::shapes # geometric primitives
CMakeSandbox::math # header-only math utilities
)#include "CMakeSandbox/geo/shapes/circle.hpp"
#include "CMakeSandbox/geo/shapes/shape.hpp"
std::unique_ptr<CMakeSandbox::geo::shapes::Shape> s =
std::make_unique<CMakeSandbox::geo::shapes::Circle>(5.0);
std::cout << s->name() << " area = " << s->area() << '\n';#include "CMakeSandbox/geo/shapes/concepts.hpp"
#include "CMakeSandbox/geo/shapes/circle.hpp"
template <CMakeSandbox::geo::shapes::Shape2D S>
void describe(const S& shape) {
std::cout << shape.name() << " area = " << shape.area() << '\n';
}
describe(CMakeSandbox::geo::shapes::Circle{3.0}); // no virtual call#include "CMakeSandbox/math/vec2d.hpp"
CMakeSandbox::math::Vec2Dd a{3.0, 0.0}, b{0.0, 4.0};
std::cout << "dot = " << a.dot(b) << '\n'; // 0
std::cout << "len = " << a.length() << '\n'; // 3The patterns proven in this sandbox are intentionally simple so they can be lifted into a large, pre-existing C++ project that is being refactored to PFL. The key insight is incremental migration: you do not need to move everything at once.
Create the include/, src/, apps/, tests/ and cmake/ directories next
to the existing source tree. Keep the old build system working during the
migration so the project stays green on CI.
myproject/
├── old_src/ ← untouched for now
├── include/ ← new PFL public headers go here
├── src/ ← new PFL compiled sources go here
└── cmake/ ← CompilerOptions.cmake, config template, …
Choose the smallest, most self-contained subsystem (a utility library, a math
module, etc.) and move its headers into include/<Project>/<subsystem>/ and
its .cpp files into src/<Project>/<subsystem>/. Write a new
CMakeLists.txt for it following this sandbox's pattern:
add_library(myproject_math)
add_library(MyProject::math ALIAS myproject_math)
# Sources and public headers declared together via FILE_SET (CMake ≥ 3.23).
# BASE_DIRS defines the include root; FILE_SET automatically populates
# INTERFACE_INCLUDE_DIRECTORIES – no target_include_directories() call needed.
target_sources(myproject_math
PRIVATE
vec2d.cpp
PUBLIC
FILE_SET myproject_math_headers
TYPE HEADERS
BASE_DIRS "${PROJECT_SOURCE_DIR}/include"
FILES
"${PROJECT_SOURCE_DIR}/include/MyProject/math/vec2d.hpp"
)
target_link_libraries(myproject_math
PRIVATE MyProject::compiler_flags
)Even when only one subsystem has been migrated, create the umbrella
MyProject::math, MyProject::geo, … targets. Consumers in the old part of
the codebase can start linking against MyProject::math without knowing the
internal target names, making future splits invisible to them.
Update #include directives one translation unit at a time to use the new
include/<Project>/… paths. A CI step that compiles both the old and new
targets in parallel catches regressions early.
Repeat Step 2–4 for each subsystem. Before publishing a release, verify
downstream integration with FetchContent_Declare pointing SOURCE_DIR at
your local checkout. This exercises the full FetchContent path without
needing a prior cmake --install:
cmake_minimum_required(VERSION 3.26)
project(MyConsumerApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(FetchContent)
# Disable the library's own apps and tests when consumed as a dependency.
set(CMSB_BUILD_APPS OFF CACHE BOOL "" FORCE)
set(CMSB_BUILD_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(MyProject
SOURCE_DIR /path/to/myproject # local checkout; replace with GIT_REPOSITORY for CI
)
FetchContent_MakeAvailable(MyProject)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE MyProject::math)The sandbox ships a ready-made consumer in tests/smoke/ that demonstrates
both the FetchContent SOURCE_DIR and find_package patterns and is wired
into CI via the smoke-gcc-debug workflow preset (see
Validate the install tree below).
-
Keep one
cmake/CompilerOptions.cmakethat is the single source of truth for warning and optimisation flags. Never sprinkletarget_compile_optionscalls across leafCMakeLists.txtfiles. -
Use
PRIVATElinkage for implementation details. If a libraryAusesBoost.Filesysteminternally but exposes none of it in its public API, link BoostPRIVATE. This prevents accidental transitive linkage in downstream targets. -
Generate a
Version.hppearly (seecmake/Version.hpp.inin this sandbox). It gives every component a single, CMake-driven source of version truth and is trivial to extend with build metadata. -
Add
CMakePresets.jsonfrom the start. Presets capture the full configure/build/test matrix (compilers, build types, static vs. shared) and make CI pipelines reproducible on developer workstations. The CI workflow in.github/workflows/ci.ymlruns every preset viacmake --workflowso the same commands work locally and on GitHub Actions. -
Pin external dependencies in
FetchContent_Declareto a commit SHA or release tag, never tomain/master, to keep builds reproducible across machines and over time. -
Validate the install tree with the
tests/smoke/consumer project included in this sandbox. It can be driven in two ways:# 1. find_package mode – validates all install() rules are correct cmake --workflow --preset smoke-gcc-debug # 2. FetchContent SOURCE_DIR mode (manual, no prior install needed) cmake -S tests/smoke -B build/smoke \ -DSMOKE_USE_FETCHCONTENT=ON \ -DCMSB_SOURCE_DIR=$(pwd) cmake --build build/smoke ctest --test-dir build/smoke --output-on-failure
The
smoke-gcc-debugworkflow preset runs configure → build → install → smoke tests in the correct order so bothsmoke_find_packageandsmoke_fetchcontentCTest tests are exercised. This catches missinginstall()rules, brokenCMakeSandboxConfig.cmakealiases, or missing headers far earlier than a downstream consumer would.
The minimum required version is 3.26. This is the lowest version that provides all features used unconditionally in this project:
| Feature | Minimum CMake version |
|---|---|
cmake_minimum_required / project() basics |
2.6 |
GNUInstallDirs, CMakePackageConfigHelpers |
3.0 |
Generator expressions ($<CXX_COMPILER_ID:…>) |
3.0 |
write_basic_package_version_file |
3.14 |
| CMakePresets.json schema version 6 | 3.25 |
CMAKE_CXX_SCAN_FOR_MODULES (optional, guarded) |
3.28 |
CMAKE_CXX_SCAN_FOR_MODULES is set only when running CMake 3.28 or later
(guarded by a CMAKE_VERSION VERSION_GREATER_EQUAL "3.28" check). On CMake
3.26 and 3.27 this variable does not exist; named-module scanning is simply
not enabled by those versions, so no guard is needed on the consumer side.
This CMake structure is fully compatible with Spack and EasyBuild:
- The install layout follows
CMAKE_INSTALL_PREFIX/{bin,lib,include,lib/cmake/CMakeSandbox}. - A
CMakeSandboxConfig.cmakeis installed, so Spack packages can usecmake_args = ["-DCMAKE_INSTALL_PREFIX=..."]and downstream packages usefind_package(CMakeSandbox). - ReFrame and JUBE test descriptions can invoke
ctest --preset <name>or individual test executables directly.