Riccardo Loggini

CMake in Visual Studio

Table of Contents

Why Using CMake

CMake is a tool that allows us to generate project files from our code for a multitude of different environments. By using it we are not tied anymore to a specific IDE environment, like Visual Studio (or a specific version of it), or NetBeans, or CLion, because the project files are going to be independently generated using CMake.
We can dictate the project structure in a platform-agnostic manner, so whoever downloads our code can generate the solution project files as we initially planned, regardless of the chosen environment.

CMake real job is configuring and using the Native Build Tool (the set of IDE+compiler environment), depending on some platform-agnostic instructions given by the programmer.

NOTE: It is important to understand that when using CMake, we are placing it “in charge” of the project configuration, that means that every change to the project configuration should be done in CMake and not in the IDE (for example) anymore. Any configuration hange made outside CMake will be unsafe, because it will be local and probably deleted when CMake is run again.

Generally we should make sure that the version of CMake that we are using got released after the compiler we are using, just to increase the chance we have all the possible updated tools at our disposal. At the time of writing this article, VisualStudio 2019 is using CMake 3.16.

Scope of the Article

The scope of this article is giving a basic but complete understanding of the CMake tool, with a focus on Visual Studio environment, in order to set up a fairly simple project. CMake offers a multitude of tools which are much more in number than the ones described in this article, many of which goes beyond the scope of a base environment setup.

Base Concepts

The fundamental objective in CMake is defining a list of Targets and defining Properties in them.

CMake Stages

Scripting Tools

Variables

Variables in CMake scripts are defined with set command set(MyVarName “variable value”), optionally include CACHE STRING after the value to save it in CMakeCache.txt, described in the next chapter.
To dereference a variable we can use the dollar-brace syntax ${MyVarName}.
Dereferencing can be nested, so the value of a variable can be used as a part of a variable name to be dereferenced: ${MyVar${MyName}}.
All variables outside cache, have a scope and a lifetime. The scope is tied to the directory in which it was defined and it will be available on each of its subdirectories.
Constructs like functions also create a child scope.
By using the set function with PARENT_SCOPE after the value, we will set it for the scope that opened the current one (and NOT for the current scope or for the parent-of-parent scope). This is also the way in which a function in CMake is able to return a value (aside from setting a value in cache).

NOTE: If a variable is not found in the current scope, it will be searched in cache automatically!
NOTE: Any subsequent value that we assign to a cached variable, will automatically update the corresponding value in cache!

The general recommendation with variables naming convention is use unspaced lower case for current scope only variables (e.g. ${myvarname}) and full underscore-spaced upper case (e.g. ${MY_VAR_NAME}) for variables used along many scopes.
Additionally, all cache variables (having global scope) should be pre-fixed with the name of the project they belong to and an underscore, in order to avoid inter-project name clashing, eg. PROJNAME_MY_VAR.

Lists have elements separated by semicolons in a single string, e.g. “a;b;c” represents a list with 3 elements. We can define them by set or list command that includes also many list operations.

IF Statements & Loops

CMake has its own conditions for IF statements that we can use, where

if(statement)  
    doSomething()  
else/elseif  
    doSomethingElse()
endif()  

Where statement can be

NOTE: This means that if statements will evaluate the string content of a variable, and will react differently based on some specific string values.

Loops are also present in CMake. Just to name the one that can turn most useful, foreach loop can iterate through a list, like

foreach( currentvar IN mylist )

doSomething()

endforeach( currentvar )

And there is also the option to iterate through a generated list of items, like in python, by using the RANGE option.

Generator Expressions

Generator Expressions are CMake commands that are evaluated during build system generation. They allow us to change value of variables depending on many factors, such as the selected configuration (e.g. Debug Win64 instead of Release Win64) to build.
While most of the commands happen at configure time, with generator expressions we can decide something at build or install time as well. Boolean Generator Expressions are the ones in the form $<conditionType:arguments> which represents an IF statement that can return either 0 or 1, depending if the condition is met.

String-Valued Generator Expressions expands to (returns) some string. They can come in different forms depending on type

NOTE: Generator expressions can be nested, so for example, if we want to make a string case-insensitive to then compare it, we can do $<STREQUAL:$<UPPER_CASE:${foo}>,”BAR”> will return 1 only if content of ${foo} is BAR , bar, Bar, bAr etc..

NOTE: don’t query CONFIG and other configuration parameters at Configuration Time, because many IDEs don’t have that information ready up until Build Time.

Functions

Functions in CMake are quite different from the average programming language, in the sense that functions do not have a return value, but they rely on creating variables for the parent scope.
What it means is, we can define a variable inside the body of the function, and by declaring it PARENT_SCOPE, we will be able to use it in the scope where the function was called. E.g.

function(myFunctionName INPUT_VAR)
    set(${INPUT_VAR} “FilledVarContent” PARENT_SCOPE)
endfunction()

myFunctionName(myVar)

message(“Content of myVar is ${myVar})

Functions will add a child variable scope to the one that called it. CMake uses a scripting language where every function argument is a string, there is no other type.
By calling ${INPUT_VAR} we are dereferencing the variable that corresponds to the string name “INPUT_VAR”.
By typing project(myProject VERSION 1.0.0) for example, it is the same as typing project(“myProject” “VERSION” “1.0.0”).
It will be a function’s responsibility to interpret that input correctly.

White space is a delimiter for different function arguments and it can be moved and shifted as we need.

We can pass any number of arguments to a function, like variadic functions in C++.
IF we expect any number of arguments, inside its body we are going to need to extract each value by calling cmake_parse_arguments.
Cmake_parse_arguments will first retrieve every passed value, and then create a variable for each one of them.

function(myOtherFunction)

cmake_parse_arguments(  
VAR_PREFIX
“FIRST_OPT_KEY;SECOND_OPT_KEY”
“FIRST_V_KEY;SECOND_V_KEY;THIRD_V_KEY”
“FIRST_MULTIV_KEY;SECOND_MULTIV_KEY”
${ARGN}  
)

endfunction()

myFunctionName(FIRST_OPT_KEY FIRST_V_KEY THIRD_V_KEY peppi SECOND_MULTIV_KEY alpha beta gamma)

Inside the body of myOtherFunction we will find the following defined variables:

VAR_PREFIX_FIRST_OPT_KEY = TRUE  
VAR_PREFIX_SECOND_OPT_KEY = FALSE
VAR_PREFIX_FIRST_V_KEY = TRUE
VAR_PREFIX_SECOND_V_KEY = <UNDEFINED>
VAR_PREFIX_THIRD_V_KEY = “peppi”
VAR_PREFIX_FIRST_MULTIV_KEY = <UNDEFINED>
VAR_PREFIX_SECOND_MULTIV_KEY = “alpha;beta;gamma”

There are much more functionalities associated with cmake_parse_arguments, but the general structure is composed by

cmake_parse_arguments(<prefix> <options> <one_value_keywords>
<multi_value_keywords> <args>...)

where

NOTE: The power of this system shows, for example, when the function is run in different stages, e.g. build vs. install of a specific configuration. When we pass variables as arguments, they can contain concrete values if we are computing one stage, or they can be undefined (resolving in empty string “”) when computing another stage. In this way the generated variables by our function will adjust according to what is defined at that specific time.

NOTE: There is also a Macro in CMake, similar to function but with some caveats on passed parameters and scope. For more information, visit the official documentation.

The CMake Lists

CMakeLists.txt is the only script file that is always necessary for CMake to work in our source tree.
We can have more than one CMakeLists, each one of them representing a directory or a subdirectory: by using the add_subdirectory(relative/path/to/subdir) in the main script, we tell CMake to search in that subdirectory for a CMakeLists script to execute, and if found it will be executed straight away.
It will follow a step-by-step of a CMakeLists configuration describing all the functions we use.

Version and Project

cmake_minimum_required( VERSION 3.1 )

project( ProjName )

The VERSION will also dictate the policy to use, so the rules that cmake will use when executing its stages. The specified version will dictate what policies to use when processing the script, even if we installed a newer version of CMake. Sometimes functionalities we use can result outdated or too new for the version of policy we are using, and that will throw warnings and errors. We can always create If statements checking what version we are using if(CMAKE_VERSION ...) to prevent any issue, or we can exclude particular policies by setting them old, via the cmake_policy function cmake_policy(SET <PolicyCode> OLD). We can even specify an entire CMake version of policies to use for the current project with cmake_policy(VERSION <VersionCode>) with any official version lower or equal than the current version of CMake we have installed.

Project( ProjName ) function will define the name of our project and ${ProjName_SOURCE_DIR} and ${ProjName_BINARY_DIR} variables will be automatically created and filled with the source and binary folder paths.
We use project to represent an undivisible set of targets.

NOTE: Using Visual Studio, a CMake project corresponds to an entire Solution. Similarly, each target (library or executable) will correspond to a different VS Project.

Project will also find and set the current compiler and so the language the project is using. Without specifying one, CMake will look for C and C++ compilers.
Additional settings are available, for those refer to the official documentation.

Adding a Target

Targets can be executables or libraries, so they get defined by

Where:

Include Directories for a Target

We can specify additional include directories for when a given target is compiled or installed.
This target property is set with target_include_directories function:

target_include_directories( MyTargetName
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src
) 

In this example, conditional expressions will decide what to include depending if we are acting a build operation (BUILD_INTERFACE) or an install operation (INSTALL_INTERFACE).

When an include directory is set as PRIVATE it is saying that it will be used in the implementation files of the target, and not in its public Api interface. Any code that makes call to the target will not need that include directory.
When an included directory is set in INTERFACE scope (nothing to do with the previous conditional expressions), it is saying that target’s implementation files will not use that include directory but it will be only used for its public Api interface (opposite to PRIVATE).
When an include directory is set as PUBLIC it will be a combination of PRIVATE and INTERFACE keywords.

NOTE: don’t add unneeded PUBLIC requirements, make PRIVATE whenever possible. This can be debugged with enabling build warning with commands like “-Wall” (warning all).

The directories specified in this way will be available as relative path for our #include headers.

For example when we build a project, all the headers files and folders containing headers inside ${CMAKE_CURRENT_SOURCE_DIR}/include will be available to get included in our code files, so if we had a folder Peppi inside include that contains Pear.h, then in our source file we can directly write #include <Peppi/Pear.h>.

Handling Properties

Properties relative to a target can be set with set_property or set_target_properties, for example

set_property(TARGET MyTargetName
PROPERTY MyPropertyName MyPropertyValue)

#And we can read back a property value with

get_property(MyOutputVariable TARGET MyTargetName PROPERTY MyPropertyName)

For a complete list of already defined properties belonging to global scope, a target or a directory, refer to the official documentation.

Adding and Linking a Library

If we want to build our own libraries inside a project, they will become a target and once created, we can link them to executable targets.
The most generic code to add and link a library to an executable target is

add_library( myLibraryName ${myLibrarySourceFiles} )

target_include_directories( myLibraryName ${myLibraryIncludeDirectory} )

target_link_libraries(myTargetName PRIVATE myLibraryName)

By default add_library will build as a Static Library. We could also specify SHARED options in add_library but that is considered not a good practice in general, and it should be used only if that particular library cannot be built in any other way than shared (for some reason).
What we should do instead, if we want to build our libraries as dynamic, we should use the global flag BUILD_SHARED_LIBS and set it to ON (default is OFF).
The linking operations goes under Transitive Usage Requirements meaning that a dependency should usually be defined PRIVATE if it is used just by that target.
If we are building a library and its header files also uses the library we are linking, then we should specify the linkage as PUBLIC dependency.
If the library we are building is not using the library we are linking but the headers still need that library, then we should specify the linkage as INTERFACE dependency.

We can also use a target subdirectory to represent a set of libraries to link to the main target.

add_subdirectory(mysubdir)  
add_executable(myprogram main.cpp)

target_link_libraries(myprogram mysubdir)

Where the CMakeLists.txt in the subdirectory has:  
add_library(myLib1 Lib1.cpp)

add_library(myLib2 Lib2.cpp)

In this way, the subdir target mysubdir will make myLib1 and myLib2 to be linked to myprogram executable target.

This happens because library dependencies are transitive: when a target is linked to another, all the linked libraries of the first one become linked to the second one. This link is stored in INTERFACE_LINK_LIBRARIES target property which can be also manually edited.
When we operate a script inside a subdirectory we will automatically create a source and binary subfolder to the respective root one. These variables ${CMAKE_CURRENT_SOURCE_DIR} and ${CMAKE_CURRENT_BINARY_DIR} represent those newly created subfolders, while we can always fetch the source and binary root folder with ${CMAKE_SOURCE_DIR} and ${CMAKE_BINARY_DIR}.

Imported Target Libraries

When we have a dependency from a library outside current source tree, for example OpenGL library for a render engine, we will need to use Imported Targets.
In most cases, imported targets are the result of executing a cmake find-module.
We can always define a find module ourselves that will search a library we need in the current machine and returns an imported target and/or CMake variables that give information about that library. Creating a find-module goes out of scope of this article, but there are already many find modules available on the internet for the most common libraries.
CMake ships with a number of example find-modules, including one to find OpenGL and its utility libraries called “FindOpenGL”.
We can execute the find-module by calling find_package() function and after that we can link the target as a normal library.

find_package(FindOpenGL REQUIRED)

target_link_libraries(myTargetName PRIVATE OpenGL::GL)

The REQUIRED option stops the processing of the script if the library cannot be found.

The CMake Cache

CMake has support for cached options: they will be stored in file CMakeCache.txt and loaded the next time we use CMake with the current solution. This is the way Cmake remembers anything we set.

The scope of these variables is global, unlike the variables defined in CMakeLists.
Every time we reference a variable, it will be first searched in the current scope, and if not found it will automatically be searched in cache.
Variables are set in cache with set(MY_VAR "peppi" CACHE STRING "") where “peppi” is the default value assigned if we do not find myvar variable in cache. If myvar was found in cache (for example, the second time we run the script with that statement) then that set function will do nothing, unless we specify FORCE (NOT recommended to use, as it is an anti-pattern). The STRING specifies the type of data that we store, but that is used only in specific cases like the CMake GUI to better display cached data, because again, in the end all values are strings.
To remove a variable from cache we can use unset(MY_VAR CACHE).

CMakeCache.txt is automatically generated each time we change CMakeLists.txt and it contains all of the CMake variables that define how the project is generated.

We can manually edit CMakeCache by right-clicking CMakeLists in solution explorer->Cache->View CMakeCache

The CMake Settings

CMakeSettings.json will store build configurations that will be used to generate command line arguments for executing CMake.exe (for either configure, build, install or other CMake operations). Such configurations will also be used to configure Intellisense accordingly.

If we use the Visual Studio multi-configuration generator (opposed to single-configuration like Ninja or Make), by default all the possible configurations will be created and stored at generation stage.
If we still want to use a platform independent generator like Ninja and to continue using CMake to drive the project entirely, Visual Studio provides the so called Open Folder mode, which is a way that opposes the usual Solution (.sln) and relies on what is found in folder content instead.

NOTE: We could have used CMake with the VisualStudio generator to generate project files and we would be finished with it. There is no need for additional files like the CMakeSettings.json because Visual Studio can take care of all internally with the usual Solution mode. The disadvantage of it is that we will remain tied to the Visual Studio environment which might be a relevant factor if we plan to support multiple platforms. If we are in this last case, using Open Folder mode is preferable instead.

CMakeSettings.json is a file exclusive to Visual Studio and it is where the build configurations and compiler environment are stored. In this way, we can still use those generators in Visual Studio but the multiple configuration settings will be stored in CMakeSettings instead.

A Build Configuration specifies a set of properties to adapt the project to a specific platform or a different level of optimization, e.g. PS4 Debug vs Development Editor vs Release Client.
Each configuration specifies an Environment which stores information about compiler toolset (e.g. MSVC vs Clang).

CMake is set to generate Build/Configuration Types (the CMake way to create configurations), found in CMAKE_CONFIGURATION_TYPES . For each one of the build types, a set of variables will be automatically generated: the list can be found in the official documentation (the ones with in the name will be generated, replacing with the configuration type). By default we have the following build types: Debug, Release, RelWithDebInfo and MinSizeRel.

We can always define build types ourselves in CMakeLists.txt scripts, but this goes beyond the scope of this article.
We can access CMakeLists in the IDE by right-clicking CMakeLists in Solution Explorer(Folder View) -> CMakeSettings.

Each configuration in CMakeSettings.json will look like this:

{
"name": "x64-Debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [ "msvc_x64_x64" ],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"installRoot": "${projectDir}\\out\\install\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "-v",
"ctestCommandArgs": "",
"variables": []
}

Where:

NOTE: From Visual Studio 2019 there is a CMake Settings Editor that let us edit CMakeSettings.json with a visual interface.

Macros Available in CMakeSettings.json

A list of Visual Studio macros is available for us to insert in the CMakeSettings

System Environment Variables

System Environment Variables are per-project variables that we can use in Visual Studio configuration to make it dynamic and less verbose, any time there is a text field.
These variables use the notation $(<VarName>).
One of the most common system environment values is $(ProjectDir) that will return the full path to the folder containing the project we are writing the configurations for.
They are also available in CMakeSettings.json by calling ${env.<var_name>}.
We have different System Environment Variables, and/or different values associated with them, for each Project in our solution.
To check the list of system environment variables, in a Solution project type, for a specific project (and the values associated to them) go to Project Properties->VC++ Directories->(select one of the fields and press the arrow on the right) ->Edit..->Macros(on the bottom right).
To see the full list of defined system environment variables for the current Solution refer to the official documentation.

The “environments” Block

The “environments” block in CmakeSettings.json allows us to define new, or change value to existing, System Environment Variables. That means they will be instantly available to EVERY “configuration” block entry defined down the line. E.g.

"environments": [
    {
    "NINJA_STATUS": " My Ninja Status variable common to every configuration block"
    }
],
"configurations": [
{  }

NOTE: Some of those system environment variables are used by the system, like NINJA_STATUS will be used by Ninja to format the build status string.

NOTE: We can use the “environments” block or the “variables” field to directly specify some of the other fields to CMake!
E.g. instead of specifying the same:
"buildRoot": "${projectDir}\\out\\build\\${name}"
field for every configurations, we just specify:
"BUILD_ROOT": "${env.USERPROFILE}\\CMakeBuilds\\LearningCMake\\build\\${name}" In the “environments” block.
Then defining buildRoot as “buildRoot”:”${BUILD_ROOT}”.

NOTE: we can override entries in the environments block on a per-configuration basis, just by adding a “environments”: [{ .. }] field inside a specific configuration.

The Debug&Launch Settings

launch.vs.json is used to pass arguments to an executable at debug time.

By default, the Startup Item drop-down will show ALL executable CMake targets found: by pressing it we will start the Visual Studio debugger with the selected target.

NOTE: As CMakeSettings.json, launch.vs.json is also a utility tool for Visual Studio’s Open Folder Mode, that opposes the usual Solution (.sln, .vcxproj) configuration to let CMake handle the whole configure-build-debug pipeline of our program. This comes handy if we plan to develop our project for different environments/platforms. If instead we plan to stay with Visual Studio, this file will not be needed and we can stick with Solution mode.

If we want to launch a specific CMake target and specify settings about it we can create a custom entry by:
Switch to CMake Targets View of the Solution Explorer -> right-click the CMake target we want to debug -> Debug and Launch Settings -> click on the first entry.
That will bring us to launch.vs.json with a new entry related to the selected target.
Alternatively, we can just edit the file ourselves from scratch:
first create it by right-clicking CMakeLists.txt in solution explorer->Add Debug Configuration->Default.
Then to open it, right-clicking CMakeLists.txt in solution explorer->Open Debug and Launch Settings.
The file launch.vs.json has a structure like the following

{
"version": "0.2.1",
"defaults": {},
"configurations": [
    {
    "type": "default",
    "project": "CMakeLists.txt",
    "projectTarget": "MyExecutableTargetToRun",
    "name": "CMakeLists.txt"
    }
]
}

Where:

NOTE: all the variables available in CMakeSettings.json are also available in launch.vs.json with the addition of ${buildRoot} and ${installRoot}.

NOTE: launch.vs.json is generated in appData folder, outside the project, so if we want to include it in the solution, we are going to need to copy and paste it in the folder of CmakeLists.txt

Launching a Configuration

Visual Studio Installer gives the option to install CMake for its environment so if we plan to just use Visual Studio we do not have to worry about installing anything else.
For a more generic version of CMake we can install the program itself that can be downloaded in the official website.

Generate Native Tool Files from Command Line

The simplest version of a build command in cmake is (from version 3.13)
cmake -S . -B _builds -G "Visual Studio 15 2017" -A x64 Where

Build Project from Command Line

Once the native tool files have been generated, as the above example, in _build folder, we can build the project solution with

cmake --build _builds

And that should be all it is needed to generate executables or libraries, depending on our project, ready to be used.

Installing a Library

CMake install is used to generate a version of our CMake driven project configured for reusability in other code bases, typically in the form of a library.
This version will contain just enough CMake variables, code and binary files to be used outside the current source tree.
After calling install on a project we want to include in our source tree, we will not have to call add_subdirectory on the initial code anymore, because we will call add_subdirectory and/or add_package in the folder generated by the install process instead.
The first step is knowing where the install folder will be created. For some reason this will have to be decided at configuration stage, so in the command line we will have to edit CMAKE_INSTALL_PREFIX cache variable:
cmake -H. -B_builds -DCMAKE_INSTALL_PREFIX=_install/config-A By default the install prefix will point to a generated folder in c:/Program Files/ which is generally not ideal.
After configuration and generation stage, we build the project with the install option

cmake --build _builds --target install

If everything completes correctly we will find our _install/config-A folder ready to be included as subdirectory.
The process of creating an install-ready source tree requires additional configuration which is not listed here. This goes beyond the scope of this article and it will be omitted, but if that turns necessary to someone, they can find instructions in the official install documentation page and a well documented practical example in the CGold packaging project.

Good Practices

Ideal Solution Structure

It is good practice to structure a C++ project similar to the following

├── CMakeLists.txt
├── lib/
│   ├── CMakeLists.txt
│   └── Project1/
│       ├── CMakeLists.txt
│       ├── LibraryType1.h
│       ├── Library1/
│       │   ├── CMakeLists.txt
│       │   ├── Library1.h
│       │   ├── LibraryClass1.cpp
│       │   ├── LibraryClass1.h
│       │   ├── LibraryClass2.cpp
│       │   └── LibraryClass2.h
│       └── Library2/
│           ├── CMakeLists.txt
│           ├── Library2.hpp
│           ├── LibraryClass3.cpp
│           └── LibraryClass3.h
└── app/
    ├── CMakeLists.txt
    └── Project1/
        ├── CMakeLists.txt
        ├── Target1/
        │   ├── CMakeLists.txt
        │   ├── SubSet1/
        │   │   ├── Class1.cpp
        │   │   └── Class1.h
        │   └── main.cpp
        └── Target2/
            ├── CMakeLists.txt
            └── main.cpp

Inspecting a Library/Executable External Symbols

In Windows we can quickly inspect the defined symbols inside a library with Dumpbin tool of the command prompt.
For static libraries we cause the /symbols option:
dumpbin /symbols “path\to\myStaticLib.lib” | findstr "External" | findstr "SECT"

Calling this function will return a set of strings each representing a symbol found in library myLib.lib where also “External” and “SECT” are present in the description.
If SECT is present in the symbol description it means that symbol has been defined, otherwise “UNDEF” will be written instead.

For shared libraries we can use the /exports option:

dumpbin /exports “path\to\mySharedLib.dll”

NOTE: Generating Undefined External Symbols happens

  • If we declare a function in the library but not implement it, that will be stored as an undefined external symbol.
  • If we use a function inside the library which is defined in another library, that will also be stored as an undefined external symbol.

NOTE: Linking errors will be notified at build stage, even if they were done at configuration stage.

Adding Pre-Compiler Definitions for Build Stage

We can add pre-compiler definitions for build stage for a given target with target_compile_definitions like
target_compile_definitions(myTarget PUBLIC "LOG_PHX_VERBOSE")

In this way, every time the target will be built, even as a dependency, that definition will be included.
The case is different for add_definitions function:
add_definitions(-LOG_PHX_VERBOSE)

That will be active ONLY for the current directory, so if the current directory will be linked as a library, that definition will NOT be used.

Further Good Practices

Are found in Manuel Binna’s Effective Modern CMake page.

Sources