The C++ logo, by Jeremy Kratz, licensed under CC0 1.0 Universal Public Domain Dedication

CMake, MSVC, and Ninja

When using the Microsoft compiler and CMake on Windows, it is typically straightforward to configure a project.

The command cmake -S <source dir> -B <build dir> works out of the box, as long as a version of Visual Studio is installed on the machine, and in case of multiple versions, it is always possible to specify explicitly which one to use with -G; for example: cmake -G"Visual Studio 17 2022" -S <source dir> -B <build dir>.

In both cases, the compiler cl.exe does not need to be in the path.

If you are not interested in using the IDE Visual Studio or would prefer to use another, possibly faster build system than MSBuild, like Ninja, then one would expect cmake -GNinja -S <source dir> -B <build dir> to work without further changes.

When using the Ninja generator, CMake does not search the compiler, and Ninja complains that there is no compiler to execute.

This issue also exists when using presets 🗄️

{
    "version": 4,
    "include": [],
    "configurePresets": [
        {
            "name": "msvc-ninja",
            "generator": "Ninja",
            "binaryDir": "${sourceParentDir}/build",
        }
    ]
}

There are multiple workarounds, and none of them is completely satisfactory

Open a developer console

The first is to open a "developer console", which adds the compiler and other tools to the PATH.

A naive approach has multiple drawbacks.

First, when using a CMakePresets.json, it is not possible to execute a script (vcsvarshall.bat) to change the environment where other commands are executed, this is a known limitation, with currently no planned fix 🗄️.

cmd.exe has many restrictions compared to PowerShell and bash, so I try to avoid it at all costs.

One of those restrictions is that there is no history.

Thus even opening the developer console and starting from there CMake or the IDE is more painful than it needs to be.

It is possible to create wrapper scripts.

For example, a script for starting the IDE (or some other programs that will execute CMake or Ninja) might look like

IDE-x64
@echo off

@call "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Auxiliary/Build/vcvars64.bat"
start "" "<path to IDE>" %*

An alternate approach is to wrap the CMake binary and let the IDE (or other desired tool) use that instead of the real CMake

cmake-x64.cmd
@echo off

@call "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Auxiliary/Build/vcvars64.bat"

<path to cmake.exe> %*

This is error-prone, especially if cmake is in the PATH as it might get called from a different environment.

Toolchain file

An alternate approach would be to use a toolchain file.

As there is no official toolchain file for the Microsoft compiler, one needs to create one.

Here I am presenting a minimal toolchain file, where minimal means:

  • incomplete

  • with insufficient error handling

  • without enough diagnostic if something fails

  • tested on only one machine

Toolchain file for the Microsoft compiler

The first step is to get vswhere and execute it.

vswhere is installed with Visual Studio at a fixed location (%ProgramFiles(x86)%\Microsoft Visual Studio\Installer), otherwise it can be downloaded separately from Github.

One could avoid it and reimplement the logic in CMake, but it probably has more disadvantages than advantages.

execute_process(COMMAND
    vswhere.exe -latest -property installationPath
    OUTPUT_VARIABLE MSVC_INSTALLATION_PATH
    RESULT_VARIABLE RET
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(NOT RET EQUAL 0 )
  message( FATAL_ERROR "Failed to execute vswhere, error code ${RET}")
endif()
if(NOT MSVC_INSTALLATION_PATH)
  message( FATAL_ERROR "output of 'vswhere -latest -property installationPath' is empty")
endif()
cmake_path(NORMAL_PATH MSVC_INSTALLATION_PATH)

MSVC_INSTALLATION_PATH points to the installation directory (for example C:/Program Files/Microsoft Visual Studio/2022/Professional), but in a given installation directory, there can be more versions of the compiler!

According to the wiki of vswhere, there is a file containing the current version number.

Once we have that version, it is possible to build a path to the compiler (and standard library), after that, it is just a matter of registering all required components

file(READ ${MSVC_INSTALLATION_PATH}/VC/Auxiliary/Build/Microsoft.VCRedistVersion.default.txt VCTOOLSVERSION)
string(STRIP ${VCTOOLSVERSION} VCTOOLSVERSION)
if(NOT VCTOOLSVERSION)
  message( FATAL_ERROR "Microsoft.VCRedistVersion.default.txt is empty")
endif()

set(MSVC_BIN_PATH     "${MSVC_INSTALLATION_PATH}/VC/Tools/MSVC/${VCTOOLSVERSION}/bin/Hostx64/x64")
set(MSVC_LIB_PATH     "${MSVC_INSTALLATION_PATH}/VC/Tools/MSVC/${VCTOOLSVERSION}/lib/x64")
set(MSVC_INCLUDE_PATH "${MSVC_INSTALLATION_PATH}/VC/Tools/MSVC/${VCTOOLSVERSION}/include")

set(CMAKE_C_COMPILER   ${MSVC_BIN_PATH}/cl.exe)
set(CMAKE_CXX_COMPILER ${MSVC_BIN_PATH}/cl.exe)
set(CMAKE_LINKER       ${MSVC_BIN_PATH}/link.exe)
set(CMAKE_AR           ${MSVC_BIN_PATH}/lib.exe)

link_directories("${MSVC_LIB_PATH}")
include_directories(SYSTEM "${MSVC_INCLUDE_PATH}")

The line set(MSVC_LIB_PATH "${MSVC_INSTALLATION_PATH}/VC/Tools/MSVC/${VCTOOLSVERSION}/lib/x64/") might not always be correct, I am not sure.

Without adding this path to link_directories, when CMake tries to compile a test program the linker reports the error LINK : fatal error LNK1104: cannot open file 'MSVCRTD.lib'

The output of find '/c/Program Files/Microsoft Visual Studio' -iname MSVCRTD.lib -exec md5sum {} + | sort is

1ad203b271879804620ab81a31d4710d */c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.36.32532/lib/x64/store/msvcrtd.lib
3e7d425cb7c76f763f851dc8d6ac5a9a */c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.36.32532/lib/onecore/x86/msvcrtd.lib
4082f04ce1ff365d79eaccc5b6e61465 */c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.36.32532/lib/onecore/x64/msvcrtd.lib
421f457a857e44dcd75d856846922aad */c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.36.32532/lib/x86/msvcrtd.lib
421f457a857e44dcd75d856846922aad */c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.36.32532/lib/x86/uwp/msvcrtd.lib
7a531a9a8d1b5455435ffd9033058e74 */c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.36.32532/lib/x86/store/msvcrtd.lib
a07acfd3ccba8e79e3d4c550b196793a */c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.36.32532/lib/x64/msvcrtd.lib
a07acfd3ccba8e79e3d4c550b196793a */c/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/MSVC/14.36.32532/lib/x64/uwp/msvcrtd.lib

At least the file in the uwp subdirectory matches the file in the parent directory, so it seems safe to ignore it.

But what about the store and onecore subdirectories? In which situations should one add them to the linker path?

At least there is only one include directory with the standard library.

Currently, the compiler (and other tools in the bin folder) points to the Hostx64/x64 subfolder. A more correct solution would be to query CMAKE_HOST_SYSTEM_PROCESSOR and CMAKE_SYSTEM_PROCESSOR for getting the correct combination.

A minimal toolchain for the Windows SDK

The Windows SDK provides the API for the Windows platform, for example, it contains the header file windows.h.

It is also necessary for compiling programs that do not depend on the Windows platform directly.

For example, the Windows SDK contains the header file assert.h (but not cassert) The cassert header file from the Microsoft compiler, includes an assert.h file that is otherwise nowhere to be found in the compiler toolchain.

Thus the SDK is necessary, or at least another library that provides assert.h and other headers of the C standard library.

vswhere does not seem to be able to give the installation path of the Windows SDK, but it is possible to query the information from the registry 🗄️.

I just learned that in CMake it is possible to query the Windows Registry without too much hassle, as long as one uses \\ as a separator.

get_filename_component(WIN_10_KITS_DIR "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0;InstallationFolder]" ABSOLUTE CACHE)

Older versions of the SDK might store the information elsewhere.

Similarly to the compiler, in one installation directory, there might be more than one SDK. This time, there seems to be no file that describes the current version, so just list them all and use the highest one

file(GLOB WIN_KITS_VERSIONS RELATIVE "${WIN_10_KITS_DIR}/lib" "${WIN_10_KITS_DIR}/lib/*")
list(FILTER WIN_KITS_VERSIONS INCLUDE REGEX "10\\.0\\.")
list(SORT WIN_KITS_VERSIONS COMPARE NATURAL ORDER DESCENDING)
list(POP_FRONT WIN_KITS_VERSIONS CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION)

At this point, define the directories and add all required tools to CMake

set(WIN_KITS_BIN_PATH        "${WIN_10_KITS_DIR}/bin/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}"      CACHE PATH "" FORCE)
set(WIN_KITS_INCLUDE_PATH    "${WIN_10_KITS_DIR}/include/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}"  CACHE PATH "" FORCE)
set(WIN_KITS_LIB_PATH        "${WIN_10_KITS_DIR}/lib/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}"      CACHE PATH "" FORCE)
set(WIN_KITS_REFERENCES_PATH "${WIN_10_KITS_DIR}/References"                                           CACHE PATH "" FORCE)


set(CMAKE_MT "${WIN_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/mt.exe")
set(CMAKE_RC_COMPILER_INIT "${WIN_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/rc.exe")
set(CMAKE_RC_FLAGS_INIT "/nologo")

set(MIDL_COMPILER "${WIN_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/midl.exe")
set(MDMERGE_TOOL "${WIN_KITS_BIN_PATH}/${CMAKE_VS_PLATFORM_TOOLSET_HOST_ARCHITECTURE}/mdmerge.exe")

include_directories(SYSTEM "${WIN_KITS_INCLUDE_PATH}/ucrt")
include_directories(SYSTEM "${WIN_KITS_INCLUDE_PATH}/shared")
include_directories(SYSTEM "${WIN_KITS_INCLUDE_PATH}/um")
include_directories(SYSTEM "${WIN_KITS_INCLUDE_PATH}/winrt")
include_directories(SYSTEM "${WIN_KITS_INCLUDE_PATH}/cppwinrt")
link_directories("${WIN_KITS_LIB_PATH}/ucrt/x64")
link_directories("${WIN_KITS_LIB_PATH}/um/x64")
link_directories("${WIN_KITS_REFERENCES_PATH}/x64")

One toolchain to rule them all

MSVC needs one Windows SDK. Just as it makes sense to have two separate toolchain files (as there is some independence between the two components), it also makes sense to have just one toolchain file to use, instead of specifying every time two:

msvc-winsdk-toolchain.cmake
include(${CMAKE_CURRENT_LIST_DIR}/msvc-toolchain.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/winsdk-toolchain.cmake)
# Eventually set some other settings

Finally, it is possible to use Ninja in any environment with

cmake -GNinja -S <source dir> -B <build dir> --toolchain=msvc-winsdk-toolchain.cmake

presets included:

{
    "version": 4,
    "include": [],
    "configurePresets": [
        {
            "name": "msvc-ninja",
            "generator": "Ninja",
            "binaryDir": "${sourceParentDir}/build",
            "toolchainFile": "msvc-winsdk-toolchain.cmake",
        }
    ]
}

Conclusion

The toolchain file helps to create a more uniform development environment but has disadvantages too.

It needs to be maintained.

I do not know how often Microsoft changes how files are organized, but the simple fact that vswhere exists proves that determining the location of the compiler is not always trivial.

The developers of CMake would surely be better maintainers than a third-party project.

They already have a similar logic when using CMake without Ninja on Windows.

And it could be possible to avoid a toolchain file altogether and rely on vcvars64.bat and vcvars.bat, which is probably the most stable API to access the compiler, as AFAIK those scripts are available in all installations of Visual Studio.


Do you want to share your opinion? Or is there an error, some parts that are not clear enough?

You can contact me anytime.