Skip to content

CMake for Complex Projects (Part 2): Building a C++ Game Engine from Scratch for Desktop and WebAssembly

A practical guide to modern CMake through a real-world C++ game engine project - Deployment & Distribution

cmakecppwebassemblygame-enginebuild-systems

A practical guide to modern CMake through a real-world C++ game engine project - Deployment & Distribution

Welcome back! In Part 1, we built a robust compilation and build system for our ColumbaEngine game engine. We covered project setup, dependency management, cross-platform builds, and testing. The response has been incredible - thank you to everyone who shared feedback, corrections, and suggestions!

Now it’s time for the deployment side - the part where most CMake tutorials stop, but arguably the most important for real projects. How do you make your carefully crafted library actually usable by other developers? How do you create professional installers? How do you handle the complex world of package distribution?

This is where CMake really shows its power (and complexity). We’ll dive deep into installation systems, export mechanisms, package configuration, and distribution strategies that turn your project from “works on my machine” to “works for everyone.”

Note: The feedback from Part 1 has been fantastic, and several CMake experts have pointed out areas where my approach could be improved (modern FILE_SET usage, superbuild patterns, better dependency defaults, etc.). I’ll be publishing an appendix soon addressing these insights - it’s amazing how much you can learn from the community!

Recap: What We Built in Part 1

Our ColumbaEngine now has:

  • 80+ source files compiled into a robust static library
  • Cross-platform support (Windows, Linux, Web)
  • Complex dependency management with vendored libraries
  • Precompiled headers for fast builds
  • Multiple executables and comprehensive testing

The Missing Piece: How do other developers use our engine?

Right now, they’d have to:

  1. Clone our entire repository (including 500MB of dependencies)
  2. Build everything from source
  3. Manually figure out include paths and linking
  4. Hope it works on their system

That’s not professional. Let’s fix it.

Step 9: Installation - Making Your Library Usable

This is where most tutorials stop, but it’s crucial for real projects. How do other developers use your library?

The installation code goes at the end of your main CMakeLists.txt, after all targets have been defined:

# Install the library and its dependencies
install(TARGETS ColumbaEngine SDL2-static glm libglew_static freetype
    EXPORT ColumbaEngineTargets
    ARCHIVE DESTINATION lib        # .a files
    LIBRARY DESTINATION lib        # .so files
    RUNTIME DESTINATION bin        # .exe/.dll files
    INCLUDES DESTINATION include   # Header search path
)

# Install headers
install(DIRECTORY src/Engine/
    DESTINATION include/ColumbaEngine
    FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp"
)

# Create the package config files
install(EXPORT ColumbaEngineTargets
    FILE ColumbaEngineTargets.cmake
    NAMESPACE ColumbaEngine::
    DESTINATION lib/cmake/ColumbaEngine
)

After running make install (or sudo make install for system-wide installation), other projects can use your library like this:

find_package(ColumbaEngine REQUIRED)
target_link_libraries(MyGame PRIVATE ColumbaEngine::ColumbaEngine)

No manual include paths, no hunting for library files - just clean, simple usage.

The Export/Import Mechanism

The installation system is probably the most complex part of CMake. Let’s break down what happens:

1. During Build (Export)

install(TARGETS ColumbaEngine SDL2-static glm
    EXPORT ColumbaEngineTargets  # ← This creates a target "export set"
    ARCHIVE DESTINATION lib
    INCLUDES DESTINATION include
)

This tells CMake: “When installing, remember these targets and their properties.”

2. Target Properties Get Captured CMake captures each target’s:

  • Include directories (target_include_directories)
  • Compile definitions (target_compile_definitions)
  • Link libraries (target_link_libraries)
  • Compile features (target_compile_features)

3. Export File Generation

install(EXPORT ColumbaEngineTargets
    FILE ColumbaEngineTargets.cmake
    NAMESPACE ColumbaEngine::        # ← Creates ColumbaEngine::ColumbaEngine
    DESTINATION lib/cmake/ColumbaEngine
)

This generates ColumbaEngineTargets.cmake containing:

# Generated file - don't edit!
add_library(ColumbaEngine::ColumbaEngine STATIC IMPORTED)
set_target_properties(ColumbaEngine::ColumbaEngine PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include/ColumbaEngine;/usr/local/include"
    INTERFACE_LINK_LIBRARIES "ColumbaEngine::SDL2-static;ColumbaEngine::glm;OpenGL::GL"
    # ... more properties
)

4. Package Configuration The ColumbaEngineConfig.cmake.in template becomes ColumbaEngineConfig.cmake:

# This runs when someone does find_package(ColumbaEngine)
find_dependency(OpenGL REQUIRED)  # Ensure dependencies are found first
include("${CMAKE_CURRENT_LIST_DIR}/ColumbaEngineTargets.cmake")  # Import our targets

5. User Experience When another project does find_package(ColumbaEngine):

  1. CMake finds ColumbaEngineConfig.cmake
  2. Runs the config script (finds OpenGL, loads targets)
  3. ColumbaEngine::ColumbaEngine target becomes available
  4. Users link to it and get all the transitive dependencies automatically

Why the Namespace?

The NAMESPACE ColumbaEngine:: is crucial:

  • Prevents conflicts: Multiple libraries might have a “Engine” target
  • Clear API: ColumbaEngine::ColumbaEngine is unambiguous
  • Import detection: Can check if(TARGET ColumbaEngine::ColumbaEngine)

Step 10: Package Configuration

Create cmake/ColumbaEngineConfig.cmake.in:

@PACKAGE_INIT@

include(CMakeFindDependencyMacro)

# Find required system dependencies that consumers need
if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
    find_dependency(OpenGL REQUIRED)
    find_dependency(Threads REQUIRED)

    # Find X11 libraries for SDL2
    find_package(X11 QUIET)
    if(X11_FOUND)
        # Create imported targets for X11 if they don't exist
        if(NOT TARGET X11::X11)
            add_library(X11::X11 UNKNOWN IMPORTED)
            set_target_properties(X11::X11 PROPERTIES
                IMPORTED_LOCATION "${X11_X11_LIB}"
                INTERFACE_INCLUDE_DIRECTORIES "${X11_X11_INCLUDE_PATH}"
            )
        endif()

        if(NOT TARGET X11::Xext AND X11_Xext_FOUND)
            add_library(X11::Xext UNKNOWN IMPORTED)
            set_target_properties(X11::Xext PROPERTIES
                IMPORTED_LOCATION "${X11_Xext_LIB}"
                INTERFACE_INCLUDE_DIRECTORIES "${X11_X11_INCLUDE_PATH}"
            )
        endif()
    endif()

    # Find other system dependencies that SDL2 might need
    find_package(PkgConfig QUIET)
    if(PKG_CONFIG_FOUND)
        pkg_check_modules(ALSA QUIET alsa)
        pkg_check_modules(PULSE QUIET libpulse-simple)
    endif()
endif()

# Include our targets
include("${CMAKE_CURRENT_LIST_DIR}/ColumbaEngineTargets.cmake")

# Provide variables for backward compatibility
set(ColumbaEngine_LIBRARIES ColumbaEngine::ColumbaEngine)

# Set include directories based on the imported target
get_target_property(ColumbaEngine_INCLUDE_DIRS ColumbaEngine::ColumbaEngine INTERFACE_INCLUDE_DIRECTORIES)

# Create an alias for easier consumption if it doesn't exist
if(NOT TARGET ColumbaEngine)
    add_library(ColumbaEngine ALIAS ColumbaEngine::ColumbaEngine)
endif()

check_required_components(ColumbaEngine)

This gets processed into ColumbaEngineConfig.cmake and installed. It ensures that when someone does find_package(ColumbaEngine), all dependencies are found automatically.

The Package Configuration Template System

The .cmake.in template system is CMake’s way of generating configuration files with proper path handling. Let’s see what each part does:

@PACKAGE_INIT@

This special macro expands to several helper functions and variables:

  • set_and_check(): Sets a variable and verifies the path exists
  • check_required_components(): Validates that requested components are available
  • PACKAGE_PREFIX_DIR: The installation prefix, relocated if needed

Why relocatable? If you install to /usr/local but someone copies the installation to /opt/columbaengine, the paths need to update automatically.

include(CMakeFindDependencyMacro)
find_dependency(OpenGL REQUIRED)

find_dependency is like find_package, but:

  • Forwards the QUIET and REQUIRED flags from the parent find_package call
  • Properly handles the dependency chain for error reporting
  • Works correctly with package components

Example: If someone calls find_package(ColumbaEngine REQUIRED QUIET), the OpenGL search will also be QUIET and REQUIRED.

include("${CMAKE_CURRENT_LIST_DIR}/ColumbaEngineTargets.cmake")

This loads the exported targets. The ${CMAKE_CURRENT_LIST_DIR} ensures we load from the same directory as the config file, making the installation relocatable.

check_required_components(ColumbaEngine)

This validates that all requested components are available. Our engine doesn’t have components yet, but you might add them later:

find_package(ColumbaEngine REQUIRED COMPONENTS Renderer Audio Networking)

Advanced: Component-Based Installation

Here’s how you could extend the engine with components:

# Install different parts as components
install(TARGETS ColumbaEngine_Core
    EXPORT ColumbaEngineTargets
    COMPONENT ColumbaEngine_Core
    ARCHIVE DESTINATION lib
)

install(TARGETS ColumbaEngine_Renderer
    EXPORT ColumbaEngineTargets
    COMPONENT ColumbaEngine_Renderer
    ARCHIVE DESTINATION lib
)

# In ColumbaEngineConfig.cmake.in
set(ColumbaEngine_Core_FOUND TRUE)

find_dependency(OpenGL)
if(OpenGL_FOUND)
    set(ColumbaEngine_Renderer_FOUND TRUE)
endif()

include("${CMAKE_CURRENT_LIST_DIR}/ColumbaEngineTargets.cmake")
check_required_components(ColumbaEngine)

Users could then request only the parts they need:

find_package(ColumbaEngine REQUIRED COMPONENTS Core)  # No renderer

Step 11: Package Distribution with CPack

Want to create installers? CPack has you covered. Currently, ColumbaEngine has basic CPack configuration:

# CPack configuration for creating installers
set(CPACK_PACKAGE_NAME "ColumbaEngine")
set(CPACK_PACKAGE_VENDOR "PigeonCodeur")
set(CPACK_PACKAGE_VERSION_MAJOR "1")
set(CPACK_PACKAGE_VERSION_MINOR "0")
set(CPACK_PACKAGE_VERSION_PATCH "0")
set(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "ColumbaEngine - A modern C++ game engine")
set(CPACK_PACKAGE_CONTACT "[email protected]")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/Gallasko/ColumbaEngine")

# Detect available generators
set(CPACK_GENERATOR "TGZ")

# Check for DEB tools
find_program(DPKG_CMD dpkg)
if(DPKG_CMD)
    list(APPEND CPACK_GENERATOR "DEB")
endif()

# Check for RPM tools
find_program(RPMBUILD_CMD rpmbuild)
if(RPMBUILD_CMD)
    list(APPEND CPACK_GENERATOR "RPM")
endif()

include(CPack)

Run cpack and get basic installers for multiple package managers. The CPack configuration is still evolving and will be enhanced with more sophisticated packaging options in future updates.

CPack - From Source to Distribution

CPack is CMake’s packaging system that transforms your built project into distributable packages. Let’s understand how it works:

1. Package Discovery and Generator Selection

# Detect available packaging tools on the system
set(CPACK_GENERATOR "TGZ")  # Always available

find_program(DPKG_CMD dpkg)
if(DPKG_CMD)
    list(APPEND CPACK_GENERATOR "DEB")
endif()

find_program(RPMBUILD_CMD rpmbuild)
if(RPMBUILD_CMD)
    list(APPEND CPACK_GENERATOR "RPM")
endif()

This auto-detection means:

  • Linux systems with dpkg: Get .deb packages for Ubuntu/Debian
  • Linux systems with rpmbuild: Get .rpm packages for RHEL/Fedora
  • All systems: Get .tar.gz archives as fallback

2. Dependency Declaration

set(CPACK_DEBIAN_PACKAGE_DEPENDS "libgl1-mesa-dev, libfreetype6-dev")
set(CPACK_RPM_PACKAGE_REQUIRES "mesa-libGL-devel, freetype-devel")

This tells the package manager what system libraries your package needs. When users install your .deb, it automatically installs OpenGL and FreeType development packages.

3. Package Structure Control

# What gets packaged?
install(TARGETS ColumbaEngine COMPONENT Runtime)
install(DIRECTORY include/ DESTINATION include COMPONENT Development)
install(DIRECTORY examples/ DESTINATION share/ColumbaEngine/examples COMPONENT Examples)

# Create separate packages for different use cases
set(CPACK_COMPONENTS_ALL Runtime Development Examples)
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION "ColumbaEngine runtime libraries")
set(CPACK_COMPONENT_DEVELOPMENT_DESCRIPTION "Headers and development files")

4. The Package Build Process When you run make package:

  1. File Collection: CPack gathers all install() commands
  2. Dependency Resolution: Checks what system libraries are needed
  3. Metadata Generation: Creates package info (version, description, etc.)
  4. Archive Creation: Builds the actual package file
  5. Validation: Runs package-specific tests

Real-World Package Examples

Debian Package Contents:

columbaengine_1.0.0_amd64.deb
├── DEBIAN/
│   ├── control          # Package metadata
│   ├── postinst        # Post-installation script
│   └── prerm           # Pre-removal script
├── usr/
│   ├── lib/
│   │   ├── libColumbaEngine.a
│   │   └── cmake/ColumbaEngine/
│   └── include/ColumbaEngine/

RPM Package Structure:

columbaengine-1.0.0-1.x86_64.rpm
├── usr/lib64/libColumbaEngine.a
├── usr/include/ColumbaEngine/
└── usr/lib64/cmake/ColumbaEngine/

Advanced CPack: Custom Package Scripts

You can add custom installation behavior:

# Custom post-installation script
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/postinst;${CMAKE_CURRENT_SOURCE_DIR}/cmake/prerm")

Example postinst script:

#!/bin/bash
# Update library cache after installation
ldconfig
# Create symlinks for backward compatibility
ln -sf /usr/lib/libColumbaEngine.a /usr/lib/libGameEngine.a

Advanced Distribution Strategies

1. Multi-Package Strategy

For complex libraries, consider splitting into multiple packages:

# Runtime package - just the libraries
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION "ColumbaEngine runtime libraries")
set(CPACK_COMPONENT_RUNTIME_REQUIRED TRUE)

# Development package - headers and CMake files
set(CPACK_COMPONENT_DEVELOPMENT_DESCRIPTION "Development files for ColumbaEngine")
set(CPACK_COMPONENT_DEVELOPMENT_DEPENDS Runtime)

# Documentation package - optional docs and examples
set(CPACK_COMPONENT_DOCUMENTATION_DESCRIPTION "Documentation and examples")
set(CPACK_COMPONENT_DOCUMENTATION_DEPENDS Development)

# Configure Debian packages
set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_DEBIAN_RUNTIME_PACKAGE_NAME "libcolumbaengine1")
set(CPACK_DEBIAN_DEVELOPMENT_PACKAGE_NAME "libcolumbaengine-dev")
set(CPACK_DEBIAN_DOCUMENTATION_PACKAGE_NAME "libcolumbaengine-doc")

This creates three separate .deb files:

  • libcolumbaengine1.deb - Runtime libraries
  • libcolumbaengine-dev.deb - Development headers (depends on runtime)
  • libcolumbaengine-doc.deb - Documentation (optional)

2. Cross-Platform Packaging

# Platform-specific package settings
if(WIN32)
    set(CPACK_GENERATOR "ZIP;NSIS")
    set(CPACK_NSIS_DISPLAY_NAME "ColumbaEngine Game Engine")
    set(CPACK_NSIS_HELP_LINK "https://github.com/Gallasko/ColumbaEngine")
    set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/Gallasko/ColumbaEngine")
    set(CPACK_NSIS_MODIFY_PATH ON)
elseif(APPLE)
    set(CPACK_GENERATOR "TGZ;DragNDrop")
    set(CPACK_DMG_FORMAT "UDBZ")
    set(CPACK_DMG_VOLUME_NAME "ColumbaEngine")
elseif(UNIX)
    set(CPACK_GENERATOR "TGZ;DEB;RPM")
endif()

3. Version Management and Compatibility

# Semantic versioning
set(COLUMBAENGINE_VERSION_MAJOR 1)
set(COLUMBAENGINE_VERSION_MINOR 2)
set(COLUMBAENGINE_VERSION_PATCH 3)
set(COLUMBAENGINE_VERSION "${COLUMBAENGINE_VERSION_MAJOR}.${COLUMBAENGINE_VERSION_MINOR}.${COLUMBAENGINE_VERSION_PATCH}")

# ABI compatibility
set(COLUMBAENGINE_SOVERSION ${COLUMBAENGINE_VERSION_MAJOR})

# Set target properties for shared libraries
if(BUILD_SHARED_LIBS)
    set_target_properties(ColumbaEngine PROPERTIES
        VERSION ${COLUMBAENGINE_VERSION}
        SOVERSION ${COLUMBAENGINE_SOVERSION}
    )
endif()

# Package version file
write_basic_package_version_file(
    "${CMAKE_CURRENT_BINARY_DIR}/ColumbaEngineConfigVersion.cmake"
    VERSION ${COLUMBAENGINE_VERSION}
    COMPATIBILITY SameMajorVersion  # 1.x.x is compatible with 1.y.z
)

Real-World Distribution Workflows

1. Automated Package Building with CI

# .github/workflows/release.yml
name: Release
on:
  release:
    types: [published]

jobs:
  build-packages:
    strategy:
      matrix:
        os: [ubuntu-20.04, ubuntu-22.04, windows-latest]

    runs-on: ${{ matrix.os }}

    steps:
    - uses: actions/checkout@v3

    - name: Install dependencies (Linux)
      if: runner.os == 'Linux'
      run: |
        sudo apt-get update
        sudo apt-get install -y libgl1-mesa-dev libfreetype6-dev

    - name: Configure CMake
      run: |
        mkdir build
        cd build
        cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=OFF

    - name: Build
      run: cmake --build build --parallel

    - name: Create packages
      run: |
        cd build
        cpack

    - name: Upload packages
      uses: actions/upload-release-asset@v1
      with:
        upload_url: ${{ github.event.release.upload_url }}
        asset_path: build/*.deb
        asset_name: columbaengine-${{ github.event.release.tag_name }}-${{ matrix.os }}.deb
        asset_content_type: application/vnd.debian.binary-package

2. Package Repository Integration

For professional distribution, integrate with package repositories:

Debian/Ubuntu PPA:

# Configure for PPA upload
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Pigeon Codeur [email protected]>")
set(CPACK_DEBIAN_PACKAGE_SECTION "libdevel")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/Gallasko/ColumbaEngine")

# Source package for PPA
set(CPACK_SOURCE_GENERATOR "TGZ")
set(CPACK_SOURCE_IGNORE_FILES "/build/;/.git/;/.github/;/import/")

3. (Future Work:) Documentation Integration

Include documentation in your packages:

# Find Doxygen
find_package(Doxygen)
if(DOXYGEN_FOUND)
    set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/docs)
    set(DOXYGEN_PROJECT_NAME "ColumbaEngine")
    set(DOXYGEN_PROJECT_VERSION ${PROJECT_VERSION})

    doxygen_add_docs(docs
        ${CMAKE_SOURCE_DIR}/src/Engine
        COMMENT "Generate API documentation"
    )

    # Install documentation
    install(DIRECTORY ${CMAKE_BINARY_DIR}/docs/html/
        DESTINATION share/doc/columbaengine
        COMPONENT Documentation
        OPTIONAL
    )
endif()

# Man pages for command-line tools
install(FILES
    ${CMAKE_SOURCE_DIR}/docs/columbaengine.1
    DESTINATION share/man/man1
    COMPONENT Documentation
    OPTIONAL
)

Testing Your Distribution

1. Installation Testing

Create a separate test project to verify your package works:

# test-installation/CMakeLists.txt
cmake_minimum_required(VERSION 3.18)
project(TestColumbaEngine)

find_package(ColumbaEngine REQUIRED)

add_executable(test_app main.cpp)
target_link_libraries(test_app PRIVATE ColumbaEngine::ColumbaEngine)
// test-installation/main.cpp
#include <ColumbaEngine/engine.h>

int main() {
    ColumbaEngine::Engine engine;
    if (engine.initialize()) {
        std::cout << "ColumbaEngine loaded successfully!" << std::endl;
        return 0;
    }
    return 1;
}

Testing script:

#!/bin/bash
# test-installation.sh

# Install the package
sudo dpkg -i columbaengine-dev_1.0.0_amd64.deb

# Test that find_package works
mkdir test-build && cd test-build
cmake ../test-installation
make

# Run the test
./test_app

# Clean up
cd ..
rm -rf test-build
sudo dpkg -r columbaengine-dev

2. Cross-Platform Validation

For consistent testing across different platforms and distributions, you can use Docker containers or virtual machines to validate that your packages install and work correctly on clean systems. This helps catch missing dependencies or platform-specific issues before your users encounter them.

The Complete User Experience

After implementing all these deployment concepts, here’s what we achieved:

For Package Maintainers:

# Build and create all packages
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j8
cpack

# Results in:
# columbaengine-1.0.0-Linux.tar.gz
# (and .deb/.rpm if tools are available)

For System Installation:

# Install to system directories
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j8
sudo make install

# Now available system-wide
find_package(ColumbaEngine REQUIRED)  # Works in any CMake project

For End Users (Ubuntu/Debian):

# Install from package
sudo dpkg -i columbaengine-dev_1.0.0_amd64.deb

# Or from PPA
sudo add-apt-repository ppa:yourusername/columbaengine
sudo apt update
sudo apt install libcolumbaengine-dev

For Developers:

# In their CMakeLists.txt
find_package(ColumbaEngine 1.0 REQUIRED)
add_executable(MyGame src/main.cpp)
target_link_libraries(MyGame PRIVATE ColumbaEngine::ColumbaEngine)

# That's it! No manual configuration needed.

(Future Work): For Package Managers

# These integrations are planned for future releases

# Conan
conan install columbaengine/1.0.0@

# vcpkg
vcpkg install columbaengine

# CPM
CPMAddPackage("gh:Gallasko/[email protected]")

Lessons Learned and Future Improvements

What Worked Well

  • Target-based approach: Made the export/import system much cleaner
  • Component separation: Allows users to install only what they need
  • Cross-platform packaging: Same CMake code generates packages for multiple platforms
  • Automated testing: Catches installation issues before users see them

Areas for Improvement

Based on the excellent feedback from Part 1, there are several areas I want to improve:

  • Modern CMake patterns: Exploring FILE_SET instead of target_include_directories
  • Superbuild approaches: For better dependency management and packaging
  • Dependency defaults: Making find_package the default with vendoring as fallback
  • Generator expression usage: Using them more judiciously

I’m working on an appendix that will address these improvements in detail - the CMake community feedback has been invaluable!

Conclusion

Building a professional distribution system with CMake involves several sophisticated concepts:

  1. Export/Import Systems - Making your targets available to other projects
  2. Package Configuration - Handling dependencies and relocatable installations
  3. Multi-format Packaging - Supporting different package managers and platforms
  4. Component Architecture - Allowing users to install only what they need
  5. Version Management - Ensuring compatibility across different versions
  6. Testing Infrastructure - Validating that your packages actually work

The deployment system we’ve built transforms ColumbaEngine from a complex, hard-to-use source project into a professional library that integrates seamlessly into any CMake-based project.

Remember: Distribution is not an afterthought - it’s a core part of your project’s success. A library that’s hard to install and use won’t get adopted, no matter how good the code is.

Your deployment system is your project’s handshake with the world. Make it count.


The complete source code for ColumbaEngine with all the CMake patterns from this series is available on GitHub. These patterns scale from small libraries to large, complex applications.

Coming Next: An appendix addressing the excellent feedback from the CMake community, covering modern patterns like FILE_SET, superbuild approaches, and improved dependency management strategies.

What distribution challenges have you faced? Share your experiences in the comments below!