Skip to Content

CMake, Visual Studio, and the Command Line

For quite some time now I’ve been working inside teams who were using Visual Studio to build complex C++ projects.

Because I’ve often been the “buildfarm guy” and because I don’t like GUIs that much, I had to find ways to build Visual Studio projects from the command line.

This is the story of everything I’ve tried.

Quick note before we begin: throughout this article, I will be using Visual Studio 2015 on Windows 10 to build the source code of CMake itself. If’s a nice project for a case study, since it’s neither too big nor too small, and has no dependencies to worry about (and of course, it uses CMake to build itself :)

Using CMake to generate Visual Studio projects #

CMake works by parsing code in CMakeLists.txt files, and then generating code that will be used by an other program that would perform the build itself.

When you use CMake, you must specify a generator. On Windows, the default generator will be the most recent Visual Studio found, and after running CMake, you’ll get a .sln file you can open in Visual Studio to edit, build, and debug the project.

So my task was to find a way to build those .sln files from the command line.

Using devenv #

The most obvious way I found was to use a tool called devenv. In fact, that’s the probably the answer you’ll find if you look up “Building Visual Studio projects from the command line” on an internet search engine. You’ll also find places where they suggest you use MSBuild.exe.

But, bad luck, if you try to run devenv directly from cmd.exe, you’ll get the famous error message:

'devenv' is not recognized as an internal or external command, operable program or batch file.

The trick is to use one of the “Command Prompt” you’ll find in the start menu:

Visual Studio command prompts in start menu

I started with the “Developer Command Prompt”:

cd c:\User\dmerej\src\cmake\build-vs
devenv CMake.sln

Visual Studio opened. Hum, that’s not what I wanted. Turns out, if you make any mistake in the command line prompt, Visual Studio will open.

The correct way is to add the /build switch:

devenv /build Debug CMake.sln

The output is quite nice:

3>  cmbzip2.vcxproj -> C:\...\src\cmake-3.7.2\build-vs\Utilities\cmbzip2\Debug\cmbzip2.lib
7>------ Build started: Project: cmjsoncpp, Configuration: Debug Win32 ------
7>  Building Custom Rule C:/Users/dmerej/src/cmake-3.7.2/Utilities/cmjsoncpp/CMakeLists.txt
7>  CMake does not need to re-run because
      C:\...\src\cmake-3.7.2\build-vs\Utilities\cmjsoncpp\CMakeFiles\generate.stamp
      is up-to-date.
2>  Generating Code...
6>  fs-poll.c
2>  Compiling...

Using MSBuild #

You can also try using MSBuild.exe, but the output is a bit uglier. (But you get more info, such as the time it took to compile a project, the full command line used, and the number of warnings/errors):

Project
  "c:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities\cmcompress\cmcompress.vcxproj.metaproj" (7)
  is building
  "c:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities\cmcompress\cmcompress.vcxproj" (8)
  on node 1 (default targets).
InitializeBuildStatus:
  Creating
  "cmcompress.dir\Debug\cmcompress.tlog\unsuccessfulbuild"
  because "AlwaysCreate" was specified.
CustomBuild:
  Building Custom Rule C:/Users/dmerej/src/cmake-3.7.2/Utilities/cmcompress/CMakeLists.txt
  CMake does not need to re-run because
    C:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities\cmcompress\CMakeFiles\generate.stamp
    is up-to-date.
ClCompile:
  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\CL.exe /c
  /I"C:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities"
  /I"C:\Users\dmerej\src\cmake-3.7.2\Utilities" /Zi /nologo /W3 /WX- /Od /Ob0
  /Oy- /D WIN32 /D _WINDOWS /D _DEBUG /D _CRT_SECURE_NO_DEPRECATE /D
  _CRT_NONSTDC_NO_DEPRECATE /D CURL_STATICLIB /D "CMAKE_INTDIR=\"Debug\"" /D
  _MBCS /Gm- /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline
  /Fo"cmcompress.dir\Debug\\" /Fd"cmcompress.dir\Debug \cmcompress.pdb" /Gd /TC
  /analyze- /errorReport:queue
  "C:\Users\dmerej\src\cmake-3.7.2\Utilities\cmcompress\cmcompress.c"
cmcompress.c
...
cmArchiveWrite.cxx
cmBase32.cxx
cmBootstrapCommands1.cxx
...

0 Warning(s)
0 Error(s)

Time Elapsed 00:00:06.93

Using CMake to build #

OK, so now I knew how to build Visual Studio projects from command line.

We had a pretty big C++ code base, that we wanted to build on Linux, macOS and Windows.

We were using Jenkins to do continuous integration, so we had to write build scripts that would run on the nodes as soon as any developer would make a merge request to make sure the proposed changes will build on all platforms.

On Linux and macOS, the default generator is “Unix Makefiles”, so the code was straightforward:

#/bin/bash -e

git pull
mkdir -p build
(
  cd build
  cmake ..
  make
)

On Windows, we used Batch files:

git pull
@call "%VS140COMNTOOLS%VsDevCmd.bat"
mkdir build
cd build
cmake ..
devenv foo.sln /build

You may wonder where the weird @call "%VS140COMNTOOLS%VsDevCmd.bat" line comes from.

First, if you go to C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio 2015\Visual Studio Tools, you can right-click on the “Developer Command Prompt” shortcut and open the “Properties” window. There you’ll find that the target is: cmd.exe /k "C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\Tools\VsDevCmd.bat"

Second, if you are lucky, someone1 will tell you that with any version of Visual Studio, an environment variable called VS<version>COMNTOOLS is set, where <version> is the 2 or 3 digits version number of your Visual Studio install.

Here is a table if you don’t know what I mean:

"Marketing" version      "Internal" version
Visual Studio 8 2005         80
Visual Studio 9 2008         90
Visual Studio 10 2010       100
Visual Studio 11 2012       120
Visual Studio 12 2013       130
Visual Studio 14 2015       140

Thus, you can avoid hard-coding the Visual Studio installation path, and use the VS140COMNTOOLS variable instead. (You still need to hard-code Visual Studio version, though).

So what the command does is concatenate the value of the VS140COMNTOOLS environment variable with the basename of the prompt file (VsDevCmd.bat), and run @call on it.

Using Python #

But, as time went by, we wanted to rewrite all the build scripts in Python, so that we could factorize some of the code. (For instance, running git pull to update the sources before building)

On Linux and macOS it was easy:

subprocess.check_call(["cmake", ".."], cwd=build_dir)
subprocess.check_call(["make"], cwd=build_dir)

But on Windows, things were a bit trickier. How were we going to implement the @call \path\to\bat_file in Python?

setuptools to the rescue! #

I discovered that setuptools – the module used by Python to run the setup.py files – was able to build things with Visual Studio, without having to use the Visual Studio command prompts.

So I looked at the implementation, and found a solution:

def source_bat(bat_file):
  interesting = {"INCLUDE", "LIB", "LIBPATH", "PATH"}
  result = {}

  process = subprocess.Popen('"%s"& set' % (bat_file),
                        stdout=subprocess.PIPE,
                        shell=True)
  (out, err) = process.communicate()

  for line in out.split("\n"):
      if '=' not in line:
          continue
      line = line.strip()
      key, value = line.split('=', 1)
          result[key] = value
    return result

The idea is to run a batch script (that’s why we are using shell=True) that will:

  • Call the .bat file we need
  • Run the built-in set command and parse its output
  • Returns the whole environment in a Python dict.

Indeed, there are several ways to use set on cmd.exe:

  • To set an environment variable: set FOO=BAR
  • To unset an environment variable: set FOO=
  • To see all the environment variable whose name start with prefix: set <prefix>
  • To dump all the environment variables: set

We parse the output of set to find the three variables we need (they are all lists of semi-colon separated paths)

  • PATH: to find the required executables (the devenv command)
  • LIB: where the compiler will look for .lib files
  • INCLUDE: where the compiler will look for headers files.

By the way, if you are wondering why the function is called source_bat, it’s because on Unix, to execute a bash script and have your environment updated, you need to use the source built-in, or, on some other shells, the . command, (but I digress).

Building the .sln file #

There was an other problem, though. On Linux and macOS, the command to build is always make.

But on Windows, I had to carefully craft the devenv command, and this meant specifying the path to the .sln file.

At first, I only had a few bad solutions:

  • Hard-code the name of the .sln file
  • Parse the top CMakeLists to find the project() call2
  • List the contents of the build directory, and hope they’ll will be only one file with the .sln extension.

Luckily, by running cmake --help I discovered there was --build switch I could use to abstract the command line to run for the project to be built.

So the code looked like:

def configure_and_build():
    if os.name == "nt":
        generator = "Visual Studio 14 2015"
    else:
        generator = "Unix Makefiles"

    subprocess.check_call(["cmake", "-G", generator, ".."], cwd=build_dir)
    subprocess.check_call(["cmake", "--build", ".", cwd=build_dir)

This meant I could run cmake --build anywhere, without having to deal with those nasty .bat files.

Using multiple CPUs at once #

By default, Visual Studio projects get built using all the CPU resources, but it’s not the case for the make command.

So the code had to be patched again to have make use all available CPUS:

import multiprocessing

def build():
    cpu_count = multiprocessing.cpu_count()
    cmd = ["cmake", "--build", "."]
    if os.name != "nt":
      cmd.extend(["--", "-j", str(cpu_count)])
    subprocess.check_call(cmd, cwd=build_dir, env=build_env)

(The -- argument is here to separate arguments parsed by cmake binary from the one sent to the underlying build command. It’s a common practice for command-line tools)

Performance issues #

So we had our Jenkins nodes running Python scripts to build the same source code on Linux, macOS, and Windows, and everything was fine, except that the builds would take much longer on Windows.

At first I thought, “Well,it’s a known fact that running executables and accessing the file system will always be slower on Windows, and there’s nothing we can do about it”.

But members of my team kept complaining about the long build times, and I was not feeling good about it: as someone said once, “When doing continuous integration, computers should be waiting for humans, and not the other way around”.

So, I looked for solutions to improve performance.

Using NMake #

If you look at the size of the files generated by CMake when using Visual Studio, you realize it will not be easy to have good performance.

For instance, to build CMake you have a .sln file with 842 lines, which references 115 .vcxproj files.

Looking at the contents of the files, it’s no wonder parsing them takes quite some time.

// In CMake.sln
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CMakePredefinedTargets", "CMakePredefinedTargets", "{0FD1CAE9-153C-32D5-915F-6AB243496DE3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CTestDashboardTargets", "CTestDashboardTargets", "{991076F0-B37F-32E8-88B9-1156BDA0D346}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{81200686-E54D-3D48-BDD9-782FCB64B8A8}"
EndProject
...
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ALL_BUILD", "ALL_BUILD.vcxproj", "{BE9010A6-FD75-30CB-B4F7-EC8DD41D6F48}"
	ProjectSection(ProjectDependencies) = postProject
		{FF7E34C4-7170-3648-BBB6-B82173FFD31E} = {FF7E34C4-7170-3648-BBB6-B82173FFD31E}
		{765BC85F-D03F-35FC-ADE0-26ED16D75F4D} = {765BC85F-D03F-35FC-ADE0-26ED16D75F4D}
        ...
<!-- in Source\cmake.vcxproj -->
<?xml version="1.0" encoding="UTF-8"?>
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>

      ...

    <ClCompile>
      <AdditionalIncludeDirectories>C:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities;C:\Users\dmerej\src\cmake-3.7.2\Utilities;C:\Users\dmerej\src\cmake-3.7.2\build-vs\Source;C:\Users\dmerej\src\cmake-3.7.2\Source;C:\Users\dmerej\src\cmake-3.7.2\build-vs\Utilities\cmcompress;C:\Users\dmerej\src\cmake-3.7.2\Source\CTest;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
      <AssemblerListingLocation>Debug/</AssemblerListingLocation>
      <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
      <CompileAs>CompileAsCpp</CompileAs>
      <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
      <ExceptionHandling>Sync</ExceptionHandling>
      ...
      <PreprocessorDefinitions>WIN32;_WINDOWS;_DEBUG;_CRT_SECURE_NO_DEPRECATE;_CRT_NONSTDC_NO_DEPRECATE;CURL_STATICLIB;LIBARCHIVE_STATIC;UNICODE;_UNICODE;WIN32_LEAN_AND_MEAN;CMAKE_BUILD_WITH_CMAKE;CMAKE_INTDIR="Debug";%(PreprocessorDefinitions)</PreprocessorDefinitions>
      <ObjectFileName>$(IntDir)</ObjectFileName>
    </ClCompile>

    ...

Also, if you look at CPU usage during build, you can see you are far from using all the CPU power:

Building with Visual Studio

So I tried to find a CMake generator that would generate simpler code.

During my research, I found out that Microsoft had their own implementation of the Make program called NMake, so I decided to use the NMake Makefile generator.

This time I’ll be using cl.exe, link.exe and their friends directly.

I tried using the MSBuild command prompt, but I got:

cmake -G"NMake Makefiles" ..

-- The C compiler identification is unknown
-- The CXX compiler identification is unknown
CMake Error at CMakeLists.txt:11 (project):
  No CMAKE_C_COMPILER could be found.

  Tell CMake where to find the compiler by setting either the environment
  variable "CC" or the CMake cache entry CMAKE_C_COMPILER to the full path to
  the compiler, or to the compiler name if it is in the PATH.


CMake Error at CMakeLists.txt:11 (project):
  No CMAKE_CXX_COMPILER could be found.

  Tell CMake where to find the compiler by setting either the environment
  variable "CXX" or the CMake cache entry CMAKE_CXX_COMPILER to the full path
  to the compiler, or to the compiler name if it is in the PATH.


-- Configuring incomplete, errors occurred!
See also "C:/Users/dmerej/src/cmake-3.7.2/build-msbuild-nmake/CMakeFiles/CMakeOutput.log".
See also "C:/Users/dmerej/src/cmake-3.7.2/build-msbuild-nmake/CMakeFiles/CMakeError.log".

Here, CMake cannot find cl.exe because it’s not in the %PATH%. And indeed, if you try to run cl.exe from the MSBuild Command Prompt, you’ll get the same “cl.exe is not recognized …” error.

So I tried using the ‘Developer Command Prompt" I already used before back when I was runnig devenv by hand:

mkdir build-nmake
cd build-nmake
cmake -G"NMake Makefiles" ..

-- The C compiler identification is MSVC 19.0.24210.0
-- The CXX compiler identification is MSVC 19.0.24210.0
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/cl.exe -- works

Huzzah, the compiler is found!

You may notice that the path to cl.exe is just VC\bin\cl.exe. (There are other folders in VC\bin, but here we are using the default, 32 bits version)

Also, I was pleased to find out that only 30 or so Makefiles files were generated.

So the next step was:

cd build-nmake
nmake
[ 27%] Built target cmsysTestsCxx
[ 27%] Built target cmsysTestDynload
[ 44%] Built target cmlibarchive
[ 61%] Built target cmcurl
[ 62%] Built target LIBCURL
Scanning dependencies of target CMakeLib
[ 62%] Building CXX object Source/CMakeFiles/CMakeLib.dir/cmCommands.cxx.obj
cmCommands.cxx
[ 62%] Linking CXX static library CMakeLib.lib

I was quite happy to see the I got the same nice output (with the percentage of progress) as on Linux and macOS.

But then I discovered that only one CPU was used during compilation.

And running nmake /? gave nothing being able to run multiple jobs in parallel.

Frack!

Using JOM #

Looking at cmake --help output again I discovered there was yet another generator called “NMake Makefiles JOM”

Jom is a tool made by Qt folks. It’s a re-implementation of the nmake command, but with support for multiple jobs. The command line switch to build with multiple CPU is also called -j, which is nice because it meant the build script code would get simpler.

That gave quite some good results, but the build was still slower than on Linux and macOS.

In order to investigate, I decided to keep the Windows resource monitor opened during a build with JOM:

Building with Jom

You can see there’s a drop in CPU usage during build. From what I understand, it happens during linking.

Using Ninja #

Finally, circa 2010, Ninja came out.

As soon as I read the description of the project: “a small build system with a focus on speed”, and the fact there was an experimental support for it in CMake, I was dying to try it out.

And it fact, it gave great results! For the first time in years, I finally had the same build times on Windows than on Linux, and the CPU usage was a nice steady line around 100% for all cores:

Building with Ninja

I also got the terse output that gave Ninja its name. 3

cmake -GNinja ..
cmake --build .
[11/11] Linking CXX executable Tests\CMakeLib\CMakeLibTests.exe

A story of cross-compiling #

After several years of using the CMake + Ninja combination, I got an error message during one of our CI builds:

Linker fatal error: LNK1102: out of memory

Googling the error lead to: https://support.microsoft.com/en-us/help/2891057/linker-fatal-error-lnk1102-out-of-memory

So I tried to follow the advice in the “Resolution” section and took a closer look at the list of command line prompts in the start menu:

Visual Studio command prompts in start menu

Below the two prompts on top I already tried, there was a few entries, all of them shortcuts to the same .bat file, but with different arguments:

VS2015 x64 ARM          "C:\...\Visual Studio 14.0\VC\vcvarsall.bat" amd64_arm
VS2015 x64 Native:      "C:\...\Visual Studio 14.0\VC\vcvarsall.bat" amd64
VS2015 x66 x86 Cross    "C:\...\Visual Studio 14.0\VC\vcvarsall.bat" amd64_x86

Aha! So, all I have to do was to call the vcvarsall.bat file with the correct arguments, which was confirmed when I took a look at the contents of the .bat file:

...
:check_platform
if /i %1 == x86       goto x86
if /i %1 == amd64     goto amd64
...
goto usage
:x86
...
call "%~dp0bin\vcvars32.bat" %2 %3
goto :SetVisualStudioVersion

:amd64
...
call "%~dp0bin\amd64\vcvars64.bat" %2 %3
goto :SetVisualStudioVersion

So the Python code was patched again:

def source_bat(bat_file, arch):
  interesting = {"INCLUDE", "LIB", "LIBPATH", "PATH"}
  result = {}

  process = subprocess.Popen('"%s %s"& set' % (bat_file, arch),
                        stdout=subprocess.PIPE,
                        shell=True)
  (out, err) = process.communicate()

And CMake output was:

c:\Users\dmerej\src\cmake-3.7.2\build-cross-64-32>cmake -GNinja ..
-- The C compiler identification is MSVC 19.0.24210.0
-- The CXX compiler identification is MSVC 19.0.24210.0
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64_x86/cl.exe
-- Check for working C compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64_x86/cl.exe -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64_x86/cl.exe
-- Check for working CXX compiler: C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC/bin/amd64_x86/cl.exe -- works
-- Detecting CXX compiler ABI info

Notice the amd64_x86 subfolder.

Code signing breakage #

Everything was OK for a while, until we decided we wanted to sign the executables before shipping them.

It seemed the most obvious way was to use signtool.exe, so I proceeded to install the Windows Driver Kit, as instructed on the Windows Dev Center 4

But then all our builds started failing with:

C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE\crtdefs.h(10):
fatal error C1083:
Cannot open include file: 'corecrt.h': No such file or directory

I looked for a solution but all I got was people telling me to set INCLUDE and LIB by hand.

Well, I already had Python code computing the environment variables, so the fix was:

def get_build_env(arch):
    vs_comntool_path = os.environ["VS140COMNTOOLS"]
    bat_file = find_bat_file(vs_comntool_path)
    env = source_bat(bat_file, env)

    inc_path = r"C:\Program Files (x86)\Windows Kits\10\Include\10.0.10069.0\ucrt\include"

    if arch == "amd64_x86":
        subfolder = "x86"
    else:
        subfolder = "x64"

    lib_path = r"C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10069.0\ucrt\%s" % subfolder

    env["INCLUDE"] += inc_path + ";"
    env["LIB"] += lib_path + ";"

Note how we have x64 instead of amd64 here :)

Conclusion #

Well, that’s all I’ve got for today.

Building Visual Studio projects with CMake and Ninja works quite well if you have build scripts in Python, providing you are willing to run .bat scripts, and carefully apply changes to the environment variables.

Soon I’ll try and see how things go with Visual Studio 2017, maybe things will get easier, who knows?

Until then, may the Build be with you!

Update: I’ve decided to cross-post this article on dev.to. Let’s see how it goes!


  1. David, if you read this, thank you so much! ↩︎

  2. More info in the CMake documentation ↩︎

  3. It’s just one line of output that disappears quickly after the build is done, do you get it? ↩︎

  4. Turned out I somehow missed this page telling me signtool.exe was already installed when I set up Visual Studio on the node … ↩︎


Thanks for reading this far :)

I'd love to hear what you have to say, so please feel free to leave a comment below, or read the contact page for more ways to get in touch with me.

Note that to get notified when new articles are published, you can either:

Cheers!