This hands-on tutorial walks seasoned C++ developers through using Conan 2.0 with CMake to manage dependencies and build a real-world multi-library project. We’ll focus on a Windows development environment (Visual Studio toolchain) using CMake’s Ninja generator, while highlighting how to stay cross-platform. Along the way, we’ll compare Conan to NuGet, implement a step-by-step project with increasing complexity, and delve into advanced debugging, dependency graph analysis, performance optimizations, and setting up GitHub Actions for CI/CD (including caching Conan packages).
C++ projects often grow into complex systems with multiple libraries and external dependencies. Manually managing library builds and ensuring the correct versions across platforms can be error-prone. Modern tools like Conan (a C/C++ package manager) and CMake can significantly streamline this process. In this tutorial, you will:
- Understand why Conan is advantageous for C++ dependency management (versus NuGet and others).
- Set up a cross-platform C++ development environment on Windows using Visual Studio’s compiler with CMake’s Ninja build system.
- Build a multi-library project step-by-step: starting from a simple library and scaling up to multiple libraries and an executable, all managed with Conan and built with CMake.
- Learn to use Conan’s tools for dependency graph analysis and debugging to resolve conflicts or issues.
- Apply performance optimizations in builds (like faster build tools and binary caching).
- Configure GitHub Actions CI/CD to automate building and dependency management, including caching of Conan packages for faster builds.
How to Use This Tutorial: Each section contains explanations of core concepts with examples, followed by hands-on steps you can follow. Try to work through the “Follow Along” and “Exercise” subsections actively – these will reinforce your understanding by doing. All code snippets are provided for reference. Let’s begin with a quick overview of Conan and how it compares to NuGet.
Before diving in, let’s compare Conan with NuGet in the context of C++ development. Both are package managers, but they differ in design and use cases:
-
Cross-Platform vs. Platform-Specific: Conan was designed for cross-platform C/C++ projects. It runs on Windows, Linux, macOS, and others, and can handle platform-specific binaries and cross-compilation seamlessly (Conan 2.0: C and C++ Open Source Package Manager) (Conan 2.0: C and C++ Open Source Package Manager). NuGet is primarily used in the .NET ecosystem and Visual C++ projects; it’s tightly integrated with Visual Studio on Windows. NuGet can manage C++ libraries (as .nupkg packages), but it’s largely Windows/MSBuild-centric and not as natural on Linux or macOS. In practice, Conan’s multi-platform support and consistent workflow across OSes is a major advantage (Maintaining multiple projects which consume conan packages - Stack Overflow).
-
Ecosystem and Packages: Conan provides access to ConanCenter, a central repository with thousands of popular C and C++ libraries pre-packaged (How to Create a Modern C Project with CMake and Conan – Abstract Expression). ConanCenter’s packages often include precompiled binaries for multiple OSes and compiler versions, or recipes to build from source if needed (How to Create a Modern C Project with CMake and Conan – Abstract Expression). NuGet’s C++ package ecosystem is much smaller; as one developer notes, “NuGet C++ doesn't have a lot of packages” compared to alternatives (The state of C++ package management: The big three : r/cpp). Conan’s broad library availability and support for building from source means you’re more likely to find the C++ library you need and have it work out-of-the-box on your platform (Maintaining multiple projects which consume conan packages - Stack Overflow).
-
Binary Compatibility and Build Configuration: C++ binaries are sensitive to compiler version, standard library, architecture, build type (Debug/Release), etc. Conan was built to handle this complexity: it tracks detailed settings (OS, compiler, runtime, architecture, etc.) for each package and ensures you get binaries compatible with your project’s configuration (Conan 2.0: C and C++ Open Source Package Manager) (Conan 2.0: C and C++ Open Source Package Manager). If a precompiled binary isn’t available for your exact setup, Conan can fall back to building from source. NuGet packages, on the other hand, often include prebuilt libs targeting a limited set of configurations (e.g. maybe only one CRT or architecture), making it easy to encounter compatibility issues if your setup differs. Conan’s model of packaging per configuration (and tools like profiles to manage those settings) gives it an edge in C++ binary compatibility management.
-
Decentralization and Repositories: NuGet by default uses the centralized nuget.org repository for packages. Conan is decentralized – while ConanCenter is popular for open-source libs, you can easily host your own Conan server or Artifactory repository for internal binaries. This offers flexibility for enterprise use and offline scenarios. Teams can store and retrieve proprietary packages in their own servers with Conan, supporting a fully offline or private package ecosystem. NuGet can work with private feeds too, but Conan’s model is built around the idea of multiple remotes and promoting packages through development/staging/production repositories.
-
Build System Integration: NuGet is tied to MSBuild/.vcxproj for C++ consumption (Visual Studio automatically adds include/lib paths for installed NuGet packages). Conan, in contrast, is build-system agnostic. It integrates well with CMake (our focus here) – e.g., generating CMake find scripts – but it can also work with other systems like Meson, Makefiles, or even Visual Studio solutions via custom generators (Conan 2.0: C and C++ Open Source Package Manager). Conan operates outside the build system: you run Conan to install deps and generate files, then run your build. This decoupling means you can use Conan with any C++ project, regardless of how it’s built.
-
Dependency Resolution and Versioning: Both tools handle transitive dependencies and version resolution, but Conan offers more C++-specific features. Conan supports version ranges and lockfiles to manage updates and ensure reproducible builds (Conan 2.0: C and C++ Open Source Package Manager) (Conan 2.0: C and C++ Open Source Package Manager). Conan can also produce a dependency graph in JSON or other formats for analysis (Conan 2.0: C and C++ Open Source Package Manager), helping in debugging version conflicts or diagnosing dependency trees. NuGet’s version resolution is more limited (primarily for .NET assemblies), and it lacks Conan’s detailed graph and build info outputs that are tailored for C++ needs.
In summary, Conan is often the better choice for C++ dependency management due to its cross-platform support, rich C++ package ecosystem, and integration with C++ build workflows (Benefits of using Conan in software development — Engineering Business Outcomes | Infogain). NuGet remains useful in Windows-only or mixed C#/.NET projects, but Conan’s flexibility and C++ focus make it a go-to for modern C++ projects. Now that we know why we’re using Conan, let’s set up our environment and tools.
In this section, we’ll configure the tools needed for our tutorial. We target Windows as the primary platform (using Visual Studio’s C++ compiler), but will keep an eye on multi-platform support. The major tools required are Conan, CMake, a C++ compiler, and Ninja.
Conan is a Python-based application. The easiest way to install Conan 2.x is via pip:
- First, ensure you have Python 3 installed. (Visual Studio 2022 comes with an optional Python, or download from Python.org.)
- Install Conan globally by running:
$ pip install conan==2.*This will install the latest Conan 2.x release. After installation, verify by
running conan --version in a terminal, which should display a 2.x version.
Multi-platform note: On Linux or macOS, Conan can be installed similarly via pip. Conan is the same across platforms; differences will come from the compiler and profile settings, not Conan itself.
We’ll use Microsoft Visual C++ (MSVC) as our compiler, which comes with Visual Studio. If you haven’t already:
- Install Visual Studio 2022 (Community Edition is fine) and include the “Desktop development with C++” workload. This provides the MSVC compiler, headers, libraries, and tools like the Visual Studio Developer Command Prompt.
- During installation, also select the CMake and Ninja components (Visual Studio’s installer has options for CMake tools and the Ninja build system). If you forget, you can separately install CMake and Ninja manually. Visual Studio 2022 bundles CMake 3.x, so you may not need a separate install (How to Create a Modern C Project with CMake and Conan – Abstract Expression).
Ninja is a fast build tool that will run our compile/link commands. We’ll use Ninja via CMake’s generation step. Having Ninja.exe on your PATH is needed for CMake to use it.
On Windows, to use MSVC from the command line (especially with Ninja), you must load the environment variables that set up the compiler paths. The recommended way is to use the “x64 Native Tools Command Prompt” that comes with Visual Studio (How to Create a Modern C Project with CMake and Conan – Abstract Expression). This is a special console that has all needed environment variables (PATH, LIB, INCLUDE) configured for the 64-bit MSVC toolchain.
- Open the Developer Command Prompt: Find “x64 Native Tools Command Prompt for VS 2022” from your Start menu (you can search for "x64 Native Tools" in the Windows search) (How to Create a Modern C Project with CMake and Conan – Abstract Expression). Launch it, and you’ll get a terminal ready for MSVC. All commands in this tutorial assume you’re in this environment on Windows. (On Linux/macOS, you can use a normal terminal as the compiler is usually in PATH by default.)
When you installed Conan, it likely created a default profile for you (which captures default settings like your OS and default compiler). Let’s ensure Conan is configured for MSVC:
- Run
conan profile listto see available profiles. By default you should see adefaultprofile. - Run
conan profile showto inspect it. On a Windows machine with Visual Studio, it should show settings likeos=Windows,compiler=msvcorcompiler=Visual Studio(depending on Conan version), the version (e.g. 17 or 16),build_type, andarch=x86_64. If it shows GCC or something else, you might need to generate a new profile usingconan profile detect --forcewhile in the Developer Command Prompt. This will auto-detect Visual Studio as the compiler.
In case you have non-default profiles set up, you will see them listed here, e.g.:
$ conan profile list
Profiles found in the cache:
vs2022_x64_profile
vs2022_x64_profile_base
vs2022_x64_profile_debug
vs2022_x64_profile_debug_asan
vs2022_x64_profile_relwithdebinfoYou can inspect the non-default profile vs2022_x64_profile using:
$ conan profile show -pr:b vs2022_x64_profileFor multi-platform builds, you can create separate profiles, e.g., a linux-gcc
profile with GCC settings, a windows-clang profile, etc. Profiles allow you to
switch compiler/OS settings easily. In this tutorial, we’ll mostly use the
default (Windows + MSVC) profile, but we will mention how to adapt commands for
other platforms as we go.
Conan uses profiles to define the settings, options, and environment for building and consuming packages. In cross-compilation scenarios, Conan distinguishes between 'host' and 'build' profiles to manage the differences between the machine where the package is built and the machine where the package will run.
This allows you to manage cross-compilation scenarios effectively. By specifying the appropriate profiles, you ensure that Conan correctly handles the differences between the build and target environments, leading to successful builds and deployments.
The 'host' profile defines the environment where the package will be used or run. This includes settings like the target operating system, architecture, and compiler. For example, if you are building a package that will run on an ARM-based embedded device, the host profile would specify the ARM architecture and the appropriate compiler for that target.
The 'build' profile defines the environment where the package is being built. This includes settings for the build machine's operating system, architecture, and compiler. For example, if you are building the package on a Windows machine with an x86_64 architecture, the build profile would specify these settings.
Here are example profiles for a cross-compilation scenario where you are building on a Windows machine for an ARM-based Linux target.
Host Profile (host_profile):
[settings]
os=Linux
arch=armv8
compiler=gcc
compiler.version=10
compiler.libcxx=libstdc++11
build_type=ReleaseBuild Profile (build_profile):
[settings]
os=Windows
arch=x86_64
compiler=Visual Studio
compiler.version=16
compiler.runtime=MD
build_type=ReleaseOur goal project is a small multi-library application. For illustration, imagine we are building a Vector Math toolkit consisting of:
- A library
vectormath2dfor 2D vector operations. - A library
vectormath3dfor 3D vector operations, which depends onvectormath2d(perhaps to reuse some common functionality). - A main application
dot_product_appthat uses both libraries to compute dot products and print results. - Additionally, we’ll bring in an external library via Conan (for example, a
logging library like
spdlogor formatting libraryfmtfor output).
This setup gives us multiple internal libraries and at least one external dependency managed by Conan. We will build it incrementally:
- Create the first library with Conan and CMake.
- Add an external dependency to that library (using Conan to fetch it).
- Create a second library depending on the first.
- Create the final executable linking everything.
- Iterate on this with debugging and performance considerations.
The directory structure will evolve, but by the end it might look like:
/MyProject
├── conanfile.txt (or conanfile.py describing deps)
├── CMakeLists.txt (top-level CMake)
├── vectormath2d/
│ ├── CMakeLists.txt
│ └── include/ and src/ files for 2D library
├── vectormath3d/
│ ├── CMakeLists.txt
│ └── include/ and src/ for 3D library
└── app/
├── CMakeLists.txt
└── src/ for main application
(If you prefer, you can start with a single CMakeLists.txt and gradually split it out as we add libraries.)
Let’s start implementing this step by step.
In this section we go from nothing to a complete multi-library project using Conan and CMake. Follow along by typing the code and commands yourself to get hands-on experience.
Goal: Set up a simple C++ library project managed by CMake, and use Conan to add an external dependency to it.
Let’s begin with vectormath2d – a library for 2D vector math. It will have a
function to compute dot product of two 2D vectors, and we’ll use an external
library for logging the result (to demonstrate Conan). We’ll use spdlog (a
header-only logging library) as our Conan dependency in this library.
Project Initialization:
- Create a new directory for your project and navigate into it:
$ mkdir MyProject && cd MyProject
- Create a
vectormath2dsubdirectory:$ mkdir vectormath2d
- Inside
vectormath2d, create a simple header and source:vectormath2d/include/vectormath2d.hpp:#pragma once #include <string> // Function to compute 2D dot product and return a formatted string std::string dot2d(int x1, int y1, int x2, int y2);
vectormath2d/src/vectormath2d.cpp:#include "vectormath2d.hpp" #include <spdlog/spdlog.h> // using spdlog from Conan #include <spdlog/fmt/fmt.h> // for formatting (if needed) std::string dot2d(int x1, int y1, int x2, int y2) { int result = x1 * x2 + y1 * y2; spdlog::info("Computed 2D dot product: {}", result); return fmt::format("({},{})·({},{}) = {}", x1, y1, x2, y2, result); }
fmt::formatto return a formatted string).
CMake Configuration for Library:
-
Create
vectormath2d/CMakeLists.txt:cmake_minimum_required(VERSION 3.15) project(vectormath2d LANGUAGES CXX) add_library(vectormath2d STATIC src/vectormath2d.cpp) target_include_directories(vectormath2d PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) # Conan will provide spdlog package, we’ll link it later after Conan integration.
This defines a static library and its include path. Note: We haven’t yet linked spdlog; that will come from Conan’s package configuration.
-
Create a top-level
CMakeLists.txtin the project root:cmake_minimum_required(VERSION 3.15) project(MyProject LANGUAGES CXX) # Add subdirectory for vectormath2d library add_subdirectory(vectormath2d)
For now, it just includes our library subfolder. We’ll expand this as we add more components.
-
Conan Configuration for Dependencies: In the project root, create a Conan recipe. The simplest way is using a
conanfile.txtfor consumption:[requires] spdlog/1.11.0 [generators] CMakeDeps CMakeToolchain
This declares that we require spdlog (a specific version from ConanCenter) and we want Conan to generate CMake files to help find the package. The
CMakeDepsgenerator will createspdlog-config.cmakefiles for CMake, andCMakeToolchainwill generate aconan_toolchain.cmakefile that sets up CMake with Conan’s settings. Conan 2.0 recommends using these generators for CMake integration. -
Install the dependency with Conan: While in the project root, run Conan install to resolve and download spdlog:
$ conan install . --output-folder build --build=missingHere we tell Conan to install requirements for the current directory (which has
conanfile.txt), put generated files into abuildfolder, and--build=missingallows Conan to build from source if a precompiled package is not found (for spdlog, it’s header-only so no build needed, but this is a good habit). Conan will:- Download spdlog/1.11.0 (and its dependency fmt/x.y.z) from ConanCenter.
- Create
build/conan_toolchain.cmakeandbuild/spdlogTarget-release.cmake, etc., files because of the CMakeDeps generator.
Note: This will use the default profile for build and host, since none is specified with
-pr. When cross-compiling, you need to specify both the host and build profiles to ensure that Conan correctly handles the differences between the build and target environments. You can specify these profiles using the-pr:hand-pr:bflags in theconan installcommand.For example, if you have a host profile named
vs2022_x64_profile_debugand a build profile namedvs2022_x64_profile, you can use the following command to install dependencies:$ conan install . --output-folder build --build=missing -pr:h vs2022_x64_profile_debug -pr:b vs2022_x64_profileTip: If you see an error or it’s taking too long, ensure you have internet access to ConanCenter, and that your profile is set correctly. On success, you should have
spdlogin your local cache and generated config files. Conan’s output will list the dependency graph and which packages were installed. -
Integrate Conan with CMake: Now that Conan prepared files, we use them in our CMake build. We’ll link
spdlogto our target:- Modify the top-level
CMakeLists.txtto ensure our CMakeLists knows to find spdlog. Add near the top ofCMakeLists.txt:This will use the files Conan generated to locate spdlog. Now update the vectormath2d CMakeLists to link spdlog:find_package(spdlog REQUIRED CONFIG) # Find spdlog via config file
The Conan CMakeDeps generator defines an imported targettarget_link_libraries(vectormath2d PUBLIC spdlog::spdlog)
spdlog::spdlogthat we can link against. This ensures our library knows aboutspdlog’s include directories (even though spdlog is header-only, linking it helps transitively if other components use spdlog too).
- Modify the top-level
-
Build the Library: We’re ready to build vectormath2d. From the project root (and ensure you’re in the Visual Studio dev prompt on Windows):
-
Call CMake to configure the project. We'll use the generated toolchain file for CMake. You can pass it via command line:
$ cmake -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release . $ cmake --build buildLet’s unpack this:
- The Conan toolchain file sets up compiler flags and defines (e.g., for any special runtime settings Conan uses).
- We call CMake to configure the project.
-B buildspecifies the build directory (where CMakeCache.txt will go).-G Ninjachooses the Ninja generator (for multi-core build). And we pass the Conan toolchain file that was generated earlier so that CMake uses the settings from Conan (this file is in thebuild/directory because we used--output-folder buildwith conan).-DCMAKE_BUILD_TYPE=Releasesets a Release build since Ninja is a single-config generator (with Visual Studio’s multi-config generator you’d specify config during build instead). - Then
cmake --build buildinvokes Ninja to compile. If all is set, this should compilevectormath2d.cpp. Conan’s toolchain set up include paths so the#include <spdlog/spdlog.h>is found (from Conan cache), and link settings (though header-only spdlog doesn’t produce a .lib, the fmt lib might, but spdlog vendored fmt in newer versions).
-
The modern way is cleaner: including the toolchain using CMake presets:
$ cmake --list-presets=all $ cmake --preset conan-release $ cmake --build build
Here,
cmake --list-presets=alllists the available CMake presets, which were generated based on the Conan profile selected during conan install. This step ensures you select a valid preset for the build. Then,cmake --preset conan-releaseconfigures the project using the appropriate Conan-generated preset. The preset handles defining the toolchain and build type.
If the build succeeds, we have our first library built and it’s using an external dependency via Conan. Congrats!
-
Follow Along – Test the Library (Optional): To verify vectormath2d works,
you could create a small temporary C++ file in build/ that calls dot2d and
run it. But we’ll soon build an actual application in Section 4.3. For now,
ensure there were no build errors. If there are errors finding spdlog,
double-check you included the Conan generated files properly in CMake and that
the Conan install step was done before CMake configure.
Now that one library is set up, let’s add the vectormath3d library, which will
depend on vectormath2d. This demonstrates an internal dependency (one library
using another within the same project).
Implement vectormath3d:
-
Create
vectormath3d/directory with include and src:vectormath3d/include/vectormath3d.hpp:#pragma once #include <string> // Function to compute 3D dot product std::string dot3d(int x1,int y1,int z1, int x2,int y2,int z2);
vectormath3d/src/vectormath3d.cpp:#include "vectormath3d.hpp" #include "vectormath2d.hpp" // use 2D for partial calculation #include <spdlog/spdlog.h> #include <spdlog/fmt/fmt.h> std::string dot3d(int x1,int y1,int z1, int x2,int y2,int z2) { int result = x1*x2 + y1*y2 + z1*z2; spdlog::info("Computed 3D dot product: {}", result); // maybe reuse 2D dot to format part of the message (demonstration) std::string xyPart = dot2d(x1, y1, x2, y2); return fmt::format("{} + ({}*{} = {})", xyPart, z1, z2, z1*z2, result); }
Here
dot3dusesdot2dfrom the first library to format part of the dot product (just for demonstration of using the other library’s function). It also logs via spdlog. -
Create
vectormath3d/CMakeLists.txt:cmake_minimum_required(VERSION 3.15) project(vectormath3d LANGUAGES CXX) add_library(vectormath3d STATIC src/vectormath3d.cpp) target_include_directories(vectormath3d PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) # Link to vectormath2d and spdlog target_link_libraries(vectormath3d PUBLIC vectormath2d spdlog::spdlog)
We link
vectormath2d(so vectormath3d can use dot2d) and also link spdlog since this source uses it too. Linking spdlog here ensures if an app links only vectormath3d, it still pulls in spdlog dependency transitively. -
Add the new library to the top-level
CMakeLists.txt:add_subdirectory(vectormath3d)Place this after adding vectormath2d to ensure the 2D library is built first (CMake will figure out dependency order from link libraries anyway).
-
No changes needed in Conan config; we’re still using spdlog. Our conanfile already has it, and vectormath3d reuses it. (If vectormath3d needed a different external package, we’d add it to conanfile requires.)
Build updated project:
-
Run Conan install again, since we added a new consumer of spdlog (the dependencies didn’t change, but generally after modifying conanfile or profiles, re-run it):
conan install . --output-folder build --build=missingIt should be quick and say everything is up to date (spdlog already in cache).
-
Build with CMake:
cmake --build build
(No need to re-run cmake configure if CMakeLists were added via add_subdirectory and you run build in the same folder; CMake will detect changes. If in doubt, run the full
cmake -B build ...orcmake --preset conan-releasecommand again, then build.)Now both libraries should compile. You have two static libs:
vectormath2d.libandvectormath3d.lib(on Windows) or.afiles on other platforms.
Follow Along – Verify internal linking: To ensure internal dependency is
resolved, you might deliberately introduce an error (e.g., call a function from
vectormath2d that doesn’t exist) to see if the build fails, then fix it.
However, if dot3d linked and compiled fine, it means CMake and Conan are
correctly linking the libraries. We’ll test everything in the next step by
building an executable.
Now we add the dot_product_app application that uses both libraries to produce
an output. This will also test that all dependencies (spdlog, fmt) are properly
linked through.
Create the application:
-
Make an
app/directory. -
Create
app/main.cpp:#include <iostream> #include "vectormath2d.hpp" #include "vectormath3d.hpp" int main() { std::string result2d = dot2d(1,2, 3,4); std::string result3d = dot3d(1,2,3, 4,5,6); std::cout << "Result from 2D library: " << result2d << "\\n"; std::cout << "Result from 3D library: " << result3d << "\\n"; return 0; }
This simply calls our two library functions and prints the results.
-
Create
app/CMakeLists.txt:cmake_minimum_required(VERSION 3.15) project(dot_product_app LANGUAGES CXX) add_executable(dot_product_app main.cpp) # Link the executable with both libraries target_link_libraries(dot_product_app PRIVATE vectormath2d vectormath3d)
We link against both libraries. Because those libraries already link spdlog, the executable will indirectly get spdlog as well (transitively, since vectormath2d and 3d link it PUBLICly in our config).
-
Add the app to top-level CMakeLists:
add_subdirectory(app)(After the library subdirectories.)
Build and Run:
-
Run Conan install if needed (no new dependencies added, still spdlog only).
-
Build the project:
cmake --build build
This will compile main.cpp and link the executable. If everything is set, the link should succeed and produce
dot_product_app.exe(on Windows) in thebuild/app/folder (Ninja’s default is to put binaries in subfolder of build corresponding to target folder structure). -
Run the application to test:
./build/app/dot_product_app.exe
Expected output:
Result from 2D library: (1,2)·(3,4) = 11 Result from 3D library: (1,2)·(4,5) = 14 + (3*6 = 18) = 32Also, since we used spdlog, you’ll see log messages printed to console (spdlog defaults to stdout):
[info] Computed 2D dot product: 11 [info] Computed 3D dot product: 32This confirms our multi-library project is working and the dependency (spdlog) is correctly integrated by Conan.
Exercise: Try switching the build to Debug mode. Clean the build/
directory (or create a new one), run Conan install with -s build_type=Debug or
use a profile for Debug, configure CMake with -DCMAKE_BUILD_TYPE=Debug, and
rebuild. Ensure that the program still runs (you should see “[debug]” log
prefixes if spdlog is configured to change log level, but by default it might
still show info). This tests Conan providing Debug-compatible libraries. On
Windows, Conan will ensure the correct MSVC runtime (MD vs MDd) is used for
spdlog’s fmt dependency if it were a binary. If anything fails, inspect the
error and ensure configurations match (Conan’s default profile might use MD/MDd
runtime for Release/Debug which matches Visual Studio default).
Now we have a fully functioning project with two libraries and an executable, using Conan to manage spdlog (and its dependency fmt). Next, we’ll discuss some advanced aspects: how to debug issues in dependency resolution, analyze the dependency graph, and optimize our build and binaries.
As projects grow, you might encounter issues such as missing dependencies, version conflicts, or unexpected behaviors (e.g., ODR violations, ABI mismatches). Conan and CMake provide tools to analyze the dependency graph and help debug such problems.
Conan 2.0 offers commands to inspect the dependency graph without actually
building everything. One useful command is conan graph info. From our project
directory, try:
conan graph info .This will output a breakdown of the dependency graph for the current project’s
conanfile. It lists each node (our conanfile as the root, then spdlog/1.11.0,
and fmt (as a dependency of spdlog)). For each node, you see settings like OS,
compiler, etc., options, and whether a binary is found. The output is detailed;
for example, you might see something like:
conanfile:
name: conanfile
ID: 0
requirements:
spdlog/1.11.0
spdlog/1.11.0:
ID: 1
recipe: Cached
binary: Cached
requires:
fmt/8.1.1
fmt/8.1.1:
ID: 2
recipe: Cached
binary: Cached
(This is a simplified representation.) The key point is you can quickly verify
which versions of packages are in use and whether they came from cache or need
building. Conan can also produce this in JSON for programmatic analysis or an
HTML graph. For instance, conan graph info . --graph file.html will generate
an interactive HTML dependency graph (Graph output for conan info command —
conan 1.66.0
documentation)
that you can open in a browser to visualize nodes and edges (helpful in complex
projects).
If you suspect a dependency conflict (e.g., two libs require different versions
of the same package), the graph will show both and Conan will typically warn or
error out during install. Resolving it might involve specifying a consistent
version or using Conan’s version ranges or overrides. As an advanced
scenario, you could add something like this to your conanfile.txt to force a
specific version:
[tool_requires]
fmt/8.1.1(This is illustrative; Conan’s model for overrides in 2.0 might differ.) The idea is to ensure only one version of fmt is used if, say, another package brought a conflicting version.
When things go wrong on the CMake side (e.g., a library not found or linking fails):
- Enable verbose makefile output: Run
cmake --build build --verbose(for Ninja) to see the actual compile/link commands. This can show if include paths or library files from Conan are missing. - Check
conan_toolchain.cmake: Open this file to see what Conan injected. It sets variables likeCMAKE_CXX_FLAGS, defines, etc., and loads Conan’s profile settings. If something like the runtime library or architecture seems off, you might need to adjust the profile or your CMake call. - Use CMake’s find_package logs: Since we used
find_package(spdlog REQUIRED CONFIG), if that fails CMake will say it couldn’t find spdlog. Conan’s CMakeDeps generator outputs files into the build directory (e.g.,build/spdlogTargets.cmake). Ensure CMAKE_PREFIX_PATH is pointing to the build folder (Conan’s toolchain should handle that). If not, you may manually add-DCMAKE_PREFIX_PATH=buildto your CMake configure.
Issue: Missing Prebuilt Binary / Long Build Times. If Conan outputs that it’s building a dependency from source (especially large ones like Boost), it can slow down your build. You have a few options:
- Build that dependency once and upload it to a server (Artifactory or a local cache) so others don’t need to rebuild it. Conan excels at storing prebuilt binaries for reuse (Conan 2.0: C and C++ Open Source Package Manager) (Conan 2.0: C and C++ Open Source Package Manager). If you’re continuously integrating, consider setting up a Conan cache on a network drive or using an artifact repository.
- Use package options to reduce bloat. For example, if using Boost, you can optionally disable parts you don’t need via Conan options, resulting in less to build.
- Ensure you’re not mixing Debug and Release inadvertently – if you built everything in Release but accidentally try to link a Debug app, Conan might rebuild dependencies in Debug. Keep your profiles consistent.
Issue: ODR Violations or Multiple Runtime Libraries. On Windows, mixing
runtime (MT vs MD or debug vs release) incorrectly can cause issues. Conan’s
default profiles typically use MD for Release, MDd for Debug (for MSVC) which
matches Visual Studio’s defaults. If you override these in CMake, be careful to
also adjust Conan settings. The Conan profile setting compiler.runtime governs
this. If you need static runtime (MT), you’d create a profile that sets
compiler.runtime=MT and Conan will prefer packages built with MT. Using
inconsistent runtime between libraries will lead to linker warnings or crashes
at runtime – Conan helps by tagging package IDs with the runtime, avoiding
accidental mix, but only if you use it correctly.
Issue: Diamond Dependency Conflict. Suppose in the future vectormath2d and
vectormath3d become independent packages, and both depend on a common third
library but at different versions – this is a diamond scenario. Conan will
detect the conflict on conan install and typically raise an error. To fix it,
align the versions or use Conan’s requires with version ranges so that a
single version can satisfy both. For example, using a range [>=1.0 <2.0] for a
dependency in both might let Conan pick a common version.
Conan Tools for Debugging: Conan has a --verbose and --debug flag you
can use on any command to see more internal logs. If something weird happens,
conan install . -pr default -o "*:some_option=value" --debug could shed light.
Also, you can always run conan search package/name to see what’s in your local
cache or what’s available remotely, in case you suspect a wrong version is being
pulled.
Use Ninja or Parallel Builds: We already chose Ninja which is typically faster than MSBuild for incremental builds and uses all cores effectively. This improves compile times.
Ccache for C++ (Linux/macOS): On Linux, you might integrate ccache to avoid
recompiling unchanged code. On Windows, you can use clcache for a similar
effect. These can be integrated via Conan as build requirements (ConanCenter has
a ccache package that you can add under [tool_requires] so that Conan sets
it up in your environment). This is advanced, but worth knowing if build times
become an issue.
Profile Guided Optimization (PGO) or Link Time Optimization (LTO): These are
outside Conan’s direct scope but can be enabled via CMake toolchain flags. If
performance of the final binary is crucial, enabling LTO (e.g., /GL and
/LTCG in MSVC or -flto in GCC/Clang) can yield faster executables. You can
add such flags via CMake’s target_compile_options or via a toolchain file.
Just ensure all dependencies are built with compatible flags. Header-only libs
like spdlog are fine; compiled libs like fmt might need to be compiled with LTO
as well. In Conan, if you needed to, you could build fmt from source with LTO by
propagating compiler flags – but this goes deep into package recipes. For our
use case, default builds are fine.
Dependency Graph Optimization: Sometimes a large graph can be optimized by
making some dependencies private or optionally disabled if not used. For
example, if a library has an optional dependency that our project doesn’t need,
turning it off in Conan can save build time. Keep an eye on the graph (conan graph info) to spot if you’re pulling in unexpected packages.
System vs Package Manager Trade-offs: While Conan can manage almost
everything, you might choose to use system-provided libraries for certain things
in development (to skip building them) and Conan for others. This is doable by
marking some requirements as external or using SystemPackageTool in Conan
recipes, but it complicates reproducibility. Generally, stick to Conan for
consistency, and rely on CI caching (next section) to mitigate performance
issues.
With these debugging and optimization tips, you can tackle most issues that arise in a Conan+CMake based build. Now, let’s automate our setup with Continuous Integration, using GitHub Actions as an example.
Automating builds and tests via CI/CD ensures your project stays buildable in clean environments and catches issues early. We will set up a basic GitHub Actions workflow for our project, focusing on two key aspects:
- Running Conan and CMake to build on each push.
- Caching Conan packages between runs to avoid re-downloading and rebuilding dependencies each time (a huge time saver, especially on Windows where compile times are high).
Create a file in your repository: .github/workflows/ci.yml (for example).
We’ll write a workflow that runs on push and pull request:
name: C++ CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install Conan
run: pip install conan==2.*
- name: Cache Conan dependencies
id: cache-conan
uses: actions/cache@v2
with:
path: C:\Users\runneradmin\.conan2
key: conan-${{ runner.os }}-${{ hashFiles('**/conanfile.txt') }}
- name: Create default profile (if cache miss)
if: steps.cache-conan.outputs.cache-hit != 'true'
run: conan profile detect --force
- name: Install deps (Conan)
run: conan install . --output-folder build --build=missing
- name: Configure CMake
run: cmake --preset conan-release
- name: Build
run: cmake --build build --config Release
- name: Run unit tests
run: ctest --output-on-failure -C Release
working-directory: buildLet’s break down what’s happening here:
-
Checkout and Setup Python: We use the official actions to get our source code and a Python environment on the runner. Conan requires Python, so we ensure Python 3.11 is installed.
-
Install Conan: Simply pip install Conan 2.x on the runner. (Alternatively, there is a community action
actions/setup-conan@v1that can install and cache Conan, but here we do it manually for clarity.) -
Cache Conan dependencies: This uses the
actions/cacheto preserve the Conan home directory between workflow runs (cmake - Installing dependencies with conan on github actions takes too long - Stack Overflow). Conan’s default home on Windows runner isC:\Users\runneradmin\.conan2for Conan v2 (for Conan1 it was.conan). We cache the whole directory. The cachekeyis composed of the OS and a hash ofconanfile.txt. This means if the conanfile doesn’t change, the cache is reused. If we update dependencies in conanfile, the hash changes and a new cache will be saved (to avoid stale data). Caching the Conan folder speeds up builds significantly by reusing downloaded packages (cmake - Installing dependencies with conan on github actions takes too long - Stack Overflow). -
Create default profile if cache missed: When the cache is restored, it includes the default profile. But on a cache miss (first run), we need to setup a default profile. We run
conan profile detect --force(cmake - Installing dependencies with conan on github actions takes too long - Stack Overflow) to auto-generate a default matching the runner’s compiler. On GitHub’s windows-latest image, that will detect MSVC (Visual Studio Build Tools are preinstalled on runners). We could also explicitly provide a profile file in the repo if needed. -
Install deps (Conan): This is the same
conan installcommand we used locally. It will find packages either in the cache (populated from the cache step) or download as needed. If the cache was hit, most likely spdlog and fmt are already there, so this step will be quick. We still allow--build=missingin case a package for the specific MSVC version isn’t in the cache (Conan might then build it, though spdlog is header-only so it won’t). -
Configure CMake and Build: We run the same CMake configuration and build as we did locally.
-
Run tests: If we had any (say, a CTest unit test), this is where we’d execute them. In our example, we might not have tests, so this step could be omitted. But demonstrating
ctestis useful if you add tests to the project. We use the-C Releaseto specify configuration because on Windows, CTest might need to know which config (since VS can have multiple, but with Ninja it’s single-config so this is just to be explicit).
This workflow essentially automates what we did manually: install Conan, install deps, build with CMake, and optionally run tests. The caching part ensures that subsequent runs (e.g., for multiple commits or PRs) don’t re-download all dependencies each time, greatly reducing CI time (cmake - Installing dependencies with conan on github actions takes too long - Stack Overflow).
Multi-platform CI: To make this truly cross-platform, you could extend the job to a matrix, for example:
jobs:
build-and-test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]And adjust steps for each OS (mostly paths and possibly installing a compiler on
Linux). Conan will handle each platform’s dependencies as long as profiles match
(Conan auto-detect on each will pick gcc/clang on Linux/macOS). You’d also want
to adjust the cache path for Linux (~/.conan2). The rest of the steps remain
largely the same. This way, your CI can verify the project builds on all major
OSes, using Conan to provide the right libraries on each.
Dependency Updates in CI: By running conan install on each build, your CI
will always pull the latest patch releases that satisfy your requirements (if
you use version ranges or update the conanfile). If you want to lock
dependencies to exact versions for reproducibility, Conan’s lockfile feature can
freeze them. You could check in a conan.lock file and use conan install . --lockfile conan.lock ... in CI to ensure the exact same dependency versions
are used everywhere. Generating and updating that lockfile would be another step
(perhaps run manually or via a scheduled job when you want to update
dependencies).
Cache Limitations: Note that GitHub Actions cache has a size limit
(currently ~5GB per cache). Conan packages, especially big ones (think Qt, Boost
compiled for multiple archs), can fill this quickly. In our simple case, spdlog
and fmt are tiny, so no issue. For large deps, consider only caching your
project’s specific dependencies, or using Conan’s own conan download/conan upload to a remote cache. Our key includes the OS and conanfile hash, which
helps prevent the cache from growing endlessly across different configurations.
One benefit of using Conan in CI is that adding or updating a dependency becomes
as simple as editing the conanfile and pushing the code. The CI will fail if
the new dependency can’t be resolved or built, alerting you early. Some tips for
automation:
- Pinning versions vs Ranges: During development you might allow version
ranges (e.g.,
spdlog/[>=1.10 <2.0]) so you automatically get minor updates. But this can lead to non-reproducible builds if a new version is released. It’s often safer to pin to a specific version in your conanfile for CI reliability, and periodically update it manually (or via a Dependabot-like service for Conan, which some exist). - Conan lockfiles for releases: When you’re preparing a release, you can generate a lockfile to freeze all dependency versions. Store that with the release or in your repo, so that you (and others) can always rebuild that exact configuration later. Conan 2’s lockfiles are simpler than Conan 1’s and are meant to be part of reproducible workflows (What's New in Conan 2.0 C/C++ Package Manager - YouTube).
- Promoting packages: In a corporate setup with multiple teams, you might
want CI to pull from an internal Artifactory rather than ConanCenter, perhaps
after your QA team approves a library version. Conan’s remotes and Artifactory
support allow staging of packages. While our example uses the public
ConanCenter, switching to an internal remote is just a matter of
conan remote addand adjusting recipes (or usingconan config installin CI to set up your remotes).
Our CI/CD setup for this tutorial is simple but covers the essentials: automated builds on multiple platforms with dependency management handled by Conan and speed boosted by caching.
In this comprehensive tutorial, we covered how to leverage Conan 2.0 and CMake to manage dependencies in a multi-library C++ project on Windows (and beyond). Let’s recap the journey and key takeaways:
-
Conan vs NuGet: You learned the differences and why Conan shines for C++: multi-platform support, a rich package ecosystem (ConanCenter) with thousands of libraries, and granular control over binary compatibility (How to Create a Modern C Project with CMake and Conan – Abstract Expression) (Maintaining multiple projects which consume conan packages - Stack Overflow). Unlike NuGet, Conan is built for C++ from the ground up, ensuring your Windows projects can easily extend to Linux/Mac with the same dependency files.
-
Environment and Tools: We set up a Windows environment using Visual Studio’s compiler with the Ninja generator in CMake. Using Ninja with MSVC required using the Developer Command Prompt so that
cl.exewas on PATH (How to Create a Modern C Project with CMake and Conan – Abstract Expression). We configured Conan profiles to match our compiler settings, a crucial step for getting the correct binaries. -
Hands-On Project Build: Step by step, we created a real multi-library project:
- Started with one library and integrated an external Conan package (spdlog), seeing how Conan installs packages and CMake find_package integrates them.
- Expanded to multiple libraries with internal dependencies (linking one library to another via CMake’s target link, which Conan handles transparently for includes/links).
- Built an executable using those libraries and verified everything links and runs, showing that Conan-managed dependencies (spdlog/fmt) were correctly linked in the final app.
- Each step was accompanied by actual code and build commands that you could follow along and reproduce.
-
Advanced Debugging & Optimization: We explored tools for diagnosing issues:
- Conan’s
graph infoto inspect the dependency graph and detect conflicts or mistakes in versioning. - CMake and Conan logging to troubleshoot missing includes or libraries.
- Common pitfalls like mismatched build types or runtimes and how to address them with Conan profiles and settings.
- Build performance improvements through using Ninja, caching, and potentially tools like ccache or LTO for further optimizations. Conan’s ability to reuse prebuilt binaries dramatically improves build times and is central to performance (Conan 2.0: C and C++ Open Source Package Manager).
- Conan’s
-
CI/CD Integration: We wrote a GitHub Actions workflow that automates the build. Key points included setting up Conan on the runner, using
actions/cacheto cache Conan’s package cache between runs (cmake - Installing dependencies with conan on github actions takes too long - Stack Overflow), and running the same CMake build. This ensures continuous integration builds are fast and consistent with local development. We also discussed scaling that to multiple platforms and how Conan facilitates that by handling different binaries for each platform in the same project.
Interactive Exercise Ideas (Post-Tutorial):
- Try adding a new dependency to the project. For example, add Eigen (a C++
math library) via Conan and use it in one of the vector functions. Update
conanfile.txtwitheigen/3.4.0, run Conan install, adjust CMake to find Eigen (Conan CMakeDeps will generateEigen3::Eigentarget), and update code. This will reinforce adding and integrating new Conan packages. - Experiment with Conan options. For instance, spdlog can be used as a
header-only or as a compiled library. By default it’s header-only. Try setting
an option in conanfile like:
This will force Conan to build spdlog as a library. See how the build process changes (Conan will compile spdlog, and your link step will link an actual spdlog.lib). This helps understand how Conan can manage not just which dependencies, but how they are built.
[requires] spdlog/1.11.0 [options] spdlog:header_only=False
- Create a deliberate version conflict: modify the project to use two different
versions of a library (perhaps add another dummy library that requires
fmt/9.xwhile spdlog wants fmt/8.x). Observe how Conan handles it (it should flag a conflict). Then resolve it by aligning versions. This exercise teaches how to manage version compatibility. - Try the project on Linux or macOS. Use a g++ or clang environment, ensure
Conan profile is set (Conan’s
defaulton Linux likely already picks gcc). You might need to adjust the logging format (spdlog default on *nix might prefix with date/time). The CMake and Conan parts remain the same. This will show the portability of the setup.
By mastering Conan with CMake, you’ve gained a powerful skill to simplify C++ project development. No more manually wrangling .libs and include paths for each dependency or worrying if everyone on the team has the right version of a library. Conan ensures a consistent, reproducible dependency setup across all developers and CI systems, while CMake handles the build in a platform-agnostic way.
Next Steps: In real-world projects, you can extend these concepts further.
Explore Conan’s ability to create your own packages (e.g., package
vectormath2d and vectormath3d as reusable components if you plan to reuse
them in other projects). Look into Conan’s workspaces or multi-repo flows if
you maintain libraries as separate repositories – Conan can help orchestrate
dependencies across repos as well. Additionally, keep an eye on Conan’s evolving
ecosystem: new tools (like the conan lock commands or recipe repositories) can
further improve C++ dependency management.
Happy coding with Conan and CMake – may your builds be fast and your dependency graphs clean and green! (Benefits of using Conan in software development — Engineering Business Outcomes | Infogain) (Conan 2.0: C and C++ Open Source Package Manager)