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
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:
- Clone our entire repository (including 500MB of dependencies)
- Build everything from source
- Manually figure out include paths and linking
- 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)
:
- CMake finds
ColumbaEngineConfig.cmake
- Runs the config script (finds OpenGL, loads targets)
ColumbaEngine::ColumbaEngine
target becomes available- 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 existscheck_required_components()
: Validates that requested components are availablePACKAGE_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
andREQUIRED
flags from the parentfind_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
:
- File Collection: CPack gathers all
install()
commands - Dependency Resolution: Checks what system libraries are needed
- Metadata Generation: Creates package info (version, description, etc.)
- Archive Creation: Builds the actual package file
- 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 librarieslibcolumbaengine-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:
- Export/Import Systems - Making your targets available to other projects
- Package Configuration - Handling dependencies and relocatable installations
- Multi-format Packaging - Supporting different package managers and platforms
- Component Architecture - Allowing users to install only what they need
- Version Management - Ensuring compatibility across different versions
- 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!