Skip to main content

Bootstrapping a vcpkg-based cmake project in Visual Studio

After the last week's 2019 Microsoft MVP Summit, I decided to give Microsoft vcpkg a shot. I've a cmake project at work and we target Linux using the Hunter package manager. So vcpkg had been on the back-burner for me.

I'm targeting a 4 part blog series.
  1. Bootstrapping a cmake project based on vcpkg in Visual Studio
  2. Bootstrapping a cmake project based on vcpkg in Linux
  3. Bootstrapping a cmake project based on Hunter in Linux
  4. Bootstrapping a cmake project based on Hunter in Windows
As of this writing I'm new to vcpkg. So I apologize in advance if you are annoyed by noob mistakes. Please leave a comment if you notice something.

If you prefer to clone/browse a github project. All contents in this blogpost are available under cpptruths/cpp0x/vcpkg_test (branch vcpkg_cmake_blog).

To start with, I've a barebones C++ project with nearly empty driver.cpp and driver.h files. Later, I'll add Boost core and optional as third party dependencies. Both are header-only. Later, we will add libraries requiring linking. So, let's get started.

A barebones C++ cmake project

The following is the project structure of my near-empty C++ project vcpkg_test
vcpkg_test
├── CMakeLists.txt
├── include
│   └── driver.h
├── src
│   └── driver.cpp
└── test
    └── driver_test.cpp

3 directories, 4 files
The driver.cpp and driver_test.cpp files have just a main function that does nothing. driver.h is empty. The CMakeLists.txt looks as follows.
cmake_minimum_required (VERSION 3.12)

project (vcpkg_test CXX)
set(CMAKE_CXX_STANDARD 17)

add_executable(driver src/driver.cpp)
target_include_directories(driver PUBLIC ${PROJECT_SOURCE_DIR}/include)

enable_testing()
include(CTest)
add_executable(driver_test ${PROJECT_SOURCE_DIR}/test/driver_test.cpp)
add_test(NAME driver COMMAND driver_test)
See the cmake tutorial if the above file is all greek. It builds two executables from the sources: driver and driver_test.

There're many ways to structure the project. In this project I've chosen to use only one CMakeLists.txt to build both the sources and the test. One could have added CMakeLists.txt in src and test sub-directories.

Open cmake Project in Visual Studio

Visual Studio 2017+ has built-in support for cmake projects. Yes, you read that right! You can open the folder containing the top-level CMakeLists.txt and Visual Studio will figure out everything. The loaded project looks very clean.

Things used to be very different not too long ago. cmake's native solution generator used to add additional targets that are not visible in the CMakeLists.txt you wrote. I always wondered what magic was going on there.

Visual Studio runs cmake automatically on the CMakeLists.txt.
Project build and rebuild works as expected.
Targets driver.exe and driver_test.exe are available in the drop-down. Here's how my loaded project looks like. No cruft!
So, that's how a toy C++ cmake project looks like. Let's use vcpkg to manage our third-party dependencies: boost-core and boost-optional.

Adding vcpkg to a cmake project

Here's a vcpkg tutorial to get your cmake project off the ground in Visual Studio. However, my goal is to create a reproducible build with maximum automation when a user clones the project directory. Perhaps something that could run as-is on AppVeyor CI servers. So the following CMakeLists.txt expects only Visual Studio 2017+ installed on a Windows machine.

The script clones the vcpkg repository and bootstraps it as necessary. We also change the CMAKE_TOOLCHAIN_FILE variable to point to the vcpkg instance the script downloaded and bootstrapped. This allows cmake to discover, include, and link packages managed by vcpkg. Here're the changes to CMakeLists.txt.
cmake_minimum_required (VERSION 3.12)
set(MY_PROJECT_DEPENDENCIES boost-core boost-optional)

if(NOT DEFINED ${CMAKE_TOOLCHAIN_FILE})
  if(NOT DEFINED ENV{VCPKG_ROOT})
    if(WIN32)
      set(VCPKG_ROOT $ENV{HOMEDRIVE}$ENV{HOMEPATH}/vcpkg_cpptruths)
    else()
      set(VCPKG_ROOT $ENV{HOME}/.vcpkg_cpptruths)
    endif()
  else()
    set(VCPKG_ROOT $ENV{VCPKG_ROOT})
  endif()

  if(NOT EXISTS ${VCPKG_ROOT})
    message("Cloning vcpkg in ${VCPKG_ROOT}")
    execute_process(COMMAND git clone https://github.com/Microsoft/vcpkg.git ${VCPKG_ROOT})
    # Use a specific SHA1 for reproducibility. The following one is Mar 31, 2019.
    execute_process(COMMAND git checkout 52870c7595a63ade069ae51d5f4ee3a85fe4123f WORKING_DIRECTORY ${VCPKG_ROOT})
  endif()

  if(NOT EXISTS ${VCPKG_ROOT}/README.md)
    message(FATAL_ERROR "***** FATAL ERROR: Could not clone vcpkg *****")
  endif()

  if(WIN32)
    set(BOOST_INCLUDEDIR ${VCPKG_ROOT}/installed/x86-windows/include)
    set(VCPKG_EXEC ${VCPKG_ROOT}/vcpkg.exe)
    set(VCPKG_BOOTSTRAP ${VCPKG_ROOT}/bootstrap-vcpkg.bat)
  else()
    set(VCPKG_EXEC ${VCPKG_ROOT}/vcpkg)
    set(VCPKG_BOOTSTRAP ${VCPKG_ROOT}/bootstrap-vcpkg.sh)
  endif()

  if(NOT EXISTS ${VCPKG_EXEC})
    message("Bootstrapping vcpkg in ${VCPKG_ROOT}")
    execute_process(COMMAND ${VCPKG_BOOTSTRAP} WORKING_DIRECTORY ${VCPKG_ROOT})
  endif()

  if(NOT EXISTS ${VCPKG_EXEC})
    message(FATAL_ERROR "***** FATAL ERROR: Could not bootstrap vcpkg *****")
  endif()

  set(CMAKE_TOOLCHAIN_FILE ${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake CACHE STRING "")

  message(STATUS "***** Checking project third party dependencies in ${VCPKG_ROOT} *****")
  execute_process(COMMAND ${VCPKG_EXEC} install ${MY_PROJECT_DEPENDENCIES} WORKING_DIRECTORY ${VCPKG_ROOT})
endif()
If everything goes well, the cmake script clones vcpkg repository under $ENV{HOMEDRIVE}$ENV{HOMEPATH}/vcpkg_cpptruths and bootstraps it (i.e., there're no pre-installed packages). From now on it will automatically use the CMAKE_TOOLCHAIN_FILE from this directory. Of course, you can override the CMAKE_TOOLCHAIN_FILE at the command prompt to point to a different vcpkg instance all different toolchain altogether. Also, feel free to change the path vcpkg_cpptruths to something you like.

Managing third-party dependencies with vcpkg

Now is the time to add the boost dependencies. Three steps are needed.
  1. Write code that uses boost-core and boost-optional
  2. Instruct vcpkg to download and install boost-core and boost-optional
  3. Update CMakeLists.txt with the right dependencies
Here's my test code that uses boost-core and boost-optional.
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <typeinfo>

#include "boost/core/demangle.hpp"
#include "driver.h"

int main() {  
  std::srand(static_cast<unsigned int>(std::time(0)));  
  boost::optional<int> i = Generator::get_even_random_number();
  if (i) {
    std::cout << std::sqrt(static_cast<float>(*i)) << "\n";
    std::cout << boost::core::demangle(typeid(boost::optional<int>).name()) << "\n";
  }
}
For #2, you could open a shell and run vcpkg install boost-core boost-optional. It's simple. However, I want a reproducible automatic build setup. So I'm going to have cmake run the same vcpkg command and install the dependencies it's going to use later.
set(MY_PROJECT_DEPENDENCIES boost-core boost-optional)
message(STATUS "***** Checking project third party dependencies in ${VCPKG_ROOT} *****")
execute_process(COMMAND ${VCPKG_ROOT}/vcpkg.exe install ${MY_PROJECT_DEPENDENCIES} WORKING_DIRECTORY ${VCPKG_ROOT})
The execute_process command gets the job done. However, I'm not sure, if there's a better to do the same thing. Is there a higher-level cmake function(s) in vcpkg.cmake that would install the libraries in the vcpkg instance (pointed by the CMAKE_TOOLCHAIN_FILE).

Saving the file CMakeLists.txt in Visual Studio runs it and installs the packages in ${MY_PROJECT_DEPENDENCIES}.
Now we update CMakeLists.txt to look for boost libraries. This part step is platform and package-manger independent.
find_package(Boost 1.67 REQUIRED)
add_executable(driver src/driver.cpp)
target_include_directories(driver PUBLIC ${Boost_INCLUDE_DIR} ${PROJECT_SOURCE_DIR}/include )
You may have to regenerate the cmake cache as the CMAKE_TOOLCHAIN_FILE has been updated. You can do that by right-clicking on CMakeLists.txt.

At this point the code builds and runs cleanly for me. No squiggles.

Observations

Some things I noted would make the experience nicer in Visual Studio 2019.
  1. The Open Project/Solution dialog box did not show CMakeLists.txt under "All Project Files" drop down. First-class support should make the experience seamless.
  2. If vcpkg is integrated with Visual Studio such that libraries get installed in the right vcpkg instance, that would be great.
  3. It would be nice to have cmake functions in vcpkg.cmake that would install libraries in the vcpkg instance. I received responses from multiple people who had some ground work here.
    1. See Package Manager Manager (pmm) mentioned on reddit/r/cpp.
    2. Google-cloud-cpp/super project uses cmake functionality such as ExternalProject_Add and other friends to bootstrap a vcpkg instance.
  4. After updating CMakeLists.txt, the output of cmake is not displayed in the IDE right-away. It takes a good minute and it appears like Visual Studio is stuck. Seems like cmake does not flush output to the IDE window right-away.

Comments

Popular Content

Multi-dimensional arrays in C++11

What new can be said about multi-dimensional arrays in C++? As it turns out, quite a bit! With the advent of C++11, we get new standard library class std::array. We also get new language features, such as template aliases and variadic templates. So I'll talk about interesting ways in which they come together.

It all started with a simple question of how to define a multi-dimensional std::array. It is a great example of deceptively simple things. Are the following the two arrays identical except that one is native and the other one is std::array?

int native[3][4];
std::array<std::array<int, 3>, 4> arr;

No! They are not. In fact, arr is more like an int[4][3]. Note the difference in the array subscripts. The native array is an array of 3 elements where every element is itself an array of 4 integers. 3 rows and 4 columns. If you want a std::array with the same layout, what you really need is:

std::array<std::array<int, 4>, 3> arr;

That's quite annoying for two r…

Covariance and Contravariance in C++ Standard Library

Covariance and Contravariance are concepts that come up often as you go deeper into generic programming. While designing a language that supports parametric polymorphism (e.g., templates in C++, generics in Java, C#), the language designer has a choice between Invariance, Covariance, and Contravariance when dealing with generic types. C++'s choice is "invariance". Let's look at an example.
struct Vehicle {}; struct Car : Vehicle {}; std::vector<Vehicle *> vehicles; std::vector<Car *> cars; vehicles = cars; // Does not compile The above program does not compile because C++ templates are invariant. Of course, each time a C++ template is instantiated, the compiler creates a brand new type that uniquely represents that instantiation. Any other type to the same template creates another unique type that has nothing to do with the earlier one. Any two unrelated user-defined types in C++ can't be assigned to each-other by default. You have to provide a c…

Inheritance vs std::variant

C++17 added std::variant and std::visit in its repertoire. They are worth a close examination. I've been wondering about whether they are always better than inheritance for modeling sum-types (fancy name for discriminated unions) and if not, under what circumstances they are not. We'll compare the two approaches in this blog post. So here it goes.

Inheritancestd::variantNeed not know all the derived types upfront (open-world assumption)Must know all the cases upfront (closed-world assumption)Dynamic Allocation (usually)No dynamic allocationIntrusive (must inherit from the base class)Non-intrusive (third-party classes can participate)Reference semantics (think how you copy a vector of pointers to base class?)Value semantics (copying is trivial)Algorithm scattered into classesAlgorithm in one placeLanguage supported (Clear errors if pure-virtual is not implemented)Library supported (poor error messages)Creates a first-class abstractionIt’s just a containerKeeps fluent interfaces…