Skip to content

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-world is a console application.
  • hello-world-qt is a Qt Widgets GUI application.
  • hello-lib is 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:

configure -> build -> test -> install -> package

They can be run individually while developing or combined through a workflow preset in CI.

uml diagram

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.

uml diagram

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:

  1. Check the job environment.
  2. Configure CMake using the selected preset.
  3. Build the targets.
  4. Run CTest, including the Catch2 and GoogleTest tests where supported.
  5. Install the targets so CPack can collect them.
  6. Generate packages.
  7. 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.

uml diagram

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:

  • .deb files are uploaded as Debian components to the selected Nexus APT hosted repository.
  • Windows installers and archive files such as .exe, .zip, and .tar.gz are 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.

uml diagram

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.

uml diagram

Typical developer responsibilities

Developers normally:

  1. Clone the repository and initialize its submodules.
  2. Install or select the required local toolchain or choose Docker/Wine.
  3. Inspect available presets with ./build.py --info <preset>.
  4. Configure and build the target needed during development.
  5. Run the relevant tests and applications locally when possible.
  6. 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

Together these files describe the complete path shown in the diagrams: source change, reproducible build, validation, package creation, and repository publication.