CMake boilerplate project and self-hosted delivery chain
This repository is a working test project for the reusable cmake/lib CMake
module. It is deliberately small at the application level but exercises the
complete path from a developer's checkout to tested, versioned packages in a
self-hosted Nexus service.
The intended reader already knows C++, Git, and the basics of CMake, but may not have operated the complete build and delivery chain before. The diagrams provide a visual overview of the main flows.
What the boilerplate gives a new project
The project combines a normal CMake source tree with reusable modules from the
cmake/lib Git submodule. A new project based on the template gets a common
build interface and repeatable conventions instead of having to recreate the
platform-specific build logic in every repository.
Application and library examples
The test project contains representative targets that show how a real project can be organized:
hello-worldis a console application.hello-world-qtis a Qt Widgets GUI application.hello-libis a shared library used by the applications.- Catch2 and GoogleTest targets demonstrate unit-test integration through CTest.
The source is intentionally uncomplicated. Its purpose is to prove that the build, test, documentation, coverage, and packaging machinery works for a consumer project.
Reusable CMake capabilities
The cmake/lib module supplies common configuration packages and templates for
- compiler and toolchain setup;
- CMake preset and workflow support;
- compiler/build checks and common macros;
- Qt library discovery or retrieval for the selected target;
- Catch2 and GoogleTest integration;
- Doxygen documentation;
- CTest coverage reporting; and
- CPack packages such as Debian packages, Windows installers, and archives.
The top-level project uses CMake presets as the source of truth. build.py
adds a practical command-line layer around those presets. It can configure,
build, test, package, run, install dependencies, and enter Docker or Wine when
the selected toolchain requires it.
The normal CMake stages are:
They can be run individually while developing or combined through a workflow preset in CI.
Building with several toolchains
The same source tree is used for different target systems. The preset family selects the compiler and target environment; the source project generally does not need platform-specific build scripts.
| Preset family | Host and execution environment | Target |
|---|---|---|
gnu-* |
Linux, native GCC/G++ | Linux x86_64 or aarch64, depending on host |
ga-* |
Linux x86_64 or Docker with GNU Arm tools | Linux aarch64 |
gw-* |
Linux or Docker with MinGW-w64 | Windows x86_64 |
mingw-* |
Windows with MinGW-w64 | Windows x86_64 |
msvc-* |
Windows, or Linux through Wine | Windows x86_64 with MSVC |
Docker supplies a controlled Linux build image containing compilers, CMake, Qt, test tools, and documentation tools. Wine supplies the Windows execution layer when Windows tools or binaries must run on Linux. The nested Docker plus Wine mode makes it possible for a Linux GitLab runner to build with the MSVC toolchain and execute Windows programs in the same isolated job.
For example, the repository documents workflows equivalent to:
# Native Linux GNU build, test, and package
./build.py -mbtp gnu-debug
# Linux x86_64 host producing Linux aarch64 binaries in Docker
./build.py docker -- -mbtp ga-debug
# Linux host producing Windows binaries with MinGW
./build.py -mbtp gw-debug
# Linux host using the Microsoft compiler through Wine
./build.py wine -- -mbtp msvc-debug
# MSVC through Wine inside the Docker build image
./build.py docker -- wine -- -mbtp msvc-debug
The exact available presets depend on the host and installed tools. The
repository's build.py --info command is the reliable way to inspect them.
What happens after a merge request is created
GitLab's main pipeline is entered for a merge-request event or for a protected branch event. A signal can explicitly skip the pipeline or invoke special test and deployment scenarios.
The main pipeline first checks the environment and formatting. It then starts child pipelines for the relevant build variants. In the normal Linux setup these include GNU, GNU-aarch64, GNU-for-Windows/MinGW, MSVC through Wine, and a coverage pipeline.
Each build child pipeline normally performs these stages:
- Check the job environment.
- Configure CMake using the selected preset.
- Build the targets.
- Run CTest, including the Catch2 and GoogleTest tests where supported.
- Install the targets so CPack can collect them.
- Generate packages.
- Deploy packages according to the branch and GitLab environment rules.
The configuration and build jobs share CMake build directories through the GitLab cache. Job artifacts carry binaries and packages to later jobs. Git tags are fetched before configuration, so the project version and package version can be derived consistently even when the runner checkout is shallow.
Coverage is built as a separate child pipeline. Its reports are exchanged via Nexus and then read by the parent pipeline, which publishes the coverage result back to GitLab.
Publishing packages through Nexus
CPack writes generated packages to the configured package directory. The
deployment job passes those files to the shared cmake/lib/bin/upload-nexus.sh
script. The script chooses the Nexus upload API from the file extension:
.debfiles are uploaded as Debian components to the selected Nexus APT hosted repository.- Windows installers and archive files such as
.exe,.zip, and.tar.gzare uploaded to the selected Nexus raw repository and subdirectory.
The destination is supplied by GitLab CI/CD variables. The environment-scoped configuration used by this project is:
| Pipeline context | APT repository | Raw package subdirectory |
|---|---|---|
| Development | develop |
dist/develop |
| Staging | staging |
dist/staging |
| Stable/release | stable |
dist/stable |
The names above describe repository destinations, not credentials. Authentication is provided to CI as protected or masked variables and is intentionally not documented here.
Deployment rules in practical terms
Development deployment is the normal automatic destination for non-release
branches. Staging is associated with the staging environment and can be used
for a release branch or manual deployment. Stable is reserved for the release
branch and is a manual release step. The deploy signal exists to exercise
deployment jobs manually; test exercises cache/artifact behavior; skip
prevents the pipeline.
The exact first-match behavior of GitLab rules matters when changing this
policy. Treat .gitlab/main.gitlab-ci.yml and
.gitlab/build-single.gitlab-ci.yml as the authoritative implementation.
Self-hosted services
The delivery chain uses several services with separate responsibilities:
- GitLab stores source, merge requests, pipeline definitions, artifacts, and pipeline results.
- Self-hosted GitLab runners execute jobs in Docker containers selected by operating-system and architecture tags.
- The self-hosted Docker registry stores the build images used by those jobs.
- MinIO provides cache storage so build jobs on different runner hosts can exchange cached CMake data.
- Nexus stores Docker images, Debian packages, Windows/raw artifacts, and temporary exchanged reports.
Typical developer responsibilities
Developers normally:
- Clone the repository and initialize its submodules.
- Install or select the required local toolchain or choose Docker/Wine.
- Inspect available presets with
./build.py --info <preset>. - Configure and build the target needed during development.
- Run the relevant tests and applications locally when possible.
- Create a merge request with the source, CMake changes, tests, and diagrams required for the change.
GitLab then performs the reproducible matrix of builds, tests, coverage, and packaging. Developers should use the job logs and artifacts to diagnose a failure rather than assuming that a successful native build proves every supported target works.
Useful source-of-truth files
README.mddescribes the project and supported environments.doc/build.mddocumentsbuild.py.-
gitlab-ci/main.gitlab-ci.ymldefines parent pipeline triggers and build variants. -
gitlab-ci/build-single.gitlab-ci.ymldefines the 'configure', 'build', 'test', 'package', and 'deploy' sequence. cmake/lib/bin/upload-nexus.shdefines extension-based Nexus routing.
Together these files describe the complete path shown in the diagrams: source change, reproducible build, validation, package creation, and repository publication.